Skip to content

[Examples] Development Documentation

tkrzielSICKAG edited this page Feb 27, 2024 · 15 revisions

In this section, project examples are described and explained. The examples shown here contain larger, more complex files which were developed specifically for the TDC-E device. The following examples are described:

1. MQTT Node-RED GPS Application

Link to Application Files

This application uses Node-RED to set up a connection to a MQTT broker and fetches GPS data via a dedicated websocket continuously to write the data to the TDC-E's internal memory.

1.1. Environment Setup

To run the application successfully, the following interfaces and services need to be set up:

  • GPS
  • MQTT

If help is needed for setting up GPS, refer to the TDC-E Interface Configuration for GPS. The MQTT broker in the application is not set, as an MQTT server is needed. The server can easily be set up inside the Node-RED application by clicking on the mqtt node and adding a new broker address. For help with setting up the MQTT broker, refer to this setup page.

1.2. Application Implementation

The application that writes messages into a file, sends the message to an MQTT broker and gets GPS data is made with the following nodes:

  • websocket in
  • function
  • debug
  • input
  • write file
  • mqtt out

A screenshot of the application design is provided below:

image

The programs listens to two inputs: those are a websocket out node that continuously fetches GPS data and sets the flow's GPS variable to the current value, and an input node that listens on port 45000 for buffer input.

The add-gps-timestamp node then parses the data into a readable and functional format, and the object created by the function is written into the debug screen on the right panel of the screen. The object is optionally sent to the mqtt broker, whose parameters should be set by the user of the application. The MQTT broker address and QoS should be set.

NOTE: Node-RED does not process QoS 2 and QoS 3 and additional functionalities need to be implemented to that effect.

2. MySql Go DIO Application

Link to Application Files

The LED DIO application is an application that can read and write sensor status by checking a websocket that provides necessary information for DIO data. The application uses a TDC-E device, a sensor, and a LED, and utilizes websockets and a MySql database for its running. When the sensor senses an object in its field, the application waits for the duration of which the object was in the field, then turns on the connected LED which needs to glow a total of seconds that the object was in the field. If another object enters its field during the glowing, the application adds the duration of which the another object was in the field to the current duration time, keeping the LED light on for additional X seconds of time. After the LED stops glowing, the object is written into the MySql database.

2.1. Environment Setup

To run the application successfully, the following interfaces and services need to be set up:

  • DIO
  • MySql database
  • LED

The LED needed for the application is connected with the Digital Input Output (DIO) interface, so DIO setup is required before turning on the application. If help for DIO setup is needed, one can refer to the DIO section of the TDC-E Interface Configuration.

A MySql database and table are used to store all sensor data. The following data is measured and stored into the diobase database and into the dios table:

id - identification number of the object duration - duration of time the object is in the sensor's field of view whattime - timestamp of occurrence

For help on how to set up the MySQL service, please refer to the MySql Environment Setup.

2.2. Application Implementation

The DIO LED detection program for Go is implemented by creating three separate packages which are interconnected and work together to achieve the desired functionalities. This section describes the program implementation by separating the section into subsections that explain the code logic behind the created packages. Additionally, a bash file called diosend.bat will be described.

2.2.1. The main package

The main package imports the following modules:

  • bytes
  • database/sql
  • dioled/webapi
  • encoding/json
  • fmt
  • net/http
  • net/url
  • sync
  • time

The program is started in the main() function which operates by creating two wait groups, used for waiting for launched goroutines. Goroutines are lightweight threads which are managed by the Go runtime and must be synchronized since they use the same address space.

Two routines are started simultaneously:

  • a goroutine running the REST API
  • a goroutine running the main program

The main part of the program is located in the main.go file and continuously fetches the current state of the sensor. This is done by sending an authenticated request to a server, using an http clientfrom net/http.

func getDio(token, url string) Dio {
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Add("Authorization", "Bearer "+token)
	resp, _ := client.Do(req)

	var dio Dio
	json.NewDecoder(resp.Body).Decode(&dio)
	return dio
}

