Arduino UNO-Q example: reading GNSS NMEA data and displaying it in a web dashboard

Hello everyone,

I have just published a small demonstration project for the Arduino UNO-Q.

The goal of this project is to show a complete data pipeline using the hybrid architecture of the board:

GNSS receiver → MCU (NMEA parsing) → Bridge → Linux Python → WebUI → HTML dashboard

The project reads NMEA frames from a GNSS module through the hardware UART, parses the useful fields on the MCU with a lightweight C++ parser, sends the data to the Linux MPU via Bridge, and exposes them through a small REST API displayed in a web dashboard.

Repository:

This example mainly demonstrates:

  • use of the UNO-Q hardware UART
  • NMEA parsing on the MCU
  • communication between MCU and Linux via Bridge
  • a simple Python REST API
  • automatic JSON handling with WebUI
  • a small browser dashboard updating every second

At the moment I have quite a lot of activities, so I may not be able to answer quickly to questions, but I will try to reply when possible.

I hope this example can help others exploring the UNO-Q architecture.

Best regards

Philippe

I have run your app and it is running without any error.

ChatGPT has provded the following working principle of your Python Script of your application:

Python Sript:

import datetime
import threading
from arduino.app_utils import App, Bridge
from arduino.app_bricks.web_ui import WebUI

print("Python ready", flush=True)

web = WebUI()

_lock = threading.Lock()
_state = {
    "lat": None,
    "long": None,
    "jour": None,
    "mois": None,   
    "annee": None,
    "heure": None,
    "minute": None,
    "seconde": None,
    "numsat": None, 
    "altitude": None,
}

def now_utc_iso():
    #ISO 8601 UTC, ex: 2026-03-01T08:22:29Z
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

def update_gps(lat,long, jour, mois, annee, heure, minute,seconde, numsat, altitude):
    with _lock:
        _state["lat"] = float(lat)
        _state["long"] = float(long)
        _state["jour"] = int(jour)
        _state["mois"] = int(mois)
        _state["annee"] = int(annee)
        _state["heure"] = int(heure)
        _state["minute"] = int(minute)
        _state["seconde"] = int(seconde)
        _state["numsat"] = int(numsat)
        _state["altitude"] = float(altitude)

Bridge.provide("update_gps", update_gps)
#Bridge.provide("presence_mm", presence_mm)

def api_state(_req=None):
    with _lock:
        payload = {
            "now_utc": now_utc_iso(),
            "lat": _state["lat"],
            "long": _state["long"],
            "jour": _state["jour"],
            "mois": _state["mois"],
            "annee": _state["annee"],
            "heure": _state["heure"],
            "minute": _state["minute"],
            "seconde": _state["seconde"],
            "numsat": _state["numsat"],
            "altitude": _state["altitude"],
        }
    return payload

web.expose_api("GET", "/api/state", api_state)

App.run()

1. Importing required modules

import datetime
import threading
from arduino.app_utils import App, Bridge
from arduino.app_bricks.web_ui import WebUI

Purpose

Module Purpose
datetime work with date and time
threading prevent data corruption
Bridge communication between MCU and Python
WebUI creates a web server
App runs the application

So this program will communicate with MCU and also run a web server.


2. Startup message

print("Python ready", flush=True)

This prints a message so you know the Python program has started.

flush=True forces the text to appear immediately.


3. Creating the WebUI server

web = WebUI()

This creates a web server object.

Later you can open a browser and access its API.


4. Thread safety lock

_lock = threading.Lock()

This creates a mutual exclusion lock (mutex).

Why needed?

Two things may access the data at the same time:

  1. MCU updating GPS data
  2. Web browser requesting data

The lock ensures only one process changes the data at a time.


5. State dictionary (data storage)

_state = {
"lat": None,
"long": None,
"jour": None,
"mois": None,
"annee": None,
"heure": None,
"minute": None,
"seconde": None,
"numsat": None,
"altitude": None,
}

This dictionary stores the latest GPS information.

