STM32 Blue Pill board, Arduino Core and USB serial output on Platformio

So I’m testing out some different language, frameworks and programming methods on STM32 using the cheap and readily available STM32F103 blue pill board.

So far I had no troubles using the USB connector as a serial device on STM32CubeIDE/CubeMX and the Zephyr RTOS.

So anyway, to keep the sotry (and post short), while using the Arduino Core for STM32 and the PlatformioIO platform for doing some tests, I’ve found out that the USB didn’t worked has expected to be a Serial output. I did know that the issue was not with the board, since all the other frameworks/platforms had no trouble using the USB port for serial output, so I did know that the issue was due to a software and/or configuration issue.

After some searching around, the solution, while not complete was found on this Platformio forum post.

In my case I still had some errors after the original recommend configuration:

[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
build_flags =
    -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC
    -D USBCON
    -D USBD_VID=0x0483
    -D USB_MANUFACTURER="Unknown"
    -D USB_PRODUCT="\"BLUEPILL_F103C8\""
Compiling .pio/build/bluepill_f103c8/FrameworkArduino/wiring_digital.c.o
/home/pcortex/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/usbd_desc.c:46:4: error: #error "USB VID or PID not specified"
   46 |   #error "USB VID or PID not specified"
      |    ^~~~~
In file included from /home/pcortex/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Inc/usbd_core.h:30,
                 from /home/pcortex/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/usbd_desc.c:21:
/home/pcortex/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/usbd_desc.c:160:10: error: 'USBD_PID' undeclared here (not in a function); did you mean 'USBD_VID'?
  160 |   LOBYTE(USBD_PID),           /* idProduct */
      |          ^~~~~~~~
/home/pcortex/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Inc/usbd_def.h:275:32: note: in definition of macro 'LOBYTE'
  275 | #define LOBYTE(x)  ((uint8_t)((x) & 0x00FFU))

This was easyly solved by adding the PID (Product ID) to the configuration settings:

[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
build_flags =
    -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC
    -D USBCON
    -D USBD_VID=0x0483
    -D USBD_PID=0x0100
    -D USB_MANUFACTURER="Unknown"
    -D USB_PRODUCT="\"BLUEPILL_F103C8\""

and low and behold, Serial.println() works as expected and the bluepill is detected at USB label with the VID and PID settings above defined:

Bus 001 Device 024: ID 0483:0100 STMicroelectronics STM32 STLink

Just a final note, all Serial.### functions could now be replaced by SerialUSB.###, for example:

SerialUSB.begin(115200);

SerialUSB.println(“Hello world\n”);

But standard Serial.## functions work now with output redirected to the USB virtual COM port.

Zephyr RTOS sample project with the STM32 blue pill board.

So this post describes more or less in detail how to build a small Zephyr RTOS project using as a target the famous and cheap STM32 blue pill board that has a ST32F103 ARM processor onboard an it is supported by Zephyr RTOS.
The project is quite simple, but it will show how to:

  1. Create a project from scratch.
  2. Create RTOS tasks
  3. Enable USB console

Creating a Zephyr RTOS project
Has documented in my previous post Zephyr RTOS – Initial setup and some tests with Platformio and the NRF52840 PCA10059 dongle and also on Zephyr documentation, we need to download, install and configure the Zephyr RTOS sources, the west tool and the supporting Zephyr SDK. This is explained on the above post.

We can create our project under the zephyr workspace directory, but then we will have trouble if we want to use git to manage our project since the zephyr workspace directory is already a git repository. So we will create our own directory outside of the zephyr workspace directory and work from there.
To do this we need to set the ZEPHYR_BASE environment variable to point to the zephyr workspace, otherwise the west tool that will compile and flash our project will fail. Since west is a python command and we are using virtual environments, as discussed on the previous post we need to first change to the virtual env that has west installed:

workon zephyr

We can now setup our project:

mkdir zSTM32usb
export ZEPHYR_BASE=/opt/Develop/zephyrproject/zephyr
cd zSTM32usb

Because I don’t want to create all the necessary files from scratch, mainly the CMakeLists.txt file, I just copy from the zephyr samples repository the simplest of the projects, blinky:

cp -R /opt/Develop/zephyrproject/zephyr/samples/basic/blinky/* .

At this point we should be able to compile and flash the STM32 blue pill board, but before that we can change the name of the project on the CMakeLists.txt file just for consistency:

# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.13.1)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zstm32usb)

target_sources(app PRIVATE src/main.c)

We can now compile the project:

west build -b stm32_min_dev_blue

or if we want to do a clean build we add the -p (pristine) flag:

west build -b stm32_min_dev_blue -p

And it should compile without any issues since this is still the basic blinky project.

...
...
-- Configuring done
-- Generating done
-- Build files have been written to: /opt/Develop/zSTM32usb/build
-- west build: building application
[1/138] Preparing syscall dependency handling

[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       27064 B        64 KB     41.30%
            SRAM:       12392 B        20 KB     60.51%
        IDT_LIST:         184 B         2 KB      8.98%
[138/138] Linking C executable zephyr/zephyr.elf

If we didn’t set correctly the ZEPHYR_BASE environment variable, we will get some errors. For example for listing out the available target boards, we can do a west boards command:

west boards
usage: west [-h] [-z ZEPHYR_BASE] [-v] [-V]  ...
west: error: argument : invalid choice: 'boards' (choose from 'init', 'update', 'list', 'manifest', 'diff', 'status', 'forall', 'help', 'config', 'topdir', 'selfupdate')

With the variable ZEPHYR_BASE (and virtual environment) correctly set, we get:

west boards | grep stm32
...
...
stm32373c_eval
stm32_min_dev_black
stm32_min_dev_blue
stm32f030_demo
...
...

So make sure the environment is correctly set.

Flashing the board:
Flashing the board is as easy as doing:

west flash

To be able to do this is necessary to have a ST-Link programmer and that it is properly connected to the STM32 blue pill board. Any issues here are probably not related with Zephyr or the west tool, since west only calls openocd to flash the board.

 west flash
-- west flash: rebuilding
[0/1] cd /opt/Develop/zSTM32USB/build/zephyr/cmake/flash && /usr/bin/cmake -E echo

-- west flash: using runner openocd
-- runners.openocd: Flashing file: /opt/Develop/zSTM32USB/build/zephyr/zephyr.hex
Open On-Chip Debugger 0.10.0+dev-01341-g580d06d9d-dirty (2020-05-16-15:41)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : clock speed 1000 kHz
Info : STLINK V2J36S7 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.212648
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* stm32f1x.cpu       hla_target little stm32f1x.cpu       running

target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08002378 msp: 0x20002768
Info : device id = 0x20036410
Info : flash size = 64kbytes
auto erase enabled
wrote 27648 bytes from file /opt/Develop/zSTM32USB/build/zephyr/zephyr.hex in 1.769166s (15.261 KiB/s)

target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08002378 msp: 0x20002768
verified 27064 bytes in 0.404006s (65.419 KiB/s)

shutdown command invoked

And now we should have a blinking led.

Creating a project from scratch – Conclusion:
So we now have a project base that we can use that it’s outside of the zephyr workspace directory, and hence, can have it’s own git repository without clashing with the zephyr workspace repository.

We can now move to add functionality to our project.

Creating the RTOS tasks
The basic blinky sample program uses a simple main() entry point and does not create any tasks so it is as simple as it can get.

A single threaded program, like it is blinky, has a main function, and it might have other tasks, either created dynamically or statically.

In our example, we will create the tasks statically. As we can see in the main.c file for our example at the zSTM32usb Github repository we define tasks by using the predefined macro K_THREAD_DEFINE:

// Task for handling blinking led.
K_THREAD_DEFINE(blink0_id, STACKSIZE, blink0, NULL, NULL, NULL, PRIORITY, 0, 0);    

// Task to initialize the USB CDC ACM virtual COM port used for outputing data.
// It's a separated task since if nothing is connected to the USB port the task will hang...
K_THREAD_DEFINE(console_id, STACKSIZE, usb_console_init, NULL, NULL, NULL, PRIORITY, 0, 0);

According to K_THREAD_DEFINE Zephyr documentation the parameters are as follows:

K_THREAD_DEFINE(name, stack_size, entry, p1, p2, p3, prio, options, delay)
Parameters
        name: Name of the thread.
        stack_size: Stack size in bytes.
        entry: Thread entry function.
        p1: 1st entry point parameter.
        p2: 2nd entry point parameter.
        p3: 3rd entry point parameter.
        prio: Thread priority.
        options: Thread options.
        delay: Scheduling delay (in milliseconds), or K_NO_WAIT (for no delay).

Based on this, we can then fine tune the task parameters, for example the stack size that is globally defined as 1024 bytes (way too much), and produces an image that takes around 12K of SRAM:

[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       27064 B        64 KB     41.30%
            SRAM:       12392 B        20 KB     60.51%
        IDT_LIST:         184 B         2 KB      8.98%
[138/138] Linking C executable zephyr/zephyr.elf

where if we cut the stacksize to 512 bytes, if frees up SRAM, which is now arounf 11K:

[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       27064 B        64 KB     41.30%
            SRAM:       11368 B        20 KB     55.51%
        IDT_LIST:         184 B         2 KB      8.98%
[138/138] Linking C executable zephyr/zephyr.elf

So while in this example the stack size is equal to both tasks, in a reality each task should have it’s stack adjusted to make the most of the available SRAM/RAM.

Also since the tasks are cooperative they need to release the processor to other tasks so they can run, hence instructions that wait for resources, or just a simple sleep are required to let all tasks to run cooperatively.

In our example this is achieved by the sleep instruction k_msleep that sleeps the tasks for the miliseconds that is passed as the parameter.

For example for blinking the Led, we have:

    // Blink for ever.
    while (1) {
	gpio_pin_set(gpio_dev, led->gpio_pin, (int)led_is_on);
	led_is_on = !led_is_on;
	k_msleep(SLEEP_TIME_MS);    // We should sleep, otherwise the task won't release the cpu for other tasks!
    }

Tasks description:
Not too much to say about them, except if they do not enter a infinite loop, like the above led blinking while loop, the task does what it has to do and it ends.

Specifically for our example the led blinking task uses the Zephyr Device Tree to retrieve the onboard led configuration, and then, with that configuration it can start blinking the led. This opens the possibility of handling multiple blinking leds with the same code, just by creating a new task for each led.

The usb console init task, initiates the USB console port and waits for a port connection, after the connection happens, it starts printing to the console using the printk function. If we connect to the USB port of the STM32 blue pill board we get:

miniterm2.py /dev/ttyACM0
Hello from STM32 CDC Virtual COM port!

Hello from STM32 CDC Virtual COM port!

....

By experience console output that isn’t used for debugging purposes and/or while in development should be centralized on a single task because: first it will avoid concurrency issues between multiple tasks, and second if nothing is connected to the USB port to consume the console data, the tasks won’t hang waiting for a USB terminal connection to consume the console output.

USB Console Output configuration:
The end this already long post, we need to configure the console output to goto the USB virtual com port. This USB com port is only used for the console output, not for bidirectional communication such as an user and the device using a terminal program.

The configuration is done on the Zephyr configuration project file prj.conf, and the necessary information to enable USB console output was gathered from a series of different sources….

CONFIG_GPIO=y
CONFIG_USB=y
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Zephyr Console"
CONFIG_USB_UART_CONSOLE=y

CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_LINE_CTRL=y
CONFIG_UART_CONSOLE_ON_DEV_NAME="CDC_ACM_0"

A simple and quick description to the above file is that this file enables a set of modules, and provides some configuration to those modules to be able to use them. An example, to use the Led, the Led is connected to a GPIO pin, so it is necessary to enable the GPIO module: CONFIG_GPIO=y.
The same is true to enable USB. It’s necessary to enable USB support and the USB stack. Some console configuration is needed such as the CONFIG_USB_UART_CONSOLE=y, since the original, it seems, console output is to an UART port.

We can see the USB port connected when reseting the board after flashing:

[30958.705584] usb 1-1.2: new full-speed USB device number 7 using xhci_hcd
[30958.818608] usb 1-1.2: New USB device found, idVendor=2fe3, idProduct=0100, bcdDevice= 2.04
[30958.818610] usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[30958.818611] usb 1-1.2: Product: Zephyr Console
[30958.818611] usb 1-1.2: Manufacturer: ZEPHYR
[30958.818612] usb 1-1.2: SerialNumber: 8701241654528651
[30958.880665] cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device

and on the device list:

....
Bus 001 Device 003: ID 0483:3748 STMicroelectronics ST-LINK/V2
Bus 001 Device 007: ID 2fe3:0100 NordicSemiconductor STM32 STLink
...

where the first entry is indeed the ST-Link programmer, and the the second entry is our USB console port.

As a final note, during compilation, a warning about the device id 2fe3:0100 is given, since for production use we need to change the default:

CMake Warning at /opt/Develop/zephyrproject/zephyr/subsys/usb/CMakeLists.txt:22 (message):
  CONFIG_USB_DEVICE_VID has default value 0x2FE3.

  This value is only for testing and MUST be configured for USB products.

CMake Warning at /opt/Develop/zephyrproject/zephyr/subsys/usb/CMakeLists.txt:28 (message):
  CONFIG_USB_DEVICE_PID has default value 0x100.

  This value is only for testing and MUST be configured for USB products.

As usual we can change this by changing the VID on the prj.conf file.

CONFIG_USB_DEVICE_VID=4440

Conclusion:
And that’s it. We now have a minimal skeleton where we can start build some applications and have some console output either for tracing or general information.
The neat part is while I’ve tested this with a STM32 Blue pill board, the same code works without any modification on other boards such as the NRF52840 dongle, which shows that with the same code base we can target different boards.

Improving SDR reception

The next few lines won’t probably add nothing new to a seasoned user or ham operator, but might help some one that stumbles on this post. So anyway here are some tips that might help anyone that is in the same situation as I.

The issue of living an apartment with few options for deploying antennas can be challenging for receiving anything in the HF bands.
So for a long time I have an SDRPlay RSP1A that covers from low frequency bands to UHF bands and above. But while I’m more or less successful to receive UHF 430MHz bands, I hadn’t much luck in lower bands. With exceptions for the FM frequencies, where I have nearby 50Kw transmitters and can pick up FM without any issues, all spectrum below 200MHz is noise and more noise.

So some steps that I took to improve things up and their outcomes:

Ferrite beads on the USB cable:
I added two clip on ferrite beads to the USB cable, right at the connection point to the SDRPlay, to see if any USB noise was affecting reception, but no change. Anyway I just left the beads on the cable, since it seems that RSP1A is not affected but USB noise at the HF bands. Verdict: No measurable improvements.

Better RSP1A shielding:
The RSP1A comes within a plastic case, but according to some people (I didn’t open it to check it), the inner case has some shielding. I tried some sort of shielding either with an external metal case, tin foil wrapping (yes I know… 🙂 ), but no measurable influence. Verdict: No measurable improvements.

Ferrite Core at the antenna input:
This was a game changer. Adding a power cord ferrite core just before the antenna sma connector on the RSP1A cleared a lot of noise and some other artifacts:

Before connecting to the SMA connector the antenna cable makes some loops on the ferrite as shown above and then it connects to the RSP1A.
While I still am deaf at a lot of bands, including the 80, 40 a 2 meter bands, I can here now FT8 on the 20m band and decoding it. At night I can also hear some ham radio chatter, all this while using a simple 9:1 unun and a random lenght wire antenna. Verdict: Good measurable improvement!.
(This is wahat is supposed called a common mode choke.)

I did some other changes, but all of them are now antenna related which leads us to the conclusion:

Conclusion:
All this just points to a what is evident from the beginning: Better antennas are need to improve reception, and while some basic things can improve reception, such as the ferrite core at the antenna output, there is no escape to getting a better antenna.

The Air Quality Monitor – Data processing

After implementing and deploying the DSM501a based Air quality monitor, data was collected into an InfluxDB database and consumed by a Grafana Dashboard:

DSM501a and BMP180 Grafana Dashboard

While we can see that Temperature (yes it’s hot!) and Pressure looks ok, data collected from the DSM501a sensor is just a mess. It’s just a bunch of samples jumping around across several values, and so doesn’t look very promising that we can take meaningful data out of it.

So we might need to process the data so that it makes sense, and for that based on the fact that:

  1. Data sample is slow: 2 samples per minute
  2. We don’t want high variations, but smooth out the data

I’ve choosen to filter data using an IIR (Infinite Response Filter) LPF (Low pass filter) to remove any high frequency data/noise, and hence obtain a smoother output.
Did it work? Yes it did:

Original data vs IIR LPF filtered data

As we can see for each collected particle size of 1.0 and 2.5 we’ve filtered it with an IIR LPF that smoothed out any wild transitions while keeping the fundamental and underlying data validation.

Implementation:
IIR implementation is quite simple since it is only a set of additions, subtractions and multiplications with some factors that define the behavior of the filter.

IIR Filter

(The picture was taken from here that also explains nicely what is an IIR and FIR filters).

The input x[]n is DSM501a sample time at t=0, t=30s, t=60s, … and so on, and y[]n is the corresponding output. The b0,b1,b2 and a0,a1 and a2 are the filter factors, that define the filter response. For testing purposes I’ve just choose factors for a 1KHz Low Pass filter and tested it during several days, and hence the above output that can be seen on the Grafana dashboard.

The IIR filtering process is done on Node-Red but it can be done easily also on the ESP8266 since there is no complicated math/algorithms involved.

Node-Red IIR LPF filter

The function that implements the IIR LPF filter is (Note that on the code I use the a’s as the input factors and b’s as the output factors which is the contrary of the above IIR picture):

// IIR LPF factors
  f_a0 = 0.0010227586546542474;     // Input factors
  f_a1 = 0.002045517309308495;
  f_a2 = 0.0010227586546542474;
  
  f_b1 = -1.9066459797557103;       // Output factors
  f_b2 = 0.9107370143743273;

// PPM 1.0 input variables
var i0_c10 = msg.payload.cPM10;
var i1_c10 = context.get('i1_c10') || 0;
var i2_c10 = context.get('i2_c10') || 0;

// PPM 1.0 output variables
var o0_c10 = context.get('o0_c10') || 0;
var o1_c10 = context.get('o1_c10') || 0;


// Calculate the IIR
var lpf =   i0_c10 * f_a0 + 
            i1_c10 * f_a1 + 
            i2_c10 * f_a2 -         // We add the negative output factors
            o0_c10 * f_b1 - 
            o1_c10 * f_b2;
            
// Memorize the variables
context.set( 'i2_c10' , i1_c10 );
context.set( 'i1_c10' , i0_c10 );

context.set( 'o1_c10' , o0_c10 );
context.set( 'o0_c10' , lpf );

// PPM 2.5 input variables
var i0_c25 = msg.payload.cPM25;
var i1_c25 = context.get('i1_c25') || 0;
var i2_c25 = context.get('i2_c25') || 0;

// PPM 1.0 output variables
var o0_c25 = context.get('o0_c25') || 0;
var o1_c25 = context.get('o1_c25') || 0;


// Calculate the IIR
var lpf25 =   i0_c25 * f_a0 + 
              i1_c25 * f_a1 + 
              i2_c25 * f_a2 -         // We add the negative output factors
              o0_c25 * f_b1 - 
              o1_c25 * f_b2;
            
// Memorize the variables
context.set( 'i2_c25' , i1_c25 );
context.set( 'i1_c25' , i0_c25 );

context.set( 'o1_c25' , o0_c25 );
context.set( 'o0_c25' , lpf25 );

msg.payload = {}
msg.payload.cfP10 = lpf;
msg.payload.cfP25 = lpf25;

return msg;

We maintain the filter state (the two previous samples from the sensor) on Node-Red global variables (which will be reset if Node-red is restarted), and calculate for each PM1.0 and PM2.5 sample the filtered value, which depends on the previous samples. The final output is then fed to an InfluxDB sink node which saves the filtered data.
The complete code is at this gist.

Conclusion:
While still this being a test by using a probably LPF filter that is not adequate to the sampled data (it was designed for Audio at 96Khz sample rate), it shows that we can do some simple processing to clean up inbound data so that it makes more sense. This mechanisms of using digital filtering signals (DSP) are applied widely in other systems such as electrocardiogram sensors or other biometric sensors the remove or damp signal noise. In this case we can see that after the filtering data looks much more promising to be processed and so be used to calculate the Air Quality Index without the index jumping around as the samples jump.

An ESP8266 Air Quality monitor based on the DSM501a dust sensor

ESP8266 Air Quality Web Page

It’s unfortunate that such bad thing as the current pandemic was the chance that I had to finish this 2018 project… but better late than never.

So, I’m walking through a path that already many people have taken with this DSM501a dust sensor and ESP8266 combination to measure air quality based on dust particle count, and so this is (another) take on this combination. I’ll be using the trusty Wemos D1 ESP8266 based boards, the DSM501a dust sensor, and since that I also have an unused temperature and pressure sensor BMP180 available, I’ll also use this to finish up the project.

Basically I’ve follow up two approaches to building this project, the Arduino site example available at create.arduino.cc and a much more detailed project available at diyprojects.io site.

I’ll also use my “framework” for this kind of projects, already used at the PZEM004 Power Meter project that provides the basic building blocks for a web server and also NTP and logging facilities.

Regarding the project itself, there isn’t really anything new that I can add, except while the Arduino site code sample gave me a Air quality between Clear and Good, the other project gave me an Air Quality Index always of Hazardous, that I suppose is due to a confusion of using PM10 vs PM1.0 that is what this sensor provides. Also I’ve found out that there is several formulas available for calculating the dust concentration in mg/m3 and from that derive the Air Quality. So at the end I just use the ESP8266 to collect data, and use Node-Red to calculate the Air Quality Index with the provided data, which is much easier to debug and test, instead of using a program and flash, test cycle.

So the formulas used to obtain the data from the DSM501a are the original Arduino and diyproject.io formulas, and also this one: 0.001915 * pow(r , 2) + 0.09522 * r – 0.04884 that was discussed in this Github Wiki Post, and provide both data to be published on the MQTT topic.

As usual the code publish data and status information in two MQTT topics that I’ve defined, namely iot/device/device_id/telemetry and iot/device/device_id/attributes. An example of the data that is fed to the MQTT broker:

[AIRQ][INFO] {"AQ":"Clean","cPM10":837.15,"cPM25":0.62,"pPM10":0.11,"pPM25":0.00,"TEMP":27.60,"PRESS":101240}
[AIRQ][INFO] AIRQ Attributes:
[AIRQ][INFO] [{"type":"ESP8266"},{"ipaddr":"192.168.1.228"},{"ssid":"ZHOME3"},{"rssi":"-29"},{"web":"http://192.168.1.228"}]

Also the collected information is provided by a page served by the ESP8266 server, so it is possible to see it directly by using a web browser:

ESP8266 Air Quality Web Page
ESP8266 Air Quality Web Page

Hardware connections:
The DSM501a is trickier to connect since we can’t follow the wire colors to know which pin is which because it varies. I have two of them and both came with cables with wires of different colors for each pin. So guide the connection by pin function and not by wire color. This picture, taken from the Arduino site shows it how:

DSM501a pinout
DSM501a pinout

In my case I’ve connected the pins to the Wemos D1 ESP8266 board this way:

  1. Wemos D1 +5V -> DSM501a +5V
  2. Wemos D1 D6 -> DSM501a PM 1.0 pin
  3. Wemos D1 D5 -> DSM501a PM 2.5 pin<
  4. Wemos D1 GND -> DSM501a GND pin

The BMP180 break out board was connected to 3.3V and directly to the Wemos SCK and SDA pins. The BPM180 is optional, so the firmware code checks if it is connected and if so, it also collects data from the sensor.

Software:
As usual the software is build by using PlatformIO which pulls all the needed libraries to compile the project. All is needed is to just connect the Wemos D1 board to the USB port and do a pio run –target upload at the project root.

We can then monitor the serial port, through the pio device monitor command or run the logServer.sh script on the target monitoring server.

As usual the code is available at Github: ESP8266 Air Quality DSM501a based monitor

ESP32/ESP8266 MQTT Socket error on client – Disconnecting

When using the MQTT library for the ESP8266 or ESP32, namely this one, when publishing data on the Mosquitto I got the bellow error, followed immediately by a client disconnect:

1589388307: New client connected from 192.168.1.155 as ESP32-node (c1, k60, u'ESP32ETHE').
1589388312: Socket error on client ESP32-node, disconnecting.

One of the key issues with this library is first to ensure that the loop() function is periodically called before the MQTT connection timeout is reached.
But this was not the issue.

The issue was that the message payload for a specific topic was too big for the pre-allocated buffer of the MQTT client. So

MQTTClient mqttClient;

must be changed to this

MQTTClient mqttClient(1024); 

where 1024 is the maximum expected payload size. So we can changed to smaller or bigger depending on the situation.

With this change, the issue was gone. So moral of the story: beware of payload size.

Establishing secure ESP8266 and NodeJs communication by using Diffie-Hellman key exchange and Elliptic Curves

Introduction:
One of the issues of my later posts ( ESP8266 and AES128 for end-to-end encryption and ESP8266 – Logging data in a backend – AES and Crypto-JS) is that uses symmetric key AES128 to encrypt and decrypt data, and that key is pre-shared, meaning that it’s hardcoded on the code and is the same at all times.

While this might not be an issue for some use cases, in the real world, if the key is not properly protected, anybody with access to it, can inject false data either on the ESP8266 or on the NodeJs Server, rendering in fact the encryption efforts useless.

The solution to not having a pre-shared key and since AES128 (and some other algorithms) require shared symmetric keys, is to somehow generate a pre-shared key on demand that is not stored anywhere, but how to do that? This is a common actual problem on standard protocols such as SSL and HTTPS, and to solve this problem is where the Diffie-Hellman Key exchange/agreement protocol comes to help.

DH (Diffie-Hellman for short) works by creating at each peer that needs to communicate a set of two keys: one that is private, and one that is public. The peers exchange their public keys, and due to some mathematical properties they can calculate a common shared key using their own private key and the others public key. The key point here is that the shared key is generated without being transmitted between peers which ensures that it is impossible to intercept it at transit. A possible attacker can see the public keys transmission, but without access to the private keys it can’t calculate the shared key.

We can just generate the shared key at boot up and keep using it until a reboot or restart, or generate a new key for each new transaction, generating in fact ephemeral symmetric keys.
Since the code that will be shown bellow is just a proof of concept to show how it works, there isn’t the concept of session and so the NodeJs Server will just accept on key and peer at a time.

Anyway the DH key agreement protocol can be used at least in two different ways: by using the standard original key pairs based on the multiple groups of integers module N or the more recent and using shorter key lengths based on Elliptic Curve Cryptography, more specifically using the Curve25519 designed for DH Key exchanges for generating the necessary key pairs.

So let’s implement a single ESP8266 Wemos D1 based board that sends data to a NodeJs Server using AES128 based encryption, but this time using ephemeral AES keys, not pre-shared keys.

NodeJS proof of concept:
From the NodeJS side we need to install a supporting module for Curve25519 DH, which is the Curve25519-N module. I’ve previously had trouble using this module, so just make sure that the used node version v13.13, where at least it compiles and works as expected.

The provided module functions are the necessary functions to generate the private/public key pair, where the private key pair is generated from a random 32 byte secret, and the function that given the own private key and the peer’s public key calculates the shared key.

A simple proof of concept is as following:

npm init
npm i curve25519-n --save

and the code testDH.js is:

// NodeJs simple DH key exchange using Ecliptic curves with the Curve25519
//
const curve = require('curve25519-n');

// Generate random 32 bytes secret
function randomSecret() {
   var result           = '';
   var characters       = 'ABCDEFGHIJKLMNOPQRSTUVXZabcdefghijklmnopqrstuvxz0123456789';
   var charactersLength = characters.length;
   for ( var i = 0; i < 32; i++ ) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
   }
   //console.log( result );
   return result;
}
// Generate the cryptographic material
var aliceServerSecret = Buffer.from( randomSecret() );

var alicePrivateKey = curve.makeSecretKey( aliceServerSecret );
var alicePublicKey  = curve.derivePublicKey( alicePrivateKey );

var bobServerSecret = Buffer.from( randomSecret() );

var bobPrivateKey = curve.makeSecretKey( bobServerSecret );
var bobPublicKey  = curve.derivePublicKey( bobPrivateKey );

var alice_shkey = curve.deriveSharedSecret(  alicePrivateKey , bobPublicKey );
var bob_shkey   = curve.deriveSharedSecret(  bobPrivateKey , alicePublicKey );

console.log("Alice public key: " ,  Buffer.from( alicePublicKey).toString('hex') );
console.log("Bob public key:   " ,  Buffer.from( bobPublicKey).toString('hex') );
console.log("");
console.log("------ Calculated shared keys: ");
console.log("Alice shared key: ", Buffer.from(alice_shkey).toString('hex') );
console.log("Bob shared key:   ",  Buffer.from(bob_shkey).toString('hex') );

Running this will held:

node testDH.js 

Alice public key:  64ec19b47ae105ca00b7e7e088fd2c809e93118fb961d33a118c95e2ee3a9d19
Bob public key:    98d9c8d93fceed09efb15d7629d449d66892ecb4bb4a16a486b1d656a2a1501d

------ Calculated shared keys: 
Alice shared key:  6692e8240a64b595698ef98440e89affbe5102082595631ebdf472897d432c2a
Bob shared key:    6692e8240a64b595698ef98440e89affbe5102082595631ebdf472897d432c2a

and lo and behold the shared keys are the same.

Now we just need to make Alice key the NodeJS key, and Bob’s public key the ESP8266/ESP32 key.

The ESP8266/ESP32 side:
On the ESP8266/ESP32 side we also have with the Arduino framework the Curve25519 ECDH functions for an Ecliptic Curve based Dilfie-Hellman key exchange.
As usual, using Platformio we need to add the Crypto library that does support ECDH Curve25519 based DH, and also AES128.

So on the ESP side, we generate again a set of public/private key pairs, and send the public key to the NodeJS server. As a response we receive the NodeJS server public key, and then we can calculate the shared key:

void    generateKeys() {
    Curve25519::dh1( m_publicKey, m_privateKey);
}

void    initSession() {
    // We contact the NodeJS server to send our Public key
    // and as a response we receive the Nodejs Public key
    generateKeys();                 // Generate a set of Curve25519 key pair for the DH key Agreement protocol

    // The Server end-point
    String url = "http://" + NODEServer_Address + ":" + NODEServer_Port + "/getSession";

    char    s_pubkey[65];
    Bytes2Str( s_pubkey, m_publicKey, KEY_SIZE );

    // Build the post body
    String postBody = "{\"pubkey\": \"" + String(s_pubkey) +"\"}";

    // Send the request
    http.begin( url );
    http.addHeader("content-type", "application/json");

    int httpCode = http.POST( postBody );
    if (httpCode > 0) {

        String payload = http.getString();
    
        if ( httpCode == 200 ) {            
            deserializeJson( jsonDoc, payload.c_str() );

            // Obtain the foreign public key
            const char *pubkey = jsonDoc["pubkey"];
            if ( pubkey != NULL) {
                Str2Bytes(m_fpublickey, (char *)pubkey, 64 );
                printHex( "Foreign Key: ", m_fpublickey , 32 );

                // Calculate now the shared key
                Curve25519::dh2( m_fpublickey, m_privateKey ); 
                printHex ( "Shared Key", m_fpublickey , 32 );     
                memcpy( m_shkey, m_fpublickey, 32 );
            }               
        }
        else {
            Serial.println("Error on HTTP request a session.");
        }
    }
    http.end();
}

The Crypto library for the Curve25519 offers two functions: Curve25519::dh1 for generating the keys pair where the public key is generated to be sent to the peer and Curve25519::dh2 function that given the private key and foreign public key, generates the shared key.

At the end, hopefully both sides end up with the same shared key, which they do, and from there we can use that key as the AES128 symmetric key to establish communications.

The resulting shared key has more bits than the necessary for the AES128 encryption/decryption, so we derive the AES128 symmetric key from the shared key. This can be done in several ways, but I just took the easier way and only used the necessary first 16 bytes of the pre-shared key to get the AES128 key. Other approaches are to take a SHA256 or SHA512 from the key to generate any missing bits if necessary.

We also can see that on the initSession() function we generate a new set of keys for each transmission so making all used keys ephemeral since they are only used once. The drawback is that for sending data we need two transactions, one for the key exchange and other for the data transmission itself.

Testing:
The testing code that shows this ECDH (Elliptic Curve DH key agreement working) is in this repository: https://github.com/fcgdam/AESCrypto_ECDH_KeyExchange.

As usual we use Platformio to flash the firmware on the ESP8266, and to run the NodeJs server, just run npm install and node server.js. Just make sure that on the ESP8266 the SSID, Password and node server IP address are correctly set.

Running we can see on the ESP side the AES128 key to be used, and compare it with the key that was generated on the NodeJS server side.

Foreign Key: :
C8 C9 74 6E BE E9 F3 63 33 46 39 A7 4C CC 88 AB 17 14 47 3F D8 10 E0 B9 4D 9C 5B BF 3A A3 30 02 
Shared Key:
4B 40 3A A1 E2 6E 56 3C B2 5B 15 3A A6 24 6F 77 D2 C5 D5 0D 96 17 73 90 09 3A B6 38 0F C4 70 40 
AES128 key to be used: :
4B 40 3A A1 E2 6E 56 3C B2 5B 15 3A A6 24 6F 77 

IV B64: wB2astutocBMfv+xsTvAKg==
------- Sending data:
 Data: wirA/v+JcsjnP9dAVml0W/20apkQqFnY4jYMrRnw9tM=

Foreign Key: :
C8 C9 74 6E BE E9 F3 63 33 46 39 A7 4C CC 88 AB 17 14 47 3F D8 10 E0 B9 4D 9C 5B BF 3A A3 30 02 
Shared Key:
E2 4A F6 17 F3 3F B5 79 3F 6F B4 B7 8A D9 5B 5C A9 6D 65 FF 88 F3 2C 9A 18 99 99 6B B0 0F C1 4A 
AES128 key to be used: :
E2 4A F6 17 F3 3F B5 79 3F 6F B4 B7 8A D9 5B 5C 

IV B64: VW1Lnm21M1UFE45E80eNfw==
------- Sending data:
 Data: V+FdoIzYORrKiA3DjyRn9CPdYREqaQWZf8fatKFFWY0=

The associated output on the server side. Note that the calculated shared key is the same, hence we can decrypt the messages without any problems.

POST /setdata 200 0.309 ms - 37
Foreign Public Key:  D9A799D46919A2B257E112678635D7061AB589B61C42714C7B7216315AAC961B
Shared key:  4b403aa1e26e563cb25b153aa6246f77d2c5d50d96177390093ab6380fc47040
AES128 key to be used:  4b403aa1e26e563cb25b153aa6246f77
POST /getSession 200 0.421 ms - 77
Data request:  {
  iv: 'wB2astutocBMfv+xsTvAKg==',
  data: 'wirA/v+JcsjnP9dAVml0W/20apkQqFnY4jYMrRnw9tM='
}
Decrypted message:  {"testdata": "346"}
POST /setdata 200 0.296 ms - 37
Foreign Public Key:  74B73F83D4E1FFD587B0A1E14C5546CEF3EEA50E517B2ED94E64BD585C278B2A
Shared key:  e24af617f33fb5793f6fb4b78ad95b5ca96d65ff88f32c9a1899996bb00fc14a
AES128 key to be used:  e24af617f33fb5793f6fb4b78ad95b5c
POST /getSession 200 0.422 ms - 77
Data request:  {
  iv: 'VW1Lnm21M1UFE45E80eNfw==',
  data: 'V+FdoIzYORrKiA3DjyRn9CPdYREqaQWZf8fatKFFWY0='
}
Decrypted message:  {"testdata": "347"}

Conclusion:
This example shows that there is no need to preset keys on the ESP8266 device to be able to encrypt data as long that both the device and the server agree on the process for the generating the necessary key(s). Of course the server must support different sets of keys for different devices, which is not the case of the provided example, it’s just proof of concept.
Also another key element is to know who is doing the key agreement since the above code accepts anyone to do the key agreement, which is another issue in itself.

ESP8266 and AES128 for end-to-end encryption

One of my older posts that has more hits is this one:ESP8266 – Logging data in a backend – AES and Crypto-JS where it’s explained how we can send data that is encrypted with AES128 from the ESP8266 to a backend server, either a NodeJS server or a Node-Red based service.
On the comments section I had a lot of questions and issues with the implementation, and so I’ve crafted a full implementation of the End-to-End encryption that works both ways.

The code is available at Github: AESCrypto_Test and implements the firmware for an ESP8266 based device, I’m using the Wemos D1, and two node programs: one is the server and other is the client.

The Node Server just starts and waits for incoming data from the ESP8266 and decrypts the incoming data and just shows it on the screen. The server is always running to receive requests at any time.
The Node client is run interactively by the user to send data encrypted data to the ESP8266. The ESP8266 then decrypts the data and can do whatever it needs to do. The decrypted data is output to the serial console as usual.

For this communication to happen both the ESP8266 and the Node client need to know the IP of each other, and so there is the need to change that on the code before things start to work ok.

In this example, the AES key being used is pre-shared, by another words, its known from the start by both the ESP8266 the Node Client and the Node Server. The initialization vector at the ESP8266 is random, and on the Node Client can be fixed (NOT SECURE!!) or random. Both cases are shown to show how it works.

Anyway this is just a sample code show how it works and the example can be used as stepping stone for implementing other things.

The key aspect on this code is nevertheless the use of a pre-shared key, that while it simplifies things up, is not really that secure, but anyway allows to see the concepts involved.

GQRX and SDRPlay RSP1A

I’ve own a SDRPlay RSP1A SDR and since I use Linux I’ve never used SDRUno, just CubicSDR that works fine with the RSP1A.

Nevertheless one thing that annoys me on CubicSdr is that we can only pipe audio out from the application, while on GQRX we can pipe data out through UDP, and hence we can pipe it out to basically anywhere and not be dependent of the audio interface and audio routing on the local PC.

But the truth is that I never was able to make GQRX to work with SDRPlay. I’ve followed some tutorials around the net such as this: http://thomasns.io/gqrx.html and this http://dk3ml.de/2019/01/12/running-sdrplay-with-gqrx-on-ubuntu-18-10/ but at the end I always had this, even with the latest versions of GQRX that support SDRPlay:

GQRX SDRPlay

While I can hear the FM station at the above frequency the frequency panadapter has a hump at the middle and that’s it, not exactly what I was expecting.

Well the issue is that GQRX at startup does not setup SDRPlay RSP1A propertly and so it shows the above behavior.

So if we get the initial GQRX settings for the SDRPlay:

SDRPlay Settings

it looks fine, but as we’ve seen the end result is not as expected. So what we need to do is to re-enter the settings again in a certain order to make this work:

  1. Stop the data capture, without exiting GQRX (press the Play button)
  2. Edit the Device settings and choose any other than SDRPlay, and then choose again SDRPlay again. Make sure Bandwidth is set to 0.6Mhz.
  3. DEfault SDRPlay Settings

  4. As we can see the default Input rate is set to 2MBps.
  5. Press Apply, and if we start the capture it should work now, but we have a RSP1A,so…
  6. Open up again the settings and change the Input Rate to the maximum that RSP1A supports: 10MBps

The end result is this:

Proper SDRPlay on GQRX

And now it works as it should.

Conclusion:
While a bit annoying it’s good to see GQRX working fine with SDRPlay RSP1A. Still through GQRX we can’t control the FM and DAB notch filters, which CubicSDR can.

Installing qDSLRDashboard on Arch Linux

A very quick post how to make qDSLRDashboard on Arch Linux to control by WIFI (or not) a DSLR camera.

1- Download the software
First download the software from the download page at the qDSLRDashboard site.
I’ve just downloaded the standard 64bit Linux tar.gz package.

2- Unpack/install the software
I just created a Nikon directory on /opt (since my camera is a Nikon DSLR):

cd /opt
mkdir Nikon
cd Nikon
tar -xvzf ~/Downloads/qDslrDashboard_V3.5.9_Linux_x64.tar.gz
mv qDslrDashboard_V3.5.9_Linux_x64/ DSLRDash

Currently the version is 3.5.9, but it will change in the future, so adapt the filename to the correct version downloaded.
I’ve also changed the package directory name to something simpler, but we can keep the original name.

3 – Install the pre-requisites
At least I had to install these Arch Linux packages or otherwise I would get errors of libraries not found:

sudo pacman -S libpng12 jasper

We also need to copy the libjasper.so.4 library to the Qt_Libraries (it’s a hack…):

cd /opt/Nikon/DSLRDash/Qt_Libraries
cp /usr/lib/libjasper.so.4 libjasper.so.1
cd ..

And we are ready.

We can and should run now the qDslrDashboard.sh script and not the executable. The script points the executable to use the correct libraries at the Qt_Libraries directory.

And that’s it.

[portex@cortex:DSLRDash]$ ./qDslrDashboard.sh 
/opt/Nikon/DSLRDash/./qDslrDashboard: /usr/lib/libtiff.so.5: no version information available (required by /opt/Nikon/DSLRDash/./Qt_Libraries/libopencv_imgcodecs.so.3.4)
qml: ScreenManagerOverlay - Focus Scope complete
qml: ScreenManagerOverlay - devicesFlickable complete
qml: ScreenManagerOverlay - managerFlickable complete