Monitoring CPU metrics on Grafana using Node-red

Grafana 8.2.2 seems does not support running a shell script natively. I would recommend parsing the CPU status via an external REST API. Node-red will be used in the following example.

In this article, you will learn:

  1. How to monitor cpu status on Node-red
  2. How to create a REST api on Node-red
  3. How to parse the REST data using Grafana Infinity plug-in

Monitoring CPU status

We will be using exec node to run shell commands. In addition, we can add buttons to shutdown and reboot as well.

Notice that the script for running CPU usage is a bit complicated: top -d 0.5 -b -n3| grep "Cpu(s)"|tail -n 1| awk '{print 100-$8}'as well as ram free | grep Mem | awk '{printf "%.2f", ($2-$7)*100/$2}'. The final number is calculated in percentage, where 100 is overloaded. You may play around with the parameters, I will make a post to briefly introduce shell manipulation in the future.

[{"id":"2653a0b0.26d638","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":2,"width":0,"height":0,"gtype":"gage","title":"CPU Temperature","label":"C","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1090,"y":120,"wires":[]},{"id":"fba68adf.14e13","type":"exec","z":"1a2e114b.5f398f","command":"vcgencmd measure_temp","addpay":false,"append":"","useSpawn":"","timer":"","name":"raspberry pi temperature","x":630,"y":160,"wires":[["fa5b499.e176cb8"],[],[]]},{"id":"7c8379de.068868","type":"inject","z":"1a2e114b.5f398f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":310,"y":200,"wires":[["fba68adf.14e13","972ece2a.3dbe8","6242be99.26ac88"]]},{"id":"fa5b499.e176cb8","type":"function","z":"1a2e114b.5f398f","name":"trim string","func":"str = msg.payload\nmsg.payload = str.substring(5,9);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":860,"y":120,"wires":[["2653a0b0.26d638","28b64a2c.b32116","7d734a89.e1e164"]]},{"id":"683a2b05.736204","type":"ui_button","z":"1a2e114b.5f398f","name":"","group":"c5f1b8aa.45bc08","order":2,"width":0,"height":0,"label":"Reboot","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":320,"y":580,"wires":[["1cf31554.2aaa63"]]},{"id":"1cf31554.2aaa63","type":"exec","z":"1a2e114b.5f398f","command":"sudo reboot","addpay":false,"append":"","useSpawn":"","timer":"","name":"sudo reboot","x":590,"y":580,"wires":[[],[],[]]},{"id":"38e53dbf.ca594a","type":"ui_button","z":"1a2e114b.5f398f","name":"","group":"c5f1b8aa.45bc08","order":3,"width":0,"height":0,"label":"Shutdown","color":"","bgcolor":"red","icon":"","payload":"","payloadType":"str","topic":"","x":320,"y":640,"wires":[["c409cae5.8e4128"]]},{"id":"c409cae5.8e4128","type":"exec","z":"1a2e114b.5f398f","command":"sudo shutdown 0","addpay":false,"append":"","useSpawn":"","timer":"","name":"sudo shutdown -h now","x":620,"y":640,"wires":[[],[],[]]},{"id":"28b64a2c.b32116","type":"ui_chart","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":3,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"1000","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":1070,"y":160,"wires":[[]]},{"id":"972ece2a.3dbe8","type":"exec","z":"1a2e114b.5f398f","command":"top -d 0.5 -b -n3| grep \"Cpu(s)\"|tail -n 1| awk '{print 100-$8}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"cpu usage","x":590,"y":240,"wires":[["b9372186.ed1a5","8144e676.4d5d68"],[],[]]},{"id":"6242be99.26ac88","type":"exec","z":"1a2e114b.5f398f","command":"free | grep Mem | awk '{printf \"%.2f\", ($2-$7)*100/$2}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"ram usage","x":590,"y":320,"wires":[["d8ede4a4.507998","9b301b09.8c0468"],[],[]]},{"id":"b9372186.ed1a5","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":1,"width":0,"height":0,"gtype":"gage","title":"Processor","label":"CPU","format":"{{value}}","min":0,"max":"102","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1080,"y":200,"wires":[]},{"id":"9b301b09.8c0468","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"9a96a8b1.92db78","order":1,"width":0,"height":0,"gtype":"gage","title":"Memory","label":"RAM","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1060,"y":300,"wires":[]},{"id":"d8ede4a4.507998","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1070,"y":340,"wires":[]},{"id":"8144e676.4d5d68","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1090,"y":240,"wires":[]},{"id":"57aad7a5.391708","type":"inject","z":"1a2e114b.5f398f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":310,"y":460,"wires":[["8c19e802.e92e88"]]},{"id":"8c19e802.e92e88","type":"exec","z":"1a2e114b.5f398f","command":"df -h / | awk '{print $5}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"disk storage usage","x":610,"y":460,"wires":[["9869bc88.1d73f"],[],[]]},{"id":"9869bc88.1d73f","type":"function","z":"1a2e114b.5f398f","name":"trim string","func":"var test = msg.payload.substr(5);  //trim off Use%\nmsg.payload = test.substring(0, test.length - 1) //trim off the next line string\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":900,"y":460,"wires":[["1ba34040.c7d4","9c3b4510.f9fbc8"]]},{"id":"9c3b4510.f9fbc8","type":"debug","z":"1a2e114b.5f398f","name":"","active":false,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1130,"y":480,"wires":[]},{"id":"1ba34040.c7d4","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"72fc319.cc425d","order":1,"width":0,"height":0,"gtype":"gage","title":"Disk","label":"Usage","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"x":1110,"y":440,"wires":[]},{"id":"7d734a89.e1e164","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":80,"wires":[]},{"id":"1890881e.83819","type":"ui_group","name":"Col1","tab":"c3173234.2636e","order":1,"disp":false,"width":"6"},{"id":"c5f1b8aa.45bc08","type":"ui_group","name":"Actions","tab":"c3173234.2636e","order":4,"disp":true,"width":"6"},{"id":"9a96a8b1.92db78","type":"ui_group","name":"Col2","tab":"c3173234.2636e","order":2,"disp":false,"width":"6"},{"id":"72fc319.cc425d","type":"ui_group","name":"Col3","tab":"c3173234.2636e","order":3,"disp":false,"width":"6"},{"id":"c3173234.2636e","type":"ui_tab","name":"RPi Control","icon":"dashboard","order":1}]

