MQTT Application Overview

The WIZ-RTU MQTT application initializes the board hardware, configures the WIZnet Ethernet interface, connects to the MQTT broker, subscribes to registered command topics, publishes the initial device state, and then starts its polling loop.

Board boots
    |
    v
Initialize hardware and Ethernet
    |
    v
Load mqtt.json
    |
    v
Connect to MQTT broker
    |
    v
Subscribe to command topics
    |
    v
Publish initial states
    |
    v
Poll hardware and process MQTT messages

The MQTT loop performs:

self.poll()
self.check_msg()
time.sleep_ms(100)

This means the device checks peripherals and incoming MQTT messages approximately every 100 ms, excluding processing and network time.


Network and Broker Configuration

The Ethernet interface uses network.WIZNET5K with the following default pins:

Signal

Pin

SPI bus

SPI(2)

SPI baud rate

8,000,000

CS

PB12

RST

PD9

PWR / enable

PE15

The device first attempts to use DHCP. If DHCP fails, it falls back to:

Field

Default value

IP address

10.0.1.109

Subnet mask

255.255.255.0

Gateway

10.0.1.254

DNS

8.8.8.8

MQTT broker settings are loaded from mqtt.json:

{
  "host": "10.0.1.238",
  "username": null,
  "password": null,
  "port": 1883
}

To create mqtt.json, simply rename the file mqtt.default.json to mqtt.json and enter your MQTT broker’s corresponding configurations.

Note

Here you should input your broker’s IP address and other information. If you use your personal PC as the broker, run ipconfig to get your PC IP under the local network and enter the IP to mqtt.json.

If a value is missing, the following defaults are used:

Field

Fallback value

Host

127.0.0.1

Username

None

Password

None

Port

1883

The MQTT client ID is generated from the board MAC address.

If umqtt.simple is unavailable, the application attempts to install it into /flash using mip.

Note

In newer firmware builds, umqtt should already be frozen into the firmware, so an online installation should normally not be required.


MQTT Topic Summary

The application uses command topics for requests received by the device and state or response topics for data published by the device.

Subscribed command topics

Topic

Payload

Description

network_config/set

JSON

Changes the Ethernet configuration.

digital_output/+/set

JSON

Changes one digital output.

analog_output/+/set

JSON

Changes one analog output voltage.

uart/write

UTF-8 data

Sends data through UART3.

eeprom/write

UTF-8 data

Writes data to EEPROM.

eeprom/read/request

JSON

Requests EEPROM data from an index.

eeprom/wipe

Text

Wipes EEPROM when the payload is exactly True.

CAN/send

JSON

Requests transmission of one CAN frame.

modem/command

Text

Sends transparent data, +++, or an AT command to the modem.

Published state and response topics

Topic

Payload

Description

network_config/state

JSON

Current Ethernet configuration.

digital_input/INx/state

JSON

Current digital input pin value.

digital_output/RYx/state

JSON

Resulting digital output value.

analog_input/state

JSON

Averaged analog input values and modes.

analog_output/AOx/state

JSON

Requested analog output voltage.

serial_mode

JSON

Reports whether the shared interface uses CAN or RS-485.

CAN/recv

JSON

Publishes one received CAN frame.

uart/read

UART data

Data received from UART3.

eeprom/read/response

Raw bytes

EEPROM data returned after a read request.

modem/recv

JSON

Publishes Base64-encoded modem data and its work mode.

Topics are registered with the add_topic() decorator:

@client.add_topic("uart/write")
def write_to_uart(msg):
    pass

For normal topics, the handler receives the payload:

def handler(msg):
    pass

For the current digital-output and analog-output wildcard topics, the handler receives the captured pin and the payload:

def handler(pin, msg):
    pass

For example, a message sent to:

digital_output/RY1/set

is matched against:

digital_output/+/set

and calls:

handler(b"RY1", msg)

Note

The current wildcard dispatcher only handles topic families beginning with digital_output or analog_output.


Initial State and Retained Messages

After connecting, the device publishes:

  • Current network configuration.

  • All eight digital input states.

  • All eight digital output states.

  • All four analog input values and modes.

  • Initial analog output states.

This allows an MQTT dashboard to display the current device state immediately after subscribing.

The general publish() helper uses:

retain=True

Therefore, state messages published through this helper are retained by the broker.

This includes:

network_config/state
digital_input/IN1/state
digital_output/RY1/state
analog_input/state
analog_output/AO0/state

The following topics are published directly and are not explicitly retained by the application:

uart/read
eeprom/read/response

Command topics should normally not be retained because a retained command could be delivered again when the device reconnects.