The client is set to create a new default http.Client{} struct and takes the address of the struct. A new HTTP Request and uses the GET method to send a request to the URL of the service, adds the authorization token, then waits for the response using client.Do. A new Dio variable is created and a JSON decoder is used to decode the response body and set it to the variable before returning it.

If the sensor is on (an object enters the sensor's field), and time has not started counting yet, the program begins counting time. Then, once the sensor is off, time is stopped and the program calculates time elapsed between the sensor registering the object and the object leaving its field. A lock from sync is used to assure that different routines do not access the total glowing time variable at the same time, the elapsed time is appended to the glowing time, and the resource is then unlocked.

// lock so only one routine may update the value

totalGlowingTimeLock.Lock()
totalGlowingTime += elapsed
totalGlowingTimeLock.Unlock()

Afterwards, the LED starts glowing, which is done by sending a JSON string to the server via POST method.

Remaining glow time is calculated and the routine sleeps for a millisecond to ensure the process is updates as soon as possible. If the total glowing time is larger than 0, the total duration of LED glowing time is inserted into the database, ensuring the variable is locked once again as it is reset to 0.

To insert into the database, the database is prepared for connections with the following line of code:

db, err := sql.Open("mysql", "root:TDC_arch2023@tcp(192.168.0.100:3306)/diobase")

An SQL query is created and the routine is completed.

2.2.2. Webapi package

The other goroutine that is started is running a REST API Web Service. To run, it imports the following modules:

  • database/sql
  • fmt
  • log
  • net/http
  • dioeld/middleware
  • github.com/gin-gonic/gin
  • github.com/gin-contrib/cors

The REST API is created using the gin module, which sets up a default router, sets up a GET website on which the values will be presented, and fetches all DIO objects from the diobase database.

Additionally, the service uses cors which currently enables communication with the "http://localhost:8201" and "http://192.168.0.100:8201"origin. If other origins should be able to communicate with the program, add them to the slice.

config := cors.DefaultConfig()
config.AllowOrigins = []string{"http://192.168.0.100:8201", "http://localhost:8201"}
outer.Use(cors.New(config))

As an added security measure, an IP white list was implemented through the middleware package. When any whitelisted page runs on port 6001, the application shows the fetched data in JSON format from the database.

The package is accessed through the InitializeWebAPI() function which is written with a capital first letter to be accessible by other packages. All other functions are private.

To see the REST Web API data, go to http://192.168.0.100:6001/api/v1/detection when running the application. To see structured data on a graphical user interface (GUI), go to the gui folder, then click on index.html to show a simple HTML page that provides data it fetches from the REST API.

2.2.3. Middleware Package

The middleware is a small package that uses the following modules:

  • net/http
  • github.com/gin-gonic/gin

It consists of a single function called IPWhiteList() which takes in a map of trusted web addresses, and if the IP address that is trying to access the server isn't one of the whitelisted addresses, the program aborts the process and exits with a forbidden status.

This way, only IP addresses that are written to be whitelisted can view the data. The current implementation lists the following IPs as whitelisted:

var IPWhitelist = map[string]bool{
	"127.0.0.1":     true,
	"192.168.0.100": true,
}

To add another IP address to the list, write it down into the dictionary. The dictionary is located in webapi.go.

3. MQTT MySql Go GPS Modem Application

Link to Application Files

This is the documentation section for the MQTT MySql Go GPS Modem Application for the TDC-E device. In it, the usage of the program is described, an environment setup description is provided and implementation is discussed.

The application tracks the current GPS location of the TDC-E, fetching the coordinates of the vehicles every X seconds, then publishing this data in a message to a MQTT broker with Quality of Service (QoS) 2. Since the TDC-E device's connection to remote server can be blocked at any time during travel, the MQTT broker ensures that the data is safely stored for some period of time as it resends the unsuccessfully published data to the broker after getting a connection. The storage size and number of unpublished messages of the MQTT buffer depends on the client device and user's configurations, but any incoming unpublished messages after the buffer is filled will override older unpublished messages. To bypass this problem, a MySql database is added into which all unpublished messages after 5 minutes are written.

When the device gets back online, the broker will resend the data in the buffer automatically, before publishing all data that is stored in the MySql database.

3.1. Environment Setup

To correctly run the applications described below, an initial setup of the working environment is needed. The following interfaces and application setup are needed to run the programs:

  • GPS
  • MQTT
  • MySql Database

Regarding GPS, a GPS antenna needs to be set up, enabled and connected to the TDC-E. An MQTT broker is needed to be able to store the messages that are published to it. For modem and gps data, websocket and REST API data respectively is needed. The MySql database structure and a link to its setup is described in further detail below. If help is needed for setting up GPS, refer to the TDC-E Interface Configuration for GPS. For help on how to set up the MySQL service, please refer to the MySql Environment Setup.

The MySql database used in the application has the following structure:

image

The Id field is the identifier of the MQTT message object and is used to identify a specific database object, as the messages are deleted from the database using this identifier. It is a positive integer which is auto incremented with each new value.

The Altitude column is a float value that signifies the altitude of the object that the gps fetches. The Course attribute is a variable string character size 45 (varchar(45)). Fix points to the strength of the GPS object and can be the value od 0, 1, or 2, with 1 being the strongest, two meaning the object has a minimum of 3 satellites, and 0 being no satellites present or picked up. It is an integer and also part of the GPS object. GpsFixAvailable is a tinyint(1) type, which translates to boolean. It tells whether Fix is available. Hdop, Latitude and Lognitude are GPS object attributes and are the float type. The NumberOfSatellites attribute tells how many satellites are available from the current object position and is the type integer. SpeedKnots and SpeedMph are GPS object attributes of the float type and signify speed. The TimeSt is also part of the GPS object and tells the timestamp the object is returned.

Rssi is an attribute that is fetched from the modem data object and shows the strength of the signal. It is an integer. DataLinkType is a varchar(45) attribute that tells the link type of the modem used. Lastly, a Rfid attribute of the varchar(45) type is provided. This column represents the Radio Frequency Identification which the program will be getting from a client device.

To create this database, the following code was used:

CREATE TABLE mqtt (
    Id int PRIMARY KEY AUTO_INCREMENT,
    Altitude float,
    Course varchar(45),
    Fix int,
    GpsFixAvailable bool,
    Hdop float,
    Latitude float,
    Longitude float,
    NumberOfSatellites int,
    SpeedKnots float,
    SpeedMph float,
    TimeSt varchar(45),
    Rssi integer,
    DataLinkType varchar(45),
    Rfid varchar(45) 
);

3.2. Application Implementation

The application encompasses publishing messages continuously to a MQTT broker, getting websocket GPS data, getting REST API modem data, storing unpublished MQTT messages into a database and deleting them from the database once a connection is reestablished. For that end, an application in the programming language Go was created and will be described in detail.

3.2.1. Application Structure

The program is comprised of the following .go files:

  • main.go - the main package of the application
  • db.go - a package for connecting, modifying and selecting values from a mysql database; the data table used is of a specific structure so if changes are necessary, this package will have to be modified
  • mqttset.go - a package for creating a client, connecting to a MQTT broker, publishing messages to said broker and subscribing to a topic
  • request.go - a package for working with OAuth2.0 authorization and requests; expanded to fetch modem data
  • websockets.go - a package for creating a connection to, listening on, and writing to a websocket

The Go application also uses a params.json file which contains OAuth2.0 configuration data used to fetch the authorization token for accessing modem data. This file should not be removed as the program fetches all needed configuration data from it.

3.2.2. Application Logic

In this section, the application logic will be described. We will start with explaining what the main.go package does, and will be describing other packages as their methods are called in the app.

3.2.2.1. Setting up the Environment

Firstly, all necessary global parameters are set and a connection to the database is opened. This is done by providing a database management system (in this case mysql) and a connection string of the following structure:

root:TDC_arch2023@tcp(192.168.0.100:3306)/gpsmqtt

The connection string tells us the following information:

  • the user is root
  • the password for the user is TDC_arch2023
  • a tcp connection is used
  • the address 192.168.0.100 is accessed
  • the port for the service is 3306 (which is the default MySql port)
  • the database that is used is called gpsmqtt

The application connects to the database using the following code:

func Connect(dbms string, connectionString string) *sql.DB {
	db, err := sql.Open(dbms, connectionString)
	if err != nil {
		fmt.Println("Couldn't connect to SQL database: ", err)
	}
	return db
}

Then, defer conn.Close() is used to close this connection once all operations with the connection are done. The program then creates a client using defined parameters, then connects to the MQTT broker using the following code:

func ConnectClientToBroker(client mqtt.Client) {
	if token := client.Connect(); token.Wait() && token.Error() != nil {
		fmt.Println("Error connecting to broker: ", token.Error())
		os.Exit(1)
	}
}

This function checks whether the client connects to the broker. If there is an error with the connection, the function will print out an error and exit the application. The application needs to connect to the broker first to be able to access the broker, as it stays connected even when publishing isn't possible.

It is also important to note that the configuration of the client needs to be correct so the client can connect to the broker. A correct brokerAddress thus needs to be provided. The application also sets up a clientId and username for the broker to be able to identify which machine is sending the data.

Next up, two timers are created. One is a messageTimer. This timer is set to time 5 minutes and is tied to message publishing. If no message is published for 5 minutes, this timer will indicate that the client can't connect to the broker at the moment, and will start writing the incoming messages to the database. If a message is published, the timer resets. The other timer is called dbTimer. This timer is set to time 30 minutes. Each 30 minutes, this service checks whether anything is stored inside the database, then tries to publish the stored messages after converting them into the correct format. After every check, the timer is reset so that the process can begin anew.

Next up, a sync.WaitGroup is set up, and 4 goroutines or lightweight threads are created to run simultaneously. They are added to the WaitGroup, and the services wait until each is finished before the program exits. The next sections describe the goroutines in greater detail.

3.2.2.2. Setting the Authorization Token

To fetch the modem data of the TDC-E device, a REST API call to the device needs to be made. An OAuth2.0 authorized call needs to be made to get the requested parameters, since it is a call to the TDC-E's Swagger page. The token that is generated with an API call, however, expires every 60 minutes, so a goroutine for periodically fetching the token data was created.

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

This is the structure of the code for fetching the token. The token is generated with the method Authorize() which will be described in further detail in a latter section, and if the token isn't an empty string, which happens when the application cannot fetch the token, the goroutine is made to sleep for 59 minutes. This is done so that the program tries to fetch the new token continuously when the website cannot be accessed, as waiting for an hour without access to a token could prove problematic.

The Authorize() function firstly creates OAuth2.0. configuration data by opening the aforementioned params.json file. This is done like so:

func createOAuth2Config() (oauth2.Config, url.Values) {
	jsonFile, err := os.Open("params.json")
	if err != nil {
		fmt.Println("Error opening config file: ", err)
	}
	defer jsonFile.Close()
	return setConfValues(jsonFile)
}

The setConfValues() method reads the jsonFile and sets the following parameters:

cfg := oauth2.Config{
		ClientID:     conf.ClientId,
		ClientSecret: conf.ClientSecret,
		Endpoint: oauth2.Endpoint{
			TokenURL: conf.TokenEndpoint,
		},
	}
	
username := "XXX"
password := "XXX"

NOTE: The parameters set in params.json should be changed for access to the values to work. A valid username and password is needed.

After fetching the configuration data, a new httpClient is made, which makes a POST request to the TokenURL defined in params.json. The needed headers are set, and so is basic authentication. Then the client processes the response, and if the status code is 200, the application continues by reading the response body and mapping the access_token string to a local variable which is then returned to the main program. Find the code for the implementation of this functionality below:

func Authorize() string {
	cfg, data := createOAuth2Config()
	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)

	resp, err := httpClient.Do(req)
	if err != nil {
		fmt.Println("Error making request:", err)
		return ""
	}
	defer resp.Body.Close()

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

	response, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Error reading response:", err)
		return ""
	}

	var responseMap map[string]interface{}
	if err := json.Unmarshal(response, &responseMap); err != nil {
		fmt.Println("Error decoding JSON response: ", err)
		return ""
	}

	accessToken, ok := responseMap["access_token"].(string)
	if !ok {
		fmt.Println("Access token not found in response")
		return ""
	}

	return accessToken
}

