Network Initialization

The Ethernet interface uses the WIZnet W5500 driver through network.WIZNET5K.

Default SPI and control pins:

Signal

Pin

SPI bus

SPI(2)

CS

PB12

RST

PD9

PWR / enable

PE15

By default, the server tries to use DHCP. If DHCP fails, it falls back to the static IP configuration defined in setup.py.

Default static configuration:

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

The network mode can also be changed from the HTTP network configuration page.


HTTP Server Behavior

The server is a lightweight MicroPython socket server. It listens on port 80, accepts one client connection at a time, handles the request, sends the response, and then closes the client socket.

Supported behavior:

  • GET and POST route registration through server.add_route().

  • Static file serving for JavaScript, CSS, and images.

  • JSON request body handling for configuration and control APIs.

  • HTTP responses with Content-Type, Connection: Close, and optional Content-Length headers.

  • Deferred network reconfiguration through a pending-task queue, so the server can respond to the current request before restarting the socket.

Static paths:

URL prefix

Content type

/js/

text/javascript

/css/

text/css

/img/

image/<extension>


Page Routes

These routes return HTML pages from the template/ directory.

Method

Path

Description

GET

/

Main index page.

GET

/netconfig

Network configuration page.

GET

/io

Digital input/output control page.

GET

/analog_io

Analog input/output page.

GET

/serial

Serial UART page.

GET

/storage

EEPROM storage page.


Network Configuration API

Read current network configuration

GET /netconfig/config

Returns the active Ethernet configuration.

Example response:

{
  "ip_addr": "10.0.1.109",
  "subnet_mask": "255.255.255.0",
  "default_gateway": "10.0.1.254",
  "dns": "8.8.8.8",
  "dhcp": true
}

Change network configuration

POST /netconfig/config
Content-Type: application/json

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"
}

When static mode is selected, the program validates each IPv4 field before applying the new configuration. A valid IPv4 address must contain four number segments separated by dots, and each segment must be between 0 and 255.

Example success response:

{
  "redirect": true
}

Note

The redirect behavior depends on the selected network mode.
Static IP mode: The webpage automatically redirects to the IP address you entered.
DHCP mode: The IP address is assigned by the network, so you need to open the URL shown in the REPL console manually.

Example validation error response:

{
  "error": true,
  "error_message": "Ensure address are all in ipv4. (x.x.x.x)"
}

After a successful change, the server schedules the network reinitialization as a pending task. This allows the HTTP response to be sent before the old socket is closed and recreated.


Digital I/O API

The program supports eight digital inputs and eight digital outputs.

Type

Names

Digital inputs

IN1 to IN8

Digital outputs

RY1 to RY8

Open digital I/O page

GET /io

Returns the digital I/O control page.

Set digital output

POST /io
Content-Type: application/json

Example request:

{
  "pin": "RY1",
  "value": 1
}

Example response:

{
  "value": 1
}

Read all digital I/O states

GET /io/state

Example response:

{
  "output": [
    ["RY1", 1],
    ["RY2", 0]
  ],
  "input": [
    ["IN1", 0],
    ["IN2", 1]
  ]
}

The actual response contains all eight outputs and all eight inputs.

Read digital inputs only

GET /din

Example response:

{
  "IN1": 0,
  "IN2": 1,
  "IN3": 0,
  "IN4": 0,
  "IN5": 0,
  "IN6": 0,
  "IN7": 0,
  "IN8": 0
}

Analog I/O API

The program supports four analog inputs and two analog outputs.

Type

Names

Analog inputs

AI0 to AI3

Analog outputs

AO0 to AO1

Analog input mode mapping:

Input

Mode

Returned unit

AI0

Current mode

mA

AI1

Current mode

mA

AI2

Voltage mode

V

AI3

Voltage mode

V

Open analog I/O page

GET /analog_io

Returns the analog I/O page.

Read all analog input states

GET /analog_io/state

Example response:

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

Mode values:

Value

Meaning

0

Voltage mode

1

Current mode

Set analog output voltage

POST /analog_out
Content-Type: application/json

Example request:

{
  "index": 0,
  "voltage": 5.0
}

The program converts the requested voltage into a 12-bit DAC value. The DAC output is calibrated with the measured minimum and maximum voltage values stored in DAC_CONFIG.

Calibration values:

Output

Minimum measured voltage

Maximum measured voltage

AO0

0.024 V

9.89 V

AO1

0.079 V

9.88 V

Values outside the supported range are clamped to the DAC range.


Serial UART API

The program exposes UART communication through the HTTP interface. The UART is initialized as UART(3), and its default settings come from UART_CONFIGS in config.py.

Default UART setting fallback values:

Field

Fallback value

Baudrate

9600

Data bits

8

Parity

None

Stop bits

1

Open serial page

GET /serial

Returns the serial communication page.

Note

You can test the UART section with our WIZnet Serial Debugging Tool.

Send UART message

POST /serial
Content-Type: application/json

Example request:

