Skip to content

[HW Interface] Development Documentation

tkrzielSICKAG edited this page May 8, 2024 · 11 revisions

This is the code documentation for the examples created using the hardware interface. The following examples using the hardware interface are created:

Examples are created in Python and Go, and mostly also contain a Node-RED example of the same functionalities as reference.

1. Digital Inputs Outputs

Python Application Example

Node-RED Application Example

Two simple separate Python programs are created. The first program is used for continuously fetching the DIO value via websocket, checking whether the sensor has detected something and setting the LED on as a response. The second program is used to set the DIO state to 1 or 0, or to set the LED on or off.

Afterwards, a Node-RED application example is provided.

1.1. Fetching DIO State from API (Python)

To fetch the state of a DIO in Python, the following libraries are used:

  • requests
  • json
  • time

The requests library is needed to access the API data as requests to the server are sent though it. The json library is needed for working with JSON data from the response object, while the time module is used to make the program wait.

This example fetches the state of DIO_B. Before fetching the state of a pin, the user first needs to be authenticated with Bearer token authentication. The token is generated as follows:

# gets authentication token for http requests  
def getToken():
    tokenUrl = "http://192.168.0.100:59801/user/Service/token"
    password = "PASSWORD"
    myobj = {'password':password}
    headers = CaseInsensitiveDict()
    # setting header
    headers["Content-Type"] ="application/x-www-form-urlencoded"
    resp = requests.post(tokenUrl, data = myobj, headers=headers)
    token = json.loads(resp.text)
    return token["token"]

The token is fetched from the http://192.168.0.100:59801/user/Service/token URL and a default password is provided and added to the request's header with the Content-Type set to application/x-www-form-urlencoded. The response uses the POST method to send the request, then returns a response.

The program fetched the DIO state of pin B every five seconds indefinitely. It does so by calling the fetchCurrState method, which calls getDio in its body. This is how the code is implemented:

# fetches DIO state
def fetchCurrState(token):
    resp = getDio(token, "http://192.168.0.100:59801/tdce/dio/GetState/DIO_B")
    return resp['Value']

# gets dio_b object from url 
def getDio(token, url):
    api_response = requests.get(url, auth=BearerAuth(token))
    data = api_response.text
    resp = json.loads(data)
    return resp

To get the DIO_B state, the URL http://192.168.0.100:59801/tdce/dio/GetState/DIO_B is accessed by sending a GET request to the address, providing an authentication token. The data is read from the API response, then the value is extracted from it, printing the value to the terminal.

1.2. Setting DIO State with API (Python)

This application was made to demonstrate setting a DIO state to 0 or 1, depending on the user input. The main program looks like this:

if __name__ == "__main__":
    payload = str(sys.argv[1])

    if(payload=="1"):
        js = '[{ "DioName": "DIO_A", "Value": 1, "Direction": "Output" }]'
    elif(payload=="0"):
        js = '[{ "DioName": "DIO_A", "Value": 0, "Direction": "Output" }]'
    else:
        print("Wrong value for DIO state.")
    token = getToken()
    setDios(token, js)

It is started by providing an argument to the Python script, which is then read into the payload variable. If the user input is equal to 1, it sets the value of the DIO to 1, and if it is 0, it sets the value to 0. It fetches the authentication token in the same way as it was fetched in the example above, then sets the state to the user input using the following code:

def setDios(token, js):
    #set LED state
    url = "http://192.168.0.100:59801/tdce/dio/SetStates"
    headers = CaseInsensitiveDict()
    headers["Content-Type"] = "application/json"
    requests.post(url, data=js, auth=BearerAuth(token), headers=headers)

It sends the created json string to the specified url, adds a application/json header, then posts the data using requests.post, successfully turning the DIOs ON and OFF.

1.3. Getting DIO objects from REST API (Node-RED)

This program was implemented to fetch a DIO object from the URL http://192.168.0.100:59801/tdce/dio/GetState/DIO_A and changes state of the DIO value. To do so, it uses the following nodes:

  • inject
  • function
  • http request
  • json
  • debug

Upon injecting into the first function node, the flow gets a token from the current flow and sets a message header with the following fields:

msg.headers = {
    "authorization": "Bearer " + String(token)
};

This will create a new authorization header with a Bearer authentication token, which will be set by the function. The http request node is then used to send a GET request to the specified URL for fetching the value of the A DIO, and it will use the authentication provided in the previous function. The response object is formatted from a json object to a Javascript object, then printed to the debug panel on the right side of the screen.

Now, if the token is not set yet, or has expired, the response we get from the server will have the following: msg.statusCode == 401. If that is the case, the program will continue with fetching the bearer authentication token from the URL http://192.168.0.100:59801/user/Service/token.

To now fetch or set the DIO state, simply click on the corresponding inject node.

NOTE: In case of extending the application, make sure to check whether the DIO state is not empty when further working with the values after fetching the http response from the DIO API. The token will expire after some time, so it is recommended to set this check in order to get only functional objects.

2. Analog Inputs

Go Application Example

Node-RED Application Example

C# Application Example

In this section, code written in the Go programming language for working with analog inputs is provided. An example in Node-RED is also provided. Fetching AIN data from a REST API, fetching from a websocket that returns new and previous values of AINs, and writing fetched data a database will be discussed. The program shown here is written in Go fetches data from a REST API and websocket in two separate goroutines and then writes some of the data fetched from the websocket into a analog_base database.

The database's structure, called analog_base with the data table analogi, is the following:

  • id (integer) - the identification number of the signal
  • whattime (timestamp) - the timestamp of the object saving
  • value (float) - the value of the analog input

Next up, a Node-RED application performing the same functions will be discussed.

2.1. Fetching AIN Data from REST API (Go)

The following section describes fetching data from REST APIs using authorized HTTP Requests. In the first goroutine of the main.go program, the following data is fetched:

  • all AIN states
  • all AIN values
  • specific AIN state
  • specific AIN value

To fetch data, an authentication token needs to be generated and fetched so that the states and values can be accessed. Find the function that fetches the token below.

func getToken() string {
	tokenURL := "http://192.168.0.100:59801/user/Service/token"
	password := "PASSWORD"

	form := url.Values{}
	form.Add("password", password)

	resp, err := http.PostForm(tokenURL, form)
	if err != nil {
		fmt.Println("Error making request:", err)
		return ""
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Println("Response status code:", resp.StatusCode)
		return ""
	}

	var tokenResp TokenResponse
	err = json.NewDecoder(resp.Body).Decode(&tokenResp)
	if err != nil {
		fmt.Println("Error decoding JSON:", err)
		return ""
	}

	return tokenResp.Token
}