Meaning of fields:

Field Meaning
lat latitude
long longitude
jour day
mois month
annee year
heure hour
minute minute
seconde second
numsat number of satellites
altitude height

Initially all values are None.


6. Function to generate current UTC time

def now_utc_iso():

This function returns the current time in ISO 8601 format.

Example output:

2026-03-01T08:22:29Z

This format is widely used in web APIs.


7. Function that receives GPS data

def update_gps(lat,long, jour, mois, annee, heure, minute,seconde, numsat, altitude):

This function is called from the MCU through Bridge.

Inside:

with _lock:

The lock ensures safe data update.

Then values are stored:

_state["lat"] = float(lat)
_state["long"] = float(long)

Type conversion is done because values arriving from Bridge are strings.

So they are converted to:

  • float
  • int

8. Making the function callable from MCU

Bridge.provide("update_gps", update_gps)

This registers the function with the Bridge.

Now the Arduino sketch can call it like:

Bridge.call("update_gps", lat,long,...)

Flow:

MCU → Bridge → Python function update_gps()


9. API function for the web page

def api_state(_req=None):

This function returns the current GPS state.

Again it uses the lock:

with _lock:

Then builds a dictionary called payload.

Example payload returned:

{
"now_utc": "2026-03-01T08:22:29Z",
"lat": 23.7456,
"long": 90.4213,
"numsat": 8,
"altitude": 21.4
}


10. Exposing the API to the web

web.expose_api("GET", "/api/state", api_state)

This creates a REST API endpoint.

So if a browser requests:

http://device-ip/api/state

The function api_state() runs and returns JSON data.


11. Starting the application

App.run()

This starts:

  • the Bridge service
  • the WebUI server
  • the event loop

The program then runs continuously.


Complete System Operation

GPS sensor


MCU (Arduino sketch)

│ Bridge.call("update_gps")

Python program
(update_gps function)

│ stores data in _state

Web API
/api/state


Browser / dashboard


Example Real Workflow

:one: GPS module sends data to MCU
:two: MCU extracts latitude, longitude etc
:three: MCU calls

Bridge.call("update_gps", ...)

:four: Python stores the values
:five: Browser requests

GET /api/state

:six: Python returns JSON data


:white_check_mark: In one sentence

This script receives GPS data from the MCU through Bridge, stores it safely, and serves it to a web dashboard via a REST API.


Hello @GolamMostafa,

I am currently reviewing the interpretation provided by ChatGPT about the working principles of my Python script. For the moment I am analyzing it empirically, mainly by observing the runtime behavior of the program, because I unfortunately do not have much time available right now. I will come back later with more detailed conclusions and explanations.

Regarding this specific point:

_state["lat"] = float(lat)
_state["long"] = float(long)

Type conversion is done because values arriving from Bridge > are strings.

So they are converted to:
float
int

This appears to be an incorrect assumption on my side.
After checking the runtime types in the Python callback, the values received from Bridge.call() are already numeric (float and int). Therefore the conversion is actually unnecessary in this case.

In practice the data flow is already:

MCU numeric values → Bridge → Python float/int → JSON → JavaScript Number

So casting float(lat) or int(numsat) does not change the type; it was added only as a precaution but it is not required.

I have already corrected this point in the GitHub repository. I will continue reviewing the rest of the analysis and will come back later to complete my observations.

Also, I would like to mention that I started learning all this from zero around April 2021, so please excuse any mistakes I may make. Exercises like this are very useful for me because they help me better understand the system.

Best regards
Philippe

I understand that you have a busy schedule, yet you still make tutorials to promote the UNO Q Platform, which is highly appreciated.

I especially like the complete and working applications that you and @jens-bongartz provide. I study them and present them to my students in modular and simplified forms whenever possible, since some of the Python constructs used are quite advanced.

I’m glad to know that you are reviewing the text of post #3 supplied by ChatGPT to check whether there might be any serious misinterpretation or incorrect description.

@GolamMostafa

Regarding the specific question of whether the use of _lock is justified in this application, the answer is yes.

Runtime observation shows that update_gps() runs in the Bridge thread (Bridge.read_loop), while api_state() runs in AnyIO worker threads handling HTTP requests. Since both functions access the shared _state dictionary concurrently, a lock is required to avoid inconsistent reads during updates.

So the general idea in the previous explanation provided by ChatGPT was correct: the shared state must be protected.

However, I would make the explanation more precise on a few points:

  • it is not the MCU itself that updates _state; the update is performed by the Python callback update_gps()
  • this is a thread-safety issue, not a process issue
  • the lock does not only protect writes; it also prevents reads from interleaving with writes

In other words, the purpose of _lock is to protect the shared mutable Python dictionary accessed by both the Bridge callback and the HTTP API handler.

I verified this empirically by printing the current thread in both functions, and in this case the use of _lock is therefore justified.

I understand that Router Bridge uses the UART (LPUART1) port to exchange messages/data between the MCU <-----> MPU in ASCII format, not in raw binary.

For example:

1. The byte 0x23 (00100011) is transmitted as 0x32 ('2') and 0x33 ('3'), which are the ASCII codes for the characters 2 and 3 .

2. Similarly, the value 25.73 is transmitted as 0x32, 0x35, 0x2E, 0x37, 0x33 (5 bytes), corresponding to the ASCII characters "25.73" , and not as the binary bytes 41CDD70A (the 32-bit binary32/IEEE-754 representation of 25.73).

Therefore, on the Python side, the received string should be converted back to its original numerical format before storing or presenting it in a dictionary or on the console.

Yes, _lock is a high-level concept in Python corresponding to the mutex (mutual exclusion) mechanism used in the FreeRTOS supervised multi-tasking environment of the ESP32. A mutex must be acquired before accessing a shared resource, such as the Serial Monitor, and released once the task is completed. This prevents multiple tasks from attempting to access the same resource concurrently, which could otherwise lead to conflicts or system deadlock.

You are absolutely right that RouterBridge transports the data in ASCII over UART.

However, after the message is decoded by RouterBridge on the Python side, the values are already converted into Python numerical types.

I verified this by printing the received types in the callback:

print(type(lat), type(jour), type(altitude))

which returns:

<class 'float'> <class 'int'> <class 'float'>

So in this specific case the explicit casting (float / int) is not strictly required, because RouterBridge already performs the deserialization step.

Therefore the conversion is redundant but harmless.

Sometimes, redundancy makes things more clearer.

Where did you apply your Arduino, Python, and HTML knowledge before you came in contact with UNO Q?

Your applications are good sources for me to learn the whole chain:
Sensor data-->MCU/Arduino-->Router Bridge-->MCU/Python-->Dashboard

Please, mention the names of the threads/tasks that run concurrently in your Python script. How do you define an independent thread/task.

In FreeRTOS/ZephyrRTOS, threads are created in setup() function and then are defined in gloabl area. How are the threads created/defined in Python?

Also mention, which line/code of your script initiates the concurrent execution of your threads.

In my Python script, I do not explicitly create threads using threading.Thread(...) . The callbacks are only registered with:

Bridge.provide("update_gps", update_gps)
web.expose_api("GET", "/api/state", api_state)

The application runtime is started by:

App.run()

To understand whether concurrent execution actually occurs, I instrumented the code to print the current thread inside both callbacks.

Test code used :

import threading

def update_gps(lat,long, jour, mois, annee, heure, minute,seconde, numsat, altitude):

    print("update_gps thread:", threading.get_ident(), threading.current_thread().name, flush=True)

    with _lock:
        _state["lat"] = lat
        _state["long"] = long
        _state["jour"] = jour
        _state["mois"] = mois
        _state["annee"] = annee
        _state["heure"] = heure
        _state["minute"] = minute
        _state["seconde"] = seconde
        _state["numsat"] = numsat
        _state["altitude"] = altitude


