Getting analog signal to read over Modbus TCP Arduino Q

I am new to the Arduino Q But i think it has promise. I work for manufacturing plants and i have gotten good at putting Arduino's in machines for data collection. i have normally used Arduino Nano and Node MCU 8266 to get my stuff online so i can read it through a PLC with Modbus. my compony has recently changed security of the network and i cant seem to get the 8266 online anymore. so i decided to try out the Arduino Q. this goes on my network fine but i cant figure out the Modbus side.

all i want for right now is to be able to read 1 analog value with my Arduino Q and send that Modbus to a PLC. i haven't worked on the PLC side yet because i cant get it to work in any Modbus programs yet, i use qModMaster to see the data before i go further. I do have codes i can share but nothing seems to work yet. i am able to run examples that came in Arduino app lab and they work fine.

If anyone can help point me in the right direction i would appreciate that. i am not verry good at Python or the App Lab yet so i will have more questions.

Thanks in advance

I know what some of the people think about AI but, i had AI summarize what i did to get this running. i hope this helps

Title:
UNO Q App Lab + Modbus TCP + qModMaster working example for reading A0, A1, A2 over network

I wanted to share a working setup because I struggled quite a bit getting this running, and I did not find a complete example.

My goal was to use an Arduino UNO Q running App Lab as a Modbus TCP server so that qModMaster could read the first 3 analog inputs over the network.

What I wanted in the end was:

  • A0, A1, A2 read on the UNO Q

  • values converted to mV

  • exposed as Modbus registers

  • readable from qModMaster over Ethernet/Wi-Fi

For example:

  • 1.500 V on A0 should read about 1500

  • 3.300 V on A0 should read about 3300

A few important things I learned:

  1. On UNO Q App Lab, I had to use Arduino_RouterBridge.h in the sketch, not Bridge.h. Arduino’s UNO Q App Lab examples use RouterBridge for the sketch/Python communication.

  2. My Python app would immediately crash until I added python/requirements.txt with pymodbus==3.8.1. App Lab uses python/main.py as the entry point and python/requirements.txt for Python dependencies.

  3. I also had to expose the Modbus TCP port in app.yaml using ports: [1502]. In my case app.yaml was view-only in the App Lab editor, so I had to SSH into the board and edit the file there. Arduino documents that App Lab apps live on the board and that SSH access is supported.

  4. qModMaster could connect after that, but Function 03/04 initially gave illegal data address. The fix was changing the PyModbus datastore blocks to start at address 1 instead of 0, then mapping the first 3 channels there. That resolved the addressing issue in my setup.

Below is the working setup I ended with.


App structure

app.yaml
python/
  main.py
  requirements.txt
sketch/
  sketch.ino
  sketch.yaml


python/requirements.txt

pymodbus==3.8.1


python/main.py

import traceback
from threading import Thread, Lock

from arduino.app_utils import App, Bridge

print("main.py starting")

try:
    import pymodbus
    print("pymodbus version:", getattr(pymodbus, "__version__", "unknown"))

    from pymodbus.server import StartTcpServer
    from pymodbus.datastore import (
        ModbusSequentialDataBlock,
        ModbusSlaveContext,
        ModbusServerContext,
    )
except Exception:
    print("PyModbus import failed:")
    print(traceback.format_exc())
    raise

MODBUS_PORT = 1502

# IMPORTANT:
# In this working setup, using address 1 fixed qModMaster "illegal data address"
# for FC03/FC04 reads.
hr_block = ModbusSequentialDataBlock(1, [0, 0, 0])
ir_block = ModbusSequentialDataBlock(1, [0, 0, 0])

store = ModbusSlaveContext(
    hr=hr_block,
    ir=ir_block,
)
context = ModbusServerContext(slaves=store, single=True)

lock = Lock()


def raw_to_mv(raw: int) -> int:
    raw = max(0, min(16383, int(raw)))
    return int(round(raw * 3300 / 16383))


def update_channel(index: int, raw_value: int):
    mv = raw_to_mv(raw_value)

    # index 0 -> register 1
    # index 1 -> register 2
    # index 2 -> register 3
    reg_addr = index + 1

    with lock:
        hr_block.setValues(reg_addr, [mv])
        ir_block.setValues(reg_addr, [mv])

    print(f"A{index}: raw={raw_value} -> {mv} mV (reg {reg_addr})")


def ai0_value_callback(raw_value: int):
    update_channel(0, raw_value)


def ai1_value_callback(raw_value: int):
    update_channel(1, raw_value)


def ai2_value_callback(raw_value: int):
    update_channel(2, raw_value)