The token URL and password are provided and added to the URL form. The form is then posted using an http object and the token is decoded from the received JSON response. This token is then used in all other HTTP requests.

The application then fetches all states and values before fetching the state and value of the AIN that was set up in the environment. During the making of the project, "AIN_A" was used as the analog input and was declared in the ain variable that can be changed according to the needed input. The code for fetching those values is very similar, the only difference being the output type and URLs, so only one example will be provided.

Below is the function for getting all analog input values.

type AnalogVal struct {
	AinName string  `json:"AinName"`
	Value   float64 `json:"Value"`
}

func getAllAnalogValues(token string) []AnalogVal {
	url := "http://192.168.0.100:59801/tdce/analog-inputs/GetValues"
	resp := sendRequest(token, url, "GET")
	defer resp.Body.Close()
	var analogValues []AnalogVal
	err := json.NewDecoder(resp.Body).Decode(&analogValues)
	if err != nil {
		fmt.Println("Error decoding JSON: ", err)
		return nil
	}
	return analogValues
}

The provided function operates as following: it sets the URL for fetching values and sends a GET request to the provided URL, authenticating the user with the previously generated token. It creates a list of analog values, a defined struct with an AinName and Value parameter, then decodes the JSON response and maps the response body to the analogValues type, returning it in case there are no errors.

2.2. Fetching AIN Data from Websocket (Go)

The websocket that is implemented in the second goroutine listens for any kind of change between values of analog inputs. Whenever the value changes, the program fetches a JSON object from the websocket and writes the ID, timestamp of the event and the new value of the object into the database. A change happens any time the DIO and AIN wire are connected or disconnected. The change will then be written into the database. This is an example of the records in the analogi table in analog_base.

mysql> select * from analogi;
+----+---------------------+--------+
| id | whattime            | value  |
+----+---------------------+--------+
|  1 | 2023-09-11 10:04:31 |  11.93 |
|  2 | 2023-09-11 10:04:31 |  3.032 |
|  3 | 2023-09-11 10:04:31 | 11.856 |
|  4 | 2023-09-11 10:04:31 |  0.054 |
|  5 | 2023-09-11 10:04:43 | 11.921 |
|  6 | 2023-09-11 10:04:43 |  0.026 |
|  7 | 2023-09-11 10:04:43 | 11.939 |
|  8 | 2023-09-11 10:04:43 |  0.035 |
|  9 | 2023-09-11 10:04:45 | 11.939 |
| 10 | 2023-09-11 10:04:45 |  1.059 |
| 11 | 2023-09-11 10:04:45 |  0.026 |
+----+---------------------+--------+
11 rows in set (0.00 sec)

The websocket is implemented in an infinite for loop to fetch all incoming changes. It listens indefinitely until a signal interrupt is provided by the user, after which it cleanly exits. The expected return result is in the following format:

{
    "AinName":"AIN_A",
    "PreviousValue":0,
    "NewValue":11.939
}

It returns the name of the analog input, its previous value and the new value of the input. To fetch this data, the program firstly has to create a serverUrl variable from the url object to specify the path to listen to.

serverUrl := url.URL{
		Scheme: "ws",
		Host:   "192.168.0.100:31768",
		Path:   "/ws/tdce/analog-inputs/value",
	}

The scheme is set to ws as websocket, the host is 192.168.0.100:31768 and the path is /ws/tdce/analog-inputs/value/. This is the equivalent of the websocket's address: ws://192.168.0.100:31768/ws/tdce/analog-inputs/value . The connection is then set up using the following lines of code:

conn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
	if err != nil {
		log.Fatal("Error connecting to WebSocket: ", err)
	}
defer conn.Close()

The URL specified previously is dialed by a default websocket dialer. An internal go function is created in which an infinite for loop is nestled. It reads messages indefinitely and saves each message as an AnalogValueChange struct type.

type AnalogValueChange struct {
	AinName       string  `json:"AinName"`
	PreviousValue float64 `json:"PreviousValue"`
	NewValue      float64 `json:"NewValue"`
}
go func() {
	for {
		_, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Error reading message: ", err)
			return
		}
		var avchange AnalogValueChange
		errM := json.Unmarshal(message, &avchange)
		if errM != nil {
			fmt.Println("Error decoding JSON: ", errM)
			continue
		}
		fmt.Printf("Received AnalogValueChange: %+v\n", avchange)
		addToDb(avchange.NewValue)
	}
}()

It then maps the JSON message to the defined struct type and prints out the fetched data before adding the object to the database. The following lines of code demonstrate how the program writes the values into the database:

func addToDb(value float64) {
	db, err := connect()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer db.Close()

	_, err = db.Exec("INSERT INTO analogi (value, whattime) VALUES (?, CURRENT_TIMESTAMP())", value)
	fmt.Println("\n1 record inserted.")
}

The program first connects to the database by opening a mysql connection using the following connection string: root:TDC_arch2023@tcp(192.168.0.100:3306)/analog_base. By deferring db.Close(), the database connection is closed once all operations with it are done. The db.Exec method is used to insert the values and timestamps of the changed objects into the database. This is done by creating an INSERT SQL statement which specifies the columns that will be affected and providing a value variable to replace the ? in the statement. The value will be inserted into the value column in the table while the CURRENT_TIMESTAMP() will fetch the current timestamp of the TDC-E device and insert it into the whattime column of the table. The id of the row is set to be AUTO_INCREMENT and thus this value will be automatically generated by incrementing each new object ID by one.

The process is then repeated until user interruption, executed by e.g. CTRL + C.

2.3. Fetching AIN Data from Websocket (Node-RED)

A small Node-RED application is created to listen to the specified websocket, printing the response object in the Debug panel. It consists of three nodes:

  • a websocket node
  • a json node
  • a debug node

The websocket node is configured to listen on ws://192.168.0.100:31768/ws/tdce/analog-inputs/value. When an AIN value changes, it returns a JSON object in the previously specified format. The json node turns the response into an object, and the debug node catches the object and makes it readable in the Debug panel. The following can be seen after generating a change.