Network Configuration Topics

Read the current network state

The device publishes its network configuration to:

network_config/state

Example payload:

{
  "ip": "10.0.1.109",
  "mask": "255.255.255.0",
  "gateway": "10.0.1.254",
  "dns": "8.8.8.8",
  "dhcp": true
}

Change the network configuration

Publish to:

network_config/set

Use DHCP:

{
  "dhcp": true
}

Use a static IP address:

{
  "dhcp": false,
  "ip": "10.0.1.109",
  "subnet_mask": "255.255.255.0",
  "default_gateway": "10.0.1.254",
  "dns": "8.8.8.8"
}

After receiving the request, the application disconnects MQTT, reinitializes Ethernet, sets the restart flag, and starts the MQTT runtime again with the new configuration.

Warning

The current MQTT handler does not call validate_ipv4() before applying a static configuration. Ensure all submitted IPv4 values are valid.


Digital I/O Topics

The program supports:

Type

Names

Digital inputs

IN1 to IN8

Digital outputs

RY1 to RY8

Set a digital output

Topic pattern:

digital_output/<pin>/set

Example:

digital_output/RY1/set

Payload:

{
  "value": 1
}

If value is missing, the handler uses 0.

The resulting pin state is published to:

digital_output/RY1/state

Example response:

{
  "value": 1
}

Monitor digital inputs

Digital inputs are published to:

digital_input/<pin>/state

Example:

digital_input/IN1/state

Payload:

{
  "value": 1
}

The device publishes a new message only when the physical input value changes.

Note

The payload contains the physical pin value. The MQTT layer does not invert active-low inputs.


Analog I/O Topics

The program supports:

Type

Names

Analog inputs

AI0 to AI3

Analog outputs

AO0 to AO1

Default analog input modes:

Input

Mode

Unit

AI0

Current

mA

AI1

Current

mA

AI2

Voltage

V

AI3

Voltage

V

Mode values:

Value

Meaning

0

Voltage mode

1

Current mode

Monitor analog inputs

All channels are published together on:

analog_input/state

Example payload:

{
  "AI0": {
    "value": 12.3,
    "mode": 1
  },
  "AI1": {
    "value": 11.8,
    "mode": 1
  },
  "AI2": {
    "value": 2.5,
    "mode": 0
  },
  "AI3": {
    "value": 3.1,
    "mode": 0
  }
}

Each channel collects ten readings before publishing an average. With the default loop delay, analog data is normally published about once per second.

Reported values are limited to:

Mode

Maximum reported value

Voltage mode

10 V

Current mode

20 mA

The conversion formulas are:

# Current mode
current_ma = (voltage / 120) * 1000

# Voltage mode
input_voltage = (voltage * 14.7) / 4.7

Set an analog output

Topic pattern:

analog_output/<pin>/set

Example:

analog_output/AO0/set

Payload:

{
  "voltage": 5.0
}

If voltage is missing, the handler uses 0.

The requested value is published to:

analog_output/AO0/state

The DAC uses 12-bit values from 0 to 4095.

Calibration values:

Output

Minimum measured voltage

Maximum measured voltage

AO0

0.024 V

9.89 V

AO1

0.079 V

9.88 V

Requested voltages outside the supported range are clamped to the DAC limits.


CAN Topics

The WIZ-RTU can use either CAN or UART/RS-485 on the shared PB8 and PB9 interface. Select the interface in config.py:

CAN_USED = True
CAN_CONFIGS = {
    "baudrate": 500000,
    "mode": 0,
}

Setting

Description

CAN_USED = True

Initializes CAN instead of UART3.

CAN_USED = False

Initializes UART3 for serial communication.

baudrate

CAN bus bit rate. The fallback value is 500000.

mode

CAN operating mode. The current configuration uses 0 for normal mode.

When CAN is enabled, the application initializes:

Field

Value

CAN controller

CAN(1)

RX pin

PB8

TX pin

PB9

Fallback baud rate

500000 bit/s

Fallback mode

CAN.NORMAL

The selected shared-interface mode is published during MQTT startup to:

serial_mode

Example payload when CAN is enabled:

{
  "Mode": "CAN"
}

When CAN_USED is False, the value is RS485.

Warning

CAN and UART3/RS-485 are mutually exclusive in the current application because both use the shared PB8 and PB9 interface. Enable only the interface required by your project.

Send a CAN frame

Publish to:

CAN/send

Payload:

{
  "id": "0x123",
  "msg": "0x1122334455667788"
}

Field

Format

Description

id

Hexadecimal string

CAN identifier, such as 0x123.

msg

