Automating PCB Inspection with Arduino UNO Q Part 4: Creating the Application

Automating inspection of printed circuit boards with Arduino UNO Q and Edge Impulse.
In this series of articles, we take a look at how the unique capabilities of Arduino UNO Q (066-5593) may be used to seamlessly integrate AI and control of physical inputs and outputs, with user-friendly training of AI/ML models courtesy of the Edge Impulse software platform.
In Part 1, we outlined the use case and introduced the sophisticated prototyping features provided by UNO Q hardware and App Lab. In Part 2, we got up and running with Arduino App Lab and Edge Impulse, and prepared a selection of PCBAs. While in Part 3 we acquired data in the form of sample images and trained a machine learning model with these, before testing the performance and working to improve this.
In this final instalment, we will now create an application which makes use of our custom-trained ML model, while also integrating physical inputs and outputs.
Note that machine learning (ML) is regarded as a subset of artificial intelligence (AI), and this series of articles may use both terms interchangeably.
Improved Performance

In the previous article, we explained how we trained a model to recognise SOT23 surface mount devices, achieving an F1 score of 97.8% and an accuracy in testing of 96.67%. Following which and as part of further experimentation, we used the Live classification feature in Edge Impulse Studio to try the model with a live video feed from the UNO Q. This is particularly handy for additional verification and to test with new samples and different lighting, etc.

A nice side effect of using Live classification is that we also gathered a small selection of new images, which we decided to also label via the Studio, raising our total from 149x to 167x images. At this point, we also went back and labelled partially cropped SOT23 devices, which we had previously omitted from labelling. Finally, we re-ran model training with the same parameters as before, and this time achieved an F1 score of 99.2% and an accuracy in testing of 100%.

Which again demonstrates the vital importance of training data, and this is probably the first place you should look when seeking to make performance improvements.
Deployment Options

Edge Impulse Studio provides many different deployment options, including C++ libraries, Docker containers, and a model binary compiled for Arduino UNO Q.

It’s also possible to very quickly test on-device performance by executing edge-impulse-linux-runner, which connects to our previously configured project, and builds and downloads the model if it is not already present, before finally executing this. Following which JSON is output, and for each object identified we have a label, confidence and coordinates.
With a PCBA under the microscope that had a full complement of 8x SOT23 transistors fitted, the JSON output looked like:
[{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":56,"y":160},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":136,"y":160},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":208,"y":160},{"height":8,"label":"SOT23","value":0.98828125,"width":16,"x":280,"y":168},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":136,"y":216},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":208,"y":216},{"height":24,"label":"SOT23","value":0.99609375,"width":16,"x":280,"y":216},{"height":8,"label":"SOT23","value":0.99609375,"width":16,"x":56,"y":224}]
If we then replaced that PCBA with another that had a single missing SOT23 transistor:
[{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":56,"y":160},{"height":16,"label":"SOT23","value":0.99609375,"width":8,"x":136,"y":160},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":208,"y":160},{"height":8,"label":"SOT23","value":0.609375,"width":8,"x":280,"y":160},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":56,"y":216},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":208,"y":216},{"height":16,"label":"SOT23","value":0.99609375,"width":16,"x":280,"y":216}]
As we would expect, only 7x SOT23 packages were identified.

We can point a web browser at our UNO Q to get a live on-device classification video feed. When edge-impulse-linux-runner was started a WebSocket URI was output to the terminal also, which could be used to enable rapid integration with web applications.
Project Setup

We started setting up our project by copying the Edge Impulse Model (EIM file) from the location where edge-impulse-linux-runner had previously downloaded this (.ei-linux-runner/models), to the standard location which is used by App Lab (.arduino-bricks/ei-models). We also used this opportunity to rename the model to something more easily identifiable.

Note that the model can also be downloaded from the Deployment page in Edge Impulse Studio.
At this point, we could have created a new project in App Lab, but since our app is going to be similar to the Detect Objects on Camera example we ran in Part 2, we decided to start with this.

This was achieved by opening the example in App Lab, then selecting Copy and edit app from top-right, entering a name, and then selecting Create new.

From a terminal session on our UNO Q we then examined the contents of the ArduinoApps directory and could see the file structure as outlined in Part 2 → App Lab 101.
Now we could set about customising this to fit our particular application.
Custom Application
name: PCB Inspection
description: This example showcases object detection within a live feed from a USB camera.
ports: []
bricks:
- arduino:video_object_detection: {}
- arduino:web_ui: {}
icon: <emoji>
Note that an emoji would be present in place of <emoji> (this would not render via the DesignSpark website)
Starting at the top level, we have the file app.yaml, which amongst other things specifies the application name, a description and the bricks which are used. Above can be seen the contents of this immediately after copying the Detect Objects on Camera example. So the first step was to amend the description to “Inspect printed circuit boards to confirm correct assembly.”
The project README.md file was similarly updated to avoid confusion if switching projects.
For an introduction to the structure of App Lab applications, please refer back to the App Lab 101 section of Part 2 in this series.