2.4. Fetching AIN Data from Websocket (.NET C#)

This simple application creates a task that listens to the TDC-E's websocket to fetch all necessary values, deserializing the received json object to a struct of the following structure:

public class AnalogValueChange
{
    public string AinName { get; set; }
    public double PreviousValue { get; set; }
    public double NewValue { get; set; }
}

The service uses a ClientWebSocket client that it uses to connect to the needed websocket, creates a receiveBuffer for bytes the websocket provides, and indefinitely asynchronously receives a result which reads the message from it, then deserializes the message to the AnalogValueChange before printing the state.

 while (true)
            {
                var result = client.ReceiveAsync(receiveBuffer, CancellationToken.None).Result;
                var message = Encoding.UTF8.GetString(receiveBuffer.Array, 0, result.Count);
                var avChange = JsonSerializer.Deserialize<AnalogValueChange>(message);

                Console.WriteLine($"Received AnalogValueChange: AinName={avChange.AinName}, PreviousValue={avChange.PreviousValue}, NewValue={avChange.NewValue}");
            }

3. CAN

Go Application Example

Node-RED Application Example RAW CAN

Node-RED Application Example CANopen

In this section, working with raw CAN and CANopen will be described in two separate subsections. The provided code is written in Go and in Node-RED. Firstly, working with the raw CAN protocol will in Go and Node-RED will be described. Next, working with CANopen will be detailed. For CANopen, Node-RED was used.

3.1. RAW CAN

RAW CAN is a pure CAN protocol that works on the CAN bus without an additional communication protocol. To fetch data from raw CAN, the TDC-E has the ability to provide the data from a websocket or directly from the device by using built-in files. The following sections describe fetching data from the CAN bus by using websockets and directly from the TDC device.

3.1.1. Working with RAW CAN (Go)

A Go application has been made to fetch data from the RAW CAN bus. It is a simple application that fetches data from a websocket it continuously listens on, then writes that data into a struct type named CanBus before printing it to the console. Since a websocket is used, data will be fetched continuously.

Find the declaration of the CanBus struct type below:

type CanBus struct {
	CanBusName                  string `json:"CanBusName"`
	Id                          int    `json:"Id"`
	Data                        []byte `json:"Data"`
	IsErrorFrame                bool   `json:"IsErrorFrame"`
	IsExtendedFrameFormat       bool   `json:"IsExtendedFrameFormat"`
	IsRemoteTransmissionRequest bool   `json:"IsRemoteTransmissionRequest"`
}

The data that is fetched from the websocket has the same object structure as the struct depicted here. The CanBus has a name, an identification number, a byte array containing data that is sent, and three boolean variables to specify whether the frame is an error frame, if the data is in the extended frame format and if it is a remote transmission request.

The main function operates in an infinite loop in a separate goroutine in which the program continuously listens to the websocket specified like so:

serverUrl := url.URL{
		Scheme: scheme,
		Host:   host,
		Path:   path,
	}

The scheme is set to "ws", the host is "192.168.0.100:31768", and the path is set to "/ws/tdce/can-a/data". The websocket is opened by using a websocket default dialer on the specified url. The infinite loop of listening and displaying data is implemented this way:

for {
	msg, erro := ListenOnWS(conn)
	if erro != nil {
		fmt.Println("Error fetching data: ", erro)
	}
	var canBus CanBus
	json.Unmarshal(msg, &canBus)
	fmt.Printf("Received Object: %v\n", canBus)
}

The program continuously listens on the websocket. Find the declaration of the ListenOnWs function below.

func ListenOnWS(conn *websocket.Conn) ([]byte, error) {

	for {
		_, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Error reading message: ", err)
			return nil, err
		}
		return message, nil
	}

}

The function is implemented so that it listens on a specific connection dubbed conn and it returns an array of bytes which is what the websocket returns. It reads the messages the websocket sends and returns the message or the error that is sent. The program then declares a canBus variable that is the type of the struct that has been created beforehand. The encoding/json package is used to parse the received data from the connection and store it into the canBus variable. The received object is then printed.

3.1.2. Working with RAW CAN (Node-RED)

A Node-RED program has been created to show another way of fetching the data via websocket. To fetch and display the data gotten from the CAN bus, three nodes are used:

  • a websocket node
  • a json node
  • a debug node

Those three nodes work in a flow to display the data fetched from the CAN bus into the debug panel on the right side of the screen. Firstly, data is fetched by setting the websocket node to listen on ws://192.168.0.100:31768/ws/tdce/can-a/data. The websocket returns the data, then sends it to the json node that formats that data, and the data is returned to the debug node for display.

If specific action regarding the data needs to be done, connect the endpoint of the json node to whichever node will be used to work the data next.

3.2. CANopen (Node-RED)

The CANopen protocol is a standardized Layer 7 protocol for CAN bus. The following example has been implemented and tested using the following configuration:

  • can0, can1
  • BitRate: 125000
  • device #1: SICK Inclination Sensor (TMM88D-PCI090)
  • device #2: SICK Absolute Encoder Multiturn (AHM36A-BDCC014X12)

CANopen was tested with two devices. Firstly, the example using TMM88D-PCI090 will be provided. To check the message structure that needs to be sent to the CAN and sensor, consult the operational manual tied to the product.

Node-RED was used to implement the following examples. The program fetches and sends data to a websocket and parses the data to display wanted values. It is important to note the structure of the CAN object that is being sent to the websocket. Every message sent to the websocket is in the following format:

{
    "Id": 1418,
    "IsErrorFrame": false,
    "IsExtendedFrameFormat": false,
    "IsRemoteTransmissionRequest": false,
    "Data": [64, 32, 96, 0, 0, 0, 0, 0]
}

The Id and Data fields are required. The IsErrorFrame, IsExtendedFrameFormat and IsRemoteTransmissionRequest attributes are set to false by default and will be treated as such when a message is sent without those parameters. The websocket then recognizes to what can interface the message was sent and returns it in another attribute called CanBusName.

NOTE: For direct communication with the CAN interface, refer to section 2.1.2. Direct Access. The process of sending and receiving data is the same, only data values are written in a hexadecimal format. Review the following CAN commands for use on the TDC-E:

  • candump [can0 | can1] - printing data
  • cansend [can0 | can1] -i 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 - sending data to a CAN interface; first byte is COB-ID

3.2.1. TMM88D-PCI090

To get position data from the TMM88D-PCI090 sensor, two data objects need to be sent. One is used to retrieve the X value of the sensor, and one is used to retrieve the Y value of the sensor. It is also important to note the Id of the node object. The COB-ID object is comprised of a Node ID and a Function ID. In this example, the node Id is A (10), and the Id is 1546.

