Monday, September 5, 2011

Front Panel Software

[This is a continuation of the Front Panel Hardware post; you should read it first]

Embedded Software

The software which runs on the front panel's microcontroller is about as simple as I could make it.  It uses the ATMega168's built-in UART to communicate with the host, with RTS and CTS controlled manually by the embedded software.

To keep things simple, I made everything query-response.  This way there's no need for timers or anything like that on the microcontroller -- the embedded software does nothing without first being asked.  There are four essential commands:
  1. Reset all output pins to 0.
  2. Set an output pin to a given state (1 or 0).
  3. Specify the pins which should be monitored for input.
  4. Return the value of all input pins.
Pins are identified using a three-digit number decimal number.  The first digit is 0 for output pins, and 1 for input pins.  The second and third digit are the zero-padded pin numbers as silk-screened on the PCB.  By separating the input and output pin spaces like this, we simplify the host program, and minimize the chances of error.  Here's a diagram of the pin numbers:

Host Software

The program running on the host is responsible for displaying the user interface, for passing output state changes to the front panel, and for keeping the indicators up to date.  I've implemented it in Python, using WxWidgets as the UI toolkit.

One of the primary requirements is that the user interface be customizable.  This is necessary because we'll be using the front panel with a number of different circuits, each of which will have different input and output configurations.  Accordingly, the configuration isn't hard-coded in the front panel program, but is instead driven by an external config file.  Here's an example:

toggle('Bit': 0='0', 1='1',)
momentary('Actions': 2='A', 3='B',)
indicator('Result': 100='1', 101='2', 102='3', 103='4',)
(Yes, it's true.  I was too lazy to tune the regexps to allow for the omission of the trailing commas)

We're using output pins 0-3, and input pins 0-3 to display a user interface with three different types of UI element.  Here's what it looks like when running:

The bit 0 and 1 buttons are toggle buttons -- one click is required to turn on the associated pin, and a second click is required to turn it off.  Action buttons A and B are momentary -- their associated pins turn on when the button is pressed, and turn off when it is released.  Indicators Result 1-4 change their background color when their input pins go high, and appear as shown when said pins go low.

Not a terribly complicated program, but I did have an interesting battle, and (re?)learned something new along the way:  UI toolkits and threads are like oil and water.  Tkinter (the default toolkit for Python) is like oil on fire.

In general, you confine all interactions with the UI to a single thread -- the UI thread.  Anyone else who needs to change the UI posts something to the UI thread, and lets the UI thread take care of it.  If you don't do this -- if you muck with the UI from a thread other than the UI thread -- the world ends in fire.  If you're lucky, you get a stack trace.  If not, just fire.

Tkinter exposes a monolithic event loop -- you call Tk.mainloop(), and it runs until it's time for your program to exit.  There doesn't appear to be a way to break the loop up into its component parts so you could, say, wait for UI events and events from your non-UI threads using an interface like poll(2).  Instead, everything is supposed to be done through the Tkinter event system.  External things wanting attention post events -- events which are passed to callbacks which have registered using bind.

This would be fine, except for the fact that the event-poster isn't thread safe either.  That is, you can't post an event from the non-UI thread.  In fact, the only way for a non-UI thread to get the attention of the UI thread appears to be through polling.  The UI thread asks to be woken up every n milliseconds, during which time it checks a queue of things that need attention.  I don't like this approach at all.

After much gnashing of teeth (I still can't believe Tkinter doesn't have a thread-safe event post) and failed attempts to get around this problem without using polling, I found wxWidgets (specifically wxPython).  WxWidgets has the same threading restrictions as Tkinter and other UI toolkits, in that it requires all UI interactions to take place on the UI thread.  The crucial difference, however, is that wxWidgets specifically allows events to be posted from non-UI threads.  Once you have that, everything is easy.

Aside: Why do I use threads?  For the most part I don't, except for the serial port handler.  Serial port reads and writes are handled by independent threads.  This allows me to have non-blocking writes, which in turn allows me to do writes from the UI thread, since I don't have to worry about blocking it.  On the read side, it's much simpler to have a separate read thread than to mix reading and writing.  The only downside of this approach is that some reads (indicator status updates) need to directly affect the UI, and thus I need a toolkit which allows me to talk to the UI thread from a non-UI thread.  WxWidgets allows that, and so everything comes together nicely.

No comments:

Post a Comment