Embedded Linux and Qt
A gas detection OEM came to us with a familiar request: “We just need some firmware and UI changes on an existing panel.”
It seemed straight forward, but that short sentence hid so much more than we initially thought. We discovered we were dealing with a fully embedded Linux system that had been used for years. This brought with it all the quirks of a mature system, along with a QT interface and remote sensors. On paper, it was a small update. In practice, it was a tour of someone else’s embedded Linux and Qt development decisions.
Context: An industrial panel already in the field
This sensor is installed in industrial spaces, monitoring air quality and driving ventilation and alarms. Under the hood is:
- An embedded PC running Linux
- A Qt-based touch UI
- I/O boards handling gas sensors, relays, and analog outputs
The client didn’t want a full redesign. But they did want to make some modifications to the existing system and our constraints were fairly simple. Keep our hardware and OS the same, and respect how the technicians in the field were already using the product. But we did need to fix any behaviors that generated confusion or support calls. The client also wanted us to add a few features to the product, but without destabilizing it.
This kind of work sits in a unique spot, somewhere between classical firmware development and higher-level software development. It is still embedded systems work, just on a different slice of the stack.
Where on the panel were the issues?
To start, the client gave us a detailed list of the changes they wanted, as well as the test notes. This list left us with issues split between three buckets: resets, warm-ups, and relays.
The bucket of problems
One of the main problems we faced was the confusion caused by how the panel treated resets. The panel gave technicians options to restore defaults or perform a deeper reset, but those options did not perform how they were expected to. This was especially so when it came to stored calibration and “zero” values. The technicians needed a reset to either put everything back to the known baseline, or explicitly leave calibration alone.
The panel’s sensor warm-up behavior was also causing issues for the technicians, with inconsistent interactions between itself and the relay outputs. The behavior depended strongly on how and when the panel was started up. Warm-up is not a fault, so it should not be creating differences in the relays behind the scenes just because a device powered up.
Ending the battle between UI and the technicians
While the QT interface did the job it needed to do, there were several issues that led to its “death by a thousand cuts”. These issues included:
- Lists that jumped back to the top whenever data refreshed
- Icons and highlights that did not always reflect actual configuration
- Interaction patterns that made sense to the original developer but felt clunky to new users
On their own, none of these issues were show-stoppers. But, together they chipped away at confidence that the panel was doing what it claimed.
Mismatch between analog outputs/errors and the mental models
The analog side of the panel supported both step and ramp style outputs, as well as delay timing. But small changes and edge cases had accumulated over time and were muddying the behavior of the system. The delay timing around threshold crossings sometimes felt off, and some gases behaved differently because they had “normal” values, not just alarm sensors. Unfortunately, this was not something that was well represented in the panel configuration.
Error and status messages had similar issues. Communication errors bore too much similarity to “unsupported device” errors, and some persistent alarms came back quickly after being silenced. The error messages on screen often didn’t line up with what the technician was seeing on the wall. This made it harder for the support team who ended up translating instead of solving the issue.
How we approached the cleanup
Because this was a mature product, we treated it like a surgical refactor.
Address the behavior first, then the code
The first step was to translate the list of requests and the test notes into a plain-language and make a behavior spec. This boiled down to several questions:
- What should each type of reset do for configuration and calibration?
- How should warm-up, faults, and normal operation interact with relays and analog outputs?
- How should “step” and “ramp” outputs behave in common scenarios?
- What does each major error message mean, and what should a technician expect to do next?
To help solve this, we decided to treat it as a design issue, not just a documentation issue, if two people on the client side described the same scenario differently. Only after behavior was agreed upon did we go looking for the parts of the codebase that needed to move.
Tight changes inside an existing Linux + Qt stack
From there, most of the work fell into a few categories:
- State handling on Linux. To make sure that warm-up and fault handling followed one consistent path, we decided to unify the scattered special cases in clear state machines.
- Qt model and view cleanup. We updated the data models in place, instead of tearing them down on every refresh. This made sure that the UI actually reflects the underlying configuration, not cached assumptions it had made.
- Persistent configuration. We also had to make sure that resets, calibration, and defaults were all pulling from the same source, and not from the several ever-evolving legacy paths they had been using previously.
Designing and making changes to a product that Spans UI, Linux services, and lower-level boards is more than just designing an “app layer” or a “firmware layer”. It involves thinking in terms of complete embedded systems design.
Matching the analog behavior to the reality of user perception
Next on the list was to get the analog side of the system back in line with what the installers had originally described. We did this by tackling the following issues:
- Updating the Delay On/Off states to ensure they behaved predictably around the thresholds
- Ensure the “step” and “ramp” modes acted like their names suggested they should.
- Ensure the factory defaults matched what the OEM was advertising for units straight out of the box.
We did not need any clever code to accomplish these goals; we simply needed to make the behavior of the system in a lab match how the users in the field were talking about it.
Clear error terms make real-world troubleshooting easy
The last major change we made to the system involved simplification and clarification of the way the panel reports trouble. Again, this broke down into several fixes, all leading to that result.
- We had to make sure “this device type is not supported” looked different from “this device is temporarily not communicating.” Those are two very different errors, and there needed to be a clear difference in how they were displayed.
- We needed to keep silenced alarms quiet for the correct amount of time, and on the correct devices. Nothing is worse than having to repeatedly silence an alarm while you are fixing the problem.
- The last change we needed to make in this section of the project was to the working of on-screen messages. These messages had to match the physical labels and configuration screens, otherwise the technicians working on them were operating with incorrect information from the get-go.
The goal of these changes, and the end result of making them, was that the panel and the technician would tell the same story on screen and in a support ticket.
Why this kind of embedded Linux and Qt development matters
From the outside, projects like this can look like “just” a firmware touch-up. Inside, they are about reconciling several generations of behavior into one predictable rule set and bringing a Qt interface back into alignment with current expectations.
For us, this kind of embedded Linux and Qt development sits in the overlap between firmware development, embedded Linux application work, and higher-level software development. Meanwhile, on other projects it can often be paired with lab builds or fast prototyping runs and be used to validate behavior before full release.
However, the key point remains the same regardless of your use case. You do not always have to fully redesign a system to reach a reliable and supportable final product. Sometimes all you need is a focused pass over the issues to make sure that all the behavior, UI, and documentation lines up properly with the actual reality of how it is used, rather than the initial idea of its use.
Lessons for teams with existing Linux + Qt products
If you are responsible for a touch panel or control system that has “just evolved” over time, a few patterns from this project might help.
- Describe real behavior plainly. Dictate how the resets, the warm-ups, the faults, and the analog outputs should behave, and make sure that lines up with reality.
- Don’t patch around special cases; bring them all together in the design. Bugs like to hide in the fringes of a project, so make sure that you minimize the number of code paths for different processes to eliminate the extra fringe.
- Treat the Qt layer as part of the system. If your lists are jumpy, your icons lie to the user, and your configuration states are stale, you have bugs in your system, not just an “unique UI.”
If your Linux + Qt panel is getting creaky
Does your embedded Linux and Qt development project feel a bit like this one, you probably do not need to tear it down to the studs. It may simply require a focused look at the behaviors, the UI patterns, or the error handling to make the changes necessary without breaking what does work.
If that sounds like something your product could benefit from, please click here to schedule a free 1-hour consultation to see if a targeted cleanup like this one might be right for you.