As has already been stated, if the request is made successfully and the token is set, the program sleeps for 59 minutes before generating a token anew, as it expires after 60 minutes. If the access token can't be fetched and the program returns an empty string, the application keeps trying to fetch the token.

3.2.2.3. Fetching GPS Data

To fetch the GPS data needed for generating a publishable message, two GPS variables are created:

  • currentGps - the currently fetched GPS object
  • lastBestGps - the latest GPS object with a positive fix value

Both objects are structured in the same way:

type gps struct {
	Altitude           float32 `json:"Altitude"`
	Course             *string `json:"Course"`
	Fix                int     `json:"Fix"`
	GpsFixAvailable    bool    `json:"GpsFixAvailable"`
	Hdop               float32 `json:"Hdop"`
	Latitude           float32 `json:"Latitude"`
	Longitude          float32 `json:"Longitude"`
	NumberOfSatellites int     `json:"NumberOfSatellites"`
	SpeedKnots         float32 `json:"SpeedKnots"`
	SpeedMph           float32 `json:"SpeedMph"`
	Time               string  `json:"Time"`
}

The lastBestGps variable is used when the currentGPS value has a worse value than the lastBestGps so the GPS object parameters are as precise as possible.

These objects are fetched through a websocket call. find the implementation of this part of the application below:

