In the world of embedded-style devices, being able to make use of a consumer-focused OS like Android along with internal hardware ports enables devices to have comfortable phone-like interfaces and additional hardware functionality with minimal customization. The problem to then solve is – how do we engineer a software solution that brings the data forward from the ports into Android SDK components? Our answer was a layered solution where each layer handles one part of the process until finally delivering the data into Google’s Jetpack Architecture components.
Implementing this stack was a collaborative effort between the Firmware and Mobile teams. Firmware handled layers closer to the hardware ports – Serial Port Setup, C Library and JNI (Java Native Interface). Mobile handled layers within the application – Android Service, RoomDB, ViewModel and UI. The Java utilities were constructed in tandem to bridge between the two efforts.
Next we’ll get down to the specifics of each layer, but first it’s important to note that some of the tech decisions were made pragmatically to satisfy basic functionality and are likely to be revisited as we discover which limitations are important within this context.
Here’s where things start from our perspective, and while we didn’t do substantial development here, it should be called out that the hardware being interfaced with are internal serial ports that connect the core Android device to additional hardware components. Any component that can be connected via serial port could now be connected through to an Android UI. Devices accessed via serial ports also means they are simply exposed as a /dev/tty device and enable us to investigate/verify any underlying functionality with basic linux terminal tools.
The first software layer is a library that connects to the serial port, manages the byte-level traffic and exposes the result as an interface for sending/receiving messages. Connecting the serial port is as simple as calling open() to create a file descriptor, and sending messages is as simple as a write() call through that file descriptor. However, reading is more complex. Any read() on the file descriptor gets you the raw bytes coming off of the port, so a ring buffer was created around the incoming traffic with whole messages parsed and peeled off of the ring buffer as they complete. The exposed read_message() interface triggers that read process until a single message is found or an error occurs. A set of utility functions were also made to wrap around message writing for convenience.
Without getting further into the details, there are two important decisions made here. First, no threading is done at this point – we defer the context (threads, etc) around the library and its file access to the application layers. Second, in the presence of Android’s UART which provides serial port access via Java, why have a C library handle this? The answer here is pragmatic in that we already have legacy C code that performs the majority of this process so re-using what we already have is a faster route to becoming functional.
Java Native Interface
The next layer exposes the C library to Java through a simple passthrough Java Native Interface(JNI). Each function in the desired interface of the C library has a corresponding JNI call. Result codes returned from C functions are converted into thrown exceptions as needed, allowing any Java that calls them to handle the errors in the language-recommended pattern. The interface into the JNI is a Java class that contains functions to open/close the serial port, read a message and the convenience functions for writing messages of a specific type.
On top of the JNI, two Java classes are constructed for an Android convenient interface. First is a class that extends the JNI class and abstracts away message reads/writes into a message write that returns any responses synchronously and a message read that performs a check to see if any incoming messages are awaiting arrival. The utility itself is a class that implements the Runnable interface, with the intent that the class itself is scheduled onto whatever thread management we want. Listeners are attached to the utility and each time the Runnable processes, the message read is polled and passed along to all attached listeners.
Threaded processing and Services are becoming increasingly dirty words in the Android ecosystem, and for the typical case of consumer phones, there’s good reason behind this. When you have a phone with dozens of apps installed, and each of those apps have full permissions to spawn Services that persist outside of the lifecycle of the application – spawning threads that do Google-knows-what – it’s very easy to have a phone that drains its battery rapidly. If we were to use the above Java utility on a traditional Android background service that constantly and endlessly polls, we would be a bad actor in that equation.
Fortunately, we’re not doing that. Newer Android OSes and SDKs actually prevent background services entirely. Without an OS modification, which is an honest possibility in this case, we are also bound to that. Fortunately, we also know that our application is a “Kiosk” style app and forced into the foreground by the OS whenever the device wakes up. That assumption is key as it allows us to encapsulate data access into a simple Foreground service which binds any time the application comes into the foreground when the device powers up. On binding, it creates a single thread to run the serial port utility. That thread can be slept or throttled to prevent poor power use patterns.
As mentioned previously, the Java utility class allows for listeners to be attached for receiving messages. The Service itself registers as a listener and processes the messages directly into the next layer, a local database, using a single HandlerThread. HandlerThreads are Threads with a job queue in front of them to manage sequential processing. Thread contention backing up that queue is not likely given the current functionality of the app, but if more data sources are introduced, we’ll have to keep an eye on this. There is also no persistence behind the HandlerThread, so the app closing can impact our data. If there’s any piece likely to be improved sooner rather than later, it’s moving DB access into a WorkManager or some other more robust mechanism.
The first layer of the Jetpack components is RoomDB. It’s simply a wrapper around SQLite that allows a simple path to database configuration, versioning with migration management, and a set of annotations that allow Java class definitions to be processed into DB tables. Our Service makes use of the database through Java interface functions annotated to run specific SQL queries. As our data requirements scale, we’ll include a Repository in between the Service and RoomDB to serve as a hub for the events coming from all of our data sources (network requests, DB operations from the Service, system settings changes, etc) and to do more complex management of asynchronous calls.
We’ve configured the database to store the serial port events as timestamped entries into a Message table with an event type field, giving us the full history of serial port events to allow for simpler debugging.
ViewModels are the last layer in the Jetpack components. They are Application-owned collections of LiveData objects which are observers to custom-defined database query functions on RoomDB entities. An Android Activity or Fragment will initialize itself by accessing any ViewModels it needs, and set listeners to all LiveData objects whose data it depends on. The Activity/Fragment configures those listeners to update its UI based on the changing data. Ultimately, it means we have an observer pattern for our UI updates that will catch data bubbling up from low level sources – a typical modern reactive UI setup.
And that’s it! It may seem like a lot of moving parts, but the goal is to keep each layer simple, following separation of concerns, so that if we find an issue with any of our decisions, we are able to tweak or replace just a single, simple layer and not feel like we have to tear apart a monolith. In addition, our data eventually arrives into Google’s modern, reactive UI framework so that Android developers can use what they’re already comfortable with.