{
  "message": "hello"
}

The message is encoded as UTF-8 and written to UART3.

Receive UART message

GET /serial/recv

Example response when data is available:

{
  "message": "OK"
}

Example response when no data is available:

{
  "message": null
}

The receive function reads all currently available UART bytes and decodes them as UTF-8. Invalid bytes are replaced during decoding.


EEPROM Storage API

The board checks for an EEPROM device on the I2C bus during startup. The I2C bus is created with software I2C on PB6 and PB7.

Signal

Pin

SCL

PB6

SDA

PB7

The EEPROM is considered connected when the scan result contains addresses from 0x50 to 0x57.

EEPROM limits:

Operation

Maximum size

Write

1000 bytes

Read

1000 bytes

Open storage page

GET /storage

Returns the EEPROM storage page.

Write data to EEPROM

POST /storage/write
Content-Type: text/plain

Example body:

hello from WIZ-RTU

The program writes the received body to EEPROM starting at address index 0. String data is encoded to UTF-8 before writing.

Example response:

Success.

Read data from EEPROM

GET /storage/read

Reads up to 1000 bytes from EEPROM starting at address index 0.

Example response:

hello from WIZ-RTU

CAN API

The CAN bus is enabled when CAN_USED = True in config.py.

Default CAN configuration:

Field

Value

CAN interface

CAN(1)

Baudrate

500000

Mode

0 (normal mode)

RX pin

PB8

TX pin

PB9

Note

CAN and serial UART use the same PB8 and PB9 pins. Set CAN_USED = True to enable CAN, or CAN_USED = False to use the serial UART interface instead.

Open CAN page

GET /can

Returns the CAN communication page.

Send CAN frame

POST /can/send
Content-Type: application/json

Example request:

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

Field

Description

id

CAN identifier as a hexadecimal string.

data

CAN payload as a hexadecimal string. Maximum 8 bytes.

Supported CAN identifier ranges:

Frame type

Range

Standard

0x000 to 0x7FF

Extended

0x00000000 to 0x1FFFFFFF

Example response:

{
  "success": true
}

A failed send returns:

{
  "success": false
}

Receive CAN frames with WebSocket

CAN receive data is sent through the WebSocket server at port 8080.

ws://<device-ip>:8080

CAN messages use type: 1.

{
  "type": 1,
  "frames": [
    {
      "id": "0x123",
      "data": "1122334455667788",
      "frame_type": "standard"
    }
  ]
}

Field

Description

type

1 identifies a CAN message.

frames

List of received CAN frames.

id

CAN identifier in hexadecimal format.

data

CAN payload as hexadecimal bytes without the 0x prefix.

frame_type

standard or extended.

The server reads and broadcasts up to three received CAN frames at a time to all connected WebSocket clients.

Example JavaScript:

const socket = new WebSocket("ws://10.0.1.109:8080");

socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);

  if (message.type !== 1) {
    return;
  }

  for (const frame of message.frames) {
    console.log(frame.id, frame.data, frame.frame_type);
  }
});

Modem API

The 4G modem is enabled when MODEM_CONFIGS is defined in config.py.

Default modem configuration:

Field

Value

UART interface

UART(4)

Baudrate

115200

Data bits

8

Parity

None

Stop bits

1

Power-control pin

PB2

The modem starts in transparent mode after it is powered on.

Open modem page

GET /4G

Returns the modem communication page.

Send modem data or AT command

POST /4G
Content-Type: text/plain

The request body is written directly to the modem UART.

Example AT command:

AT

Example response:

{
  "success": true
}

To enter AT command mode, send:

+++

To restart the modem and return to transparent mode, send:

AT+Z

Note

Newline characters in a modem command are converted to \r\n before being sent to the modem.

Receive modem data with WebSocket

Modem receive data is sent through the WebSocket server at port 8080.

ws://<device-ip>:8080

Modem messages use type: 2.

{
  "type": 2,
  "msg": "QVQNCk9LDQo=",
  "work_mode": 0
}

Field

Description

type

2 identifies a modem message.

msg

Modem data encoded as a Base64 string.

work_mode

Current modem work-mode value.

The msg field must be Base64-decoded before it is displayed or processed.

Example JavaScript:

const socket = new WebSocket("ws://10.0.1.109:8080");
const decoder = new TextDecoder("utf-8");

socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);

  if (message.type !== 2 || !message.msg) {
    return;
  }

  const binary = atob(message.msg);
  const bytes = Uint8Array.from(
    binary,
    (character) => character.charCodeAt(0)
  );

  console.log(decoder.decode(bytes));
});

CAN and Modem WebSocket Protocol

The application starts a separate WebSocket server on port 8080 when the HTTP server starts.

ws://<device-ip>:8080

For example, when the device IP address is 10.0.1.109:

ws://10.0.1.109:8080

The WebSocket is a receive-only event stream for the web application. CAN transmission and modem transmission use HTTP POST requests; received CAN frames and modem bytes are broadcast from the device to connected WebSocket clients.