func fetchGpsData() {
	conn, _ := ws.OpenWebsocket("ws", "192.168.0.100:31768", "/ws/tdce/gps/data")
	defer conn.Close()

	for {
		currentGps = getWsData(conn)
		/* If the fix of the currently fetched gps object is equal to or better than the value in lastBestGps, set lastBestGps to this current value */
		// 0 - no fix, 1 - best fix, 2 - ok fix
		if currentGps.Fix <= lastBestGps.Fix && currentGps.Fix != 0 {
			lastBestGps = currentGps
		}
	}

}

This function opens a websocket on the address ws:192.168.0.100:31768/ws/tdce/gps/data, then defers its closing. The function getWsData() calls websockets.go's ListenOnWs(), which fetches the current data sent to the websocket. This function looks the following:

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

	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("Error reading message: ", err)
		return nil, err
	}
	/* returns bytes from the websocket */
	return message, nil

}

The function listens on the websocket connection object, reads the latest message and returns it in []byte format. It is then mapped to a variable of the previously shown gps struct type, before it is returned to the fetchGpsData() function, setting the currentGps as the one that was fetched, and checking whether the lastBestGps should be updated. In this case, the value is updated if the following logical expression is true:

currentGps.Fix <= lastBestGps.Fix && currentGps.Fix != 0