Creating REST api on Node-red

We will use the http in and http response node to set up the REST API. Please note that you cannot initiate an HTTP response by itself. You should rather trigger the flow by going into the URL, or by an HTTP request node or by Grafana in this case. The http in node (Method: GET) will initiate the flow once the URL has been requested. Here is an example of setting up a simple payload as a content of the URL /cpumetrics. Open the browser and navigate to http://localhost:1880/cpumetrics.

[{"id":"c01ca5bf.a938b8","type":"http in","z":"9566abd8.7dc4d8","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":140,"y":140,"wires":[["1b98b2af.7af11d"]]},{"id":"8ccf75b3.ddd338","type":"comment","z":"9566abd8.7dc4d8","name":"REST endpoint","info":"","x":140,"y":80,"wires":[]},{"id":"1b98b2af.7af11d","type":"function","z":"9566abd8.7dc4d8","name":"simple payload","func":"msg.payload = {\"message\": \"this is the payload\"};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":140,"wires":[["26f72364.1a687c"]]},{"id":"26f72364.1a687c","type":"http response","z":"9566abd8.7dc4d8","name":"","statusCode":"","headers":{},"x":510,"y":140,"wires":[]}]

REST api with query parameters

Sometimes you want your reply to be changed dynamically based on different parameters. You could use an if-else statement on the function to give a specific reply to different parameters. After deploying the flow, navigate to the URL: http://localhost:1880/cpumetrics?metrics=cpu. If the metric parameter is equal to “cpu”, the page will reply a {“message”: “cpu loading”} otherwise, {“message”: “some other metrics”} will be replied.

[{"id":"59ff2a1.fa600d4","type":"http in","z":"1adc4b7f.bb9655","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":240,"y":200,"wires":[["a3d72dd3.05bc2","2ec4bd64.072302","e6e4ab4c.254ea8","8a2f5529.8baa88","54c1e70d.ab3e18","c4d1aa9b.1bb7c8"]]},{"id":"4eefdf83.b473c","type":"comment","z":"1adc4b7f.bb9655","name":"REST endpoint","info":"","x":240,"y":140,"wires":[]},{"id":"266c286f.d993d8","type":"http response","z":"1adc4b7f.bb9655","name":"","statusCode":"","headers":{},"x":1030,"y":220,"wires":[]},{"id":"a3d72dd3.05bc2","type":"function","z":"1adc4b7f.bb9655","name":"request with Query Parameters","func":"if (msg.payload.metrics === \"cpu\"){\n    msg.payload = {\"message\": \"cpu loading\"};\n}else{\n    msg.payload = {\"message\": \"some other metrics\"};\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":100,"wires":[["266c286f.d993d8","2ec4bd64.072302"]]}]