Find the examples of getting X and Y positions below:

msg.payload = {
    "Id": 1546,
    "IsExtendedFrameFormat": false,
    "Data": [64, 16, 96, 0, 0, 0 , 0 ,0]
};
msg.payload = {
    "Id": 1546,
    "IsExtendedFrameFormat": false,
    "Data": [64, 32, 96, 0, 0, 0 , 0 ,0]
};
return msg;

The data that is used for returning X and Y values can be read from the TMM88D-PCI090 operation manual. To get the longitudinal inclination value, index 6010h is used, while 6020h is used for the lateral inclination value.

The first byte in the Data used in msg.payload is the command specifier (B0). Here, 64 is used for reading data, its hexadecimal value 40. The other two bytes refer to the data that needs to be read. 16 is 10 in hexadecimal, and 96 is 60. Thus, reading the X longitudinal value is 6010. Meanwhile, to read the Y value of the device, 6020 functions as the data payload.

Profile-specific part

3.2.2. AHM36A-BDCC014X12

The next device used for retrieving sensor data is AHM36A-BDCC014X12. Connect the device to your CAN, and a boot message whose ID is equal to 700 + node ID (5) should appear. Note the node Id of the device, as it will be used in the process of sending data.

In the program described below, the position of the sensor is read and the revolution is calculated by dividing the position with the number of steps per revolution. To do so, the device's operation manual is consulted. The position value can be fetched with the 6004h object. Object 6004h

The following data is sent to the corresponding websocket:

msg.payload = {
    "Id": 1541,
    "Data": [64, 4, 96, 0, 0, 0 ,0 ,0]
};
return msg;

64 tells the sensor the data is read (command specifier 40), and 96 and 4 are decimal for 6004h. Sending the object to the websocket on the address ws://192.168.0.100:31768/ws/tdce/can-[a|b]/data results in a response such as the following: Result

The array positions [4:7] indicate the position data the sensor sends.

3.2.3. Node-RED Example

The CANopen example was implemented using Node-RED. The application sends data to the CAN websocket to return a response object, then parses the object for better understanding of the data. It is implemented on the example of two previously described sensors, the code being divided into two groups for clearer understanding.

The TMM88D-PCI090 group functions so that upon injecting a value, the websocket receives two requests: reading X and Y as positions. The Id of the requests is 1546, and data that is sent through CAN is [64, 16, 96, 0, 0, 0, 0 ,0] and [64, 32, 96, 0, 0, 0 , 0 ,0] for X and Y value respectively. The websocket receives the requests via the websocket in node, then responds to each accordingly.

The websocket out node returns a response from the websocket. The content of the response is sent to a function node which processes the information but first parsing the object, identifying the sensor and real IDs, then parses data by reading the bytes of the message. If the bytes contain 16 and 32 at appropriate locations, the response message is meant to represent the X value, and if the bytes contain 32 and 96, the response message represent the Y value, taking prefixes into consideration. These values are then printed in the Debug screen on the right panel.

The same process is implemented for fetching the position of the sensor with the AHM36A-BDCC014X12 device. To read the position, an Id of 1541 is used, alongside the following data: [64, 4, 96, 0, 0, 0 , 0 ,0]. This tells CAN and the sensor to read the position. The position is read, and the websocket out node returns the response. The response is parsed via a json node, and the value of the position is then parsed using the following line of code:

if (receivedData[1] == 4 && receivedData[2] == 96) {
    info += "Position = " + (receivedData[7] * 255 * 255 * 255 + receivedData[6] * 255 * 255 + receivedData[5] * 255 + + receivedData[4]);
    info += "\nRev = " + (receivedData[7] * 255 * 255 * 255 + receivedData[6] * 255 * 255 + receivedData[5] * 255 + + receivedData[4])/16384;
}

The seventh, sixth, fifth and fourth byte contain position data. It is parsed by being multiplied by 255 depending on the position of the byte, and the values are then summed. For revolution, this number is divided by 16384, which is the number of steps per revolution specified in the device details. This data is then shown in the Debug panel on the right side of the screen.

Note: The websocket out node of the sensor that isn't in use should be disabled not to cause any parsing errors.

4. GPS

Go Application Example

Node-RED Application Example

4.1. Fetching GPS Data via Websocket (Go)

A simple Go application to fetch data about the device's current GPS location has been implemented. The program first imports the following packets from the official Go package repository:

import (
	"fmt"
	"log"
	"net/url"
	"os"
	"os/signal"
	"time"

	"github.com/gorilla/websocket"
)

In the main() function, the getData() function is called, which sets up the server URL and connection to the wanted websocket. The defer keyword is used so that the application closes the connection after finishing all tasks related to it.

// setting up URL to fetch data from
	serverUrl := url.URL{
		Scheme: "ws",                  //wss is secure
		Host:   "192.168.0.100:31768", // address
		Path:   "/ws/tdce/gps/data",   //endpoint path
	}

	// dials a websocket with the created URL
	conn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
	if err != nil {
		log.Fatal("Error connecting to WebSocket: ", err)
	}
	// makes sure to close the connection after work with websocket has finished
	defer conn.Close()

Additionally, the application implements a channel that utilizes an os.Signal to interrupt the program, considering the main program loops indefinitely as it continuously reads the messages that appear in the websocket. This part is implemeted with a nameless internal go function that prints the received message until an interruption is made.

go func() {
		for {
			_, message, err := conn.ReadMessage()
			if err != nil {
				log.Println("Error reading message: ", err)
				return
			}
			// prints the message
			fmt.Printf("received message: %s\n", message)
		}
	}()

4.2. GPS Application with Database (Node-RED)

The GPS Application developed in Node-RED consists of connected nodes, a worldmap to show the current location of your device, and a database that was created to store the fetched data. The MySql database and table are used to store all GPS data that is fetched from the web socket. The following data is extracted from the GPS signal and stored:

  • Id
  • Latitude
  • Longitude
  • Time
  • Altitude
  • Speed knots
  • Speed in mph
  • Speed in kmh
  • Course
  • Fix
  • Number of satellites
  • GPS fix available
  • HDop

Additionally, another requirement for the application is the installation of the following nodes:

  • node-red-node-mysql
  • node-red-contrib-web-worldmap
  • node-red-mysql-r2