Hexadecimal string

CAN data beginning with 0x. A maximum of eight data bytes is accepted.

The handler converts id with:

can_id = int(can_id, 16)

It converts the data after removing the leading 0x:

can_data = bytes.fromhex(can_data[2:])

CAN identifiers are validated in the range:

0x0 to 0x1FFFFFFF

CAN data must be a bytes or bytearray object containing no more than eight bytes after conversion.

Warning

The current handler expects both id and msg to be correctly formatted hexadecimal strings. Invalid hexadecimal text, a missing 0x prefix, or an odd number of hexadecimal digits may cause conversion to fail.

The MQTT payload does not contain a separate transmit frame_type field. The current handler passes only the converted identifier and data to CAN.send().

Receive CAN frames

Received CAN frames are published to:

CAN/recv

Example payload:

{
  "id": "0x123",
  "data": "1122334455667788",
  "frame_type": "standard"
}

Field

Description

id

CAN identifier formatted as a hexadecimal string.

data

Received CAN bytes formatted as hexadecimal text without a 0x prefix.

frame_type

Either standard or extended.

The device checks the CAN controller during every polling cycle. It reads up to three queued frames in one cycle and publishes each frame separately.

CAN/recv messages are published with:

retain=False

This prevents an old CAN frame from being delivered as the latest retained state when a subscriber reconnects.


Serial UART Topics

UART communication uses UART(3).

Fallback settings:

Field

Fallback value

Baud rate

9600

Data bits

8

Parity

None

Stop bits

1

Send UART data

Publish UTF-8 data to:

uart/write

Example:

hello from MQTT

Receive UART data

UART data is published to:

uart/read

The device checks UART during every polling cycle, reads all available bytes, and decodes them as UTF-8 with replacement characters.


EEPROM Topics

The EEPROM uses software I2C:

Signal

Pin

SCL

PB6

SDA

PB7

The EEPROM is considered connected only when the I2C scan contains every address from 0x50 to 0x57.

Limits:

Operation

Maximum size

Write

1000 bytes

Read

1000 bytes

Write data

Publish UTF-8 data to:

eeprom/write

Example:

hello from WIZ-RTU

The default write index is 0.

Read data

First, request a read from eeprom by publishing to:

eeprom/read/request

Payload:

{
  "index": 0
}

If index is missing, the handler uses 0.

THen, the eeprom read result will be published as raw bytes to:

eeprom/read/response

Wipe data

Publish to:

eeprom/wipe

Required payload:

True

The comparison is case-sensitive.

Warning

Wiping EEPROM removes its stored contents and normally cannot be undone.


4G Modem Topics

The 4G modem is enabled when MODEM_CONFIGS exists and is not empty in config.py:

MODEM_CONFIGS = {
    "baudrate": 115200,
    "bits": 8,
    "parity": None,
    "stop": 1
}

The modem interface uses:

Field

Value

UART

UART(4)

Default baud rate

115200

Data bits

8

Parity

None

Stop bits

1

RX buffer

8192 bytes

UART timeout

500 ms

Modem power pin

PB2

To disable the modem manager, remove MODEM_CONFIGS or set it to an empty dictionary:

MODEM_CONFIGS = {}

During initialization, the application powers the modem through PB2, clears old UART data, enters command mode with +++, sends AT+Z, and attempts to recover the modem work mode from its startup responses.

Modem modes

The modem manager tracks an internal communication mode and a transparent work mode.

Internal communication modes:

Value

Meaning

0

Unknown

1

Booting

2

Transparent mode

3

Command mode

Transparent work modes reported through MQTT:

work_mode

Meaning

0

Off or not identified

1

Network mode, including TCP or UDP

2

HTTP mode

3

MQTT mode

The work mode is updated when the modem returns recognized status text such as NET, HTTP, MQTT, or the corresponding connection-status message.

Send data or commands to the modem

Publish to:

modem/command

The MQTT payload is forwarded as bytes to the modem UART.

In transparent mode, normal payloads are transmitted as modem data. Publishing exactly:

+++

enters command mode and resets the reported work mode to 0.

While in command mode, restart commands such as the following return the manager to transparent mode after they are sent:

AT+S
AT+CLEAR
AT+Z

The modem writer replaces every line-feed byte (\n) with carriage-return plus line-feed (\r\n) before transmission.

Note

For an AT command that requires a line ending, submit the command with a single \n. Do not pre-format it with \r\n, because the current replacement logic would add another carriage return.

Receive modem data

Data received from the modem is published to:

modem/recv

Example payload:

{
  "msg": "SGVsbG8=",
  "work_mode": 1
}