This is done so since 0 means no GPS fix is available, and since 2 is the strongest GPS fix.

3.2.2.4. Client Simulation Application

To simulate a TCP client, a small application for sending messages to a TCP server was created. This application can be found under the name main.go inside the client-simulation folder.

This file connects to a TCP connection, set to localhost:5247, using the net package, and sends it data that is set to "Hello, there!". This message is then sent to the server by writing data to the set-up connection.

3.2.2.5. Serving TCP Port

To fetch the RFID mentioned previously in the document, a TCP server is created. This server listens to any incoming messages, and upon receiving a message, creates a message to be published to the MQTT broker, setting all necessary data.

The goroutine responsible for serving calls the following function:

func serveTcp() {
	listener, err := net.Listen("tcp", "localhost:5247")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer listener.Close()

	fmt.Println("Server is listening on port 5247")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error:", err)
			continue
		}
		go handleClient(conn)
	}
}

This function creates a listener variable which is set to listen on localhost:5247 via TCP protocol. The closing of the connection is deferred so that the application only cleans the listener once all needed operations are done. It implements an infinite loop which accepts incoming messages, printing errors if there are any and handling clients.

func handleClient(conn net.Conn) {
	defer conn.Close()
	buffer := make([]byte, 1024)
	clientDisconnected := false

	for !clientDisconnected {
		n, err := conn.Read(buffer)
		if err != nil {
			if err == io.EOF {
				clientDisconnected = true
			} else {
				fmt.Println("Error:", err)
			}
		}
		if n > 0 {
			fmt.Printf("Received: %s\n", buffer[:n])
			rfid = string(buffer[:n])
			fetchModemData()
			msge, err := createMessage(rfid)
			check(err)
			publishToMqtt(msge)
		}
	}
}