The Node-RED application is implemented so that the program reads data from a TDC-E websocket. The websocket returns JSON data which is read from a function node that writes the needed data into the gpslocation database that is made after deploying the MySql stack. A MySql node writes into the database.

Simultaneously, the same data is processed for connection to a the Worldmap node. A lat, lon and name parameter is needed for the map to properly mark the location of the device. The Worldmap node is configured to show an OpenStreetMap as a base map, but the option can be set by double-clicking the Base map option and choosing another from the drop-down menu.

To open the world map, go to http://192.168.0.100:1880/worldmap.

Additionally, to set multiple markers onto the map instead of one, the nameset node can be changed. If the msg.payload.name is set to a string value, a single object will appear and change its position, but if it is set to e.g. timestamp by selecting another value from the drop-down menu, it will create a new marker for each object that arrives from the web socket.

5. Modem

Go Details and Statistics

Go Sending and Receiving Messages

Node-RED Sending and Receiving Messages

In this section, working with the modem of the TDC-E will be described. There are two applications to go through - one is used to fetch the current details and statistics of the modem, while another is used to send and receive messages to and from the modem.

5.1. Details and Statistics

In this section, the application written in Go for fetching details and statistics from the modem will be discussed. Please note that both services are of the same structure and contact the same server for fetching data, with the only differences being the addresses and return types of the REST APIs. Following that, any other GET REST API call can be implemented changing only those parameters.

It is important to note that the modem data is fetched from TDC-E's Swagger UI, and thus any call made to the server needs to be authorized. Authorization is done with OAuth2.0, so before making any REST API call, firstly a token needs to be generated.

To that end, the program starts with creating two wait groups, and two goroutines, light-weight threads that handle specific tasks. One fetches an access token, while the other fetches modem data.

The first goroutine calls the setToken() function. This function is implemented the following way:

func setToken() {
	for {
		token = o2.Authorize()
		/* Sleep for 59 minutes before reset */
		if token != "" {
			time.Sleep(59 * time.Minute)
		}
	}
}

An access token of the string type is stored into a token variable after fetching its value with the function Authorize(), implemented in the request.go file which uses the golang.org/x/oauth2 package for working with OAuth2.0. If the token is fetched correctly, this goroutine sleeps for 59 minutes before restarting in an infinite for loop. This is done so that the token is fetched continuously if fetching fails, since the program cannot continue with details and statistics data fetching if there is no token, and would return an Unauthorized failure code.

NOTE: The 59 minute interval stems from the fact that the received token expires every 60 minutes. And thus needs to be fetched anew.

The Authorize() function firstly fetches configuration data written to the attached params.json file. The file is read to set the needed OAuth2.0 parameters:

  • ClientId
  • ClientSecret
  • TokenURL

It additionally sets the following parameters:

  • Username
  • Password

NOTE: The configured parameters are identical to the parameters set up for the SICK WebDash.

This data is then used for creating a POST request. A client needs to be established, a request needs to be made by setting the method (POST), the TokenURL and data to encode, and a header for content type is set. Additionally, basic authentication is set by providing the ClientID and ClientSecret.