Bridge.provide("ai0_value", ai0_value_callback)
Bridge.provide("ai1_value", ai1_value_callback)
Bridge.provide("ai2_value", ai2_value_callback)


def modbus_server_thread():
    try:
        print(f"Starting Modbus TCP server on 0.0.0.0:{MODBUS_PORT}")
        print("FC03/FC04 registers 0..2 map to A0..A2 in mV")
        StartTcpServer(
            context=context,
            address=("0.0.0.0", MODBUS_PORT),
        )
    except Exception:
        print("Modbus server thread crashed:")
        print(traceback.format_exc())
        raise


Thread(target=modbus_server_thread, daemon=True).start()

print("Calling App.run()")
App.run()


sketch/sketch.ino

#include <Arduino_RouterBridge.h>

void setup() {
  Bridge.begin();
  analogReadResolution(14);
}

void loop() {
  int a0 = analogRead(A0);
  int a1 = analogRead(A1);
  int a2 = analogRead(A2);

  Bridge.call("ai0_value", a0);
  Bridge.call("ai1_value", a1);
  Bridge.call("ai2_value", a2);

  delay(100);
}


app.yaml

In my case, App Lab showed this file as view-only in the UI, so I had to edit it on the board over SSH.

The important part was:

ports: [1502]

My file ended up looking like this:

name: Corvac Chiller Pressure
icon: 😀
ports: [1502]
bricks: []


qModMaster settings

These are the settings that worked for me:

  • Host: IP address of the UNO Q

  • Port: 1502

  • Unit ID: 1

  • Function: 03 Read Holding Registers

  • Start Address: 0

  • Quantity: 3

Function 04 Read Input Registers should also work since I mirrored the values into input registers too.


Register mapping

This is how the final mapping worked:

  • Register 0 = A0 in mV

  • Register 1 = A1 in mV

  • Register 2 = A2 in mV

So if A0 is 1.513 V, qModMaster shows about:

1513


Problems I hit and fixes

Problem 1

#include <Bridge.h> failed with:

fatal error: Bridge.h: No such file or directory

Fix

Use:

#include <Arduino_RouterBridge.h>


Problem 2

Python app started for about 1 second and stopped.

Fix

Create:

python/requirements.txt

with:

pymodbus==3.8.1


Problem 3

qModMaster would not connect.

Fix

Expose the port in app.yaml:

ports: [1502]


Problem 4

qModMaster connected, but FC03/FC04 returned:

illegal data address

Fix

Change the PyModbus data blocks to start at address 1:

hr_block = ModbusSequentialDataBlock(1, [0, 0, 0])
ir_block = ModbusSequentialDataBlock(1, [0, 0, 0])

That was the piece that finally made the registers readable.


Final result

This now lets me feed 0–3.3 VDC into A0, A1, and A2 on the UNO Q and read those 3 analog inputs over the network from qModMaster as Modbus registers in mV.

Hopefully this saves someone else some time.

1.

Please, post your project in the GitHub as a repository so that I can import it in my App Lab project. After that I will study it and will come with questions and queries.

2.

Say, a0 = 0x2805 (10245)

Is 2805 is being transmitted as 0x32, 0x38, 0x30, 0x35 (ASCII coded four UART frames) to the MPU over the Router Bridge?

or

as 0x28, 0x05 (Binary coded two UART frames)?

3.
Why have you preferred to send the raw value of a0 instead of the corresponding mV or V -- 2063 mV or 2.063 V?

i have never posted anything to GitHub but i have used it. i will try to figure this out and get something posted.as for questions 2&3

Bridge.call("ai0_value", a0) sends the numeric value, not the ASCII characters "2805".
So it is binary/typed data, not 0x32 0x38 0x30 0x35.

It is also not just plain 0x28 0x05 by itself, because the bridge wraps the value inside its own RPC/message format. But the key point is: it is sent as a binary integer, not as text.

I used the raw ADC value over the bridge because that keeps the MCU-to-MPU transfer simple and preserves the original reading. Then Python converts it to mV for Modbus, which is more useful for the client. Sending mV directly would also work; raw was just the cleaner design choice.

If I want to send the floating-point value 28.75, then according to your explanation, it would be transmitted as a 4-byte (32-bit IEEE 754 binary32) representation.

My question is: how does the receiver (MPU) interpret this data? Specifically, how does it know that the incoming 4 bytes represent a binary32 floating-point value?

In contrast, if 28.75 is sent in ASCII format, the MPU can easily interpret it as a floating-point number by detecting the decimal point. However, in the binary32 format, there is no such explicit indicator; so, how is the data type identified at the receiving end?