This is the function that is called once a client sends a message to the TCP server. For as long as the client doesn't disconnect, it reads the buffer from the client, and if it isn't empty, it stores the sent message as rfid. Then, modem data is fetched.

This is done by making a ROPC request to the Swagger URL responsible for returning full modem data. The returned JSON object should be of the following structure:

type modemFull struct {
	GsmRegistrationStatus   string `json:"gsmRegistrationStatus"`
	UtranRegistrationStatus string `json:"utranRegistrationStatus"`
	AccessTechnology        string `json:"accessTechnology"`
	DataLinkType            string `json:"dataLinkType"`
	OperatorName            string `json:"operatorName"`
	SimStatus               string `json:"simStatus"`
	LatestError             string `json:"latestError"`
	Imei                    string `json:"imei"`
	Ccid                    string `json:"ccid"`
	Imsi                    string `json:"imsi"`
	Rssi                    int    `json:"rssi"`
	Ip                      string `json:"ip"`
	Gateway                 string `json:"gateway"`
	Dns1                    string `json:"dns1"`
	Dns2                    string `json:"dns2"`
	LocalIp                 string `json:"localIp"`
	RemoteIp                string `json:"remoteIp"`
	Segment                 string `json:"segment"`
	SegmentType             string `json:"segmentType"`
	Persist                 bool   `json:"persist"`
	Name                    string `json:"name"`
	Type                    string `json:"type"`
	Enabled                 bool   `json:"enabled"`
	State                   string `json:"state"`
}

For now, the modem data that is displayed in the published message is only DataLinkType and Rssi, but the program can easily be modified to store additional data about the modem. This request is made by sending the needed access token fetched beforehand and the URL of the service to the getModemData function. Find the implementation of this function below.

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

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

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

The function first sets up a HTTP Client and makes a GET request to the provided URL. Authorization is set by adding a header and setting the value to "Bearer " + accessToken. The request is read and returned as an array of bytes that is then mapped to a variable of the modemFull struct type. The needed modem data is set.

Next, the goroutine creates a message by setting all needed data. The following code shows the structure of the message that will be published to the MQTT.

type messageObject struct {
	Rfid      string `json:"Rfid"`
	Gps       gps    `json:"Gps"`
	ModemData modem  `json:"ModemData"`
}

Then, the data is published to the MQTT broker. This is the structure of the publishToMqtt function:

func tryToPublish(msge []byte) {
	success := mq.PublishMessage(topic, msge, client, byte(quos))
	if success == 1 {
		messageTimer.Reset(5 * time.Minute)
	}
}

func publishToMqtt(msge []byte) {
	select {
	case <-messageTimer.C:
		modifyDB(conn, "INSERT", msge, 0)
		tryToPublish(msge)
	default:
		tryToPublish(msge)
	}
}

The function has two possible cases: the first is that the message timer has run out, and the message is inserted into the mqtt data table, as in five minutes, the buffer might start overwriting older messages. The first case then tries to publish the message, and if it succeeds, the timer is reset, and the messages aren't sent to the database anymore. By default, the message immediately tries to be published, resetting the timer if the publishing is successful.

The publishing of the message is done in the following way: a topic, message, client and quality of service is chosen, and a success channel is made to signify whether the message has been published.

An internal function is then created. The token variable is used for that matter. The github.com/eclipse/paho.mqtt.golang's built in Publish() method is used for publishing a message to the set broker. A WaitTimeout() is set to wait for 5 seconds before cancelling the published message. If the message is published in that interval, the application prints out that the message is published and returns 1 to signify success, and if the timeout runs out, the message isn't published and the successChan is returned a 0.

3.2.2.6. Checking Database

The last goroutine that is run by the application is one that checks whether the database contains data, converts the database objects to message objects that can be worked with in Go, then tries to publish the messages that have been queued in the database one by one. If a message is successfully published, it deletes this message from the database, and if not, the object remains stored in it.

After checking all messages, the timer is reset to 30 minutes to schedule the next checkup of whether the database contains new data.