httpClient := &http.Client{}
req, err := http.NewRequest("POST", cfg.Endpoint.TokenURL, strings.NewReader(data.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(cfg.ClientID, cfg.ClientSecret)

The response is then set by running httpClient.Do(req). If the token fetching was unsuccessfully completed in any way, the function returns an empty string and the error message is printed, and the process is repeated. Otherwise, the response body is read, parsed to a variable and the access token value is extracted from it.

We are now authorized for the next 60 minutes and can proceed with making REST API calls. The second goroutine fetches all modem data. Firstly, it makes a request for details, then it makes a request for statistics of the ppp0 modem.

func fetchData() {
	for {
		if token != "" {
			fetchModemData()
			fmt.Printf("Received data: %v\n", modfull)
			fmt.Printf("Received data: %v\n", modstat)
			time.Sleep(time.Second)
		}
	}
}

This function runs in an infinite loop and fetches and prints both details and statistics, before sleeping for a second. It does so by calling fetchModemData().

func fetchModemData() {
	err := json.Unmarshal(o2.MakeROPCRequest("http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/details", token), &modfull)
	if err != nil {
		handleError(err)
	}
	err = json.Unmarshal(o2.MakeROPCRequest("http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/statistics", token), &modstat)
	if err != nil {
		handleError(err)
	}
}

NOTE: If ppp0 is not your device name, change the call address by writing its name in the place of ppp0. The structure of the REST APIs has been shown in the previous sections.

MakeROPCRequest() is another function implemented in the request.go file. For parameters, it takes the call address and the previously generated token. This function is called in relation to json.Unmarshal(), which parses JSON encoded data and stores the result in a variable pointed at. The variables in question need to be a type that corresponds to the JSON object that is returned so that the values can be mapped neatly. To that end, new struct types are created to match the structure of the JSON for both modem details and statistics.

func MakeROPCRequest(urlConn string, accessToken string) []byte {
	httpClient := &http.Client{}
	req, err := http.NewRequest("GET", urlConn, nil)
	if err != nil {
		return nil
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil
	}
	return responseBody
}

This function creates a httpClient, makes a GET request to the specified URL without offering additional data, sets the authorization token to the value that was passed to it, then processes the request. The response body is read and returned before the process is repeated.

5.2. Sending and Receiving Messages

In this section, programs for receiving and sending SMS messages written in the Go and Node-RED programming languages will be described. Firstly, we describe the program implementation in Go. Secondly, we provide a Node-RED solution for the problem of sending and receiving SMS messages.

NOTE: The TDC-E stores the sent messages on the inserted SIM card, which means that the memory size is the same as the card's memory size.

5.2.1. Sending and Receiving Messages (Go)

In this section, the Go programming language will be used to implement modem communication. This application requires two SIM cards - one inside the TDC-E, and one on any other device which can send SMS messages. The device's number needs to be known and inserted as a parameter into the code.

As was the case in the previous details and statistics fetching application, the program relies on TDC-E's Swagger UI, which requires OAuth2.0 authorization for data access. The program is thus implemented using 3 goroutines - one fetches the access token from Swagger by providing required parameters in the related params.json file, one fetches a list of all messages the modem received every second, while another goroutine posts a message to the modem every 10 seconds.

The first goroutine functions in the same way as described in the example above. It sends a POST request to the server, reads the received response body, then returns the access token string. This string will be used in further data processing, so it is required for proceeding with the program. If one is fetched, the routine sleeps for 59 minutes since the token expires in 60 minutes. Otherwise, it keeps trying to fetch the data until successful since it is implemented in an infinite for loop.

func setToken() {
	for {
		token = o2.Authorize()
		/* Sleep for 59 minutes before reset */
		if token != "" {
			time.Sleep(59 * time.Minute)
		}
	}
}

The second goroutine fetches SMS messages from the specified modem (set to ppp0). It does so using the function MakeROPCRequest() with the URL http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages and fetched access token as parameters. This function is called from the attached request.go package.

func MakeROPCRequest(urlConn string, accessToken string) []byte {
	httpClient := &http.Client{}
	req, err := http.NewRequest("GET", urlConn, nil)
	if err != nil {
		return nil
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil
	}
	return responseBody
}

This function creates a new http.Client and creates a GET request to the specified URL, adding the sent accessToken parameter to the header for resource authorization. The client then processes the request and reads the response body it receives from the server, returning the read object.

The message is parsed into a modemMsg variable which is of the following structure:

type modemMsg struct {
	Index   int    `json:"index"`
	Sender  string `json:"sender"`
	Content string `json:"content"`
	Time    string `json:"time"`
	Pdu     string `json:"pdu"`
}

The message sent from the server has an index field, contains the sender's phone number, the content that was sent to the device, the timestamp of the event and the PDU of the messaging. The server returns an array of SMS objects that are then stored in a modemMsg variable.

err := json.Unmarshal(o2.MakeROPCRequest("http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages", token), &msg)

This message is then printed. This process is repeated every two seconds, meaning that all modem messages are displayed by calling the REST API each 2 seconds.

NOTE: For a message to appear in the list of message objects, the modem first needs to be sent a message. This can be done with any device with a SIM that can send messages by sending a message addressed to the TDC-E's SIM's phone number.

The third goroutine is used for posting SMS messages to a specified SIM number, and is done only once. This goroutine calls the postMessage() function to complete its assigned task. The function is implemented as below:

func postMessage() {
	time.Sleep(5 * time.Second)
	if token != "" {
		// insert phone number here
		phoneNumber := "+XXXXXXXXXXXX"
		content := "hello world"
		url := "http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages"

		// setting values
		var poster postMsg
		poster.PhoneNumber = phoneNumber
		poster.Content = content

		// setting up a JSON object from created object
		jsonData, err := json.Marshal(poster)
		if err != nil {
			handleError(err)
		}
		o2.PostROPCMessage(url, token, jsonData)
	} else {
		fmt.Println("Token wasn't fetched. Try again.")
	}
}

Upon entering the postMessage() function, the service is first told to sleep for 5 seconds as assurance that the access token is fetched before the continuation of the message posting. If the token is still an empty string, the function types that the token isn't fetched and the function will exit. If the token has been fetched successfully, a phoneNumber, content, and url variables are initialized.

NOTE: The phoneNumber variable in the code is a placeholder. To send the message to a SIM device, change the variable to the corresponding number.

A variable poster is then created and parameters are set before it is parsed into a JSON object. This object is then sent to the SIM with the specified phone number through the PostROPCMessage() function located inside the request.go package.

Find the implementation of this function below.

func PostROPCMessage(url string, accessToken string, msg []byte) {
	var err error

	httpClient := &http.Client{}
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(msg))
	if err != nil {
		fmt.Println("Error preparing request: ", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+accessToken)

	res, err := httpClient.Do(req)
	if err != nil {
		fmt.Println("Cannot process request: ", err)
	}
	defer res.Body.Close()

	if res.StatusCode == 200 {
		fmt.Println("Message successfuly sent!")
	} else {
		fmt.Printf("Message could not be sent. Status code: %d\n", res.StatusCode)
		fmt.Println("response Headers:", res.Header)
		body, _ := io.ReadAll(res.Body)
		fmt.Println("response Body:", string(body))
	}
}

This function creates a new http.Client and calls a new POST request, providing the correct URL and message parameter. Headers for Content-Type and Authorization are set. Then, the client processes the request. If the status code is 200, the program print that the message was successfully sent.

If the phone number was inserted correctly, the device the modem is sending to should now receive the message set in the program. Otherwise, the application print out the received status code, headers and body.

The goroutine is then closed, and the program continues to fetch the data.In this section, the Go programming language will be used to implement modem communication. This application requires two SIM cards - one inside the TDC-E, and one on any other device which can send SMS messages. The device's number needs to be known and inserted as a parameter into the code.As was the case in the previous details and statistics fetching application, the program relies on TDC-E's Swagger UI, which requires OAuth2.0 authorization for data access. The program is thus implemented using 3 goroutines - one fetches the access token from Swagger by providing required parameters in the related params.json file, one fetches a list of all messages the modem received every second, while another goroutine posts a message to the modem every 10 seconds.

The first goroutine functions in the same way as described in the example above. It sends a POST request to the server, reads the received response body, then returns the access token string. This string will be used in further data processing, so it is required for proceeding with the program. If one is fetched, the routine sleeps for 59 minutes since the token expires in 60 minutes. Otherwise, it keeps trying to fetch the data until successful since it is implemented in an infinite for loop.

func setToken() {
	for {
		token = o2.Authorize()
		/* Sleep for 59 minutes before reset */
		if token != "" {
			time.Sleep(59 * time.Minute)
		}
	}
}

The second goroutine fetches SMS messages from the specified modem (set to ppp0). It does so using the function MakeROPCRequest() with the URL http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages and fetched access token as parameters. This function is called from the attached request.go package.

func MakeROPCRequest(urlConn string, accessToken string) []byte {
	httpClient := &http.Client{}
	req, err := http.NewRequest("GET", urlConn, nil)
	if err != nil {
		return nil
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil
	}
	defer resp.Body.Close()

	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil
	}
	return responseBody
}

This function creates a new http.Client and creates a GET request to the specified URL, adding the sent accessToken parameter to the header for resource authorization. The client then processes the request and reads the response body it receives from the server, returning the read object.The message is parsed into a modemMsg variable which is of the following structure:

type modemMsg struct {
	Index   int    `json:"index"`
	Sender  string `json:"sender"`
	Content string `json:"content"`
	Time    string `json:"time"`
	Pdu     string `json:"pdu"`
}

The message sent from the server has an index field, contains the sender's phone number, the content that was sent to the device, the timestamp of the event and the PDU of the messaging. The server returns an array of SMS objects that are then stored in a modemMsg variable.

err := json.Unmarshal(o2.MakeROPCRequest("http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages", token), &msg)

This message is then printed. This process is repeated every two seconds, meaning that all modem messages are displayed by calling the REST API each 2 seconds.NOTE:_ For a message to appear in the list of message objects, the modem first needs to be sent a message. This can be done with any device with a SIM that can send messages by sending a message addressed to the TDC-E's SIM's phone number._The third goroutine is used for posting SMS messages to a specified SIM number, and is done only once. This goroutine calls the postMessage() function to complete its assigned task. The function is implemented as below:

func postMessage() {
	time.Sleep(5 * time.Second)
	if token != "" {
		// insert phone number here
		phoneNumber := "+XXXXXXXXXXXX"
		content := "hello world"
		url := "http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages"

		// setting values
		var poster postMsg
		poster.PhoneNumber = phoneNumber
		poster.Content = content

		// setting up a JSON object from created object
		jsonData, err := json.Marshal(poster)
		if err != nil {
			handleError(err)
		}
		o2.PostROPCMessage(url, token, jsonData)
	} else {
		fmt.Println("Token wasn't fetched. Try again.")
	}
}

Upon entering the postMessage() function, the service is first told to sleep for 5 seconds as assurance that the access token is fetched before the continuation of the message posting. If the token is still an empty string, the function types that the token isn't fetched and the function will exit. If the token has been fetched successfully, a phoneNumber, content, and url variables are initialized.

NOTE: The phoneNumber variable in the code is a placeholder. To send the message to a SIM device, change the variable to the corresponding number.

A variable poster is then created and parameters are set before it is parsed into a JSON object. This object is then sent to the SIM with the specified phone number through the PostROPCMessage() function located inside the request.go package.

Find the implementation of this function below.

func PostROPCMessage(url string, accessToken string, msg []byte) {
	var err error

	httpClient := &http.Client{}
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(msg))
	if err != nil {
		fmt.Println("Error preparing request: ", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+accessToken)

	res, err := httpClient.Do(req)
	if err != nil {
		fmt.Println("Cannot process request: ", err)
	}
	defer res.Body.Close()

	if res.StatusCode == 200 {
		fmt.Println("Message successfuly sent!")
	} else {
		fmt.Printf("Message could not be sent. Status code: %d\n", res.StatusCode)
		fmt.Println("response Headers:", res.Header)
		body, _ := io.ReadAll(res.Body)
		fmt.Println("response Body:", string(body))
	}
}

This function creates a new http.Client and calls a new POST request, providing the correct URL and message parameter. Headers for Content-Type and Authorization are set. Then, the client processes the request. If the status code is 200, the program print that the message was successfully sent.

If the phone number was inserted correctly, the device the modem is sending to should now receive the message set in the program. Otherwise, the application print out the received status code, headers and body.

The goroutine is then closed, and the program continues to fetch the data.

5.2.2. Sending and Receiving Messages (Node-RED)

In this section, a program with a similar function will be implemented using Node-RED. The flow that will be used for this functionality looks the following:

For the application to work, the following nodes are required:

  • an inject node
  • an oauth2 node (node-red-contrib-oauth2)
  • a function node
  • a http request node
  • a json node
  • a debug node

First, we will discuss fetching messages that were sent to the TDC-E's modem. To do so, the GET SMS group inside the flow is used. to start the message fetch, use the inject node labeled timestamp. This message will send a request for an oauth2 token with the grant type Password, providing the Acess Token URL, username, password, Client Id and Client Secret parameters to do so. The access token is extracted in the get-msgs function and an authorization header is set.

An http request with the GET method is then sent to the specified URL, and the server returns an array of json objects that contain the messages sent to the modem. Those are then displayed in the debug panel on the right side of the screen.

For sending an SMS to another device, the SEND SMS group inside the flow is used. The inject node and oauth2 node are identical to the nodes described above. The set-msg-parameters function is important to note.

var token = msg.oauth2Response.access_token;
// insert phone number here
var phoneNumber = "+XXXXXXXXXXXX";

msg.headers = {
    "authorization": "Bearer " + token,
    "Content-Type": "application/json",
    "accept": "*/*"
};
msg.payload = {
    "phoneNumber":phoneNumber,
    "content":"hello world"
};
return msg;

This is the implementation of this function. For it to work. The token is extracted from the oAuth2Response, and a phone number of the device you want to send a message to needs to be provided here. Authorization, Content-type and Acceptance headers are set, and then a msg.payload is created. This payload will be sent in the body of our POST request made to the modem. It needs to contain a valid phone number and SMS content, which will be the message body. A POST request will then be sent to the address http://192.168.0.100/devicemanager/api/v1/networking/modem/ppp0/sms/messages. The device with the corresponding phone number should now receive a message with the same text as what was written in the program.

6. RS-232

Go Application

Node-RED Application

In this section, working with RS-232 websockets, meaning fetching data from the TDC-E and sending data to the TDC-E device, will be described. Firstly, the implementation in the Go programming language will be shown. Next, the Node-RED application will be discussed.

6.1. Working with Websockets (Go)

A simple Go application was made to demonstrate working with RS 232 by using websockets. The Go program is comprised of a main.go file which server as the main application, and a websocket.go package that is set up to connect to, listen on and send data to a specified websocket.

The main.go file has a main function that first makes a wait group that is used to create a goroutine. A websocket is opened using the following function:

conn, err := websocket.OpenWebsocket("ws", "192.168.0.100:31768", "/ws/tdce/rs232/data")
if err != nil {
	fmt.Println("Error opening websocket: ", err)
}
defer conn.Close()

The websocket is opened with a default websocket dialer after the needed parameters are passed to a url object.

func OpenWebsocket(scheme, host, path string) (*websocket.Conn, error) {
        serverUrl := url.URL{
		Scheme: scheme,
		Host:   host,
		Path:   path,
	}
        conn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
	if err != nil {
		log.Fatal("Error connecting to WebSocket: ", err)
	}
        return conn, nil
}

The defer conn.Close() makes sure the connection closes after all operations that use this connection are done.

A new goroutine is then established. It is implemented in an internal, nameless function and works with an infinite loop that fetches data from the websocket whose connection we opened. It receives any string that is sent to the websocket and prints it into the terminal.

Below is the code for listening on a websocket. The following code fetches only the last message to be sent to the websocket, which is why it is implemented in a infinite for loop in the main package. This code receives a websocket connection to work with. The message is read with ReadMessage() available with the github.com/gorilla/websocket GitHub package and returns either the data in []byte format or an error.

func ListenOnWS(conn *websocket.Conn) ([]byte, error) {
	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("Error reading message: ", err)
		return nil, err
	}
	return message, nil
}

