-
Notifications
You must be signed in to change notification settings - Fork 0
Code Samples
Section covers code samples for working with various interfaces and TDC-X capabilities.
- Digital Input / Output Examples
- Analog Input Examples
- Serial Examples
- IMU Examples
- Temperature Sensor Examples
- Controller Area Network Examples
- GNSS Examples
- Networking Examples
- Wireless Personal Network Examples
In this section, the Digital Inputs/Outputs of the TDC-X device are discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. From it, the list of DIO devices, their input / output directions and states can be read and set. To that end, a simple Go application demonstrating said usage is created.
The application uses Golang's gRPC service to fetch and send data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
protoc --go_out=pkg/pb --go-grpc_out=pkg/pb pkg/dio.proto
This installs needed Protoc
files, exports the path and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please see instructions on how to generate an access token for the TDC-X here: gRPC Usage.
Once you've obtained the token, navigate to grpc-dio/pkg/auth/token.json
. Paste your generated token in the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The main application first creates a new gRPC client that connects to the TDC-X IP address on port 8081
. It does so by using the generated gRPC Go file specification.
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
To demonstrate DIO functionality, the application lists all available DIO devices, then reads the current state of DIO_A
before changing the state in an infinite for
loop. A different DIO can be set by modifying the code. These functionalities require the generated gRPC code.
To list all DIO devices, an empty protos
request is sent to the server. The client specifies the function ListDevices
to receive all devices from the TDC-X device, which are then printed to the user.
req := &emptypb.Empty{}
res, err := client.ListDevices(context.Background(), req)
After reading the DIO device set to DIO_A
in the example, a gRPC request for writing is created and sent to the gRPC server, setting the state to the opposite value.
switch currentState {
case "LOW":
nextState = 2
case "HIGH":
nextState = 1
default:
nextState = 1
}
req := &protos.DigitalIOWriteRequest{
Name: DIODevice,
State: nextState,
}
_, err := client.Write(context.Background(), req)
if err != nil {
log.Fatalf("could not write: %v", err)
}
Reading and setting values is implemented in an infinite for
loop which sleeps for two seconds before restarting the process.
The Proto file used for the Digital Input/Output service is the following:
/**
* DigitalIO Service.
*
* Service that enables a control of the Digital IO features of device
*/
syntax = "proto3";
package hal.digitalio;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
enum IOType {
INPUT = 0;
OUTPUT = 1;
BIDIRECTIONAL = 2;
}
enum IODirection {
IN = 0;
OUT = 1;
}
enum IOState {
ERROR = 0;
LOW = 1;
HIGH = 2;
}
message IODevice {
string name = 1;
IOType type = 2;
IODirection direction = 3;
}
/**
* Represents the ListDevices response data.
*/
message DigitalIOListDeviceResponse {
repeated IODevice devices = 1;
}
/**
* Represents the SetDirection request data.
*/
message DigitalIOSetDirectionRequest {
string name = 1;
IODirection direction = 2;
}
/**
* Represents the Read request data.
*/
message DigitalIOReadRequest {
string name = 1;
}
/**
* Represents the Read response data.
*/
message DigitalIOReadResponse {
IOState state = 1;
}
/**
* Represents the Write request data.
*/
message DigitalIOWriteRequest {
string name = 1;
IOState state = 2;
}
/**
* Represents the Attach response data.
*/
message DigitalIOAttachResponse {
string name = 1;
IOState state = 2;
int32 error = 3;
string timestamp = 4;
}
/**
* Service exposing DigitalIO functions.
*/
service DigitalIO {
/// Used to retrieve all available digital IO devices.
rpc ListDevices(google.protobuf.Empty) returns (DigitalIOListDeviceResponse) {}
/// Used to set pin direction value of a particular gpio pin.
rpc SetDirection(DigitalIOSetDirectionRequest) returns (google.protobuf.Empty) {}
/// Used to read value of a particular gpio pin.
rpc Read(DigitalIOReadRequest) returns (DigitalIOReadResponse) {}
/// Used to write value of a particular gpio pin.
rpc Write(DigitalIOWriteRequest) returns (google.protobuf.Empty) {}
/// Used to stream input events form device.
rpc Attach(google.protobuf.Empty) returns (stream DigitalIOAttachResponse) {}
}
To list all available Digital Input/Output services, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, see gRPC Usage.
To list all available Digital Input/Output calls, use the following line:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> list hal.digitalio.DigitalIO
The token
field is the fetched TDC-X authorization token. For help fetching this token, refer to gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to list all available Digital Input/Output services.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 list hal.digitalio.DigitalIO
The response should be in this format:
hal.digitalio.DigitalIO.Attach
hal.digitalio.DigitalIO.ListDevices
hal.digitalio.DigitalIO.Read
hal.digitalio.DigitalIO.SetDirection
hal.digitalio.DigitalIO.Write
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o dio-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/dio-grpc .
CMD ["./dio-grpc"]
Open a terminal and paste the following commands:
docker build -t dio-grpc-app .
docker save -o dio-grpc-app.tar dio-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as dio-grpc
.
RUN go build -o dio-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the dio-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/dio-grpc .
CMD ["./dio-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes the creation and usage of a Node-RED gRPC example using digital input / output devices. The application makes listing all DIO devices, reading DIO states, writing the DIO state and streaming changes in the DIO devices possible.
For implementation, the following nodes are used:
-
inject
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below:
A Lua application is provided as a DIO usage example. The example demonstrates setting a DIO device as output or input, setting an output state and reading DI states.
The script prints the engine version at the start of the script. It then creates a DIO AO
output and sets the value of the output to HIGH
.
dioAO = Connector.DigitalOut.create('DIO_AO')
dioAO:set(true);
print("Set DO A output to HIGH.")
To set a DO output to LOW
, uncomment the following lines:
dioAO:set(false);
print("Set DO A output to LOW.")
The script then sets three DIO devices to inputs (DIO_BI
, DIO_CI
and DIO_DI
). The state of said inputs is then read using the following function:
function PrintDIOStateIn(dio, name)
local state = dio:get()
print(string.format("%s state: %s", name, state))
end
Another way of reading the state is registering the DI onChangeStamped
, printing the DI state on event.
dioDI = Connector.DigitalIn.create('DIO_DI')
function Print_DIODI()
local state = dioDI:get()
print(string.format("State of DIO_DI is: %s", state))
end
Connector.DigitalIn.register(dioDI, "OnChangeStamped", Print_DIODI)
See the result of the application run below.
[15:55:53.721: INFO: AppEngine] Starting app: 'dio' (priority: LOW)
Engine version: 0.8.3
Set DO A output to HIGH.
DIO BI state: false
DIO CI state: false
DIO_DI state: false
Download the example code from the link below:
In this section, the Analog Inputs of the TDC-X device are discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. From it, the list of AIN devices and their current value can be read. The application also has an example of how to change the AIN mode which can be used if uncommented. To that end, a simple Go application demonstrating said usage is created.
The application uses Golang's gRPC service to fetch and send data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
protoc --go_out=pkg/pb --go-grpc_out=pkg/pb pkg/ain.proto
This installs needed Protoc
files, exports the path and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please see instructions on how to generate an access token for the TDC-X on gRPC Usage.
Once you've obtained the token, navigate to grpc-ain/pkg/auth/token.json
. Paste your generated token in the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The application starts by creating a new gRPC client that connect to the TDC-X address and the port 8081
. The client creation is tied to the Proto file and generated gRPC files. This is done using the following code:
conn, err := grpc.NewClient("192.168.0.100:8081", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := protos.NewAnalogINClient(conn)
The application then sends a request to the server that lists all AIN devices available on the device. This is done by sending an empty request with the protos
function ListDevices
defined in the generated Go files. The devices are printed to the terminal.
req := &emptypb.Empty{}
res, err := client.ListDevices(context.Background(), req)
if err != nil {
log.Fatalf("could not write: %v", err)
}
The program then enters an infinite for
loop that continuously checks the voltage or current value from the selected AIN device, sleeping for one second between each server call. The example is written using the AI_A
device. The request is created by providing the correct device name parameter, which is sent to the server.
req := &protos.AnalogInReadRequest{
Name: AINDevice,
}
res, err := client.Read(context.Background(), req)
if err != nil {
log.Fatalf("could not read: %v", err)
}
The response is printed to the terminal and contains the following values:
adcRaw
Converted
Unit
To use the function for changing modes from VOLTAGE
to CURRENT
or vice-versa, uncomment the changeAINMode
code blocks. The Mode
variable in the code is set to Current
. Set it to one of the following values:
AnalogInMode_Voltage
AnalogInMode_Current
The Proto file used for the Analog Input service is the following:
/**
* AnalogIn service.
*
* Service that enables a control of the AnalogIn features of device
*/
syntax = "proto3";
package hal.analogin;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
enum AnalogInMode {
Voltage = 0;
Current = 1;
}
enum AnalogInUnits {
V = 0;
mA = 1;
}
message AnalogInDevice {
string name = 1;
AnalogInMode mode = 2;
}
/**
* Represents the ListDevice response data.
*/
message AnalogInListDeviceResponse {
repeated AnalogInDevice devices = 1;
}
/**
* Represents the Read request data.
*/
message AnalogInReadRequest {
string name = 1;
}
/**
* Represents the Read response data.
*/
message AnalogInReadResponse {
uint32 adcRaw = 1;
float converted = 2;
AnalogInUnits unit = 3;
}
/**
* Represents the SetMeasureMode request data.
*/
message AnalogInSetMeasureModeRequest {
string name = 1;
AnalogInMode mode = 2;
}
message AnalogInAttachResponse {
string channel = 1;
string status = 2;
string timestamp = 3;
}
/**
* Service exposing AnalogIn functions.
*/
service AnalogIN {
/// Used to retrieve all available analog input devices.
rpc ListDevices(google.protobuf.Empty) returns (AnalogInListDeviceResponse) {}
/// Used to read value of a particular analog input channel.
rpc Read(AnalogInReadRequest) returns (AnalogInReadResponse) {}
/// Used to set measure mode of a particular analog input channel.
rpc SetMeasureMode(AnalogInSetMeasureModeRequest) returns (google.protobuf.Empty) {}
/// Used to monitor for overcurrent events
rpc Attach(google.protobuf.Empty) returns (stream AnalogInAttachResponse) {}
}
To list all available Analog Input services, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, refer to gRPC Usage.
To list all available Analog Input calls, use the following line:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> list hal.analogin.AnalogIN
The token
field is the fetched TDC-X authorization token. For help fetching this token, refer to gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to list all available Analog Input services.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 list hal.analogin.AnalogIN
The response should be in this format:
hal.analogin.AnalogIN.Attach
hal.analogin.AnalogIN.ListDevices
hal.analogin.AnalogIN.Read
hal.analogin.AnalogIN.SetMeasureMode
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o ain-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/ain-grpc .
CMD ["./ain-grpc"]
Open a terminal and paste the following commands:
docker build -t ain-grpc-app .
docker save -o ain-grpc-app.tar ain-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as ain-grpc
.
RUN go build -o ain-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the ain-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/ain-grpc .
CMD ["./ain-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes the creation and usage of a Node-RED gRPC example using analog inputs. The application makes listing all AIN devices, reading AIN values, changing AIN mode and streaming changes in the AIN devices possible.
For implementation, the following nodes are used:
-
inject
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below:
A Lua application is provided as an AIN usage example. The example sets up three AIN devices (AI_A
, AI_B
, and AI_C
) and sets their measure mode, then reads the measure mode, raw value and converted value of the specified AIN device.
The application starts with setting up three AINN devices.
local a1 = setupDevice('AI_A', 'Voltage', createCallback, nil)
local a2 = setupDevice('AI_B', 'Voltage', createCallback, nil)
local a3 = setupDevice('AI_C', 'Current', createCallback, 4)
The setupDevice
function prints the current device name, creates an AIN handle, sets the measure mode of the device and provides the needed callback function.
local device = AnalogIn.create(deviceName)
device:setMeasureMode(initialMode)
device:register('Overcurrent', createCallback(device, countLimit))
Then it gets the device's measure mode, raw value and converted value and prints it to the user.
local measureMode = device:getMeasureMode()
print("Measure mode is set to: " .. measureMode)
local rawValue = device:readRaw()
print(string.format("Raw value is: %s", rawValue))
local convertedValue = device:readConverted()
print(string.format("Converted value is: %s", convertedValue))
The script prints the engine version at the end of the script. See the result of the application run below.
[13:53:36.283: INFO: AppEngine] Starting app: 'ain' (priority: LOW)
-----------Device is 'AI_A'---------------
Measure mode is set to: Voltage
Raw value is: 0
Converted value is: 0.00756137492
-----------Device is 'AI_B'---------------
Measure mode is set to: Voltage
Raw value is: 0
Converted value is: 0.00756137492
-----------Device is 'AI_C'---------------
Measure mode is set to: Current
Raw value is: 0
Converted value is: 0.0378068723
Engine version: 0.8.3
Download the example code from the link below:
In this section, the Serial interface of the TDC-X device and examples of its usage are discussed. Programming examples are given and thoroughly explained.
This section handles setting up a serial device using gRPC, and creating serial Go applications.
The first application is made and tested with the Leaf Wetness Sensor
, which operates with a Baudrate
of 9600
, using the communication protocol MODBUS
, and with RS485
. For this application, an Isolated Soil Sensor
was used.
The second application was created by simulating a RS422
device using two TDC-X devices and connecting their serial interfaces. The reading and writing functionalities are implemented as separate applications.
In this subsection, setting up the serial device is discussed. Using the dedicated serial HAL service, setup of the following parameters is possible:
- Mode
- Transceiver Power
- Slew Rate
- Termination
In the following subsections, setting up the mode and termination of a serial device is discussed. This is done using the Serial HAL Service hal.serial.Serial
. Examples using grpcurl
are given below. For more information about using gRPC services, refer to gRPC Usage.
The Serial HAL service allows seting up the serial mode. There are two possible modes, each configured to set up your serial device according to its protocol. Possible modes include:
- RS422
- RS485
To set up your serial mode, use the hal.serial.Serial.SetMode
service. An example of setting the mode to RS422
is given below.
grpcurl -emit-defaults -H 'Authorization: Bearer {token}' -plaintext -d '{"interfaceName":"serial","mode":"RS422"}' 192.168.0.100:8081 hal.serial.Serial.SetMode
{}
To check changes to the serial mode, use the HAL service hal.serial.Serial.GetMode
.
grpcurl -emit-defaults -H 'Authorization: Bearer {token}' -plaintext -d '{"interfaceName":"serial"}' 192.168.0.100:8081 hal.serial.Serial.GetMode
{
"mode": "RS422"
}
The Serial HAL service provides a means to set up serial termination. The HAL service hal.serial.Serial.SetTermination
is used. An example of setting the termination of the connected serial device on is given below.
grpcurl -emit-defaults -H 'Authorization: Bearer {token}' -plaintext -d '{"interfaceName":"serial","enableTermination":"true"}' 192.168.0.100:8081 hal.serial.Serial.SetTermination
{}
Checking changes to the termination is done by using the hal.serial.Serial.GetTermination
HAL service.
grpcurl -emit-defaults -H 'Authorization: Bearer {token}' -plaintext -d '{"interfaceName":"serial"}' 192.168.0.100:8081 hal.serial.Serial.GetTermination
{
"terminationEnabled": true
}
The Serial HAL service provides a means to view serial statistics. The HAL service hal.serial.Serial.GetStatistics
is used. An example of seeing serial statistics is given below.
grpcurl -d '{"interfaceName":"SERIAL"}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.serial.Serial.GetStatistics
{
"txCount": "1050",
"rxCount": "982"
}
In this section, the RS485 application is implemented.
The application uses a single .go
file to run. The go.bug.st/serial
package is used to work with the serial port on /dev/ttyUSB0
. The port mode is set and the communication to the port is opened.
func setupPort() serial.Port {
mode := &serial.Mode{
BaudRate: 9600,
Parity: serial.NoParity,
DataBits: 8,
}
port, err := serial.Open("/dev/ttyUSB0", mode)
if err != nil {
log.Fatal(err)
}
return port
}
A message for fetching the temperature and humidity of the sensor is created. For the Isolated Soil Sensor
, a message in a specific format is sent to the serial port which prompts the sensor to return the required values.
To send a message to the serial port, the Write
function is used.
_, err := port.Write(message)
if err != nil {
log.Fatalf("Error writing to serial port: %v", err)
}
The program then sleeps for a second so that the sensor has enough time to process the message and send data back to the host. For reading the serial port data, the Read
function is used.
response := make([]byte, 256)
n, err := port.Read(response)
if err != nil {
log.Fatalf("Error reading from serial port: %v", err)
}
The result is then printed and parsed to a human-readable format.
func parseValues(rawData []byte) {
tempHigh := rawData[3]
tempLow := rawData[4]
temperatureRaw := (uint16(tempHigh) << 8) | uint16(tempLow)
realTemperature := float64(temperatureRaw) / 10.0
humidityHigh := rawData[5]
humidityLow := rawData[6]
humidityRaw := (uint16(humidityHigh) << 8) | uint16(humidityLow)
realHumidity := float64(humidityRaw) / 10.0
fmt.Printf("Temperature: %.2f °C\n", realTemperature)
fmt.Printf("Humidity: %.2f %%\n", realHumidity)
}
The results are printed to the console.
This section describes the Go application deployment.
1.2.2.1. Dockerfile
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o modbus-serial .
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/modbus-serial .
CMD ["./modbus-serial"]
Open a terminal and paste the following commands:
docker build -t modbus-serial .
docker save -o modbus-serial.tar modbus-serial:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
1.2.2.2. Dockerfile Breakdown
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
file to the directory, then downloads necessary files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application image is built as modbus-serial
.
RUN go build -o modbus-serial .
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the serial
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/modbus-serial .
CMD ["./modbus-serial"]
1.2.2.3. Deploying to Portainer
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
In this section, the RS422 application is discussed.
Both applications need to connect to the TDC-X device's port responsible for serial communication in order to read data sent via RS422. To achieve this, a function is created to connect to the /dev/ttyUSB0
port using the go.bug.st/serial
Go package.
This function configures the serial connection, setting the parity, data bits (8), stop bits, and baud rate. It then attempts to open the /dev/ttyUSB0 port. If successful, the port is returned; otherwise, an error is logged, and the application is terminated.
func setupPort() serial.Port {
mode := &serial.Mode{
Parity: serial.NoParity,
DataBits: 8,
StopBits: serial.OneStopBit,
BaudRate: 9600,
}
port, err := serial.Open("/dev/ttyUSB0", mode)
if err != nil {
log.Fatal(err)
}
return port
}
Then, a simple goroutine is started for both the witing and reading application.
To read the data, the following function is used:
func readData(port serial.Port) {
buf := make([]byte, 128)
for {
n, err := port.Read(buf)
if err != nil {
log.Printf("Error reading data: %v\n", err)
return
}
if n > 0 {
fmt.Printf("Received: %s\n", string(buf[:n]))
}
}
}
The readData function continuously reads from the port, storing the incoming data in a byte buffer. If data is received, it is printed to the console.
To send data through the port, the writeData
function is used in the writer application:
func writeData(port serial.Port, message []byte) {
for {
_, err := port.Write(message)
if err != nil {
log.Printf("Error writing data: %v\n", err)
return
} else {
fmt.Printf("Sent: %s\n", message)
}
time.Sleep(2 * time.Second)
}
}
This function accepts the port and a message in byte format. In this example, the application sends a Hello world!
message every two seconds. If writing to the port fails, an error message is logged.
This section describes the Go application deployment.
1.3.2.1. Dockerfile
To deploy the applications, Go containers should be created and deployed to the TDC-X devices. To that end, a Dockerfile
is created for both applications. As Dockerfiles
are identical, this documentation will focus on showing a single Dockerfile
, but to create two applications, make sure to rename the serial
tag in the file. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o serial .
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/serial .
CMD ["./serial"]
Open a terminal and paste the following commands:
docker build -t serial .
docker save -o serial.tar serial:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
1.3.2.2. Dockerfile Breakdown
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
file to the directory, then downloads necessary files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application image is built as serial
.
RUN go build -o serial .
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the serial
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/serial .
CMD ["./serial"]
1.3.2.3. Deploying to Portainer
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
A Lua application is provided as a Serial link usage example. Two scripts are given; one writes data to the TDC-X serial port, while te other reads from this port. For RS485
communication, a DIGITUS USB to Serial Adapter
is connected to the TDC-X device for reading and writing data. For RS422
communication, two TDC-X devices are connected via a serial interface.
To change between the RS422
and RS485
standards, find the following line of code in the .lua
example and set it accordingly.
-- sets type of communication to [RS422 | RS485]
S1:setType("RS422")
The first .lua
script writes data to the RS4XX device. It creates a serial connection to the device and sets its termination to false
. The type of the device is set and baud rate is set to 115200
.
S1 = SerialCom.create('SER1')
S1:setTermination(false)
S1:setType("RS485")
S1:setBaudRate(115200)
A connection is opened and a Hello world!
message is created. Then, a timer is created to send this message to the RS485 device periodically every 5 seconds. The message is transmitted the following way:
local Retb = S1:transmit(message)
print("Transmitted " .. Retb .. " bytes.")
Note that a timer is implemented instead of a Sleep
service. This is because Sleep
will cause all code to wait for the specified time, while timers operate locally, meaning that only the rs-write.lua
application script will sleep for the specified time.
The script prints the engine version at the end of the script. See the result of the application run below.
[15:52:06.041: INFO: AppEngine] Starting app: 'serial' (priority: LOW)
Transmitted 13 bytes.
Transmitted 13 bytes.
Transmitted 13 bytes.
Download the example code from the link below:
The second .lua
script reads data from the RS485 device. The script creates a serial connection, sets the termination to false
and the baudrate to 115200
, then opens the connection. A Callback
function is created to read from the device.
S1:register('OnReceive', Callback)
All received data is then printed to the console.
function Callback()
data = S1:receive(1000)
print(data)
end
See the result of the application run below.
[16:23:45.843: INFO: AppEngine] Starting app: 'serial' (priority: LOW)
Received data: Hello world!
Received data: Welcome to TDC-X!
Download the example code from the link below:
In this section, the IMU sensor of the TDC-X device and examples of its usage are discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. From it, the list of IMU sensors can be read, sampling frequency changed and their stream of data read. To that end, a simple Go application demonstrating this usage is created.
The application uses Golang's gRPC service for fetching and sending data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
protoc --go_out=pkg/pb --go-grpc_out=pkg/pb pkg/imu.proto
This installs needed Protoc
files, exports the path and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please see instructions on how to generate an access token for the TDC-X here: gRPC Usage.
Once you've obtained the token, navigate to grpc-dio/pkg/auth/token.json
. Paste your generated token into the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The application starts by creating a new gRPC client that connects to the TDC-X address and the port 8081
. The client creation is tied to the Proto file and generated gRPC files. This is done using the following code:
conn, err := grpc.NewClient("192.168.0.101:8081", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
client := protos.NewImuClient(conn)
The application then sends a request to the server that lists all IMU sensors available on the device. This is done by sending an empty request with the protos
function ListDevices
defined in the generated Go files. The sensors are printed to the terminal.
req := &emptypb.Empty{}
res, err := client.ListDevices(context.Background(), req)
if err != nil {
log.Fatalf("could not write: %v", err)
}
The program then sends a request to the server that changes sampling frequency for specified sensor. This is done by sending predefined request with the protos
function SetSamplingFrequency
defined in the generated Go files. The request is created by providing the correct device name parameter, which is then sent to the server. The sampling frequency is printed to the terminal.
req := &protos.SetSamplingFrequencyRequest{
DeviceName: "accelerometer",
Frequency: 100,
}
_, err := client.SetSamplingFrequency(ctx, req)
if err != nil {
log.Fatalf("could not set sampling frequency: %v", err)
}
The program then enters an infinite for
loop that waits for a response from the server. The request is created by providing the correct device name and buffer length parameter, which is sent to the server.
req := &protos.ReadBufferedDataStreamRequest{
DeviceName: "accelerometer",
FilterLength: 10,
}
stream, err := client.ReadBufferedDataStream(ctx, req)
if err != nil {
log.Fatalf("failed to call ReadBufferedDataStream: %v", err)
}
fmt.Println("Starting to receive data from the buffered stream...")
for {
res, err := stream.Recv()
if err != nil {
if err.Error() == "EOF" {
fmt.Println("Stream ended.")
break
}
log.Fatalf("error receiving data from stream: %v", err)
}
fmt.Printf("Received data: %v\n", res)
}
Each response is printed to the terminal and contains the following values:
{
"raw": {
"X": 1455,
"Y": 244,
"Z": 16232
},
"scaled": {
"X": 0.8703883,
"Y": 0.14596203,
"Z": 9.710064
},
"max": {
"raw": {
"X": 1481,
"Y": 280,
"Z": 16250
},
"scaled": {
"X": 0.8859416,
"Y": 0.16749741,
"Z": 9.720832
}
},
"min": {
"raw": {
"X": 1418,
"Y": 196,
"Z": 16216
},
"scaled": {
"X": 0.84825474,
"Y": 0.117248185,
"Z": 9.700493
}
},
"timestamp": "2024-11-15T18:02:12Z"
}
The Proto file used for the IMU service is the following:
/**
* IMU Sensor Service.
*
* Service that enables a control of the Magnetometer and IMU sensors
*/
syntax = "proto3";
package hal.imu;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
message Channels {
string name = 1;
float value = 2;
}
/**
* Represents the ReadSampleRaw response data.
*/
message ReadSampleRawResponse {
repeated Channels channels = 1;
}
/**
* Represents the ReadSampleScaled response data.
*/
message ReadSampleScaledResponse {
repeated Channels channels = 1;
}
/**
* Represents the ReadBufferedDataStream request data.
*/
message ReadBufferedDataStreamRequest {
string deviceName = 1;
int32 filterLength = 2;
}
message RawDataStream {
int32 X = 1;
int32 Y = 2;
int32 Z = 3;
}
message ScaledDataStream {
float X = 1;
float Y = 2;
float Z = 3;
}
message MaxDataStream {
RawDataStream raw = 1;
ScaledDataStream scaled = 2;
}
message MinDataStream {
RawDataStream raw = 1;
ScaledDataStream scaled = 2;
}
/**
* Represents the ReadBufferedDataStream response data.
*/
message ReadBufferedDataStreamResponse {
RawDataStream raw = 1;
ScaledDataStream scaled = 2;
MaxDataStream max = 3;
MinDataStream min = 4;
string timestamp = 5;
}
/**
* Represents the GetCurrentScale response data.
*/
message GetCurrentScaleResponse {
float scale = 1;
}
/**
* Represents the GetAvailableScales response data.
*/
message GetAvailableScalesResponse {
repeated float scales = 1;
}
/**
* Represents the SetScale request data.
*/
message SetScaleRequest {
string deviceName = 1;
float scale = 2;
}
/**
* Represents the GetCurrentSamplingFrequency response data.
*/
message GetCurrentSamplingFrequencyResponse {
float frequency = 1;
}
/**
* Represents the GetAvailableSamplingFrequency response data.
*/
message GetAvailableSamplingFrequencyResponse {
repeated float frequencies = 1;
}
/**
* Represents the SetSamplingFrequency request data.
*/
message SetSamplingFrequencyRequest {
string deviceName = 1;
float frequency = 2;
}
message ChooseDeviceRequest {
string deviceName = 1;
}
message ImuDeviceResponse {
string name = 1;
}
/**
* Represents the ListDevice response data.
*/
message ImuListDevicesResponse {
repeated ImuDeviceResponse devices = 1;
}
/**
* Service exposing IMU sensor functions.
*/
service Imu {
/// Used to retrive raw samples from sensors
rpc ReadSampleRaw(ChooseDeviceRequest) returns (ReadSampleRawResponse) {}
/// Used to retrive scaled samples from sensors
rpc ReadSampleScaled(ChooseDeviceRequest) returns (ReadSampleScaledResponse) {}
/// Used to stream scaled and raw samples from sensors
rpc ReadBufferedDataStream(ReadBufferedDataStreamRequest) returns (stream ReadBufferedDataStreamResponse) {}
/// Used to retrieve current scales
rpc GetCurrentScale(ChooseDeviceRequest) returns (GetCurrentScaleResponse) {}
/// Used to retrieve available scales
rpc GetAvailableScales(ChooseDeviceRequest) returns (GetAvailableScalesResponse) {}
/// Used to write scale value
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty) {}
/// Used to retrieve current sampling frequency
rpc GetCurrentSamplingFrequency(ChooseDeviceRequest) returns (GetCurrentSamplingFrequencyResponse) {}
/// Used to retrieve available sampling frequency
rpc GetAvailableSamplingFrequency(ChooseDeviceRequest) returns (GetAvailableSamplingFrequencyResponse) {}
/// Used to write sampling frequency value
rpc SetSamplingFrequency(SetSamplingFrequencyRequest) returns (google.protobuf.Empty) {}
// Used to get all available devices
rpc ListDevices(google.protobuf.Empty) returns (ImuListDevicesResponse) {}
}
To list all available IMU services, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, see gRPC Usage.
To list all available IMU devices, use the following line:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> list hal.imu.Imu
The token
field is the fetched TDC-X authorization token. For help fetching this token, refer to gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to list all available IMU services.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 list hal.imu.Imu
The response will be in this format:
hal.imu.Imu.GetAvailableSamplingFrequency
hal.imu.Imu.GetAvailableScales
hal.imu.Imu.GetCurrentSamplingFrequency
hal.imu.Imu.GetCurrentScale
hal.imu.Imu.ListDevices
hal.imu.Imu.ReadBufferedDataStream
hal.imu.Imu.ReadSampleRaw
hal.imu.Imu.ReadSampleScaled
hal.imu.Imu.SetSamplingFrequency
hal.imu.Imu.SetScale
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o imu-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/imu-grpc .
CMD ["./imu-grpc"]
Open a terminal and paste the following commands:
docker build -t imu-grpc-app .
docker save -o imu-grpc-app.tar imu-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as imu-grpc
.
RUN go build -o imu-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the imu-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/imu-grpc .
CMD ["./imu-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes usage of the IMU sensors on the TDC-X device. A Node-RED gRPC application was created to demonstrate listing all IMU sensor devices, reading data from said devices and setting parameters(ex. sampling frequency).
For implementation, the following nodes are used:
-
inject
node -
function
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided.
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below:
In this section, temperature sensors on the TDC-X device are discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. An application that lists all temperature sensors and prints their current value is created.
The application uses Golang's gRPC service to fetch and send data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
protoc --go_out=pkg/pb --go-grpc_out=pkg/pb pkg/temperature-sensor-service.proto
This installs needed Protoc
files, exports the path and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please refer to gRPC Usage for instructions on how to generate an access token for the TDC-X.
Once you've obtained the token, navigate to grpc-temperature/pkg/auth/token.json
. Paste your generated token in the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The main application first creates a new gRPC client that connects to the TDC-X IP address on port 8081
. It does so by using the generated gRPC Go file specification.
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
To demonstrate the listed functionalities, the application lists all available temperature sensors on your TDC-X device, and then the temperature value of each specific sensor measured in °C
is printed.
To list all temperature devices, an empty pb
request is sent to the server. The client specifies the function ListDevices
to receive all devices from the TDC-X device, which are then printed to the user.
req := &emptypb.Empty{}
res, err := client.ListDevices(context.Background(), req)
Afterwards, each sensor's temperature is read.
func readTemperature(ctx context.Context, client protos.TemperatureSensorClient, device string) {
req := pb.TemperatureSensorReadRequest{
Name: device,
}
res, err := client.Read(ctx, &req)
if err != nil {
log.Fatalf("couldn't read from temperature sensor: %v", err)
}
fmt.Printf("%s temperature: %.2f %s\n", device, res.Value, res.Unit)
}
This function creates a proto request for reading the temperature sensor from the given device name and prints the float result in °C.
The Proto file used for the Temperature sensor service is the following:
/**
* Temperature sensor service.
*
* Service that provides device temperature sensor reading
*/
syntax = "proto3";
package hal.temperaturesensor;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
message TemperatureSensorDevice {
string name = 1;
}
/**
* Represents the ListDevice response data.
*/
message TemperatureSensorListDeviceResponse {
repeated TemperatureSensorDevice devices = 1;
}
message TemperatureSensorReadRequest {
string name = 1;
}
message TemperatureSensorReadResponse {
float value = 1;
string unit = 2;
}
/**
* Service exposing TemperatureSensor functions.
*/
service TemperatureSensor {
/// Used to retrieve all available temperature sensors
rpc ListDevices(google.protobuf.Empty) returns (TemperatureSensorListDeviceResponse) {}
/// Used to read value of a particular temperature sensor.
rpc Read(TemperatureSensorReadRequest) returns (TemperatureSensorReadResponse) {}
}
To list all available temperature sensor services, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, refer to gRPC Usage.
Use the following line for listing them:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> list hal.temperaturesensor.TemperatureSensor
The token
field is the fetched TDC-X authorization token. For help fetching this token, see gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to list all available temperature sensor devices.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 list hal.temperaturesensor.TemperatureSensor
The response should be in this format:
hal.temperaturesensor.TemperatureSensor.ListDevices
hal.temperaturesensor.TemperatureSensor.Read
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o temp-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/temp-grpc .
CMD ["./temp-grpc"]
Open a terminal and paste the following commands:
docker build -t temp-grpc-app .
docker save -o temp-grpc-app.tar temp-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as temp-grpc
.
RUN go build -o temp-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the temp-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/temp-grpc .
CMD ["./temp-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes usage of the temperature sensors on the TDC-X device. A Node-RED gRPC application was created to demonstrate listing all temperature sensor devices and reading temperature values from said devices.
For implementation, the following nodes are used:
-
inject
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided.
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below:
A Lua application is provided as a temperature sensor usage example. The example demonstrates listing all temperature sensor devices and reading their temperature value. This is done by creating monitors which read the temperature value of devices. See an example below.
PCB0 = Monitor.Temperature.create('PCB_TEMP0')
print(string.format("PCB_TEMP0: %.2f°C", extractTemperature(PCB0)))
The extractTemperature
function checks whether the passed variable is a monitor. If not, the function returns the nil
value. Otherwise, it uses the monitor's get()
function to fetch the temperature value of the device.
local function extractTemperature(temperatureMonitor)
if not temperatureMonitor then
return nil
end
return temperatureMonitor:get()
end
See the result of the application run below.
[13:34:00.046: INFO: AppEngine] Starting app: 'temperaturesensor' (priority: LOW)
PCB_TEMP0: 44.00°C
PCB_TEMP1: 41.00°C
A53_CPU_TEMP: 61.00°C
SOC_ANAMIX_TEMP: 61.00°C
Download the example code from the link below:
In this section, the Controller Area Network (CAN) interface of the TDC-X device and examples of its usage are discussed. Programming examples are given and thoroughly explained.
In this section, setting up the CAN device is discussed. Using the dedicated CAN HAL service, setup of the following parameters is possible:
- Transceiver Power
- Termination
- Interface Mapping (namespace)
In the following sections, setting up the transceiver power, termination, and CAN namespace is discussed. Additionally, reading CAN statistics is described. This is done using the CAN HAL Service hal.can.Can
. Examples using grpcurl
are given below. For more information about using gRPC services, refer to gRPC Usage.
The CAN HAL service provides a means to set up transceiver power for the CAN interface. The HAL service hal.can.Can.SetTransceiverPower
is used. An example of setting the transceiver power of the connected CAN device on is given below.
grpcurl -d '{"interfaceName":"CAN1","powerOn":true}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/SetTransceiverPower
{}
Checking changes to the CAN state is done by using the hal.can.Can.GetTransceiverPower
HAL service.
grpcurl -d '{"interfaceName":"CAN1"}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/GetTransceiverPower
{
"powerOn": true
}
The CAN HAL service provides a means to set up CAN termination. The HAL service hal.can.Can.SetTermination
is used. An example of setting the termination of the connected CAN device on is given below.
grpcurl -d '{"interfaceName":"CAN1","enableTermination":true}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/SetTermination
{}
Checking changes to the termination is done by using the hal.can.Can.GetTermination
HAL service.
grpcurl -d '{"interfaceName":"CAN1"}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/GetTermination
{
"terminationEnabled": true
}
When working with the CAN interface, one can use different namespaces. The CAN interface is then bound to a namespace and can be accessed from there. Examples include:
- AppEngine
- Host
- Any other running container
To set the namespace of the CAN interface, the HAL service hal.can.Can.SetInterfaceToContainer
is used. Possible values for the dockerContainerName
parameter include:
-
app-engine
- maps the CAN interface to AppEngine - empty string - maps the CAN interface to the host
-
container-name
- maps the CAN interface to a running Docker container
See an example of exposing the CAN interface to the host below.
grpcurl -d '{"interfaceName":"CAN1","dockerContainerName":""}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/SetInterfaceToContainer
{}
To see the changes to the interface mapping, the hal.can.Can.GetInterfaceTocontainerMapping
service is used.
grpcurl -d '{}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/GetInterfaceToContainerMapping
{
"items": [
{
"interfaceName": "CAN1",
"dockerContainerName": ""
}
]
}
NOTE: Mapping the CAN interface results in exclusive access to the interface.
Mapping the CAN interface to AppEngine will render access to the interface exclusive to AppEngine. Example usage can be see in the Lua Example below. Mapping the interface to the host, the CAN interface is visible only to the host. Example usage can be seen in the Go Example below.
To see statistics of your CAN device, the HAL service hal.can.Can.GetStatistics
is used. See an example of its usage below.
grpcurl -d '{"interfaceName":"CAN1"}' -H 'Authorization: Bearer token' -plaintext 192.168.0.100:8081 hal.can.Can/GetStatistics
{
"RxPackets": "1118",
"TxPackets": "92",
"RxBytes": "4783",
"TxBytes": "736",
"RxErrors": "0",
"TxErrors": "0",
"RxDropped": "0",
"TxDropped": "1",
"Multicast": "0",
"Collisions": "0",
"RxLengthErrors": "0",
"RxOverErrors": "0",
"RxCrcErrors": "0",
"RxFrameErrors": "0",
"RxFifoErrors": "0",
"RxMissedErrors": "0",
"TxAbortedErrors": "1",
"TxCarrierErrors": "0",
"TxFifoErrors": "0",
"TxHeartbeatErrors": "0",
"TxWindowErrors": "0",
"RxCompressed": "0",
"TxCompressed": "0"
}
A Go application is provided as a CAN usage example. The application creates a CAN bus, binds it to a CAN port, and sends and receives CAN data simultaneously. For application testing, the Kvaser Leaf Light v2
device was used. For generating and testing data, Kvaser's CanKing
application and drivers were used.
The application is implemented using the canbus
Go package to communicate with the CAN bus.
NOTE: Check your CAN usage namespace, and check if the CAN link is up. Check the previous section to see CAN namespace assignment. If the CAN link is already enabled, skip the next step, and run the application normally.
If the CAN link is down, whilst running the application, add a --setup=true
flag to the starting arguments, which will set up the CAN interface according to specifications in the /pkg/canSetup
file. The setup sets the CAN bitrate, powers on the transceiver and brings the interface link up.
./can --setup=true
Otherwise, run the application normally. Firstly, a new CAN bus connection is established using the following lines:
can, err := canbus.New()
if err != nil {
log.Fatalf("Failed to open CAN socket: %v", err)
}
defer can.Close()
The bus is then bound to the can0
interface.
err = can.Bind("can0")
if err != nil {
log.Fatalf("Cannot bind to can socket: %s", err)
}
Two goroutines are started simultaneously. One sends sample data from the CAN device, while the other listens to received data and prints the data to the console. The goroutines are synced using a wait group that waits for all routines to finish execution. See both goroutines explained below.
The goroutine code snippet for sending data is shown below.
data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
frame := canbus.Frame{
ID: 0x123,
Data: data,
}
for {
if _, err := can.Send(frame); err != nil {
log.Printf("Failed to send CAN frame: %v", err)
} else {
fmt.Println("Sent CAN frame:", frame)
}
time.Sleep(1 * time.Second)
}
The specified code creates a data structure which contains the ID and data to be sent by a message, and attempts to send the CAN frame to the CAN device each second.
See how the goroutine receives CAN data below. A frame variable is used which listens to the CAN device, and the device logs all received data as it's sent.
frame, err := can.Recv()
if err != nil {
log.Printf("Failed to receive CAN frame: %v", err)
} else {
log.Println("Received CAN frame:", frame)
}
See a screenshot of sending and receiving CAN data below.
An example print from the application can be seen below.
Received CAN frame: {1122 [220 3 0 0 0] SFF}
Received CAN frame: {586 [221 3 0] SFF}
Received CAN frame: {1792 [222 3 0 0 0 0 0 0] SFF}
Received CAN frame: {538 [223 3 0 0] SFF}
Received CAN frame: {511 [225] SFF}
Received CAN frame: {931 [226 3 0 0] SFF}
Received CAN frame: {1904 [227 3 0 0 0 0] SFF}
Received CAN frame: {653 [] SFF}
Received CAN frame: {1520 [229] SFF}
Received CAN frame: {1565 [230 3 0 0 0 0 0] SFF}
Received CAN frame: {854 [231 3 0 0 0] SFF}
Sent CAN frame: {291 [1 2 3 4 5 6 7 8] SFF}
Sent CAN frame: {291 [1 2 3 4 5 6 7 8] SFF}
Sent CAN frame: {291 [1 2 3 4 5 6 7 8] SFF}
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o can ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/can .
CMD ["./can"]
Open a terminal and paste the following commands:
docker build -t can-app .
docker save -o can-app.tar can-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as can
.
RUN go build -o can ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the can
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/can .
CMD ["./can"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
A Lua application is provided as a CAN usage example. The application creates a CAN handler, opens the handler and sends or receives CAN data. For application testing, the Kvaser Leaf Light v2
device was used. For generating and testing data, Kvaser's CanKing
application and drivers were used.
The application first initializes the CAN socket. The CAN1
handle is created, and the baud rate is set to 500000
. Termination is set to true
, and the handle is opened.
Handle = CANSocket.create('CAN1')
Handle:setBaudRate(500000)
Handle:setTermination(true)
Handle:open()
For printing received data and logging errors, two local functions were implemented.
--@handleOnReceive(id:int,buffer:binary)
local function handleOnReceive(id, buffer)
print("Received data.")
print(id)
end
--@handleOnReceive(errorCode:int)
local function handleOnError(errorCode)
Log.info("Error code " .. errorCode)
end
CANSocket.register(Handle, "OnReceive", handleOnReceive)
CANSocket.register(Handle, "OnError", handleOnError)
Reading is tested with the CanKing
application. See a screenshot of sending CAN data below.
To send data to the device, three test IDs and messages are created. The script then sends the defined data in an infinite loop.
Ids = {20, 21, 22}
Msgs = {"\x41\x42\x43\x44\x45\x46\x47\x48", "\x51\x52\x53\x54\x55\x56\x57\x58", "\x61\x62\x63\x64\x65\x66\x67\x68"}
while true do
Handle:transmit(Ids, Msgs)
Script.sleep(1000)
end
See the result of the application run below.
[12:52:10.131: INFO: AppEngine] Starting app: 'can' (priority: LOW)
Received data.
776
Received data.
779
Received data.
693
Received data.
857
Download the example code from the link below:
In this section, GNSS on the TDC-X device is discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. An application that checks device availability and fetches GNSS data is provided.
The application uses Golang's gRPC service to fetch and send data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"
protoc --go_out=pkg/pb --go-grpc_out=pkg/pb pkg/gnss.proto
This installs needed Protoc
files, exports the path and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please refer to gRPC Usage for instructions on how to generate an access token for the TDC-X.
Once you've obtained the token, navigate to grpc-gnss/pkg/auth/token.json
. Paste your generated token in the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The main application first creates a new gRPC client that connects to the TDC-X IP address on port 8081
. It does so by using the generated gRPC Go file specification.
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
The application first checks the GNSS device status by calling the HAL service for getting the device status. If the transceiver isn't set up, it sends a request to enable the receiver and antenna of the device.
func setUpGnss(client protos.GnssClient, ctx context.Context) {
req := &protos.DeviceManagementRequest{
EnableReciever: true,
EnableAntenna: true,
}
_, err := client.SetDeviceStatus(ctx, req)
if err != nil {
log.Fatalf("could not set status: %v", err)
}
}
The application then starts streaming GNSS data. This includes a timestamp, the position of the device, device speed, course, the number of satellites, fix type, fix quality and HDOP. An example log of the application is provided below:
2023/09/20 13:01:52 Timestamp: 2024-12-19T11:13:53Z
2023/09/20 13:01:52 Latitude: 46.286890516666666
2023/09/20 13:01:52 Longitude: 16.321311299999998
2023/09/20 13:01:52 Altitude: 183.8
2023/09/20 13:01:52 Speed (Kph): 0
2023/09/20 13:01:52 Speed (Mph): 0
2023/09/20 13:01:52 Course: 0
2023/09/20 13:01:52 Satellites: 10
2023/09/20 13:01:52 Fix Type: 3
2023/09/20 13:01:52 Fix Quality: 1
2023/09/20 13:01:52 HDOP: 0.8
To stream the GNSS data, the application sends a HAL Service request to the TDC device. A stream is opened, which lists GNSS messages indefinitely. This is shown in the code below:
func streamGnssJson(client protos.GnssClient, ctx context.Context) {
stream, err := client.StreamDataJson(ctx, &empty.Empty{})
if err != nil {
log.Fatalf("Error while calling StreamDataJson: %v", err)
}
log.Printf("--------------------------------")
for {
data, err := stream.Recv()
if err != nil {
log.Fatalf("Error while receiving data: %v", err)
}
log.Printf("Timestamp: %v", data.Timestamp)
log.Printf("Latitude: %v", data.Latitude)
log.Printf("Longitude: %v", data.Longitude)
log.Printf("Altitude: %v", data.Altitude)
log.Printf("Speed (Kph): %v", data.SpeedKph)
log.Printf("Speed (Mph): %v", data.SpeedMph)
log.Printf("Course: %v", data.Course)
log.Printf("Satellites: %v", data.Satellites)
log.Printf("Fix Type: %v", data.FixType)
log.Printf("Fix Quality: %v", data.FixQuality)
log.Printf("HDOP: %v", data.Hdop)
log.Printf("--------------------------------")
time.Sleep(1 * time.Second)
}
}
The Proto file used for the GNSS service is the following:
/**
* GNSS Service.
*
* Service for reading GNSS data
*/
syntax = "proto3";
package hal.gnss;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
enum NMEAFrequency {
_1Hz = 0;
_2Hz = 1;
_5Hz = 2;
_10Hz = 3;
}
message NMEAFrequenciesList {
repeated string frequencies = 1;
}
message NMEAFrequencySettings {
NMEAFrequency frequency = 1;
}
message GnssDataJson {
double latitude = 1; // The latitude of the GNSS position in decimal degrees.
double longitude = 2; // The longitude of the GNSS position in decimal degrees.
double altitude = 3; // The altitude of the GNSS position in meters.
string timestamp = 4; // The timestamp of the GNSS record in ISO 8601 format.
double speed_kph = 5; // The speed of the object in kilometers per hour.
double speed_mph = 6; // The speed of the object in miles per hour.
double course = 7; // The direction of travel in degrees, where 0 is north.
double satellites = 8; // The number of satellites used to determine the position.
double fix_type = 9; // Indicates the type of GNSS fix (e.g., 1 for no fix, 2 for 2D fix, 3 for 3D fix).
double fix_quality = 10; // Indicates the GNSS fix quality (e.g., 0 for invalid, 1 for GPS fix, 2 for DGPS fix).
double hdop = 11; // The Horizontal Dilution of Precision, a measure of the GNSS signal's accuracy.
}
message GnssData{
string sentence = 1;
}
message DeviceManagementRequest {
bool enableReciever = 1;
bool enableAntenna = 2;
}
message DeviceManagementResponse {
bool enableReciever = 1;
bool enableAntenna = 2;
bool sessionActive = 3;
bool rebootRequired = 4;
}
message GnssConstellationsSettings {
bool GLONASS = 1;
bool BDS = 2;
bool Galileo = 3;
}
message GnssConstellations {
repeated string constellations = 1;
}
/**
* Service exposing GNSS functions.
*/
service Gnss {
// Provides parsed Gnss data as JSON
rpc StreamDataJson(google.protobuf.Empty) returns (stream GnssDataJson) {}
// Provides raw stream that is returned by the Gnss receiver
rpc StreamDataRaw(google.protobuf.Empty) returns (stream GnssData) {}
// Provides a way do get receiver and antenna status
rpc GetDeviceStatus(google.protobuf.Empty) returns (DeviceManagementResponse) {}
// Provides a way do disable/enable receiver and antenna
rpc SetDeviceStatus(DeviceManagementRequest) returns (google.protobuf.Empty) {}
// Provides a way to set preferred constellations
rpc SetConstellations(GnssConstellationsSettings) returns (google.protobuf.Empty) {}
// Provides a way to get current preferred constellations
rpc GetConstellations(google.protobuf.Empty) returns (GnssConstellationsSettings) {}
// Provides a way to get a list of available constellations
rpc ListAvailableConstellations(google.protobuf.Empty) returns (GnssConstellations) {}
// Provides a way to set NMEA output frequency
rpc SetNmeaOutputFrequency(NMEAFrequencySettings) returns (google.protobuf.Empty) {}
// Provides a way to set NMEA output frequency
rpc GetNmeaOutputFrequency(google.protobuf.Empty) returns (NMEAFrequencySettings) {}
// Provides a way to get a list of available NMEA output frequencies
rpc ListNmeaOutputFrequencies(google.protobuf.Empty) returns (NMEAFrequenciesList) {}
}
To get the GNSS device status, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, refer to gRPC Usage.
Use the following line:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> hal.gnss.Gnss.GetDeviceStatus
The token
field is the fetched TDC-X authorization token. For help fetching this token, see gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to see the device status of the GNSS service.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 hal.gnss.Gnss.GetDeviceStatus
The response should be in this format:
"enableReciever": true,
"enableAntenna": true,
"sessionActive": true,
"rebootRequired": false
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o gnss-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/gnss-grpc .
CMD ["./gnss-grpc"]
Open a terminal and paste the following commands:
docker build -t gnss-grpc-app .
docker save -o gnss-grpc-app.tar gnss-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as gnss-grpc
.
RUN go build -o gnss-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the gnss-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/gnss-grpc .
CMD ["./gnss-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes usage of the the GNSS device on the TDC-X. A Node-RED gRPC application was created to demonstrate reading from the GNSS device.
For implementation, the following nodes are used:
-
inject
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided.
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below:
A Lua application is provided as a GNSS usage example. The script first creates a GNSS handle and enables the GNSS service. The handle is used to fetch all needed data from the receiver.
gnssHandle = Gnss.create()
gnssHandle:enable()
The following metrics are fetched:
- position,
- signal metrics,
- NMEA sentences,
- speed, and
- course.
It also provides a timestamp of the precise time when the application fetches the data. To get the position of your device, which includes latitude, longitude and altitude, the following code is used:
local x,y,z = gnssHandle:getPosition()
print("Latitude: " .. x)
print("Longitude: " .. y)
print("Altitude: " .. z)
To get signal metrics, the following code is used:
local q,w,e,r = gnssHandle:getSignalMetrics()
print("Number of satellites: " .. q)
print("Fix type: " .. w)
print("Fix quality: " .. e)
print("HDOP: " .. r)
This includes the number of satellites, the type of the fix used, fix quality and HDOP.
NMEA sentences are fetched using the code below:
local h=gnssHandle:getNmeaSentence('RMC')
print('RMC sentence: ' .. h)
h=gnssHandle:getNmeaSentence('GSA')
print('GSA sentence: ' .. h)
h=gnssHandle:getNmeaSentence('GGA')
print('GGA sentence: ' .. h)
h=gnssHandle:getNmeaSentence('GSV')
print('GSV sentence: ' .. h)
h=gnssHandle:getNmeaSentence('VTG')
print('VTG sentence: ' .. h)
In the end, speed and course is fetched:
print("Speed in KPH: " .. gnssHandle:getSpeed('KPH'))
print("Speed MPH:" .. gnssHandle:getSpeed('MPH'))
print("Course " .. gnssHandle:getCourse())
See the result of the application run below.
[12:08:27.359: INFO: AppEngine] Starting app: 'gnss' (priority: LOW)
GNSS Receiver is enabled
-------------------------------
Timestamp: 2024-12-19T10:21:54Z
Latitude: 46.28689185
Longitude: 16.321358916667
Altitude: 181.2
Speed [KPH]: 0.0
Speed [MPH]: 0.0
Course: 36.3
Satellite number: 9.0
Fix type: 3.0
Fix quality: 1.0
HDOP: 0.9
-------------------------------
Download the example code from the link below:
In this section Lua code examples will be shown.
To get a required administrator token, see the snippet bellow how to request it through the TDC login interface.
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/auth/login"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("POST")
-- Prepare credentials.
request:addHeader("Accept", "application/json")
request:setContentType("application/json")
request:setContentBuffer('{"username":"admin","password":"myadminpassword","realm":"admin"}')
-- Execute request.
local response = client:execute(request)
local response_code = response:getStatusCode()
local response_body = response:getContent()
-- Extract token
local json = JSON.create()
local json_body = json:parseFromString(response_body)
-- Token
local login_token = json_body:getValue("/token")
A JWT token is returned that needs to be included in every API request.
- Example:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhZGRyIjoiMTcyLjE4LjAuMiIsImVtYWlsIjoiYWRtaW5Ac2ljay5jb20iLCJleHAiOjE3MzY3NzQ5OTQsImlhdCI6MTczNjc3MTM5NCwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMC4xMDAvYXV0aC9sb2dpbiIsImp0aSI6IjFab0dsR0xTOFJvQ3kwVEJkUHhhNFZ5V1RkeTlIWVpUQUJ3dnBEazciLCJuYmYiOjE3MzY3NzXjnxjdnsajkfbajiI6ImFkbWluIiwicmVhbG0iOiJhZG1pbiIsInJvbGVzIjpbImF1dGhwL3VzZXIiLCJjY2F1dGgvYWRtaW4iXSwic3ViIjoiYWRtaW4ifQ.pNikIbs9JRzkaRI3gU9GXqyXYn2Z-de9Na22bi8NJe7sGo-pzUTV61RipNWEnYOxn66x-snzie_Wy_17fdxOtg
Note: Please download required helper script: Util script.
Get a list of all network interfaces.
The list is returned as a JSON array containing the name and all interface properties.
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/interfaces/"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
[
{
"isConfigurable": true,
"name": "eth1",
"settings": {
"defaultGateway": "192.168.0.1",
"dhcpFallbackAddress": "",
"dhcpFallbackGateway": "",
"dhcpFallbackMode": 0,
"enabled": true,
"mac": "f8:dc:7a:b0:05:1e",
"staticAddress": "192.168.0.100/24",
"useDhcp": false
},
"status": {
"address": "192.168.0.100/24",
"defaultGateway": "192.168.0.1",
"enabled": true,
"state": 6
},
"type": 1
}
]
Get the settings of an interface by name.
http://192.168.0.100/api/v1/network/interfaces/{name}/settings
-- Define endpoint
local endpoint = "http://192.168.0.100" .. "/api/v1/network/interfaces/eth1/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
{
"defaultGateway": "192.168.0.1",
"dhcpFallbackAddress": "",
"dhcpFallbackGateway": "",
"dhcpFallbackMode": 0,
"enabled": true,
"mac": "f8:dc:7a:b0:05:1e",
"staticAddress": "192.168.0.100/24",
"useDhcp": false
}
Set DHCP or static mode for a network interface.
When settings are accepted and applied, status code 204
is returned.
Note: Value of all true or false type variables must be boolean.
To set a DHCP mode for an interface, JSON with settings must be sent using the PUT HTTP method.
Note: When setting DHCP mode, a static fallback address must be set.
- JSON data
{
"enabled":true,
"useDhcp":true,
"staticAddress":"",
"defaultGateway": "",
"dhcpFallbackAddress":"192.168.2.1/24",
"dhcpFallbackGateway":"0.0.0.0",
"dhcpFallbackMode":0
}
- LUA example code
-- Define endpoint
local endpoint = "http://192.168.0.100" .. "/api/v1/network/interfaces/eth2/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("PUT")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Prepare setting data.
request:setContentType("application/json")
request:setContentBuffer('{"enabled":true, "useDhcp":true, "staticAddress":"", "defaultGateway": "", "dhcpFallbackAddress":"192.168.2.1/24", "dhcpFallbackGateway":"0.0.0.0", "dhcpFallbackMode":0}')
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- JSON data
{
"enabled":true,
"useDhcp":false,
"staticAddress":"192.168.2.100/24",
"defaultGateway": "192.168.2.1",
"dhcpFallbackAddress":"",
"dhcpFallbackGateway":"",
"dhcpFallbackMode":0
}
- LUA example code
-- Define endpoint
local endpoint = "http://192.168.0.100" .. "/api/v1/network/interfaces/eth2/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("PUT")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Prepare setting data.
request:setContentType("application/json")
request:setContentBuffer('{"enabled":true, "useDhcp":false, "staticAddress":"192.168.2.100/24", "defaultGateway": "192.168.2.1", "dhcpFallbackAddress":"", "dhcpFallbackGateway":"", "dhcpFallbackMode":0}')
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
Fallback address can be set using the same code snippet as for setting dhcp or static IP, see sections:
Check sections below for further WLAN setup details.
Get TDC Wireless interface name.
- LUA example code
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/wlan"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
[
"wlan0"
]
To get details of a specific WLAN interface, the WLAN name must be known.
- LUA example code
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/wlan/wlan0"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
{
"name": "wlan0",
"settings": {
"apMode": {
"channel": 1,
"encryption": "",
"hidden": false,
"restricted": false,
"ssid": "TDCE-Next"
},
"countryCode": "HR",
"enabled": true,
"mode": "station",
"stationMode": {
"scanningEnabled": true
}
},
"status": {
"connectedSsid": "my-network1-name",
"signalLevel": 96,
"state": "connected"
}
}
Get a list of all available networks (saved or discovered) for one WLAN interface.
The list of available networks can be used when adding a new network.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/wlan/wlan0/networks"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
[
{
"actualConnectionState": "connected",
"bssid": "0a:17:68:88:5d:0d",
"channel": 0,
"events": [],
"flags": [],
"isInRange": true,
"quality": 96,
"reconnect": false,
"ssid": "my-network1-name",
"stored": true,
"strength": -52,
"username": ""
},
{
"actualConnectionState": "disconnected",
"bssid": "d4:01:ff:01:f5:aa",
"channel": 0,
"events": [],
"flags": [],
"isInRange": true,
"quality": 64,
"reconnect": false,
"ssid": "other-network1",
"stored": false,
"strength": -68,
"username": ""
},
...
Connection to a new network requires a JSON body to be sent.
This same example is used when modifying an existing network connection.
{
"connectionState": "connected",
"passphrase": "wifipassword",
"reconnect": true,
"username": ""
}
Connection state of a new network can be predefined.
-
connectionState
should beconnected
.
- unknown
- connected
- connecting
- disconnected
- disconnecting
- LUA example code
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/wlan/wlan0/networks/my-network1-name"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("PUT")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Prepare setting data.
request:setContentType("application/json")
request:setContentBuffer('{"connectionState": "connected","passphrase": "networkpassword","reconnect": true,"username": ""}')
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
Removing previously defined wireless network.
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/wlan/wlan0/networks/my-network1-name"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("DELETE")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
Get or set gateway priority list.
Request current gateway priority list.
-- Define endpoint
local endpoint = "http://192.168.0.100" .. "/api/v1/network/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. TOKEN_HASH)
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
{
"defaultGatewayPriority": [
"wwan0",
"eth1",
"eth2",
"wlan0"
],
"networkDriver": "ControlCenter"
}
Priority list must be sent in the same form as current json response.
- Content JSON
{
"defaultGatewayPriority": [
"eth1",
"eth2",
"wlan0",
"wwan0"
],
"networkDriver": "ControlCenter"
}
- LUA example code
-- Define endpoint
local endpoint = "http://192.168.0.100" .. "/api/v1/network/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("PUT")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Prepare setting data.
request:setContentType("application/json")
request:setContentBuffer('{"defaultGatewayPriority": ["eth2","eth1","wlan0","wwan0"], "networkDriver": "ControlCenter"}')
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
Modem status and setup examples.
Get list of all modem devices on a system.
- LUA example code
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/modem"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
[
"wwan0"
]
Get all modem settings and statuses.
Note: Settings section will not contain password data.
- Lua example
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/modem/wwan0"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
{
"name": "wwan0",
"settings": {
"apn": "internet.ht.hr",
"apnPassword": "",
"apnUsername": "",
"connectionEnabled": true,
"dialString": "",
"enabled": true,
"persistConnection": true
},
"status": {
"ccid": "8938599202301824969",
"currentAccessTechnologies": [
"lte"
],
"currentCapabilities": [
"gsmUmts",
"lte"
],
"dataConnectionInfo": {
"attempts": 1,
"connected": true,
"connectionError": "",
"dns": [
"195.29.247.161",
"195.29.247.162"
],
"downlinkSpeed": 0,
"duration": 8940,
"failedAttempts": 0,
"gateway": "10.153.202.125",
"ipCidr": "10.153.202.126/30",
"rxBytes": 1383,
"startDate": "2025-01-14T07:13:05Z",
"totalDuration": 8940,
"totalRxBytes": 1383,
"totalTxBytes": 678,
"txBytes": 678,
"uplinkSpeed": 0
},
"imei": "869283050806985",
"imsi": "219019912652496",
"lockSimStatus": "simPin2",
"modemState": "connected",
"operatorIdentifier": "21901",
"operatorName": "HT HR",
"powerState": "on",
"signalStrength": 100,
"simActive": true,
"stateFailedReason": "none"
}
}
Get modem settings.
Note: Settings section will not contain password data.
- Lua example
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/modem/wwan0/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("GET")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
- Response
{
"apn": "internet.ht.hr",
"apnPassword": "",
"apnUsername": "",
"connectionEnabled": true,
"dialString": "",
"enabled": true,
"persistConnection": true
}
Add modem settings.
To add modem connection setting JSON data is required.
{
"apn": "string",
"enabled": true,
"connectionEnabled": true,
"apnPassword": "",
"apnUsername": "",
"dialString": "",
"persistConnection": true
}
- Lua example
-- Define endpoint.
local endpoint = "http://192.168.0.100" .. "/api/v1/network/modem/wwan0/settings"
-- Create client.
local client = HTTPClient.create()
-- Create request.
local request = HTTPClient.Request.create()
request:setURL(endpoint)
request:setMethod("PUT")
-- Prepare credentials.
request:addHeader("accept", "application/json")
request:addHeader("Authorization", "Bearer " .. "TOKEN_HASH")
-- Prepare setting data.
request:setContentType("application/json")
request:setContentBuffer('{"apn": "internet.ht.hr","enabled": true,"connectionEnabled": true,"apnPassword": "","apnUsername": "","dialString": "","persistConnection": true}')
-- Execute.
local response = client:execute(request)
-- Get status code & json response.
local response_code = response:getStatusCode()
local response_body = response:getContent()
Code 204
is expected when success.
In this section, the Wireless Personal Network (WPAN) service on the TDC-X device is discussed. Programming examples are given and thoroughly explained.
In this section, a Go gRPC application is created and documented. A gRPC client is created from a Proto file to match the gRPC server the TDC-X device is serving. An application that checks device availability, connects to a device, and reads data from it is provided.
The application uses Golang's gRPC service to fetch and send data to the server.
To implement a gRPC client, a Proto file matching the gRPC server's specifications is needed. This Proto file is then placed in a separate package, and from it, gRPC Go files are generated using the following commands:
go install google.golang.org/protobuf/cmd/[email protected]
go install google.golang.org/grpc/cmd/[email protected]
GO111MODULE=on go install github.com/bufbuild/buf/cmd/[email protected]
buf build pkg/
buf generate pkg/
This installs needed Protoc
files, and generates files for the gRPC service. The application also requires an access token to connect to the gRPC service the TDC-X provides. Please refer to gRPC Usage for instructions on how to generate an access token for the TDC-X.
Once you've obtained the token, navigate to grpc-wpan/pkg/auth/token.json
. Paste your generated token in the access_token
field. This token is then used to create a context
which will be used to create gRPC calls.
The main application first creates a new gRPC client that connects to the TDC-X IP address on port 8081
. It does so by using the generated gRPC Go file specification.
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
Providing WPAN discovery is on, the application first reads all available WPAN devices and prints them to the console in a readable format. Otherwise, the application retries fetching the WPAN devices 5 times before exiting the application.
for retries < maxRetries {
resp, err := client.GetDevices(ctx, &emptypb.Empty{})
if err != nil {
log.Fatalf("could not read: %v", err)
}
if len(resp.Devices) > 0 {
for _, device := range resp.Devices {
log.Println("Device Alias:", derefString(device.Alias))
log.Println("Device Address:", derefString(device.Address))
log.Println("Device UUIDs:", device.Uuids)
log.Println("Device RSSI:", derefInt32(device.Rssi))
log.Println("Paired:", derefBool(device.Paired))
log.Println("Bonded:", derefBool(device.Bonded))
log.Println("Connected:", derefBool(device.Connected))
log.Println("Trusted:", derefBool(device.Trusted))
log.Println("Blocked:", derefBool(device.Blocked))
log.Println("Services Resolved:", derefBool(device.ServicesResolved))
log.Println("----")
}
return
}
}
To see how to turn the device discovery process on, go to the next section. It's a good practice to refresh the discovery process once in a while if your device doesn't appear on the list.
The application then sleeps for 2 seconds.
In the code, set the deviceMacAddress
variable, which currently holds a placeholder value. The application will then attempt to pair with and connect the WPAN device to the TDC-X. To do so, the application first checks the availability of the set MAC address using the hal.wpan.WPAN.GetDevice
HAL service.
func checkDevice(macAddress string) bool {
req := protos.DeviceAddress{
Address: macAddress,
}
_, err := client.GetDevice(ctx, &req)
if err != nil {
log.Printf("MAC address %s not in available devices: %v. Stopping application...", macAddress, err)
return false
}
return true
}
If the device MAC address is not discovered, the application prints an appropriate log and exits. Otherwise, the Go application proceeds to pair with and connect to the device.
NOTE: Pairing is a stream in which the pairing status is possibly preceded by displaying a pass key
. Pairing usually precedes connecting to the device, but it's also possible that pairing is handled automatically when trying to establish a connection.
The application enters a streaming mode which prints all data received by the WPAN device to the console.
func attachToHDIInput(macAddress string) {
req := protos.DeviceAddress{
Address: macAddress,
}
stream, err := client.AttachToHIDInput(ctx, &req)
if err != nil {
log.Fatalf("Failed to pair %s: %v", macAddress, err)
}
for {
inputEvent, err := stream.Recv()
if err != nil {
if err == io.EOF {
log.Println("Stream closed.")
break
}
log.Fatalf("Error receiving input event: %v", err)
}
log.Printf("Received input event: %+v\n", inputEvent)
}
}
See an example print of the data stream after connecting to a device below:
2025/01/10 19:09:43 ----
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:747679000} type:4 code:4 value:458835
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:747679000} type:1 code:69 value:1
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:747679000}
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:758890000} type:17 value:1
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:758890000} type:4 code:4 value:458835
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:758890000} type:1 code:69
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:758890000}
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:770142000} type:4 code:4 value:458835
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:770142000} type:1 code:69 value:1
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:770142000}
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:780145000} type:17
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:780145000} type:4 code:4 value:458835
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:780145000} type:1 code:69
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:780145000}
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:781398000} type:4 code:4 value:458784
2025/01/10 19:09:55 Received input event: time:{sec:1736536195 nsec:781398000} type:1 code:4 value:1
The Proto file used for the WPAN service is the following:
/**
* WPAN service.
*
* Service that enables WPAN functionalities using bluez.
*/
syntax = "proto3";
package hal.wpan;
import "google/protobuf/empty.proto";
option go_package = "./protos;protos";
/**
* Represents WPAN service status.
*/
message ServiceStatusData {
bool enabled = 1;
}
/**
* Represents type of the scan.
*/
enum ScanType {
Auto = 0;
BrEdr = 1;
Le = 2;
}
/**
* Represents discovery filters.
*/
message DiscoveryFilters {
repeated string uuids = 1;
optional int32 rssi = 2;
optional int32 pathloss = 3;
optional ScanType transport = 4;
optional bool duplicateData = 5;
optional bool discoverable = 6;
optional string pattern = 7;
}
/**
* Represents device data.
*/
message DeviceData {
optional string alias = 1;
optional string address = 2;
repeated string uuids = 3;
optional int32 rssi = 4;
optional bool paired = 5;
optional bool bonded = 6;
optional bool connected = 7;
optional bool trusted = 8;
optional bool blocked = 9;
optional bool servicesResolved = 10;
}
/**
* Represents devices.
*/
message Devices {
repeated DeviceData devices = 1;
}
/**
* Represents device address.
*/
message DeviceAddress {
string address = 1;
}
/**
* Represents device connection request.
*/
message DeviceConnection {
string address = 1;
optional string profile = 2;
}
/**
* Represents type of pairing process display.
*/
enum DisplayType {
Pincode = 0;
Passkey = 1;
}
/**
* Represents device pairing process display.
*/
message PairingProcessDisplay {
DisplayType type = 1;
string content = 2;
}
/**
* Represents device pairing status.
*/
message PairingStatus {
bool paired = 1;
string errMsg = 2;
}
/**
* Represents device pairing response.
*/
message PairingResponse {
oneof response {
PairingStatus status = 1;
PairingProcessDisplay pairing = 2;
}
}
/**
* Represents input HID device basic information.
*/
message HidDeviceData {
string address = 1;
string name = 2;
}
/**
* Represents array of input HID devices.
*/
message HidDevices {
repeated HidDeviceData hidDevices = 1;
}
/**
* Represents syscall UNIX time value.
*/
message SyscallTime {
int64 sec = 1;
int64 nsec = 2;
}
/**
* Represents input event from input HID device.
* HID input events based on:
* - https://raw.githubusercontent.com/torvalds/linux/v6.7/include/uapi/linux/input.h
* - https://raw.githubusercontent.com/torvalds/linux/v6.7/include/uapi/linux/input-event-codes.h
*/
message InputEvent {
// Time in seconds since epoch at which event occurred.
SyscallTime time = 1;
// Event type.
int32 type = 2;
// Event code related to the event type.
int32 code = 3;
// Event value related to the event type.
int32 value = 4;
}
/**
* Service exposing WPAN functions.
*/
service WPAN {
// Used to enable/disable WPAN on device.
rpc ToggleService(ServiceStatusData) returns (google.protobuf.Empty) {}
// Used to get WPAN service status.
rpc ServiceStatus(google.protobuf.Empty) returns (ServiceStatusData) {}
// Used to start WPAN discovery.
rpc StartDiscovery(DiscoveryFilters) returns (google.protobuf.Empty) {}
// Used to stop WPAN discovery.
rpc StopDiscovery(google.protobuf.Empty) returns (google.protobuf.Empty) {}
// Used to retrieve found (and stored) devices.
rpc GetDevices(google.protobuf.Empty) returns (Devices) {}
// Used to retrieve device with specified address.
rpc GetDevice(DeviceAddress) returns (DeviceData) {}
// Used to remove all devices.
rpc RemoveDevices(google.protobuf.Empty) returns (google.protobuf.Empty) {}
// Used to remove device with specified address.
rpc RemoveDevice(DeviceAddress) returns (google.protobuf.Empty) {}
// Used to pair to device with specified address.
rpc Pair(DeviceAddress) returns (stream PairingResponse) {}
// Used to connect with device with specified address.
rpc Connect(DeviceConnection) returns (google.protobuf.Empty) {}
// Used to disconnect from device with specified address.
rpc Disconnect(DeviceConnection) returns (google.protobuf.Empty) {}
// Used to get all HID inputs connected via WPAN HID profile.
rpc GetHIDInputs(google.protobuf.Empty) returns (HidDevices) {}
// Used to attach to and listen for input events from HID input.
rpc AttachToHIDInput(DeviceAddress) returns (stream InputEvent) {}
}
To get the WPAN service status, use grpcurl
, which is an open-source utility for accessing gRPC services via the shell. For help setting up the grpcurl
command, refer to gRPC Usage.
Use the following line:
grpcurl -expand-headers -H 'Authorization: Bearer <token>' -emit-defaults -plaintext <device_ip>:<grpc_server_port> hal.wpan.WPAN.ServiceStatus
The token
field is the fetched TDC-X authorization token. For help fetching this token, see gRPC Usage. The device-ip:grpc_server_port
is the TDC-X IP address and the gRPC serving port. For example, if the token
value was token
and the address and port were 192.168.0.100:8081
, you would use the following line to see the device status of the WPAN service.
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 hal.wpan.WPAN.ServiceStatus
The response should be in this format:
{
"enabled": true
}
If not enabled, use the following line to do so:
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext -d '{"enabled":true}' 192.168.0.100:8081 hal.wpan.WPAN.ToggleService
{}
To start the discovery process of WPAN devices, use the following grpcurl
command:
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext -d '{"uuids":["00001101-0000-1000-8000-00805f9b34fb"]}' 192.168.0.100:8081 hal.wpan.WPAN.StartDiscovery
{}
This ensures the discovery process starts early. This particular command searches for all devices which use the 00001101-0000-1000-8000-00805f9b34fb
UUID, which serves as a filter for WPAN devices. To stop the discovery process, use the following command:
grpcurl -expand-headers -H 'Authorization: Bearer token' -emit-defaults -plaintext 192.168.0.100:8081 hal.wpan.WPAN.StopDiscovery
{}
Additionally, you can use the gRPC Clicker
VSCode extension for working with gRPC services. For help setting the service up, refer to gRPC Usage.
NOTE: If experiencing gRPC service timeouts when accessing a WPAN device, you can resolve this by adding a max_time
parameter to the gRPC call. Below is an example of how to set the timeout to 10 seconds. Adjust the timeout as needed.
grpcurl -d '{"address":"MAC_ADDR"}' -H 'Authorization: Bearer token' -plaintext -max-time 10 192.168.0.100:8081 hal.wpan.WPAN/GetDevice
To deploy the application, a Go container should be created and deployed to the TDC-X. To that end, a Dockerfile
is created. The file is shown below.
# build image for Go app
FROM golang:1.22.0 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# setting environment
ENV GOOS=linux
ENV GOARCH=arm64
RUN go build -o wpan-grpc ./cmd/main.go
# runtime image
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/wpan-grpc .
CMD ["./wpan-grpc"]
Open a terminal and paste the following commands:
docker build -t wpan-grpc-app .
docker save -o wpan-grpc-app.tar wpan-grpc-app:latest
This will build the docker container and save the application as a .tar
file which can be used for Portainer upload.
The Dockerfile
first creates a build image that is used to build the Go application. It sets the working directory as /app
, then copies the go.mod
and go.sum
files to the directory and downloads all needed files.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
Next, the Go environment is set. This needs to be done as the TDC-X device has the arm64
architecture and is based on Linux
. With this in mind, the application is set to the following:
ENV GOOS=linux
ENV GOARCH=arm64
The application is built as wpan-grpc
.
RUN go build -o wpan-grpc ./cmd/main.go
Finally, a runtime image is created from the latest alpine
version for a smaller image size. The working directory is once again set to /app
, and the application is copied. The last line specifies that, upon deployment, the wpan-grpc
application is started.
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/wpan-grpc .
CMD ["./wpan-grpc"]
To deploy the application to the TDC-X device, Portainer can be used. To see instructions on the process, refer to Working with Portainer. As soon as the image and container are set up, the application starts running.
Download the example code from the link below:
This section describes usage of the WPAN service on the TDC-X. A Node-RED gRPC application was created to demonstrate the following WPAN functionalities:
- getting / setting service status
- starting / stopping discovery
- listing devices
- listing device parameters
- pairing device
- connecting device
- attaching HID inputs from device
For implementation, the following nodes are used:
-
inject
node -
gRPC call
node -
debug
node.
The gRPC
node set is not part of the initial Node-RED package list and will have to be installed to the Palette. For gRPC
node installation, import the following file in the Manage Palette section:
Download Node
The gRPC
node server needs to be set properly to be able to connect to the gRPC server and interpret server results correctly. The following configuration is used:
-
Server
:192.168.0.100
-
Port
:8081
- a
Proto File
is provided.
To test out any of the listed functionalities, use the inject
node. The result should appear in the debug
node.
See a screenshot of the application below:
Download the example code from the link below: