Serial tunneling (through Ethernet) - A Lua example

Goal

We establish a network connection (e.g. over Ethernet) to send serial commands from one device to another (or vice versa).

  1. Receive serial input on device 1 (Server/Client).

  2. Send received data over TCP socket connection to device 2 (Server/Client).

  3. Transmit received data (from device 1) to serial output on device 2.

Overview


Code

Let's start with importing the necessary packages.

rs232 = require("luars232") local socket = require("socket") require("uci")

Package

Function

<a href="https://luarocks.org/modules/luarocks/luars232">luars232</a>

Interaction with the serial port (based on <a href="https://github.com/ynezz/librs232">librs232</a>).

<a href="https://luarocks.org/modules/luarocks/luasocket">socket</a>

Support for TCP/UDP transport (see "Glossary" section IP/TCP).<br>

<a href="https://oldwiki.archive.openwrt.org/doc/techref/uci">uci</a>

Uci bindings to access systems config settings (see section "Config File" below).

x = uci.cursor() SERIAL = { PORT_NAME = x:get("flexa_app.serial.port_name"), } SERVER = { MODE = x:get("flexa_app.server.mode"), SERVER_IP = x:get("flexa_app.server.ip"), PORT = tonumber(x:get("flexa_app.server.port")), }

Now defining some table structures holding possible options for our serial connection. For an explanation of these options, see section "Glossary" below.

baud_rates = { [9600] = rs232.RS232_BAUD_9600, [19200] = rs232.RS232_BAUD_19200, [38400] = rs232.RS232_BAUD_38400, [57600] = rs232.RS232_BAUD_57600, [115200] = rs232.RS232_BAUD_115200, } data_bits = { [6] = rs232.RS232_DATA_6, [7] = rs232.RS232_DATA_7, [8] = rs232.RS232_DATA_8, } stop_bits = { [1] = rs232.RS232_STOP_1, [2] = rs232.RS232_STOP_2, } parity_bits = { ["NONE"] = rs232.RS232_PARITY_NONE, ["ODD"] = rs232.RS232_PARITY_ODD, ["EVEN"] = rs232.RS232_PARITY_EVEN, } flow_control = { ["OFF"] = rs232.RS232_FLOW_OFF, ["HW"] = rs232.RS232_FLOW_HW, ["XONXOFF"] = rs232.RS232_FLOW_XON_XOFF, }

Going further, defining all functions we want to invoke later. We will look at each one separately, from top to bottom.

function serial_setup() local out = io.stderr e, p = rs232.open(SERIAL.PORT_NAME) if e ~= rs232.RS232_ERR_NOERROR then -- handle error out:write(string.format("can't open serial port '%s', error: '%s'\n", port_name, rs232.error_tostring(e))) return end assert(p:set_baud_rate(baud_rates[SERIAL.BAUD]) == rs232.RS232_ERR_NOERROR) assert(p:set_data_bits(data_bits[SERIAL.DATA_BITS]) == rs232.RS232_ERR_NOERROR) assert(p:set_parity(parity_bits[SERIAL.PARITY]) == rs232.RS232_ERR_NOERROR) assert(p:set_stop_bits(stop_bits[SERIAL.STOP_BITS]) == rs232.RS232_ERR_NOERROR) assert(p:set_flow_control(flow_control[SERIAL.FLOW_CONTROL]) == rs232.RS232_ERR_NOERROR) out:write(string.format("OK, port open with values '%s'\n", tostring(p))) end

First we define a variable *out *(a handler for the predefined C stream io.stderr), to be able to later send a message directly to the error stream (console).

Next we access the serial port with the port name (SERIAL.PORT_NAME) declared in the config file.

If we receive no error back, we can go on and set the serial options defined in the config file. If an error occurred, it will be send to the error stream (console).

function client_f(host, port) tcp = assert(socket.tcp()) tcp:connect(host, port); end

Function on the client side to establish a TCP socket connection to the host (Server device).

  • host: IP address of the Server

  • port: Port at which the connection should start

function server_f(port) local server = assert(socket.bind("*", port)) local ip, port = server:getsockname() print("Please telnet to localhost on port " .. port) print("After connecting, you have 30s to enter a line to be echoed") client = server:accept() end

Function on the server side to establish a TCP socket server.

  • port: Port at which the connection should start

Warning

Server side determines the port number, client has to use the same number (check in config)

Next we define a function to handle the serial input (for each device).

function serial_receive() local e, data_read = p:read(1,100) if data_read ~= nil then server_send(data_read) end end

First reading from the serial connection and storing the received data in data_read. If there is any data (data_read ~= nil), we send the data to the other device (see server_send() below) over our TCP socket connection.

function serial_transmit(data_send) if data_send ~= nil then e, len_written = p:write(data_send) end assert(e == rs232.RS232_ERR_NOERROR) end

This function is going to forward the received data to the serial port. After we check if an error occurred during the transmission.

function tcp_receive() if mode == "SERVER" then client:settimeout(0.1) local line, err = client:receive(1) serial_transmit(line) else tcp:settimeout(0.1) local s, status, partial = tcp:receive(1) serial_transmit(s or partial) end end

Next, a function to handle the incoming data on the TCP socket connection. It consists of 2 parts, one if the code is running on the server side, one for client side.

In both cases, we change the timeout values for the socket object to 0.1sec..

Info

By default, all I/O operations are blocking. So the receive call is blocking indefinitely. The settimeout() method sets a limit on the amount of time the I/O method can block.

Now we can go ahead and receive data from the TCP connection (the 1 indicates that we are going to read 1 byte at a time).

function server_send(data) if mode == "SERVER" then client:send(data) else tcp:send(data); end end

Send the data received from the serial input to the other device.

That's it for the function part. Now we can take a look at the execution logic.

serial_setup() mode, port = SERVER.MODE, SERVER.PORT print("MODE: " .. mode .. "\n") if mode == "CLIENT" then host = SERVER.SERVER_IP print("Host to connect: ".. host .."\n" .. "Used port: " .. port .. "\n") client_f(host,port) elseif mode == "SERVER" then print("Used port: " .. port .."\n") server_f(port) end
  1. First setting up the serial connection.

  2. Reading socket mode and port from the config.

  3. Establish socket connection (as server or client, depending on config).

while true do serial_receive() tcp_receive() end tcp:close() client:close()

Main part for handling the application. Running forever (while true).

Finally closing the socket connection.

Config

Serial

Option

Value (Default)

PORT_NAME

Default for serial communication using RS232: "/dev/ttyS1"

BAUD

9600, 19200, 38400, 57600, 115200 (default 115200)

DATA_BITS

 6, 7, 8 (default 8)

PARITY

"NONE", "ODD", "EVEN" (default "NONE")

STOP_BITS

1, 2 (default 1)

FLOW_CONTROL

"OFF", "HW", "XONXOFF" (default "OFF")

Server

Option

Value

MODE<br>

"SERVER", "CLIENT"

SERVER_IP

Enter the IP of the device in SERVER MODE. The device in CLIENT MODE will then connect to this specific IP

PORT<br>

<p>Enter the port you want to use. If you enter 0 in the SERVER device config, it will chose an ephemeral port by <span style="font-family: var(--font);">himself and print it out. You will then have to enter this printed port in the config of the CLIENT device.  </span></p>


Glossary

  • Ethernet

  • IP/TCP

  • Port

  • Serial

  • Serial characteristics (Baud rate, Data bits, Stop bits, Parity bits, Flow control)

  • Server/Client

  • Tunneling