The for loop is implemented in the following way:

for {
	msg, erro := websocket.ListenOnWS(conn)
	if erro != nil {
		fmt.Println("Error fetching data: ", erro)
		break
	}
	receivedString := string(msg)
	fmt.Printf("Received string: %s\n", receivedString)
}

To demonstrate sending data to websockets, a single message is sent to the websocket by using another function available in the websockets.go package. The part of code that sends the message to the websocket we are connected to is the following:

/* Sending data to websocket */
/* Specify data here... */
message := "data"
if err := websocket.SendToWS(conn, []byte(message)); err != nil {
	log.Println("Error sending message: ", err)
}

This particular websocket expects a simple message that is of type string. In the program, this message is "data", and will be sent to the websocket after casting it to []byte. The websocket uses github.com/gorilla/websocket to write a message to the specified websocket connection. If the message is handled properly, the program will print "Message received." and return nil, otherwise it will print the error message. See the code snippet for sending data to the websocket below:

err := conn.WriteMessage(websocket.TextMessage, message)

It is important to note that the message that is sent to the websocket will be in the same format as it is in the code and there will be no type conversion, unlike when sending the data directly to the TDC-E device. See the implementation of a simple direct access RS 232 program below in section 2.2. Direct Data Access (Go).

6.2. Working with Websockets (Node-RED)

The Node-RED application works with the following nodes:

  • websocket in
  • websocket out
  • inject
  • debug

It is a relatively simple application that listens to ws://192.168.0.100:31768/ws/tdce/rs232/data via the websocket in node, then sends that data to the debug node it is connected to, displaying the data in the right Debug panel. All data that is sent to the websocket will be displayed here.

Sending data to the websocket is implemented so that the send-data node is clicked. This node has a text message in its msg.payload property. The message is then sent to the websocket in node.

7. 1-Wire

Go Application

Node-RED Application

In this section, working with websockets, meaning fetching data from the TDC-E and sending data to the TDC-E device, will be described. Firstly, the implementation in the Go programming language will be shown. Next, the Node-RED application will be discussed.

7.1. Working with Websockets (Go)

A simple Go application was made to demonstrate working with 1-Wire by using websockets. The Go program is comprised of a main.go file which server as the main application, and a websocket.go package that is set up to connect to, listen on a specified websocket.

The main.go file has a main function that first makes a wait group that is used to create a goroutine. A websocket is opened using the following function:

conn, err := websocket.OpenWebsocket("ws", "192.168.0.100:31768", "/ws/tdce/onewire/data")
if err != nil {
	fmt.Println("Error opening websocket: ", err)
}
defer conn.Close()

The websocket is opened with a default websocket dialer after the needed parameters are passed to a url object.

func OpenWebsocket(scheme, host, path string) (*websocket.Conn, error) {
        serverUrl := url.URL{
		Scheme: scheme,
		Host:   host,
		Path:   path,
	}
        conn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
	if err != nil {
		log.Fatal("Error connecting to WebSocket: ", err)
	}
        return conn, nil
}

The defer conn.Close() makes sure the connection closes after all operations that use this connection are done.

A new goroutine is then established. It is implemented in an internal, nameless function and works with an infinite loop that fetches data from the websocket whose connection we opened.

Below is the code for listening on a websocket. The following code fetches only the last message to be sent to the websocket, which is why it is implemented in a infinite for loop in the main package. This code receives a websocket connection to work with. The message is read with ReadMessage() available with the github.com/gorilla/websocket GitHub package and returns either the data in []byte format or an error.

func ListenOnWS(conn *websocket.Conn) ([]byte, error) {
	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("Error reading message: ", err)
		return nil, err
	}
	return message, nil
}

The code fetched from the websocket is then mapped to an array of variables of the following struct type:

type Wire1 struct {
	Family         interface{} `json:"Family"`
	FamilyAsString string      `json:"FamilyAsString"`
	FullPath       string      `json:"FullPath"`
	Id             interface{} `json:"Id"`
	IdAsString     string      `json:"IdAsString"`
	LastSeenTime   string      `json:"LastSeenTime"`
	DeviceDetails  string      `json:"DeviceDetails"`
}

After that, the code then prints all items of the wire1 variable. This happens indefinitely, as it is set in an infinite for loop. See the complete function for this functionality below:

for {
		msg, err := websocket.ListenOnWS(conn)
		if err != nil {
			fmt.Println("Error fetching data: ", err)
			return
		}

		/* 1wire object */
		var wire1 []Wire1
		err = json.Unmarshal(msg, &wire1)
		if err != nil {
			fmt.Println("Error decoding JSON: ", err)
			return
		}
		for _, item := range wire1 {
			fmt.Printf("Received object: %+v\n", item)
		}
}

7.2. Working with Websockets (Node-RED)

The Node-RED application for fetching 1-Wire data works with the following nodes:

  • websocket in
  • inject
  • debug

It is a relatively simple application that listens to ws://192.168.0.100:31768/ws/tdce/onewire/data via the websocket in node, then sends that data to the debug node it is connected to, displaying the data in the right Debug panel. All data that is sent to the websocket will be displayed here.

If the wire is connected correctly, a data object of the following structure should be sent to the debug panel:

Family: 0
FamilyAsString: "10"
Id: 0
IdAsString: "000803676B81"
FullPath: null
LastSeenTime: "2023-09-19T08:22:09.0266736+00:00"
DeviceDetails: "33.8"

The sensor temperature the device returns is the value of the DeviceDetails key and signifies a temperature of 33.8°C.

Final Thoughts

The examples described here have been implemented as simple code snippets to demonstrate the usage of the described interfaces. Still, even with more complicated application needs, often times the same techniques are used, so it is beneficial to take a look at the way they work. Since most of the data is fetched from websockets and/or REST APIs, the process will mostly look the same each time one works with fetching and storing data this way, with differences in data structure and usage logic.

Make sure to check out TDC-E Configuration to set up your working environment properly before implementing the described logic. Happy coding!