Direction

CAN

Modem

Browser to device

POST /can/send

POST /4G

Device to browser

WebSocket message type: 1

WebSocket message type: 2

Every WebSocket message is JSON and contains a numeric type field.

WebSocket type

Source

Required payload

1

CAN bus

frames

2

4G modem

msg, work_mode

CAN WebSocket message: type: 1

{
  "type": 1,
  "frames": [
    {
      "id": "0x123",
      "data": "1122334455667788",
      "frame_type": "standard"
    }
  ]
}

Field

Type

Description

type

Number

Always 1 for CAN data.

frames

Array

One or more received CAN frames.

frames[].id

String

CAN identifier with a 0x prefix.

frames[].data

String

Payload as hexadecimal byte pairs, without a 0x prefix.

frames[].frame_type

String

standard or extended.

The server reads at most three CAN frames per processing cycle. A message is sent only when at least one CAN frame is available.

Modem WebSocket message: type: 2

{
  "type": 2,
  "msg": "QVQNCk9LDQo=",
  "work_mode": 0
}

Field

Type

Description

type

Number

Always 2 for modem data.

msg

String

Base64-encoded raw bytes received from the modem UART.

work_mode

Number

The current modem transparent work-mode value reported by the modem manager.

The server reads modem UART data in chunks of up to 2048 bytes. A message is sent for each non-empty chunk. Decode msg from Base64 to recover the received modem bytes.

Client handling example

const socket = new WebSocket("ws://10.0.1.109:8080");
const decoder = new TextDecoder("utf-8");

socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);

  if (message.type === 1) {
    for (const frame of message.frames ?? []) {
      console.log("CAN", frame.id, frame.data, frame.frame_type);
    }
    return;
  }

  if (message.type === 2 && message.msg) {
    const binary = atob(message.msg);
    const bytes = Uint8Array.from(
      binary,
      (character) => character.charCodeAt(0)
    );

    console.log("Modem", decoder.decode(bytes, { stream: true }));
  }
});

Note

The WebSocket server supports up to two connected clients. The server broadcasts each CAN or modem message to all connected clients.


Error Handling

Common error behavior:

Situation

Response

Unknown route

404 Not Found

Invalid JSON body

400 Bad Request

Header larger than maximum size

Plain text response: Header too large

Invalid IPv4 field

JSON response with error: true and error_message

Invalid digital output pin

JSON response with error: true

The server closes the client connection after each response.


Example Usage With curl

Read network configuration:

curl http://10.0.1.109/netconfig/config

Set static IP configuration:

curl -X POST http://10.0.1.109/netconfig/config \
  -H "Content-Type: application/json" \
  -d '{"dhcp":false,"ip":"10.0.1.109","subnet_mask":"255.255.255.0","default_gateway":"10.0.1.254","dns":"8.8.8.8"}'

Turn on RY1:

curl -X POST http://10.0.1.109/io \
  -H "Content-Type: application/json" \
  -d '{"pin":"RY1","value":1}'

Read digital I/O state:

curl http://10.0.1.109/io/state

Set AO0 to 5.0 V:

curl -X POST http://10.0.1.109/analog_out \
  -H "Content-Type: application/json" \
  -d '{"index":0,"voltage":5.0}'

Send a UART message:

curl -X POST http://10.0.1.109/serial \
  -H "Content-Type: application/json" \
  -d '{"message":"hello"}'

Read UART data:

curl http://10.0.1.109/serial/recv

Write EEPROM data:

curl -X POST http://10.0.1.109/storage/write \
  -H "Content-Type: text/plain" \
  -d 'hello from WIZ-RTU'

Read EEPROM data:

curl http://10.0.1.109/storage/read

Send a standard CAN frame:

curl -X POST http://10.0.1.109/can/send \
  -H "Content-Type: application/json" \
  -d '{"id":"0x123","data":"0x11223344"}'

Send an extended CAN frame with an eight-byte payload:

curl -X POST http://10.0.1.109/can/send \
  -H "Content-Type: application/json" \
  -d '{"id":"0x18FF50E5","data":"0x0102030405060708"}'

Send a basic AT command:

curl -X POST http://10.0.1.109/4G \
  -H "Content-Type: text/plain" \
  --data-binary "AT\n"

Enter modem command mode:

curl -X POST http://10.0.1.109/4G \
  -H "Content-Type: text/plain" \
  --data-binary "+++"

Restart the modem and return to transparent mode:

curl -X POST http://10.0.1.109/4G \
  -H "Content-Type: text/plain" \
  --data-binary "AT+Z\n"

Send modem data in transparent mode:

curl -X POST http://10.0.1.109/4G \
  -H "Content-Type: text/plain" \
  --data-binary "hello from WIZ-RTU"

Note

curl can send CAN frames and modem data through HTTP, but it cannot receive the application’s CAN or modem events through an HTTP endpoint. Receive data through ws://<device-ip>:8080 using a WebSocket client.