Field

Description

msg

Received modem bytes encoded as Base64 text.

work_mode

Current modem transparent work-mode value.

The application reads modem UART data in chunks of up to 512 bytes while data remains available. Each chunk is Base64-encoded before publication, which preserves binary data and prevents MQTT text encoding from corrupting the received bytes.

To decode the example Base64 value on a PC:

import base64

raw = base64.b64decode("SGVsbG8=")
print(raw)

modem/recv messages are published with:

retain=False

This prevents previously received modem data from being replayed as retained state to a new MQTT subscriber.

Note

The current polling code adds work_mode to the outgoing payload before processing the same received chunk for a new mode-status message. Therefore, a status banner may update the work mode for the following published chunk rather than the chunk containing the banner itself.

Runtime, Restart, and Error Behavior

When the network configuration changes, the application:

  1. Disconnects from MQTT.

  2. Reinitializes Ethernet.

  3. Sets client.restart = True.

  4. Exits the current MQTT loop.

  5. Starts the MQTT runtime again.

Common error behavior:

Situation

Behavior

MQTT connection fails

run() returns -1.

Unknown topic is received

The message is ignored.

Invalid JSON command payload

The handler returns without applying the command.

Invalid output name

The device prints an error and returns -1.

EEPROM is unavailable

EEPROM operations fail their assertion check.

Runtime exception

The error is printed, MQTT disconnects, and the application exits.

KeyboardInterrupt

MQTT disconnects and the application exits without printing a normal runtime error.

Debug output is controlled by:

DEBUG = False

Set it to True to enable dprint() messages.


Customizing the MQTT Application

Edit mqtt_app.py to add, remove, or change subscribed command topics and their handlers.

Example:

@client.add_topic("custom/topic")
def handle_custom_topic(msg):
    print("Received:", msg)

Use device_mqtt.py only when changing lower-level behavior such as:

  • MQTT connection handling.

  • Subscription and dispatch logic.

  • Polling intervals.

  • Automatic state publication.

  • Retained-message behavior.

Use device.py only when changing the underlying GPIO, ADC, DAC, UART, I2C, or EEPROM behavior.


Example Usage With Mosquitto

Replace 10.0.1.238 with your broker address.

Monitor all topics:

mosquitto_sub -h 10.0.1.238 -t "#" -v

Digital IO

Turn on RY1:

mosquitto_pub -h 10.0.1.238 \
  -t "digital_output/RY1/set" \
  -m '{"value":1}'

Listen on IN1

mosquitto_sub -h 10.0.1.238 \
  -t "digital_output/IN1/state"  -v \

Analog IO

Set AO0 to 5.0 V:

mosquitto_pub -h 10.0.1.238 \
  -t "analog_output/AO0/set" \
  -m '{"voltage":5.0}'

Monitor analog inputs:

mosquitto_sub -h 10.0.1.238 \
  -t "analog_input/state" -v

CAN

Send a CAN frame:

mosquitto_pub -h 10.0.1.238 \
  -t "CAN/send" \
  -m '{"id":"0x123","msg":"0x1122334455667788"}'

Monitor received CAN frames:

mosquitto_sub -h 10.0.1.238 \
  -t "CAN/recv" -v

Monitor the selected CAN or RS-485 mode:

mosquitto_sub -h 10.0.1.238 \
  -t "serial_mode" -v

Serial

Send UART data:

mosquitto_pub -h 10.0.1.238 \
  -t "uart/write" \
  -m "hello"

EEPROM

Write EEPROM data:

mosquitto_pub -h 10.0.1.238 \
  -t "eeprom/write" \
  -m "hello from WIZ-RTU"

Request EEPROM data:

mosquitto_pub -h 10.0.1.238 \
  -t "eeprom/read/request" \
  -m '{"index":0}'

Request DHCP:

mosquitto_pub -h 10.0.1.238 \
  -t "network_config/set" \
  -m '{"dhcp":true}'

Modem/4G module

Monitor modem data:

mosquitto_sub -h 10.0.1.238 \
  -t "modem/recv" -v

Enter modem command mode:

mosquitto_pub -h 10.0.1.238 \
  -t "modem/command" \
  -m "+++"

Send transparent data:

mosquitto_pub -h 10.0.1.238 \
  -t "modem/command" \
  -m "hello from MQTT"

Send modem command (Linux/Mac):

mosquitto_pub -h 10.0.1.238 \
  -t "modem/command" \
  -m $'AT\n'

Send modem command (PowerShell):

mosquitto_pub -h 10.0.1.238 \
  -t "modem/command" \
  -m "AT`n"