REST api with cpu metrics

In the last example, we will put the real-time cpu metrics as content. A join node is used to put together the disk space and ram space into a single payload. Besides, we will use a http request node to invoke the endpoint. The payload could be checked on the debug window.

[{"id":"4512cdf1.3cd794","type":"exec","z":"608a04eb.6011bc","command":"free | grep Mem | awk '{printf \"%.2f\", ($2-$7)*100/$2}'","addpay":"","append":"","useSpawn":"false","timer":"","oldrc":false,"name":"RAM","x":490,"y":220,"wires":[["c5b1a07e.aa5fb"],[],[]]},{"id":"1ade78d7.8383f7","type":"http in","z":"608a04eb.6011bc","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":120,"y":160,"wires":[["334a3882.c0e9b8","696e606d.dc86f"]]},{"id":"696e606d.dc86f","type":"function","z":"608a04eb.6011bc","name":"append topic","func":"msg.topic = \"ram\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":220,"wires":[["4512cdf1.3cd794"]]},{"id":"334a3882.c0e9b8","type":"exec","z":"608a04eb.6011bc","command":"df -h / | awk '{print $5}'","addpay":"","append":"","useSpawn":"false","timer":"","oldrc":false,"name":"disk space","x":350,"y":160,"wires":[["c9c3ed28.056db"],[],[]]},{"id":"c9c3ed28.056db","type":"function","z":"608a04eb.6011bc","name":"trim off string","func":"var test = msg.payload.substr(5);  //trim off Use%\nmsg.payload = test.substring(0, test.length - 2) //trim off the next line string\nmsg.topic = \"storage\";\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":160,"wires":[["c5b1a07e.aa5fb"]]},{"id":"c5b1a07e.aa5fb","type":"join","z":"608a04eb.6011bc","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":710,"y":180,"wires":[["a00ebb59.59d158","2b0fcd92.4fd282"]]},{"id":"a00ebb59.59d158","type":"http response","z":"608a04eb.6011bc","name":"","statusCode":"","headers":{},"x":870,"y":160,"wires":[]},{"id":"4899725b.041d9c","type":"comment","z":"608a04eb.6011bc","name":"REST endpoint","info":"","x":120,"y":100,"wires":[]},{"id":"5f21f1c0.0f39c","type":"http request","z":"608a04eb.6011bc","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"localhost:1880/cpumetrics","tls":"","persist":false,"proxy":"","authType":"","x":290,"y":320,"wires":[[]]},{"id":"7904b22.74ae64c","type":"inject","z":"608a04eb.6011bc","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":320,"wires":[["5f21f1c0.0f39c"]]},{"id":"2fc5dcef.b8ce74","type":"comment","z":"608a04eb.6011bc","name":"Invoke REST endpoint","info":"","x":160,"y":280,"wires":[]},{"id":"2b0fcd92.4fd282","type":"debug","z":"608a04eb.6011bc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":890,"y":200,"wires":[]}]

Setting up Grafana plug-in

After preparing the data for display, we could set up the Grafana dashboard to display those data. Go to Configurations -> plugins, search for Infinity and install it.

Then go to Configurations -> Data sources -> Add data source.

The current version (Version 0.7.8) of Infinity does not require connectivity testing in this setup stage, so leave all fields blank and click Save & test.

Head over to your dashboard and add a new panel. Select a Stat graph, and select Infinity as the Data source. Put http://localhost:1880/cpumetrics in the URL field. Click Add Colums and put storage and ram as the selector, Number as type. You should now see the data shown in the preview panel.

Using TFT_eSPI library on esp32

TFT_eSPI library supports many drivers, I tested the following procedure with a 2.4 inch TFT LCD module with a ILI9341V driver.

Other than ILI9431, the library also supports:
ILI9341_2, ST7735, ILI9163, S6D02A1, RPI_ILI9486, HX8357D, ILI9481, ILI9486, ILI9488, ST7789, ST7789_2, R61581, RM68140, ST7796, SSD1351, SSD1963_480, SSD1963_800, SSD1963_800ALT, ILI9225, GC9A01.

Downloading the library

You can install the library on the Arduino IDE: Tools -> Manage Libraries…
Search for “TFT_eSPI”, and we are finding the library by Bodmer.

Editing the User_Setup.h file

By default, this library setup will work for NodeMCU using a ILI9341 driver. But in our case, we are using ESP32, so some of the setups have to be changed.

Open the User_Setup.h file with your favorite editor, it is ok to use notepad. (If you do not know where is the User_Setup.h file, you can File -> Preferences -> Sketchbook location:
Inside the directory, go into libraries -> TFT_eSPI. You could now see the User_Setup.h file.)

Uncomment the driver you are using, the default driver is already ILI9341, so we can skip Section 1.

//                            USER DEFINED SETTINGS
//   Set driver type, fonts to be loaded, pins used and SPI control method etc
//
//   See the User_Setup_Select.h file if you wish to be able to define multiple
//   setups and then easily select which setup file is used by the compiler.
//
//   If this file is edited correctly then all the library example sketches should
//   run without the need to make any more changes for a particular hardware setup!
//   Note that some sketches are designed for a particular TFT pixel width/height


// ##################################################################################
//
// Section 1. Call up the right driver file and any options for it
//
// ##################################################################################

// Define STM32 to invoke optimised processor support (only for STM32)
//#define STM32

// Defining the STM32 board allows the library to optimise the performance
// for UNO compatible "MCUfriend" style shields
//#define NUCLEO_64_TFT
//#define NUCLEO_144_TFT

// STM32 8 bit parallel only:
// If STN32 Port A or B pins 0-7 are used for 8 bit parallel data bus bits 0-7
// then this will improve rendering performance by a factor of ~8x
//#define STM_PORTA_DATA_BUS
//#define STM_PORTB_DATA_BUS

// Tell the library to use 8 bit parallel mode (otherwise SPI is assumed)
//#define TFT_PARALLEL_8_BIT

// Display type -  only define if RPi display
//#define RPI_DISPLAY_TYPE // 20MHz maximum SPI

// Only define one driver, the other ones must be commented out
#define ILI9341_DRIVER       // Generic driver for common displays
//#define ILI9341_2_DRIVER     // Alternative ILI9341 driver, see https://github.com/Bodmer/TFT_eSPI/issues/1172
//#define ST7735_DRIVER      // Define additional parameters below for this display
//#define ILI9163_DRIVER     // Define additional parameters below for this display
//#define S6D02A1_DRIVER
//#define RPI_ILI9486_DRIVER // 20MHz maximum SPI
//#define HX8357D_DRIVER
//#define ILI9481_DRIVER
//#define ILI9486_DRIVER
...
...
...

In section 2, we need to select the suitable pinout for your MCU. The default setup is for NodeMCU 8266, so comment out those lines by adding two slash characters //. And uncommenting those lines for esp32.

// ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP8266 SETUP ######

// For NodeMCU - use pin numbers in the form PIN_Dx where Dx is the NodeMCU pin designation
//#define TFT_CS   PIN_D8  // Chip select control pin D8
//#define TFT_DC   PIN_D3  // Data Command control pin
//#define TFT_RST  PIN_D4  // Reset pin (could connect to NodeMCU RST, see next line)
//#define TFT_RST  -1    // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V

//#define TFT_BL PIN_D1  // LED back-light (only for ST7789 with backlight control pin)

...
...
...

// ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP32 SETUP   ######

// For ESP32 Dev board (only tested with ILI9341 display)
// The hardware SPI can be mapped to any pins

#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   15  // Chip select control pin
#define TFT_DC    2  // Data Command control pin
#define TFT_RST   4  // Reset pin (could connect to RST pin)
//#define TFT_RST  -1  // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST

// For ESP32 Dev board (only tested with GC9A01 display)
// The hardware SPI can be mapped to any pins
...
...
...

Connecting the Esp32 to the display

TFT Display side Esp32 sideESP32 side
GNDGND
3.3v3.3v
SCL (sometimes called SCLK)GPIO18
SDA (sometimes called MOSI)GPIO23
RES (sometimes called Reset)GPIO4
DC (sometimes called data/ command) GPIO2
CS ( sometimes called SS)GPIO15
BLK-LED backlight signalN/A

Now you are ready to use the display module, you may try some built-in examples, File -> Examples -> TFT_eSPI.

Assigning fixed names for USB devices in Raspberry Pi

You can find the USB devices by ls /dev/*USB*

pi@raspberrypi:~ $ ls /dev/*USB*
/dev/ttyUSB0  /dev/ttyUSB1
pi@raspberrypi:~ $

and more information by lsusb or dmesg | grep ttyUSB.

pi@raspberrypi:~ $ lsusb
Bus 002 Device 003: ID 05e3:0626 Genesys Logic, Inc.
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 006: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
Bus 001 Device 005: ID 046d:c534 Logitech, Inc. Unifying Receiver
Bus 001 Device 004: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 001 Device 003: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter
Bus 001 Device 002: ID 2109:3431 VIA Labs, Inc. Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
pi@raspberrypi:~ $
pi@raspberrypi:~ $
pi@raspberrypi:~ $ dmesg | grep ttyUSB
[    5.284638] usb 1-1.3: ch341-uart converter now attached to ttyUSB0
[    5.312424] usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB1

But sometimes, you may want to use the /dev/ path to descript your USB, such as selecting the serial port in NodeRed.

Procedure of assigning fixed name

  1. Finding unique attributes for your USB devices
  2. Creating udev rules for your applications
  3. Creating match keys and assignment keys to bind the USB devices to your desired name.
  4. Applying rules

Finding unique attributes of USB devices

Using udevadm info [options] [devpath|file|unit…]to check the details of the USB devices. -a option stands for --attribute-walk.

pi@raspberrypi:/etc/udev/rules.d $ udevadm info -a /dev/ttyUSB0

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0/ttyUSB0/tty/ttyUSB0':
    KERNEL=="ttyUSB0"
    SUBSYSTEM=="tty"
    DRIVER==""

  looking at parent device '/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0/ttyUSB0':
    KERNELS=="ttyUSB0"
    SUBSYSTEMS=="usb-serial"
    DRIVERS=="ch341-uart"
    ATTRS{port_number}=="0"

  looking at parent device '/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0':
    KERNELS=="1-1.3:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="ch341"
    ATTRS{bNumEndpoints}=="03"
    ATTRS{bInterfaceProtocol}=="02"
    ATTRS{bInterfaceSubClass}=="01"
    ATTRS{bInterfaceClass}=="ff"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bInterfaceNumber}=="00"
    ATTRS{supports_autosuspend}=="1"
    ATTRS{authorized}=="1"

  looking at parent device '/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3':
    KERNELS=="1-1.3"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{bcdDevice}=="0264"
    ATTRS{busnum}=="1"
    ATTRS{authorized}=="1"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{urbnum}=="31"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bDeviceClass}=="ff"
    ATTRS{devspec}=="(null)"
    ATTRS{bMaxPower}=="98mA"
    ATTRS{idProduct}=="7523"
    ATTRS{rx_lanes}=="1"
    ATTRS{tx_lanes}=="1"
    ATTRS{product}=="USB Serial"
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{speed}=="12"
    ATTRS{removable}=="unknown"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{configuration}==""
    ATTRS{devpath}=="1.3"
    ATTRS{quirks}=="0x0"
    ATTRS{bMaxPacketSize0}=="8"
    ATTRS{bmAttributes}=="80"
    ATTRS{ltm_capable}=="no"
    ATTRS{version}==" 1.10"
    ATTRS{devnum}=="65"
    ATTRS{maxchild}=="0"
    ATTRS{idVendor}=="1a86"

...
...
...

Some unique attributes are ” idProduct”, “idVendor”. Make sure you are searching these attributes under the corresponding Port, in my case, usb1/1-1/1-1.3. Repeat the udevadm info command and search for attributes for the other devices. Remember to change the /dev/ttyUSB0 to /dev/ttyUSB1 or else only plug a single device at a time.

To understand more about USB ports, please visit Extra readings down below.

Creating udev rules

Linux stores file-like device nodes in /dev directory. Each node points to a part of a system or device, no matter it exists or not. Because the /dev directories contain every device that may exits in the system, it may be very large and become difficult to manage.

udev plays an important role to manage /dev directories by providing a path forward, by matching information provided by sysfs and rules provided by users.

udev files should be kept in /etc/udev/rules.d directories and with a .rulessuffix. Files in /etc/udev/rules.d are parsed in lexical order. In general, you want your rule file to be parsed first, so it is suggested that /etc/udev/rules.d/10-local.rulesis a good choice.

sudo touch /etc/udev/rules.d/10-local.rules to create a rule file.

In a rules file, lines starting with “#” are treated as comments. Every other non-blank line is a rule. Rules cannot span multiple lines.

To learn more about udev, you may man udev man udevadmor visit http://www.reactivated.net/writing_udev_rules.html#udevinfo

Writing rules

Each line of rule should contain at least one match key and at least one assignment to construct a key-value pair. When all match keys are fulfilled, the rule will apply and the action of assignment will be performed.

Do not insert any line breaks in the rules, udev will see your one rule as multiple rules. And here are my rules to assign a new name for the 2 USB devices. Just add the following lines in your 10-local.rules file.

SUBSYSTEM=="tty", ATTRS{idProduct}=="7523", ATTRS{idVendor}=="1a86", SYMLINK+="ttyUSB_HL-340_device"
SUBSYSTEM=="tty", ATTRS{idProduct}=="6001", ATTRS{idVendor}=="0403", SYMLINK+="ttyUSB_FT232_device"

SUBSYSTEM will match against the subsystem of the device.
SYMLINK is the alternative name(s) you want to assign to the USB device. This will not change or hide the original name but only provide an alternative name to link to the device.

Besides an exact matching of strings, you could also use shell-style pattern matching, such as *, ? and [].

Applying rules

Run udevadm trigger to apply the new rules.

sudo udevadm trigger

To check the result.


pi@raspberrypi:/etc/udev/rules.d $ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0 Dec  3 12:05 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1 Dec  3 12:05 /dev/ttyUSB1
lrwxrwxrwx 1 root root         7 Dec  3 12:05 /dev/ttyUSB_FT232_device -> ttyUSB1
lrwxrwxrwx 1 root root         7 Dec  3 12:05 /dev/ttyUSB_HL-340_device -> ttyUSB0

If you know the top-level device path, you can use udevadm test to show the action. Or else, just use

pi@raspberrypi:/etc/udev/rules.d $ udevadm test -a -p  $(udevadm info -q path -n /dev/ttyUSB_FT232_device)

Serial port name in Node-red

Now you can specify the USB you are pointing to in Node-red rather than guessing which is ttyUSB0 and ttyUSB1.


———————-This is separator———————–

Extra readings

You may use lsusb -t to explore what devices are connected to your pi. The following corresponds to no USB device connected.

pi@raspberrypi:~ $ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M

And the following corresponds to 4 USB devices connected. Dev 31 is a wireless mouse and keyboard. All devices are via USB2.0.

pi@raspberrypi:~ $ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 27, If 0, Class=Vendor Specific Class, Driver=ch341, 12M
        |__ Port 2: Dev 31, If 0, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 2: Dev 31, If 1, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 3: Dev 26, If 0, Class=Vendor Specific Class, Driver=ftdi_sio, 12M
        |__ Port 4: Dev 30, If 0, Class=Hub, Driver=hub/4p, 480M

If you have a USB hub, then those connected devices will be under the hub port. In my case, the hub is connected to Port 3.


pi@raspberrypi:~ $ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 3: Dev 36, If 0, Class=Hub, Driver=hub/4p, 480M
            |__ Port 1: Dev 37, If 0, Class=Human Interface Device, Driver=usbhid, 12M
            |__ Port 1: Dev 37, If 1, Class=Human Interface Device, Driver=usbhid, 12M
            |__ Port 2: Dev 41, If 0, Class=Vendor Specific Class, Driver=ftdi_sio, 12M
            |__ Port 3: Dev 42, If 0, Class=Vendor Specific Class, Driver=ch341, 12M

If you have USB 3.0 devices, they will be using Bus 02. In my case, both the USB hub and USB flash drive support USB 3.0.

pi@raspberrypi:~ $ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
    |__ Port 1: Dev 13, If 0, Class=Hub, Driver=hub/4p, 5000M
        |__ Port 4: Dev 14, If 0, Class=Mass Storage, Driver=usb-storage, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 56, If 0, Class=Hub, Driver=hub/4p, 480M

The number after the word ‘usb’ in dmesg is actually the USB Bus and Port. In my case, the ch341-uart converter is under bus 1, port 1.3. In similar manner, usb 3.0 devices will be usb 2-X.X.

[161104.469392] usb 1-1.3: ch341-uart converter now attached to ttyUSB1