This is a test that controls an LED using a button on a web page. It creates a web UI button that lets you turn the LED on and off with a single click. Other computers can also control the UNO Q's LED by connecting to the web page remotely.
Behind it all is the combined work of three technologies: "dual-core heterogeneous architecture + WebSocket + RPC bridge." This document first gets you up and running, then breaks down the underlying principles layer by layer.
Step 1: Make sure UNO Q is connected to Wi-Fi
During the initial setup in App Lab, enter your Wi-Fi password. Once connected, the UNO Q will obtain a local IP address.
Step 2: Run the Blink LED with UI example
In App Lab, find "Blink LED with UI" in the Examples list on the left, and click Run in the top-right corner. App Lab will automatically compile and flash the Arduino code to the MCU, and start the Python program on the Linux side.
Step 3: Verify on the UNO Q itself
Open http://localhost:7000 in App Lab's built-in browser or terminal, and confirm that the control interface opens correctly and the button can control the LED. This step verifies that the internal signal path on the UNO Q is working.
Tip: Once the page opens, you'll see a button. Click it and watch the red LED on the UNO Q board — it should turn on and off properly. Press once to turn it on, press again to turn it off, and so on. If the LED toggles correctly with the button, it means the board itself, the wiring (the onboard LED is connected to pin 6, which corresponds to
LED_BUILTIN), and the internal communication path are all working.
The biggest difference between the UNO Q and a regular Arduino is that it has two independent processors:
| Processor | Architecture | Operating System | Responsibility |
|---|---|---|---|
| MPU (Microprocessor) | Qualcomm Dragonwing QRB2210 | Debian Linux | Network communication, web services, user logic |
| MCU (Microcontroller) | STM32U585 | Real-time firmware | Hardware control, GPIO operations |
The LED is physically connected to the MCU's LED_BUILTIN pin (pin 6 on the UNO Q), and only the MCU can control it directly. When a browser sends a command over Wi-Fi, the command must first reach the Python program on the Linux side, and then be handed off to the MCU through the Bridge (RPC mechanism).
The complete communication path is:
Browser clicks button
→ WebSocket message reaches Python (Linux side)
→ Python issues an RPC call via Bridge.call()
→ UART serial transmission to MCU
→ MCU executes set_led_state() function
→ Hardware GPIO toggles LED state
→ State is sent back, Python broadcasts the update to all web pages
This code runs on the STM32 microcontroller. Its job is to expose a function called set_led_state that can be invoked remotely.
#include <Arduino_RouterBridge.h>
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH); // Start with LED off (the UNO Q LED is active-low)
Bridge.begin(); // Initialize Bridge communication
Bridge.provide("set_led_state", set_led_state); // Register the callable function
}
void loop() {}
void set_led_state(bool state) {
digitalWrite(LED_BUILTIN, state ? LOW : HIGH); // LOW = on, HIGH = off
}
Key points:
Bridge.begin(): Starts the RPC communication channel between the MCU and the Linux MPU, using UART serial transmission underneath.Bridge.provide("set_led_state", set_led_state): Registers the set_led_state function as a remote procedure call (RPC) interface. Once registered, Python on the Linux side can call it just like a local function.digitalWrite(LED_BUILTIN, state ? LOW : HIGH): The UNO Q's onboard LED is active-low — the LED turns on when the pin outputs LOW, and turns off when it outputs HIGH. So when state=true (turn the LED on), it writes LOW.Advanced note: Arduino's official documentation points out that callbacks registered with
Bridge.providerun on a high-priority background thread. If the function directly calls standard Arduino APIs likedigitalWrite, there's a risk of concurrency conflicts. A safer approach is to useBridge.provide_safe, which ensures the function executes in theloop()context and avoids thread-safety issues.
Bridge is the cross-processor remote procedure call library Arduino designed for the UNO Q. It is essentially a lightweight RPC (Remote Procedure Call) framework.
It works like this:
Bridge.provide("name", function_pointer) exposes a local function.Bridge.call("name", argument) to remotely execute that function on the MCU.You can think of it this way: Bridge lets Python "call across the gap" into a C++ function on the MCU, just as if that function were in the Python process itself.
The Python program runs on the Linux MPU. It has three responsibilities: maintain state, respond to web messages, and call Bridge.
led_is_on = False
def get_led_status():
return {
"led_is_on": led_is_on,
"status_text": "LED IS ON" if led_is_on else "LED IS OFF"
}
A global boolean variable tracks the current LED state, and a helper function wraps it into a dictionary format that's easy to send to the frontend.
def toggle_led_state(client, data):
global led_is_on
led_is_on = not led_is_on # Toggle the state
Bridge.call("set_led_state", led_is_on) # RPC call to the MCU function
ui.send_message('led_status_update', get_led_status()) # Broadcast to all web pages
This is the heart of the whole program, and every line has a clear purpose:
led_is_on = not led_is_on: Python maintains its own state variable to keep the logic consistent.Bridge.call("set_led_state", led_is_on): This is the "call across the gap" statement that invokes the MCU function. It passes the value of led_is_on as an argument over Bridge to the MCU, triggering the previously registered set_led_state() to execute and ultimately changing the LED's physical state.ui.send_message('led_status_update', ...): Pushes the latest state to all connected browser pages over WebSocket, so the UI updates in real time.def on_get_initial_state(client, data):
ui.send_message('led_status_update', get_led_status(), client)
When a new browser page opens, the frontend sends a get_initial_state message. This function is responsible for sending the current LED state to just that new client, preventing a mismatch between what the UI shows and the actual state.
ui = WebUI()
ui.on_message('toggle_led', toggle_led_state)
ui.on_message('get_initial_state', on_get_initial_state)
App.run()
WebUI(): Starts a lightweight WebSocket server, listening on port 7000 by default.ui.on_message('toggle_led', ...): Registers a message handler. When a browser sends a toggle_led message over WebSocket, toggle_led_state is called automatically.App.run(): Starts the application's main loop, keeping the Python program listening for WebSocket connections.When you click Run in App Lab, the UNO Q internally performs the following steps in order:
First, the MCU starts up and registers its RPC service. The STM32 microcontroller runs setup(), configures LED_BUILTIN as an output pin, then calls Bridge.begin() to establish the serial communication channel with the Linux side. It then uses Bridge.provide("set_led_state", ...) to "hang" the function onto the RPC service table. At this point, the MCU enters an idle loop(), waiting for calls from the Linux side.
Second, the Linux side starts the WebSocket server. When the Python program reaches ui = WebUI(), it starts a network service on the MPU, listening on port 7000 by default. Then ui.on_message(...) binds the two message names toggle_led and get_initial_state to their respective handler functions.
Third, App.run() enters the event loop. The Python main thread continuously listens for two things: WebSocket messages from browsers, and Bridge responses from the MCU. The whole program enters a "standby" state.
Fourth, receiving a message triggers the full chain. When a browser sends a toggle_led message, the WebSocket server catches it and calls toggle_led_state(). That function flips the led_is_on variable, then sends the new state to the MCU via Bridge.call(). The MCU's background thread receives the RPC request, executes set_led_state(), and changes the GPIO level. Finally, Python broadcasts the new state back to all browsers via ui.send_message().
Key point: Bridge is bidirectional and asynchronous. After Python calls Bridge.call(), it can continue processing other messages without blocking and waiting for the MCU to respond. That's why the interface stays responsive even when multiple browsers are operating at the same time.
Sections 1 through 6 covered how things work on the UNO Q itself. Now let's move the control interface to another computer — it only takes three steps.
Run this in the App Lab terminal:
ip addr show
Find the inet address for wlan0, in the form 192.168.1.100. This IP is the target address other devices need to access.
An easier way: If you find the command line intimidating, you can also return to the Arduino App Lab home page, click the settings gear in the bottom-left corner, and find the Network Connections section. The IP shown there is the UNO Q's IP address.
Enter this in your computer's browser:
http://<UNO_Q_IP>:7000
For example, http://192.168.1.100:7000. You'll see the exact same control interface as on the UNO Q itself, with a button in the middle.
After pressing the button, the browser sends a toggle_led message over WebSocket → Python flips the state variable → Bridge.call() triggers the MCU to execute set_led_state() → the LED toggles on or off → the state is sent back and the interface updates. The whole process usually completes within tens of milliseconds.
If your computer is close to the UNO Q, there's an even simpler approach: just open Arduino App Lab on your computer and choose Wi-Fi connection to the board instead of USB. This way, App Lab itself communicates with the UNO Q over Wi-Fi, and you don't need to manually look up the IP or type a URL — you can enter and control the application directly from App Lab.
This is essentially a pure Wi-Fi board connection, suitable when the computer and UNO Q are in the same room. The manual IP approach in sections 7.1–7.3 is better when the computer and UNO Q are in different locations and can only be reached over the local network.
Key fact: WebUI listens on 0.0.0.0:7000 by default, which means it accepts connections from any network interface, including Wi-Fi.
Here's an explanation of the difference between localhost and the actual IP — this is often the most confusing point for beginners:
localhost: Points only to "the device you're currently on." Opening localhost:7000 on the UNO Q accesses the UNO Q itself; opening localhost:7000 on your computer accesses your own computer.So when accessing from another computer, the correct approach is:
http://<UNO_Q_IP>:7000
You can find the UNO Q's IP by running ip addr show in the App Lab terminal, by checking the device list in your router's admin page, or by using the alternative method in section 7.1 to view it in App Lab's settings.
Section 7 covered "how one computer connects." This section covers "what happens when multiple devices connect at the same time."
All devices (the UNO Q, your computer, other computers/phones) must be on the same Wi-Fi network. The UNO Q was already connected to Wi-Fi during initial setup and has a local IP address.
Enter the same address in each device's browser:
http://<UNO_Q_IP>:7000
For example, http://192.168.1.100:7000.
Each device will load the exact same control interface you saw on the UNO Q itself. After clicking the button, the flow is:
Other computer browser → Wi-Fi → UNO Q's WebSocket server → Python logic → Bridge → MCU → LED
Suppose your computer and another computer both have the control interface open. When you click the button on your computer to turn the LED on, the other computer's interface will also automatically update to show "LED IS ON."
This is because the toggle_led_state function on the Python side ends with a call to ui.send_message('led_status_update', ...), which broadcasts the latest state to all connected browsers. The led_is_on global variable maintained on the Python side is the single source of truth, and every client's display is based on it.
This also means: no matter how many devices have the control interface open, there is always only one physical LED state. You'll never see a situation where "computer A shows on and computer B shows off" due to multiple devices operating at once.
Check these three things in order:
① Is the IP correct? Run ip addr show in the App Lab terminal, find the IPv4 address for wlan0, and confirm that's the address you entered in the browser, plus :7000.
② Router client isolation. Many routers enable "AP isolation" or "client isolation" by default, which blocks devices on the same Wi-Fi from communicating with each other. You'll need to log into your router's admin page and turn this off.
③ App Lab's connection method. Some users have reported that when App Lab connects to the UNO Q over Wi-Fi, external devices may be unable to access the WebUI. Switching to a USB connection fixes it. If you run into this, try connecting the UNO Q to a computer with a USB cable, then access it from other devices via IP.
① Check for port conflicts. Make sure port 7000 isn't being used by another Brick (such as Streamlit). Run ss -tlnp | grep 7000 in the terminal. If the process shown isn't your application, stop whatever is occupying the port first.
② Check whether Bridge is working. Check the Python logs in the App Lab terminal to confirm that Bridge.call("set_led_state", ...) returns successfully. If it errors out, the MCU-side code may not have been flashed correctly — click Run again to re-flash it.
③ Check the LED polarity. The UNO Q's onboard LED is active-low. If your code logic is reversed (for example, writing HIGH to turn it on), the LED's behavior will be opposite to the button state. Confirm that set_led_state() uses state ? LOW : HIGH. (Only need to check this if you modified the original code.)
If computer A clicks the button but computer B's interface doesn't change, check whether toggle_led_state() ends with a call to ui.send_message('led_status_update', get_led_status()). This line is responsible for broadcasting the latest state to all connected clients. Without it, only the device that initiated the action will see its interface update.
Open your browser's developer tools (F12) and switch to the Network → WS (WebSocket) tab. Watch whether a toggle_led message is sent when you click the button, and whether the server returns led_status_update. If no message is sent, the frontend JS binding has a problem. If a message is sent but there's no response, the Python-side ui.on_message('toggle_led', ...) registration failed or the handler threw an exception.
| Symptom | Possible Cause | Solution |
|---|---|---|
| Other computers can't open the web page | Not on the same Wi-Fi / router isolation / wrong IP | Check the network, disable AP isolation, confirm the IP with ip addr show |
| Web page opens but the button does nothing | Port conflict / Bridge not initialized / JS binding failure | Check port 7000, review logs, inspect WebSocket with F12 |
| LED behavior is opposite to the button | LED polarity reversed | Confirm state ? LOW : HIGH |
| State not syncing across multiple devices | Forgot to broadcast state | Add ui.send_message('led_status_update', ...) |