Next, we navigated to assets/index.html and replaced instances of Video Generic Object Detection with the text PCB Inspection. Another quick and simple change for clarity.
Video Object Detection Brick Configuration

For the first functional change, we updated the application to use our custom-trained model.
If we select the Video Object Detection brick in App Lab, we can then click on Brick configuration and from here we are presented with two out-of-the-box options, as seen above. To configure the brick to use the model trained via Edge Impulse we went back and updated app.yaml.
name: PCB Inspection
description: Inspect printed circuit boards to confirm correct assembly.
ports: []
bricks:
- arduino:video_object_detection: {
variables: {
EI_OBJ_DETECTION_MODEL: /home/arduino/.arduino-bricks/ei-models/SOT23.eim
}
}
- arduino:web_ui: {}
icon: <emoji>
Note that an emoji would be present in place of <emoji> (this would not render via the DesignSpark website)
Here, we set a variable for the brick to specify the custom model we copied into place earlier.
First Success!

At this point, we decided to try running the application and launched this from App Lab.
Application startup completed, and the console text turned green.

A browser was pointed at the UNO Q, the web interface loaded and, success! The application correctly identified the 7x SOT23 devices soldered to the test PCBA.
Though, why the small circle and not a bounding box? The reason for this is that the FOMO (Faster Objects, More Objects) model we are using provides centroids as inferencing output.
Once we were able to detect SOT23 devices with our application, we moved on to the MCU part.
Integration of Physical Inputs and Outputs

If we take a look at one of the App Lab examples, which makes use of the UNO Q MCU — such as the classic Blink example updated for this platform — we will see that it has an additional sketch directory where the MCU code lives.
Hence, our first step was to create a sketch directory. Within which we needed a sketch.ino file and this will be familiar to anyone who has used a classic Arduino board, along with a sketch.yaml file.
profiles:
default:
platforms:
- platform: arduino:zephyr
libraries:
- Arduino_RouterBridge (0.2.2)
- dependency: Arduino_RPClite (0.2.0)
- dependency: ArxContainer (0.7.0)
- dependency: ArxTypeTraits (0.3.2)
- dependency: DebugLog (0.8.4)
- dependency: MsgPack (0.4.2)
default_profile: default
sketch.yaml contains details of the MCU platform — UNO Q is built on the Zephyr real-time operating system (RTOS) — along with a list of dependencies. Hence, this is akin to a package manager for our MCU; rather than having to download and copy libraries in place, we can simply list them here, and App Lab takes care of the rest.
#include "Arduino_RouterBridge.h"
int buttonPin = 5;
int redPin = 4;
int greenPin = 7;
int bluePin = 8;
int buzzerPin = 12;
int buttonState;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(buzzerPin, OUTPUT);
Bridge.begin();
Bridge.provide("setTowerLight", setTowerLight);
Bridge.provide("setBuzzer", setBuzzer);
}
void setTowerLight(bool red, bool green, bool blue) {
digitalWrite(redPin, red ? HIGH : LOW);
digitalWrite(greenPin, green ? HIGH : LOW);
digitalWrite(bluePin, blue ? HIGH : LOW);
}
void setBuzzer(bool state) {
digitalWrite(buzzerPin, state ? HIGH : LOW);
}
In our sketch.ino we included the header for the arduino-router which provides a convenient remote procedure call (RPC) service for integrating MPU and MCU application parts. We also defined a few variables, and in setup() configured the IO pins we planned to use.
RPC integration from MPU → MCU was then as easy as using Bridge.provide() to map incoming calls from Python to C++ functions. The functions can be seen above, and these allow us to toggle the red, green and blue LEDs in a tower light, and turn a buzzer on and off.
void loop() {
int reading = digitalRead(buttonPin);
if (reading != lastButtonState) {
// reset the debouncing timer
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// if the button state has changed:
if (reading != buttonState) {
buttonState = reading;
if (buttonState == LOW) {
Bridge.notify("buttonPressed");
}
}
}
lastButtonState = reading;
}
In our main loop, we read an IO pin configured as an input with internal pull-up, which is connected to ground via a normally open industrial push button. Since switches can be noisy, we also integrated debounce.
Following which sending button presses to our Python script was straightforward, using Bridge.notify() to call the appropriate function.
Of course, this is a trivial MCU example, and the STM32 we have at our disposal could be put to use for far more sophisticated tasks, such as driving a wide variety of displays, implementing all manner of bus protocols, integrating an endless list of sensors and actuators, and performing real-time processing such as the implementation of DSP algorithms.
Python Updates
On the Python side, we needed to respond to a button press by counting the number of SOT23 devices, following which setting the tower light appropriately, to either green for a pass, or red and as an extra indication sounding a buzzer if PCBA visual inspection failed.
At the time of writing, there was a bug in the Video Object Detection brick, which meant that with multiple objects of the same type it would only return one — the last. However, a workaround was simple enough, as we could just connect directly to the Edge Impulse model, parse the JSON output shown earlier and count the number of SOT23 items returned. It should be noted that the Arduino team quickly confirmed the issue and the fix may even be live by the time this article is published.
Rather than walk through the Python script verbatim, we will just cover the key parts here, and for further details, please see the GitHub repo.
from arduino.app_utils import *
At the top of our script we needed to make sure that we were importing the corresponding arduino-router functionality, so that we could communicate with the MCU. We also imported JSON parsing and WebSockets libraries, since we’d now be connecting to the Edge Impulse model directly.
At this point, we should again note that it wasn’t necessary to manually install libraries, since App Lab takes care of dependencies in the background, and we simply needed to update the imports.
sot23_fitted = 8
checkPCBA = False
We defined how many SOT23 devices we expected to see and initialised a variable which would be set to True upon a button press.
def buttonPressed():
global checkPCBA
print("Button pressed!")
checkPCBA = True
We then used Bridge.provide() to map an incoming call from the MCU to the Python function.
Bridge.provide("buttonPressed", buttonPressed)
Now on to the function which is executed by the main loop every time we’ve detected all the objects in a frame.
# Send detections to the UI.
for key, value in detections.items():
# Multiple detections for the same object type will be sent as separate messages
for detection in value:
entry = {
"content": key,
"confidence": detection.get("confidence"),
"timestamp": now
}
ui.send_message("detection", message=entry)
# If requested evaluate the PCBA based on the number of detected SOT23 components
if checkPCBA:
if len(detections.get('SOT23', [])) == sot23_fitted:
Bridge.call("setTowerLight", False, True, False)
time.sleep(3)
Bridge.call("setTowerLight", False, False, False)
else:
Bridge.call("setTowerLight", True, False, False)
Bridge.call("setBuzzer", True)
time.sleep(3)
Bridge.call("setBuzzer", False)
Bridge.call("setTowerLight", False, False, False)
checkPCBA = False
The function from the Detect Objects on Camera example was extended, so that in addition to updating the web UI, if checkPCBA was True we would also check how many SOT23 items were present in the dictionary output by the Edge Impulse model. A pass is indicated by this matching sot23_fitted and otherwise it is a fail. In each case, we then make the appropriate Bridge calls to invoke C++ functions on the MCU. The result of which being 3 seconds of green light for a pass, or 3 seconds of red light plus buzzer for a fail.
It’s that simple.
Running, Debugging and Startup