The code for the described scenario is written below:

func checkDatabase() {
	for {
		select {
		case <-dbTimer.C:
			queuedMessages := db.SelectValues(conn, "SELECT * FROM mqtt")
			messageObjects := convertToMessageObjects(queuedMessages)

			for i, msg := range messageObjects {
				jsonData, err := json.Marshal(msg)
				if err != nil {
					fmt.Println("Error marshaling JSON: ", err)
					return
				}
				success := mq.PublishMessage(topic, jsonData, client, byte(quos))
				if success == 1 {
					modifyDB(conn, "DELETE", nil, queuedMessages[i].Id)
				}
			}
			dbTimer.Reset(30 * time.Minute)
		}
	}
}

4. Reboot Node-RED DIO Application

Link to Application Files

The reboot Node-RED DIO Application is a simple project that demonstrates how to reboot your device. It does so by accessing the device manager and sending a PUT request to the TDC-E. There are two program files inside the linked folder:

  • DIO restart.json
  • api reboot.json

The api reboot.json file is a Node-RED program that reboots the entire TDC-E device by clicking on a button. The DIO restart.json file uses the API file, but adds a layer of logic as it automatically reboots the device as soon as a digital output's value is set from 0 to 1. If the program reads a 1 from the DIO, it sends a PUT request to the server and the device is rebooted as soon as the request is made.

4.1. Environment Setup

To run the application successfully, the following interfaces and services need to be set up:

  • DIO

In this example, DIO A is used, which corresponds to the pin gpio496. For help with setting up the TDC-E interface configuration, refer to this page.

4.2. Application Implementation

In this section, application implementation is documented. Firstly, the api reboot.json is discussed. Then, DIO restart.json is described as the main focus of this section.

4.2.1. The api reboot.json Flow

In this flow, rebooting is done through sending a PUT request to the TDC-E's Device Manager Swagger. To that end, authentication and authorization is needed, and the following nodes should be downloaded via Manage palette or npm before proceeding with the application:

  • node-red-contrib-oauth2

The program's structure is the following:

  • an inject node
  • an oauth2 node
  • a function node
  • a http request node
  • a debug node

The image below shows how the application appears in the editor.

image

The oauth2 node, as previously stated, connects to Swagger and makes an authorization request. The needed parameters are already set. Once the node returns an authorization token, the program uses the function node to set the headers and payload of the message, as the PUT request needs to be structured in a specific way to accept the request for rebooting the device. Once all set, the message is sent into the http request node, and the TDC-E's reboot should soon begin.

To start the reboot process, only a click on the timsestamp button is needed.

4.2.2. The DIO restart.json Flow

In this flow, rebooting is done after the value of the DIO device changes from 0 to 1. Once this happens, a PUT request is sent to the TDC-E's Device Manager Swagger. To that end, authentication and authorization is needed, and the following nodes should be downloaded via Manage palette or npm before proceeding with the application:

  • node-red-contrib-oauth2

The program's structure is the following:

  • an inject node
  • an exec node
  • an oauth2 node
  • 4 function nodes
  • a http request node
  • 2 debug nodes

The image below shows how the application appears in the editor.

image

