Episode 22: Burkhard on Qt Embedded Systems
Welcome to Episode 22 of my newsletter on Qt Embedded Systems!
The big news of September is that Qt 6.2 LTS was released. Qt 6.2 is the first Qt 6 release that has feature parity with Qt 5 and that is suitable for Qt embedded systems. Migrating Qt applications from Qt 5 to Qt 6 is straightforward. Now, it is also complete.
My favourite new features are:
Defining QML modules is a lot easier in Qt 6 than in Qt 5 (see my summary below).
Much improved support for CMake, which is the standard build system for Qt 6.
Qt 6 is based on C++17.
Qt 6 has been a very incremental update of Qt 5 - with no exciting new features. I hope that this will change now that Qt 6.2 has achieved feature parity with Qt 5. We'll see.
Enjoy reading and take care - Burkhard 💜
My Blog Posts
Setting up a Yocto build environment and building a custom Linux image is a tedious and error-prone process (see my post Qt Embedded Systems – Part 1: Building a Linux Image with Yocto). Kas reduces this process to a single command. Given a YAML file specifying the layer repositories and the configuration file local.conf, kas sets up the Yocto build environment and starts the build automatically.
Super simple and super fast! We only have to convince the SoC, SBC and terminal makers to provide kas project configuration. Or, we do it ourselves. In my post, I explain how to create a kas project configuration for the Toradex Verdin iMX8M Mini.
In August, I started giving legacy code workshops as a complement to James Grenning's trainings Test-Driven Development for Embedded C/C++. The goal is to get embedded C or C++ code into a test harness.
In the first four workshops, the code under test was embedded C code running on microcontrollers. The development teams used C++ unit test frameworks: GoogleTest and CppUTest. So, the C++ test frameworks call C code.
The C++ test files include the C headers in an extern "C" section. All the C functions and variables have C linkage. When the C++ compiler compiles the C++ test file, it doesn't apply C++ name mangling to the included C headers. However, it compiles the C headers as C++! Therefore, C headers included both from C++ and from C files must be both valid C++ code and valid C code.
You find example code with some C-C++ incompatibilities on Github. Please send me your incompatibilities.
My Thoughts on the IoT Architecture of Parking Meters
My wife and I love hiking in the nearby Bavarian Alps. The car parks are at remote places in the mountains. Mobile coverage ranges from non-existent to excellent. The parking fees are 3-7 Euros per day and must be paid in coins. Credit or debit cards are not accepted. We and other hikers often have problems to scrape together the coins.
The local authorities installed the parking meters in the last 10 years. The parking meters will work perfectly fine for another 10-15 years. They are cash cows for the local authorities, who want to avoid costly replacements or upgrades as long as possible.
A good architecture is technically sound, a right architecture meets the stakeholder needs, and a successful architecture delivers value to the stakeholders (see for a detailed discussion). So, how does a good, right and successful aarchitecture look?
Good But Neither Right Nor Successful
Our first solution might be to upgrade the parking meter with a credit and debit card reader, which has a built-in cellular modem. This is certainly a technically sound architecture, but not more.
Some remote car parks don't have any mobile reception. The hikers might not be able to pay the fee with their cards. Even worse, they could be fined for not paying, as they could still pay with coins. The architecture doesn't meet the hikers needs and hence is not right.
This solution would require the local authorities to upgrade or replace the parking meters. This would imply significant extra costs and violate the authorities' duty to save costs. Higher costs entail lower value. Hence, this solution is not successful either.
Good and Successful But Not Right
Our second solution might be to let the hikers pay with a phone app, as almost all phones have Internet these days. The local authorities hire one of the many companies providing parking apps. The companies put up a sign with their name and with the ID of the car park. Hikers download the app at home or on the spot and connect the app with a payment method.
I need three apps to cover the car parks in my favourite hiking grounds in south-eastern Bavaria. The three apps offer different payment methods. The first app emails me an invoice at the end of the month, which I pay by bank transfer. The second app charges my Paypal account at the end of the month. The third app charges my Amex card directly when parking ends.
This solution keeps the costs for the local authorities at a minimum and maximises the value from the "applified" parking meters. One provider charges 10% of the regular parking fee as an extra service fee. The other two providers get their cut from the local authorities so that the price for the users stays the same. This solution delivers value to the local authorities and the app providers.
Although the solution is successful, I wouldn't consider it right. It doesn't satisfy the needs of the people parking their cars. The end users must juggle around multiple parking apps with different payment options. This is an awful user experience.
When my phone has a signal, the parking app downloads a list of nearby car parks and map data. I can also search for the car park by its ID either in the just downloaded list or in the database on the remote server. I select the correct car park from the list or on the map, select the end time, and start parking.
When my phone doesn't have a signal, I cannot start parking in the app. When the phone finally has a signal (sometimes only after two hours), I cannot start parking at the earlier time when I left the car park. If the local authorities control the car park in the time between parking the car and starting parking in the app, I don't have a valid parking ticket and can be fined. This is not an uncommon scenario for remote car parks and doesn't satisfy my needs - another reason why this solution is not right.
Good, Right and Successful
Our third solution must improve the user experience and allows us to start parking without mobile reception. When I pay for parking, I don't care who gets the fee and how they get it. Shopping on Amazon comes to mind. When I buy a book on Amazon, Amazon takes care of channeling my money to the book seller. I can pay by the most common means: all credit cards, debit cards and direct debit. Similarly, I would download a single parking app that handles the payments for all parking providers and that offers all the common payment means. This solution fixes the awful user experience.
When I don't have mobile reception on the car park, I enter the car park ID from the sign next to the meter and start the parking process. The app stores the starting time. The app can check every 15 minutes whether my mobile has a signal. When it does, it sends the start-parking request to the server. Otherwise, it retries in 15 minutes. The data volume should be small enough to be transferred over an Edge connection or by SMS. Additionally, the local authorities must abstain from fines when they receive the start and end time of parking within a reasonable time, say, 24 hours late.
Now we have solution that is not only good and successful but also right for all stakeholders.
The Qt Project made the implementation of QML modules a lot easier in Qt 6.2. The magic comes from the CMake function qt_add_qml_module. This function generates the qmldir file, registers C++ types with the QML engine, compiles QML to C++, generates QML plugin code, lints QML code, adds QML components to the Qt resource system, and more.
Fabian gently walks us through the definition of the CMakeLists.txt file for a main QML application and for a QML library. The qt_add_qml_module call for a QML library providing dialogs could look as follows.
RESOURCES images/icReject.png images/icAccept.png
SOURCES quantity.h quantity.cpp
We can use the dialogs in other QML files with import cool.dialogs (no version needed). The dialogs use the images listed after RESOURCES. The QML and image files are added to the library as Qt resources. The SOURCES files are the C++ models exposed to QML. The class Quantity must include the macro QML_ELEMENT in addition to Q_OBJECT.
Defining QML modules in Qt 6.2 is much much much easier than in Qt 5!
I have seen my fair share of legacy C code recently (see above). Many C headers contain a definition of a data structure (a C struct) followed by some functions manipulating the data. Clients can and will access the data directly bypassing the functions, the supposed interface. The C module defined in the header lacks encapsulation, which makes code changes difficult and error-prone.
The Opaque Pointer Pattern - better known, especially for Qt developers, as the Pimpl idiom - remedies this situation. It moves the definition of the data structure into the source file and hides it from the clients. The header contains only a typedef of an opaque pointer or handle. Every function takes a handle as its first argument. The implementation of the function uses the handle to access the instance of the data structure. Clients call a constructor function to create an instance and call a destructor function to delete the instance. The pattern introduces objects in C.
If we know how many instances of a module are needed during runtime, we can define a static array of these instances on the stack - at compile time. The handle becomes an index into the static array.
Nick recommends to use the pattern only if the program needs multiple instances of a module. If there is only a single instance of a module (singleton), we define the data variables as file-scope or static variables in the source file. We need not pass the handle to each function any more and we can remove the constructor and destructor functions. James Grenning calls this the single-instance method in his book Test-Driven Development for Embedded C (p. 194).
GeePaw gives ten reasons why he personally uses TDD. Other people may and will have other reasons. GeePaw first published his ten statements in this Twitter thread - with many reactions I selected four statements that resonate most with me. The comments on the four statements are mine.
(1) I practice TDD because it enables me to ship more value faster.
That's my experience as well. TDD is the best programming practice we currently have to ship more value faster. Nicole Forsgren points out in Chapter 4: Technical Practices of her book Accelerate (see my review) that CI and CD are "[important] enabler[s] of more frequent, higher-quality, and lower-risk software releases". And - TDD is the core enabler for CI and CD. For me, TDD takes a lot of stress out of software development, because it gives me confidence in providing high value to my customers.
(2) I design all the time when I am TDD’ing, before, during, and after.
Many developers take the rejection of up-front design by "Agile gurus" as an excuse to write code without doing any design. In striking contrast, GeePaw designs all the time. That's not surprising, because test-driving code makes code testable. Testable code leads to loosely coupled and deep interfaces, which “maximize the amount of complexity that is concealed” according to John Ousterhout (see my review).Hence, test-driving code forces me to think about interfaces and interactions of components all the time. According to Nicole Forsgren, a loosely coupled design is the single most important architectural factor enabling high-performance teams (see Chapter 5: Architecture).
(4) I alter my code fairly often for no other reason than to make it easier to TDD in and around.
Hard-to-write or frequently changing tests indicate rotten code. So, it's time to refactor the code, until the code smells better, and until the tests get simpler and stabler. This segues nicely into GeePaw's Ten I-Statements about Refactoring.
(7) I also use my testkit to do a bunch of different things that aren’t properly "tests" at all, and I greatly value doing that.
When I work on code unfamiliar to me, I use TDD to try out my hypotheses about the code. This method helps me to learn new libraries and even new programming languages (Python, currently). Unsurprisingly, TDD is my way to understand legacy code.