def api_state(_req=None):

    print("api_state thread:", threading.get_ident(), threading.current_thread().name, flush=True)

    with _lock:
        payload = {
            "now_utc": now_utc_iso(),
            "lat": _state["lat"],
            "long": _state["long"],
            "jour": _state["jour"],
            "mois": _state["mois"],
            "annee": _state["annee"],
            "heure": _state["heure"],
            "minute": _state["minute"],
            "seconde": _state["seconde"],
            "numsat": _state["numsat"],
            "altitude": _state["altitude"],
        }

    return payload

Observed output

Example runtime output:

update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
api_state thread: 281473107751296 AnyIO worker thread
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
update_gps thread: 281473172214144 Bridge.read_loop
api_state thread: 281473107751296 AnyIO worker thread

Interpretation

Runtime observation shows that:

  • update_gps() runs in the Bridge thread (Bridge.read_loop)
  • api_state() runs in AnyIO worker threads handling HTTP requests

Therefore the two functions can access the shared _state dictionary concurrently.

Justification of _lock:

The purpose of

_lock = threading.Lock()

is to protect the shared mutable dictionary _state.

Without a lock, the following situation could occur:

update_gps() starts updating the dictionary
api_state() reads the dictionary simultaneously

This could result in inconsistent reads (for example a new latitude with an old longitude).

Using:

with _lock:

ensures that only one thread at a time can access the critical section where _state is read or updated.

So even though threads are not explicitly created in my Python script, the runtime started by App.run() executes callbacks in different threads, and the use of _lock is therefore justified.

Runtime observation shows that update_gps() runs in the Bridge thread (Bridge.read_loop ), while api_state() runs in an AnyIO worker thread handling HTTP requests. Since both functions access the shared _state dictionary from different threads, the use of _lock is justified to avoid inconsistent reads during updates.

Best regards
Philippe

This is an academic discussion where I might be wrong. For most of my questions/queries, I collect my answers from ChatGPT and test them for validation whenever I can design proper test zig; else, I submit them in the forum to receive authentication from the author.

In your python script, I observe the following user defined functions.

def now_utc_iso():
def update_gps(lat,long, jour, mois, annee, heure, minute,seconde, numsat, altitude):
def api_state(_req=None):

There is a statement: Bridge.provide() statemet

There is another statement: web.expose_api("GET", "/api/state", api_state)

What does the Arduino API.run() framework do here in the context of executing the above tasks?

I think that :

In my script the following functions are not tasks:

def now_utc_iso()
def update_gps(...)
def api_state(...)

They are simply Python functions (callbacks).
They are executed when triggered by the framework:

  • update_gps() is called by the Bridge when the MCU sends data
  • api_state() is called by the HTTP server when a browser requests /api/state

Similarly, this line does not create a task:

web.expose_api("GET", "/api/state", api_state)

It only registers an HTTP endpoint and associates it with the api_state() function.
The concurrency appears when the application runtime is started:

App.run()

From runtime observation:

  • update_gps() runs in the Bridge thread (Bridge.read_loop)
  • api_state() runs in an AnyIO worker thread handling HTTP requests

So the functions themselves are not tasks; they are callbacks executed by different threads created internally by the framework once App.run() starts the runtime.

I subitted your post #12 to ChatGPT and supports your proposition with the following structure.

Very advanced activities are going on in your Python script, which are almost beyond the easy understanding of a novice like me. Still, I am trying to connect the ideas and follow the logic.

Thank you very much.

@philippe86220
This is a working application-style version of your program’s script. I created it with the assistance of ChatGPT to make it clearer that your program is multitasking of three threads. Please feel free to provide your comments.

import datetime
import threading
import time
from arduino.app_utils import App, Bridge
from arduino.app_bricks.web_ui import WebUI

print("Python ready", flush=True)

web = WebUI()

_lock = threading.Lock()

_state = {
    "lat": None,
    "long": None,
    "jour": None,
    "mois": None,
    "annee": None,
    "heure": None,
    "minute": None,
    "seconde": None,
    "numsat": None,
    "altitude": None,
}