The program does not need to be run manually, as it starts as soon as the Node-RED container is up. The 7inject` node reads the value of the connected DIO device (set as DIO A) using the command:

cat /sys/class/gpio/gpio496/value 

If another GPIO is used as the digital output, the file path the program looks up should change according to your DIO pin.

The value fetched from the exec node is now parsed, and the state of the DIO is checked. If the payload that we fetched from the terminal command is equal to 1 and the state as of now is "OFF", which is "OFF" by default, the msg.payload is set to the simple keyword "yes". If this condition is not true, the payload is set to a "no". Now, the message is forwarded to the next node. The next node checks the restart condition. If the value of the payload is equal to "yes", the flow continues, and the last part of the code is the same as the api reboot.json file.

The oauth2 node connects to Swagger and makes an authorization request. The needed parameters are already set. Once the node returns an authorization token, the program uses the function node to set the headers and payload of the message, as the PUT request needs to be structured in a specific way to accept the request for rebooting the device. Once all set, the message is sent into the http request node, and the TDC-E's reboot should soon begin.

5. MQTT Go AIN Application

Link to Application Files

This application uses Node-RED to set up a connection to a MQTT broker and fetches GPS data via a dedicated websocket continuously to write the data to the TDC-E's internal memory.

5.1. Environment Setup

To run the application successfully, the following interfaces and services need to be set up:

  • AIN
  • MQTT

If help is needed for setting up AIN, refer to the TDC-E Interface Configuration for AIN. For help with setting up the MQTT broker, refer to this setup page.

5.2. Application Implementation

This is a relatively simple MQTT AIN example that relies on the github.com/gorilla/websocket and github.com/eclipse/paho.mqtt.golang GitHub packages to connect and send AIN data to a set MQTT broker. It also implements two local packages that were made using those libraries to help build needed functions for the application. The time package is used to fetch time.

This application works with a broker that is set up on the TDC-E and a remote (local) computer device as a client.

The application starts with connecting to the MQTT broker using the following parameters:

brokerAddress := "tcp://192.168.0.100:1883"
clientId := "clientest"

username := "user1"
password := "password"

The broker listens to port 1883 on our device and the username and password are set as such because the Broker server has been set up like the aforementioned Getting Started Mosquitto page.

A client is then created by using the following function:

/* Creates options for new mqtt client with password */
func CreateMqttClientPass(brokerAddress string, clientId string, username string, password string) mqtt.Client {
	opts := mqtt.NewClientOptions().AddBroker(brokerAddress).SetClientID(clientId).SetUsername(username).SetPassword(password)
	opts.OnConnect = connectHandler
	opts.OnConnectionLost = connectLostHandler
	client := mqtt.NewClient(opts)
	return client
}

This code creates options that add configuration of the MQTT. This adds a broker address and client ID, and then a username and password of the client that is connecting to the broker. A new client is created with the given options and returned to the main part of the code.

Then the client connects to the server, and a confirmation message is written. Then, a websocket is opened using the correct AIN parameters that will fetch a JSON object that stores current and previous AIN values. To fetch the AIN value, the fetchAinVal function is used. This function merely listens on the websocket using the following code:

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

	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("Error reading message: ", err)
		return nil, err
	}
	/* returns bytes from the websocket */
	return message, nil

}

The application reads the message by using conn.ReadMessage(), where the connection gets bytes that the message sends to it. The object is of the following structure:

{
	"AinName":"AIN_A",
	"PreviousValue":0.045,
	"NewValue":23.862
}

The type of this message is []byte, so it matches the required type of the MQTT message. In the last line of the code, we pass through an infinite for loop which continuously publishes AIN values that are fetched from the web socket. Then, the program sleeps for 10 seconds before once again fetching the current AIN value to send it to the MQTT broker.

for {
		mq.PublishMessage("ainval", fetchAinVal(conn), client, 0)
		time.Sleep(10 * time.Second)
	}

The PublishMessage looks the following:

func PublishMessage(topic string, message []byte, client mqtt.Client, quos byte) int {
	/* creating a success channel because of internal goroutine */
	successChan := make(chan int, 1)
	defer close(successChan)
	go func() {
		token := client.Publish(topic, quos, false, message)
		if token.WaitTimeout(5 * time.Second) {
			fmt.Printf("Published message: %s\n", message)
			successChan <- 1
		} else {
			/* Message is queued */
			fmt.Printf("Failed to publish message: %s. Message queued.\n", message)
			successChan <- 0
		}
	}()

	/* waiting for the goroutine to signal success or failure */
	success := <-successChan
	return success
}

This is essentially a function that uses a channel to see whether the message has been successfully published. The message that is published needs the following:

  • a topic
  • the message in []bytes
  • the client
  • the quality of service qos

The topic is the theme of the message that functions as a filter for the MQTT messages. The message is what is sent to the broker, by the client that we set up via options. Lastly, the quality of service specifies the level of delivery guarantee of the message. As quality of service is set to 0 for the time being, the message is sent and then promptly forgotten, meaning there is no extra mechanism that keep the message safe before publishing. The larger the level of the qos, the bigger the chance that no message packet gets dropped.