In addition to launching the application via the App Lab IDE, we can also use the arduino-app-cli for this and more. So while the IDE provides a great deal of convenience, we are by no means tied to this and are free to use our editor of choice, together with the Arduino App CLI tool.
$ arduino-app-cli app start pcb-inspection
Starting our application with the App CLI.

Since App Lab uses Docker containers for brick functionality, we could use the Docker CLI to view container logs. However, the App Lab CLI can also be used to view logs, and the benefit of this is that we can also see any output from our Python script, such as that from print statements added during debugging. Furthermore, we can also generate debug output from our MCU code by using Monitor.print() in sketches.
$ arduino-app-cli app logs pcb-inspection --all --follow
Following application log output.

We can use the CLI to list the available applications with:
$ arduino-app-cli app list
Here we can see our pcb-inspection in the user namespace.

And we can set this to automatically start at boot with:
$ arduino-app-cli properties set default user:pcb-inspection
Demonstrator

The application has been packaged into a self-contained demonstrator, and it will be on the RS stand at Embedded World 2026. The key components used in the build are as follows:
- Arduino UNO Q (066-5593)
- Arduino 4 Relays Shield (875-0292)
- Fortex HD5-MIC USB 2.0 Digital Microscope (028-3421)
- RS PRO Clear Signal Tower (271-2221)
- RS PRO Push Button Complete Unit (145-0613)
- RS PRO 5U3 Series Grey Grey Lid ABS General Purpose Enclosure (201-0177)
- RS PRO 40W Plug-In AC/DC Adapter 12V DC (251-9226)
- RS PRO Aluminium Structural Systems – 30mm profile, 6mm groove
- StarTech.com 3840 x 2160 pixel USB C Docking Station (236-8297)
- Alfatronix Car Charger USB C Socket, USB A Socket (283-6476)
- Werma 60 dB Screw Continuous Internal 61 mm Buzzer (224-0665)
The 4 Relays Shield is used to drive the red, green, and blue (currently unused) LEDs in the signal tower, along with the buzzer. The shield also features four inputs — two analogue and two digital — each on a 3-pin Molex 2.54mm pitch KK connector, together with 5V and ground. One of the digital inputs being used for the push button input, which triggers the test.
30x30mm RS PRO aluminium extrusion was used to provide a robust frame, tied together with corner cubes secured to M6 tapped extrusion ends.
Final Words
Arduino UNO Q, App Lab and Edge Impulse together provide a powerful combination, with a clear focus on convenience and enabling the rapid development of sophisticated applications, which might integrate physical inputs and outputs, perform real-time processing, leverage an expansive ecosystem of Linux-based technologies, and make use of custom-trained machine learning.
Here we have barely scratched the surface of what’s possible, and obvious improvements for this particular application would include the addition of QR code scanning, integration with quality systems, machine control — e.g. a conveyor belt — and more comprehensive model training.
— Andrew Back
Comments