def now_utc_iso():
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def update_gps(lat, long, jour, mois, annee, heure, minute, seconde, numsat, altitude):

    with _lock:
        _state["lat"] = float(lat)
        _state["long"] = float(long)
        _state["jour"] = int(jour)
        _state["mois"] = int(mois)
        _state["annee"] = int(annee)
        _state["heure"] = int(heure)
        _state["minute"] = int(minute)
        _state["seconde"] = int(seconde)
        _state["numsat"] = int(numsat)
        _state["altitude"] = float(altitude)


def api_state(_req=None):

    with _lock:
        payload = {
            "now_utc": now_utc_iso(),
            "lat": _state["lat"],
            "long": _state["long"],
            "jour": _state["jour"],
            "mois": _state["mois"],
            "annee": _state["annee"],
            "heure": _state["heure"],
            "minute": _state["minute"],
            "seconde": _state["seconde"],
            "numsat": _state["numsat"],
            "altitude": _state["altitude"],
        }

    return payload


# Optional monitoring thread (similar idea to LED blinking loop)
def monitor_state():

    while True:
        with _lock:
            print("GPS state:", _state, flush=True)

        time.sleep(5)


def main():

    # register Bridge function
    Bridge.provide("update_gps", update_gps)

    # expose REST API
    web.expose_api("GET", "/api/state", api_state)

    # create a monitoring thread
    t1 = threading.Thread(target=monitor_state)

    # start thread
    t1.start()

    # start framework (web server + bridge loop)
    App.run()

    # join thread
    t1.join()


if __name__ == "__main__":
    main()

Thank you for the illustrative version of the program. The additional monitoring thread makes the concurrent execution easier to visualize, although it is not required for the original application since the concurrency already comes from the Bridge thread and the HTTP worker threads created by App.run().
I would also note that the explicit type conversions (float() / int()) are not strictly necessary here. RouterBridge uses MsgPack serialization, which preserves numeric types during transmission between the MCU and Python. I verified this by printing the received types, which are already <class 'float'> and <class 'int'>.

Thank you for the feedback. I will continue studying your programs.

Kindly, post the screen shot of the actual data you have received from the GPS which I will hard code in the application and see them on the dashboard.

def main():

    # register Bridge function
    Bridge.provide("update_gps", update_gps)

    # expose REST API
    web.expose_api("GET", "/api/state", api_state)

    # create a monitoring thread
    t1 = threading.Thread(target=monitor_state)

    # start thread
    t1.start()

    # start framework (web server + bridge loop)
    App.run()

    # join thread
    t1.join()


if __name__ == "__main__":
    main()

I think that t1.join() does not have any practical effect in this program. Since App.run() starts the runtime loop (Bridge communication and the HTTP server) and blocks the main thread, the execution never reaches the join() call while the application is running.

Moreover, even if t1.join() were reached, the monitor_state() function runs in an infinite loop (while True:), so the thread would never terminate. As a result, the join() condition would never be satisfied and the main thread would remain blocked indefinitely.

The multitasking implementation mechanism in Python is conceptually quite different from that of FreeRTOS and Zephyr RTOS though the principles are the same. In those systems, it is visually clear that tasks are created in setup() and defined separately outside setup()/loop(). Therefore, as a beginner, I needed to reformat your script so that I could understand it in a way that is more familiar to me.

I have run the moderated script and have seen that the dashboard is active with my hard-coded parameters.

Yes, the script runs correctly and the dashboard works because the monitoring thread is started with t1.start().
My point was specifically about t1.join(). In this structure, App.run() blocks the main thread, so the execution does not normally reach the join() statement.
In addition, even if t1.join() were reached, monitor_state() contains an infinite loop (while True:), so thread t1 never terminates by itself. Therefore join() would wait indefinitely and would still have no practical effect here.
So the issue is not whether the modified script runs, but that t1.join() has no meaningful role in this particular implementation.