-
Notifications
You must be signed in to change notification settings - Fork 0
[HW Interface] Development Documentation
This is the code documentation for the examples created without using the hardware interface, so the data is accessed directly via your TDC-E device. The following examples using direct access are created:
- Digital Inputs/Outputs (DIO)
- Analog Inputs (AIN)
- Controller Area Network (CAN)
- Global Positioning System (GPS)
- RS-232
- RS-4xx
- 1-Wire
- Accelerometer / Magnetometer (ACC/MAG)
- Bluetooth
Examples are created in Python and Go, and mostly also contain a Node-RED example of the same functionalities as reference.
This part of the documentation touches on direct access to DIO values in Python and Go. The examples shown below indefinitely switch the light from the DIO LED on and off using the following Linux terminal commands, where The X
part is the GPIO pin corresponding to a digital input and/or output.
echo 1 > /sys/class/gpio/gpioX/value
echo 0 > /sys/class/gpio/gpioX/value
The first command sets the DIO value to 1, effectively turning on the LED. The latter command turns the DIO off as it sets the value to 0.
The following application is created to read and set the value state of the GPIO and is used to execute a Linux TDC-E command directly on the device. It is a program that runs indefinitely and selects one of the commands specified in the commands
array.
commands = ["echo 1 > /sys/class/gpio/gpio496/value", "echo 0 > /sys/class/gpio/gpio496/value"]
while True:
# executes a random command from the list
execute(random.choice(commands))
time.sleep(5)
It turns the DIO ON
or OFF
each 5 seconds. Every time the command is executed, the Linux file is changed. The command is executed using the following logic:
def execute(cmd):
try:
output = subprocess.check_output(["sh", "-c", cmd], stderr=subprocess.STDOUT, universal_newlines=True)
print(output)
except subprocess.CalledProcessError as e:
print(e.output)
The execute()
function takes a command specified in the script and uses the subprocess
module to check the output of the command that is executed, then prints the output of the console. In case of an error, it prints the error.
NOTE: The file structure of the TDC-E specifies specific paths for individual gpios. It's important to note that the GPIO pin 496 is specifically made for the pin labeled A. Pins with different labels will have different address and can be read from the PWR+AIN/DIO cable diagram or the How to use Digital-I/O blocks manual.
Other commands can be given to the execute()
function. For example, reading the gpio value of pin A would require the following command:
cat /sys/class/gpio/gpio496/value
The following application is created to read and set the value state of the GPIO and is used to execute a Linux TDC-E command directly on the device. It is a program that runs indefinitely and selects one of the commands specified in the commands
slice.
The program runs in an indefinite for loop, executing a random command from the commands
slice, then sleeps for 5 seconds so the difference in DIO states can be seen clearly. The following code demonstrates how to fetch a random command from the commands slice, sending it to the execute
function.
// ON, OFF; LED is on A (496)
var commands = [2]string{"echo 1 > /sys/class/gpio/gpio496/value", "echo 0 > /sys/class/gpio/gpio496/value"}
func main() {
// infinite loop
for {
// executes a random command from the slice
// go versions before 1.20 have to use rand.Seed(time.Now().Unix())
execute(commands[rand.Intn(len(commands))])
time.Sleep(5 * time.Second)
}
}
It's important to note that the GPIO pin 496 is specifically made for the pin labeled A. Pins with different labels will have different address and can be read from the PWR+AIN/DIO cable diagram or the How to use Digital-I/O blocks manual.
The command is executed with the following Golang code:
out, err := exec.Command("sh", "-c", cmd).Output()
This code executes a shell command using the os/exec
Go package. The cmd
parameter is the parameter for changing the LED state.
This part of the documentation describes directly accessing current AIN values in raw and scaled format in both voltage and current mode.
The program functions as follows: it executes three consecutive commands, then calculates the value of the voltage and prints out the result infinitely, writing the new value of the current voltage into the terminal each second.
The following commands are used to access needed parameters:
cat /sys/bus/iio/devices/iio:device1/in_voltage5_raw
cat /sys/bus/iio/devices/iio:device1/in_voltage5_scale
The first command reads the current raw value of the voltage.
The path to the values change depending on the AIN used as the Output. For AIN_F
used in the program, the value can be found in voltage5
. This means that the raw voltage will be found at in_voltage5_raw
, the offset at in_voltage5_offset
, the scale at in_voltage5_scale
and so on. The format of the files is in_voltageN_X
, where N
is in range from 0 to 7
. Below is a table of AINs and their corresponding number.
AIN | Corresponding Number |
---|---|
AIN_A | 0 |
AIN_B | 1 |
AIN_C | 2 |
AIN_D | 3 |
AIN_E | 4 |
AIN_F | 5 |
INT_5V | 6 |
VIN | 7 |
Those commands are all executed by the following function:
func execute(cmd string) float64 {
out, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
fmt.Printf("%s", err)
return 0
}
output := string(out)
/* spaces removed */
output = strings.TrimSpace(output)
outputFloat, err := strconv.ParseFloat(output, 32)
if err != nil {
fmt.Println("Problem parsing: ", err)
}
return outputFloat
}
This function executes a shell
command, fetches the output into the out
variable and removes any unnecessary spaces before parsing the value and returning it, providing additional checks of whether an error has occurred. The value of the current or voltage is then calculated from raw value. Whether current or voltage is to be measured is set in a mode
variable. Accounting for voltage divisor is set to true in the account
variable which can also be changed according to measurement needs.
The following code shows the initial setup of the calculation.
command := "cat /sys/bus/iio/devices/iio:device1/in_voltage5_raw"
raw := execute(command)
command2 := "cat /sys/bus/iio/devices/iio:device1/in_voltage5_scale"
scale := execute(command2)
avg_clean := raw * scale
err := 0.0059*avg_clean - 0.013
avg_comp := avg_clean + err
Firstly, the commands are run. The avg_clean
variable is set to show the clean value of the voltage or current and is calculated by multiplying raw values with the set scale. If the scale needs to be changed, the following Linux command can be used to perform the task:
echo 0.002500 > in_voltage6_scale
The error is calculated by multiplying the clean average with 0.0059, then substracting 0.013. avg_comp
is avg_clean
with the added error. The following code calculates the reminder of the value:
switch mode {
case "voltage":
if account {
if avg_comp < 0 {
vg_comp = 0
}
avg_comp = (avg_comp * 27.5) / 7.5
}
if !account {
avg_comp = avg_clean
}
fmt.Printf("Current voltage: %f\n", avg_comp)
break
case "current":
if account {
if avg_comp < 0 {
avg_comp = 0
}
avg_comp = (avg_comp * 27.5) / 7.5
avg_comp = avg_comp * 1000 / 100.2
}
if !account {
avg_comp = avg_clean
}
fmt.Printf("Current voltage: %f\n", avg_comp)
break
}
This code snippet shows how the value is calculated depending on the mode that is used. In the case of using voltage
mode, and if accounting for voltage divisor, the final value is calculated by being multiplied with 27.5, then divided by 7.5 if the value is larger or equal to zero. In the case of the average being smaller than 0, the value is displayed as a 0.
If the voltage divisor is not to be accounted for, the final value is set to the clean value; scale
multiplied by the raw value
.
The current is calculated similarly, but an additional operation is performed. The avg_comp
is multiplied by another 1000 and divided by 100.2 in the case of accounting for the voltage divisor. The SICK Distance Sensor DT50-P2113
is a sensor that measures distance from 4 mA to 20mA and uses current to show its values, which means it will use this form of calculation.
NOTE: Make sure to change the value of the mode
and account
variables to set whether the measurement mode is voltage or current, and whether to take the voltage divisor into account depending on the device you are connecting to the TDC-E analog input to get the correct values.
The Go application example that has been chosen to represent direct access to CAN values is using the RAW CAN protocol as the logic is simpler and doesn't vary much, no matter the device.
To directly access the data when not listening on a websocket, the internal command candump
can be used. For this, a small Go application is built, containing only one function for executing data and the main function that specifies the command to be run.
func execute(cmd string) {
out, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
fmt.Printf("%s", err)
}
output := string(out[:])
fmt.Println(output)
}
func main() {
execute("candump | head -n 11")
}
So as to show the data in the terminal, the program fetches the first ten messages from the bus alongside the header as the first row of the result, specified by candump | head -n 11
. This command is sent to the execute
function, and exec.Command
from the os/exec
package is used to run the command on the system. It's also important to note that this program will work only from an environment where the CAN is connected and where the command is available. The value of the first ten packages to come is then printed to the terminal.
The command line can also be changed to specify which CAN is used. If using can0
, the command can be replaced with candump can0
. If using can1
, it can be replaced with candump can1
.
This GPS application uses both websockets and direct access to work with GPS data. In this section, the entire application will be shown and described to show a good example of what a GPS application in Node-RED can do, but in the examples folder, there are two files which you can go through. The first one, called flows (40).json
is the application in its entirety, while the second one, gps-raw-focus.json
, is used to show the focus on the raw GPS fetching, as this is the part that implements direct TDC data access.
The application provides the ability to check raw data messages as they are received to check additional parameters of the geolocation data and display simplified objects on the map. The raw format of the incoming NMEA messages feature these headers:
- GNRMC
- GNGSA
- GPGSV
- GLGSV
- GNVTG
- GNTXT
To receive the objects, an exec function is used. It starts the service for fetching the current geolocation and searches the fetched string by selecting the current message. The output is generated while the command is running (spawn mode). For added logging, the exit code and potential error messages debug nodes are added to the exec node.
To fetch the GPS data, the following command is used:
cat /dev/ttymxc6
In short, this command reads everything that is on the location of /dev/ttymxc6
on the TDC-E device, which happens to be the file into which GPS data is received and saved to.
When the message reaches [CR][LF], which indicates the end of a NMEA message, the string is joined and split by \r\n and the output is cleaned. Raw output can be viewed with the debug node. The raw output is sent to a web socket which is set to listen on ws://192.168.0.100:1880/ws/gps. Data is sent continuously by checking the Inject once after X seconds option on the inject node.
Below is an image of the output of the RAW format of the incoming NMEA messages. For the view, Google Chrome's Web Socket Test Client was used.
The web socket can also request data. When an object is added, the program is configured to receive the message by listening on the same socket, generating an output in the Debug tab.
Another feature of this program is reading specific data from different kinds of objects, turning them into JSON format and showing them on a map. All raw data can be extracted from the objects if the NMEA message format is known. As a sample, latitude and longitude was extracted from GNGLL, GNRMC and GNGGA NMEA messages. This is done in the get-lat-long function.
This function splits the NMEA message by the ',' separator, creating an array of values whose length depends on the NMEA message type. The function then checks the first value in the array, which contains the message header, to check what kind of message the object is. JSON objects are created depending on the switch statement's cases.
As previously mentioned, the program currently created extract longitude and latitude information and stores that information in a JSON object. An internal function called createObject handles this. It takes in needed parameters, converts latitude and longitude formats from degrees/minutes to decimal degrees, then returns the finished JSON object.
Raw latitude and longitude are stored in the following data format:
- latitude: ddmm.mmmmmmm (0-7 decimal places)
- longitude: dddmm.mmmmmmm (0-7 decimal places)
The function also sets a global latitude and longitude variable. The state of the variables can be checked by a three-node segment in the code under the comment 'check variable state'. if the timestamp is injected into the check-global-long-lat function, the current state of the longitude and latitude variables will be printed out in an array in the Debug tab.
To expand the object information in the future, the switch statement and the createObject function will need to be updated.
Lastly, the JSON object is reformatted to fit the worldmap's criteria, a name is set to the object, and the object is subsequently shown on the Worldmap on http://192.168.0.100:1880/rawworldmap.
It is important to note that the main program should not be run simultaneously with the raw data message view program, since the latter takes priority.
In this section, accessing data directly from the TDC-E will be described. For the implementation of this simple application, the Go programming language was chosen.
This simple app consists of a single main.go
file which runs Linux commands to execute reading and writing into the ttymxc5
file which is used for RS 232 data. It is comprised of two goroutines which execute commands simultaneously; one fetches data indefinitely and one sends a random string to the interface each 3 seconds.
First, a wait group is created, and two processes are added to it. The first goroutine is made to fetch data from the TDC-E, while the other sends data to the ttymxc5
.
Fetching data from the TDC-E device goes as following:
go func() {
defer wg.Done()
command := "cat /dev/ttymxc5 | head -n 1"
for {
execute(command)
}
}()
A simple goroutine, executing the command specified above indefinitely, closing the wait group after the goroutine has finished.
The other goroutine sends a random string from a defined slice to the interface. The code looks the following:
go func() {
defer wg.Done()
for {
command := "echo " + datasend[rand.Intn(len(datasend))] + " > /dev/ttymxc5"
execute(command)
time.Sleep(3 * time.Second)
}
}()
The execute(command)
function works as following:
func execute(cmd string) {
out, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
fmt.Printf("%s", err)
}
output := string(out)
if cmd == "cat /dev/ttymxc5 | head -n 1" {
fmt.Println("Data received: ", output)
}
}
The execution of the Linux command is handled by the internal "os/exec"
package. The shell executes the sent command and returns the output of the command. If there is an error, a message will be printed. Otherwise, the output is casted into a string, and printed to the terminal if the command is the fetch command.
In the end, wg.Wait()
is added to wait for all the goroutines to finish.
NOTE: _Since the program operates in an infinite for loop and goroutine, only the first send to the ttymxc5
interface will be sent to the websocket. This is because of process priorities, we should be taken into account when working with interfaces. _
Find the specified commands for working with the RS 232 data directly from the TDC-E below:
-
cat /dev/ttymxc5
- fetching data -
echo "some-string" > /dev/ttymxc5
- sending data
In this section, directly accessing values from the TDC-E regarding RS 4xx data will be provided. The application was made and tested with the Leaf Wetness Sensor, which operates with a Baudrate of 9600, using the communication protocol MODBUS, and with RS 485, which are parameters that are needed to read data from the sensor when using RS-4xx**.** The implementation is made in the Go programming language.
The application uses a single .go
file to run its functionalities. The github.com/goburrow/modbus
package is used to work with the MODBUS communication protocol. First, a handler is created. The handler is used to manage the slave ID, baud rate, data bits, parity, stop bits and timeout of the MODBUS. The following code sets up the handler and creates a serial client.
/* Creates modbus serial client */
func createHandler() *modbus.RTUClientHandler {
/* setting handler */
handler := modbus.NewRTUClientHandler("/dev/ttymxc1")
handler.SlaveId = 254
handler.BaudRate = 9600
handler.DataBits = 8
handler.Parity = "N"
handler.StopBits = 1
handler.Timeout = 2 * time.Second
return handler
}
Note that the handler is created using a modbus
object and using the /dev/ttymxc1
path. The MODBUS client is then created using the handler that was just created before connecting to the client.
client := modbus.NewClient(handler)
err := handler.Connect()
if err != nil {
log.Fatal("Failed to connect to Modbus device:", err)
}
defer handler.Close()
The defer handler.Close()
line ensures the client connection is closed as soon as all operations with it cease. Next up, the results are viewed. In this part of the code, the start address and the quantity of the data is specified so that the client could read holding registers.
startAddress := uint16(0)
quantity := uint16(2)
results, err := client.ReadHoldingRegisters(startAddress, quantity)
Only two values will be extracted from the sensor, and those are the temperature and humidity of the sensor. If the length of the results is larger or equal to two, which is the quantity number, the humidity and temperature are read from the registers 1 and 3 respectively and the result is mapped to a JSON, displaying the data in JSON format.
if len(results) >= 2 {
humidity := float64(results[1]) / 10.0
temperature := float64(results[3]) / 10.0
/* Create result and map */
result := map[string]float64{
"Humidity": humidity,
"Temperature": temperature,
}
resultJSON, err := json.Marshal(result)
if err != nil {
log.Fatal("Failed to marshal JSON:", err)
}
fmt.Println(string(resultJSON))
}
For direct access to values received from the 1-Wire sensor, a Go program was created. This program specifies the Linux command that is to be used in your TDC-E device for fetching the current value of the sensor, then fetches this value infinitely in a for loop, waiting in a five-second interval. In this example, a DS1820 Digital Thermometer pin was used to read temperature.
The command to fetch the data is as follows:
cat /sys/bus/w1/devices/10-000803676b81/w1_slave
This command is then executed using the built-in Go library "os/exec"
. The following lines of code are used to do so:
out, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
fmt.Printf("%s", err)
}
output := string(out)
If the method runs successfully, it returns the output of the command, and in case of an error, it returns an error message. The value is then split and parsed to show the real temperature value in °C. The temperature is located at the end of the output string, is parsed to float and then divided by 1000 to show the correct temperature.
For the implementation of the main.py
file, which is used as the central part of the application, the DirectTdc.py
custom class is used. This class imports the modules subprocess
and os
and is used to run Linux terminal commands via Python. The Echo()
function, for example, is used for issuing the echo
commands described above to enable the accelerometer and magnetometer.
import subprocess
import os
#Reads data directly from TDC-E
class DirectTDC:
def ReadData(self, path):
try:
output = subprocess.check_output(['cat', path], universal_newlines=True)
return output
except subprocess.CalledProcessError as e:
print("Error: {e}")
return e
def Echo(self, command):
try:
os.system(command)
except Exception as e:
print(f"Error: {e}")
The main application runs in the following way. The classes DirectTdc
, math
and time
are imported. The program runs in an infinite while
loop, continuously checking accelerometer and magnetometer data. It is of the following structure:
accPath = "/sys/class/misc/FreescaleAccelerometer/data"
magPath = "/sys/class/misc/FreescaleMagnetometer/data"
datai = ""
if __name__ == "__main__":
while(1):
datai = dtdc.DirectTDC()
acc = datai.ReadData(accPath)
acc = splitter(acc, "\n")
print("Accelerometer data: " + acc[0])
mag = datai.ReadData(magPath)
mag = splitter(mag, "\n")
print("Magnetometer data: " + mag[0])
del datai
print("\n-----------ACC/MAG-DATA-----------")
parseAllData(acc[0], mag[0])
time.sleep(5)
The while loop starts if the name of the function is main
. Initially, an instance of the DirectTDC
class is created so that the methods for issuing terminal commands defined in the class can be accessed.
First, the data from the accelerometer is read by reading from the path that is defined in the accPath
variable using the ReadData()
method. This method tries to execute a Linux command. The program runs the line subprocess.check_output()
. This function checks what kind of answer the terminal receives from the command. In this case, the command is reading the forwarded path (cat
) and a string output is expected. If the output is a string file, the function will return the accelerometer data. The command could also return an error, in which case the Python program will catch the error, print and return it instead.
The data is then formatted as it contains a new line after the numeric values, so the data is split by a new line ("\n"
) and is printed in the next step. The process is repeated for the magnetometer data, with the only difference being the path that the data is stored at.
The datai
object is then deleted, and the accelerometer and magnetometer data is passed onto the next function which cleanly prints all values of the given object. Both accelerometer and magnetometer have an X, Y and Z values for each axis and are printed in their measuring unit.
# parses ACC and MAG data
def parseAllData(acc, mag):
# parsing ACC
accData = splitter(acc, ",")
print("ACC X: " + accData[0] + "g")
print("ACC Y: " + accData[1] + "g")
print("ACC Z: " + accData[2] + "g")
print("\n")
# parsing MAG
magData = splitter(mag, ",")
print("MAG X: " + magData[0] + "μT")
print("MAG Y: " + magData[1] + "μT")
print("MAG Z: " + magData[2] + "μT")
print("\n")
parseCompassHeadings(magData[0], magData[1])
A function for parsing compass headings has been added to demonstrate working with the magnetometer. To calibrate the yaw of the device, the X and Y value of the magnetometer is needed, which is contained in magData[0]
and magData[1]
respectively. The implementation of the function is as following:
def parseCompassHeadings(x, y):
offset_x = 0
offset_y = 0
scale_x = 1.0
scale_y = 1.0
# Calibrating MAG data
calibrated_x = (float(x) - offset_x) * scale_x
calibrated_y = (float(y) - offset_y) * scale_y
# Calculating yaw - compass heading
yaw = math.atan2(calibrated_y, calibrated_x)
yaw = math.degrees(yaw)
# Top of the device (Y-axis) pointing in a direction X° counterclockwise from magnetic North
if yaw < 0:
yaw += 360.0
print("Yaw (Compass Heading): {:.2f} degrees".format(yaw))
print("\n")
Firstly, four variables are declared which can be used to calibrate the compass data. Those are offsets
and scales
for both x
and y
. For the TDC-E's magnetometer, the those values do not have to be set, so they are set to their default values, which is 0 for the offset, and 1 for the scale. Calibrating consists of subtracting the offset from the original numeric value and multiplying it with the scale, which defaults to the same value in this case.
To calculate the yaw value from the magnetometer's data, the math.atan2()
method is used, which returns the arc tangent of y/x in radians, where x
and y
are the coordinates of the point, and the return value is in radians. Since we want to show the yaw in degrees, math.degrees()
is used. The program then checks whether the yaw value is a value that is less than zero. If it is, the yaw is added 360 to form a complete angle. The value is then printed to the screen.
After printing, the program sleeps for five seconds, then repeats the process indefinitely.
The example provided shows how to read Bluetooth advertisement data and is implemented in Python. But before that, the environment to work with Bluetooth needs to be set up.
This section describes how to work with Bluetooth devices. It describes turning Bluetooth on, scanning for available devices, making your device visible, pairing, trusting and connecting the wanted device.
To enter the working environment for connecting devices with your TDCE, connect to ssh [email protected]
. Authenticate using your credentials, then follow the steps listed below.
To turn Bluetooth on, type the following line into the terminal.
hciconfig hci0 up
This will initialize your HCI device. To check the interface status now, use hciconfig
. It will display the Type
of your device, BD Address
, ACL MTU
, SCO MTU
, and RX
and TX bytes
. To display the MAC address of the Bluetooth module, you can use hcitool dev
.
Next up, we will start the Bluetooth interface. To do so, type the following command:
bluetoothctl
You will enter the [bluetooth]
terminal, and the terminal will show your controller MAC address and whether the device is pairable.
List the pairable devices using devices
. This will show you all the devices which you can pair with your TDC-E at the moment. The MAC address of the pairable device is listed, and so is its name. If the device you want to pair isn't listed, make sure to set the following parameters:
pairable on
discoverable on
If you want to turn on advertising, also type the following line:
advertise on
Now, type scan on
. This should list all the Bluetooth devices that can be paired with your device. To stop scanning, type scan off
. Next up, find the MAC address of the device you want to pair with the TDC-E. For example, we want to connect our P T 80111D
device to the TDC-E. It is listed in the following format:
Device E1:6B:1A:7E:C5:BE P T 80111D
The MAC address of the device is E1:6B:1A:7E:C5:BE
. Copy the address and proceed to the following steps.
Different types of Bluetooth devices require different types of connections. This guide discusses pairing, trusting and connecting a device, though a lot of Bluetooth devices use advertisements to send data to other devices, which doesn't always require connection. To pair the device, use:
pair [dev]
Here, the [dev]
part of the command should be replaced with your copied MAC address. The TDC-E should now attempt to pair with the device. Pairing is a requirement before the TDC-E can connect to the wanted Bluetooth device. If a problem arises during pairing, check the Bluetooth device parameters or restart your Bluetooth service.
Sometimes, the device needs to be trusted before connecting to it. To do so, type:
trust [dev]
The [dev]
part is once again related to the MAC address of the device you want to connect. Now, to connect to a device, use:
connect [dev]
Your Bluetooth device should now be connected t the TDC-E. To check the devices parameters, you can type info [dev]
, which will list the device's name, alias, whether it is paired, trusted, blocked, connected or a legacy pairing. It will also list the manufacturer data key and value. Another useful command that provides access to the Bluetooth subsystem monitor infrastructure for reading HCI traces is btmon
.
To exit the bluetooth
interface, type exit
or quit
. To turn Bluetooth off, type hciconfig hci0 down
.
To test the Bluetooth interface and fetch data from devices, a BLUE PUCK T EN12830
BLE temperature sensor was used. The following link leads to the ELA Innovation GitHub documentation used for connecting to the desired sensor. Find the catalogue of the BLUE PUCK T EN12830 on this link.
The code shown in this section was written in Python and uses the bluepy
module for scanning and processing data from Bluetooth devices. Except from the code, it is important to note the Dockerfile and docker-compose.yml detailed in a latter section as working with Bluetooth entails running the Python script in a correctly set-up container.
As was already mentioned, the code for fetching advertisement data from our BLUE PUCK T EN12830
device uses the bluepy.btle
module to import the Bluetooth Scanner
and DefaultDelegate
.
Firstly, the target_mac
address is set to the value of the MAC address that we want to monitor. In this case, the MAC address of the device was found and set up as a variable in the program.
target_mac = "e1:6b:1a:7e:c5:be"
The script is set up to scan for Bluetooth devices with a ScanDelegate
set to the value DefaultDelegate
.
if __name__ == "__main__":
scanner = Scanner().withDelegate(ScanDelegate())
print("Scanning for advertisements...")
devices = scanner.scan(10.0)
for dev in devices:
pass
When the scan starts, the script prints out "Scanning for advertisements"
and scans for devices for 10 seconds. This value can be updated according to the approximate duration of time it would take for the TDC-E device to fetch the data from this specific BLE device, which sends advertisements periodically according to its device configuration. Keep in mind that the device might not send the needed data in the time period set in the scan function.
The data is set to be processed with the ScanDelegate
class. As it is processed there, each other device dev
should be skipped as we do not want to process data for every advertisement scan of every device. This is why every dev
in devices
is set to pass
. The implementation of processing the needed data is shown below.
class ScanDelegate(DefaultDelegate):
def __init__(self):
DefaultDelegate.__init__(self)
def handleDiscovery(self, dev, isNewDev, isNewData):
if dev.addr == target_mac:
print("Received advertisement from", dev.addr)
print("RSSI:", dev.rssi)
scan = dev.getScanData()
print("Advertisement data:", scan)
parseTemperature(scan)
sys.exit()
The ScanDelegate
is initialized, then handles the discovery of data in the handleDiscovery
method, where the device's MAC address is read and compared to the target_mac
that was set at the beginning of the script. If so, the device's address, RSSI and scan data is printed.
The parseTemperature()
function is implemented to parse the hexadecimal data that is received from the temperature sensor. The format of the advertisement data is the following:
Advertisement data: [(1, 'Flags', '06'), (22, '16b Service Data', '6e2a6009'), (9, 'Complete Local Name', 'P T 80111D')]
The temperature data is located in the 16b Service Data
part with the value of 6e2a6009
.
The first four digits 2a6e
represent a UUID (Universally Unique Identifier) assigned to a standard GATT (Generic Attribute Profile) characteristic, which corresponds to Temperature Measurement. The temperature data that is sent from the sensor is 6009, or rather 0960
, which translates to 2400
. This data needs to be divided by 100 to get the measured temperature in °C. The following code extracts this data from a list of tuples returned from the sensor, then divides the result by 100 to print the result.
def parseTemperature(scan):
tempData = scan[1][2][4:]
firstVal = tempData[-2:]
secondVal = tempData[-4:-2]
val = str(firstVal) + str(secondVal)
val = int(val,16) / 100
print("Temperature value (Celsius): " + str(val))
The temperature data is extracted from the scan data by taking the third ([2]
) data value from the second ([1]
) tuple in the given list, extracting only the data from the fourth index onward ([4:]
). The value of the data is split, parsed to string, then divided by 100 before being printed. Upon finding one scanned data, the program exits.
The examples described here have been implemented as simple code snippets to demonstrate the usage of the described interfaces. Still, even with more complicated application needs, often times the same techniques are used, so it is beneficial to take a look at the way they work. These examples show usage of direct access to TDC-E data, which is faster than fetching data remotely, but are dependent on good environment setup, knowledge of data structure and data location.
Make sure to check out TDC-E Configuration to set up your working environment properly before implementing the described logic. Happy coding!