-
Notifications
You must be signed in to change notification settings - Fork 0
[HW Interface] Development Documentation
This is the code documentation for the examples created using the hardware interface. The following examples using the hardware interface are created:
- Digital Inputs/Outputs (DIO)
- Analog Inputs (AIN)
- Controller Area Network (CAN)
- Global Positioning System (GPS)
- Modem
- RS-232
- 1-Wire
Examples are created in Python and Go, and mostly also contain a Node-RED example of the same functionalities as reference.
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.
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.
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.
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.
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.
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.
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
.
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.
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}");
}
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.
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.
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.
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.
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
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.
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.
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:
The array positions [4:7] indicate the position data the sensor sends.
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.
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)
}
}()
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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)
}
}
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.
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!