In my previous article on this topic, we discussed how I'm using a 29-GPIO Simblee breakout board to control my cunning Chronograph.
In the image above, the large blue board (front left) is my Arduino Mega. On top of this is my audio spectrum analyzer shield. At the back on the large breadboard we see one of Adafruit's ChronoDot Ultra-Precise Real-Time Clock (RTC) modules. In the fullness of time, this will be augmented with a temperature/pressure sensor module and a 9DOF (nine degrees of freedom) module boasting a 3-axis accelerometer, a 3-axis gyroscope, and a 3-axis magnetometer. The RTC and these sensor modules will all be mounted on a custom shield, but that's a tale for a future blog.
The smaller breadboard (front right) contains the Simblee breakout board. The 10mm x 7mm x 2mm Simblee chip itself is seen in the nearside corner of the breakout board. This chip boasts a 32-bit ARM Cortex-M0 microcontroller coupled with a Bluetooth Smart engine. As we've previously discussed, I'm using my Simblee to receive commands from my iPad, and to then present corresponding control signals to my Arduino.
Recently, I've been talking to quite a few people who are using Simblees. My impression is that approximately half of them are using their Simblees as stand-alone modules. By this I mean that the Simblee performs all of the processing functions and all of the interfacing to the outside world. The other half are doing what I'm doing -- using their Simblees to control other microcontrollers.
Now, I'm still a beginner at all of this, but I've created what I believe to be a rather cunning interface, and the folks I've already shared this with have said that they've found it to provide a useful starting point for their own projects, so I thought I'd share my current offering with you here.
Max's magnificent hierarchical interface
Let's start by reminding ourselves that we program the Simblee using the Arduino IDE (Integrated Development Environment). A regular Arduino program has two main functions: setup()
and loop()
. In the case of a Simblee program, we augment this with two more functions: ui()
and ui_event()
.
The ui()
function is where the majority of the SimbleeForMobile User Interface (UI) is defined in textual format. For example, we might call a function that says "Draw a switch at this XY location on the screen and make its ON color green," and so on for other objects like buttons, sliders, and so forth. This is the interface that will be uploaded to the mobile platform -- smartphone or tablet -- where it will be rendered (presented) as a Graphical User Interface (GUI).
The ui_event()
function provides the callback mechanism by which the GUI on the mobile platform can communicate any actions back into the body of the sketch. We might think of this as acting a bit like an interrupt service routine (ISR). When the user performs some action on the mobile platform's screen, the system communicates this information back to the ui_event()
function in our sketch.
One important point to note -- and one that wasn't clear to me at the beginning -- is that we don’t actually call the ui()
and ui_event()
functions ourselves. Instead, these are called by other functions hidden in the depths of the Simblee library.
Each graphical object we instantiate is automatically assigned its own unique identifier (ID). The ui_event()
function supports a single parameter of type event_t
, which is a pointer to a structure that contains all sorts of useful information, including the ID of the object being affected, the type of event, the value currently associated with the object, and so forth.
Now, before I show you this code, let's first take a look at my interface as it appears on my iPad:
This top-level menu reflects the fact that my Cunning Chronograph has (or, at least, it's going to have) eight main modes: Clock, Calendar, Lunar Calendar... and so forth. Before I transmit the mode I wish to display to the Arduino, I binary encode it into a 4-bit field (obviously, I only need three bits for eight modes, but I added an extra bit for future expansion). Selecting a new mode on this interface immediately causes the Cunning Chronograph to reflect that selection.
One point is that -- in the case of my Cunning Chronograph -- there must always be one mode selected. Thus, when you look at my code, you'll see that if I try to turn the currently selected mode off by dragging its switch, then the code ignores this and turns that switch back on again.
Each of my modes can have up to eight options. If you click the "Options" button associated with the "Music Wow" mode, for example, then you'll be presented with the following sub-menu:
As always when creating a program, there are multiple ways to do everything. For example, I could have created nine completely separate menus, each with their own switches and text annotations. In my case, however, I decided to re-use the same switches for all of the menus and to simply change the text annotations associated with the switches. Observe, for example, that the "Options" annotation associated with the bottommost switch in the top-level menu has been updated to read "Main Menu" in this sub/options-menu.
Now let's quickly skim through a couple of high-points in my interface (if you right-mouse Click Here and select the "Open in a separate window" option, you will be presented with a text file containing the full program in a pop-up window; also, you can Click Here to access a compressed ZIP file containing the program in the form of an Arduino *.ino file).
I'm assuming that you've opened the text file in a separate window as discussed above, so we can just talk about things here. We start off with a few definitions, including numStatePins
and numOptionPins
, which define the number of GPIOs we're using to present the current state and its associated options to the outside world. Also, numSwitches
says that we have nine switches in all, while numStates
and numOptions
say that eight of these switches are used to specify the current state or options depending on where we are in the menu hierarchy.
Next, we declare some global variables as follows: menu
is used to keep track of the menu level we're in (0 = the main menu; 1 = one of the options menus associated with the modes in the main menu) and state
is used to keep track of what state (mode) we're in. Of particular interest is the options[8][8]
array, which is used to keep track of which options have been selected for each main mode. As we'll see, this means that we can pop up and down between the top-level screen and options screens without losing track of any options we've previously selected.
Now we declare the GPIOs that will interface to the outside world: pinsState[4]
specifies the four pins we are using to represent the currently selected state, while pinsOptions[8]
specifies the eight options associated with the currently selected state. Meanwhile, pinValid
is used to inform whatever MCU we are communicating with whether or not the information on the other pins is currently valid or not (0 = Invalid; 1 = Valid).
Next up are the xySwitches[9][2]
and xyLabels[9][2]
arrays. These contain the XY coordinates of our nine switches and nine text annotations on the screen. The only reason for doing things this way is that, later on, we can use a simple for()
loop to draw everything at the desired locations on the screen.
Remember we said that every object we draw on the GUI is automatically assigned a unique ID number? Well, the idSwitches[9]
and idLabels[9]
arrays are going to be used to store the IDs associated with our switches and labels, respectively.
Next, we see two arrays of pointers to text strings: *labelsStates[9]
and *labelsOptions[8][9]
. These are the text strings that will be displayed on the screen. If you refer back to the screenshots shown above, this will all start to make sense.
OK, let's skip the setup()
and loop()
functions and bounce over to the ui()
function. Let's start with what's between the beginScreen()
and endScreen()
functions, which is where we actually describe what's going to be in our interface.
First of all, we draw the gray rectangle and black outline that's going to sit behind our switches; also, we write the name of this GUI ("Cunning Chronograph"). Next, we loop around drawing our nine switches along with their associated text annotations. In both cases, we store the unique IDs of these objects that are returned by the drawSwitch()
and drawText()
functions.
Actually, one clever part of all this occurs in the two statements following the endScreen()
function call. Let's assume that we've just powered up our Simblee and launched this interface for the first time. In this case, the state
will be 0 (indicating that we're in the Display Time mode. Now observe the updateValue()
function call:
updateValue(idSwitches[state],1);
This sends a command to the GUI to update the value of the switch whose ID is pointed to by state
with a value of 1, which automatically turns this switch to its On position.
Now, let's assume that we start playing with our interface, moving between different states and setting the various options associated with each of those states. It would be a pain to lose all of these settings when we exit this GUI on the mobile platform.
This is the part that can be tricky to wrap one's brain around. Suppose we are currently in the Display Music Wow mode (state = 6
); maybe we are even in this mode's Options sub-menu. Now suppose we exit the GUI on the mobile platform. At this time, nothing is going to happen with the Cunning Chronograph -- the Simblee is just going to keep on working away.
But what happens when we re-connect the Simblee app? Well, when we re-launch the GUI, it will call our ui()
function again. Once we've re-drawn all of our switches, we set menu = 0
to ensure we're on the top level menu. This time, state = 6
, so this is the switch that will be affected by the updateValue()
function, thereby ensuring that our GUI reflects the currently selected state in the Simblee. Pretty cool, eh?
Finally, we arrive at the event_ui()
function. This is where I originally made a mistake, because all I did was check for my switches being pressed. What I hadn’t realized is that every object in the GUI will trigger an event when it's touched, and this includes the rectangles I'm using to bound the switches and the text annotations. Thus, I added a quick test right at the beginning of this function to check whether or not the event was associated with one of my switches; if not, I simply ignore that event and move on with my life.
If it was one of my switches that were pressed, then there are four possibilities that need to be handled as follows:
- We're on the main screen and we press the "Options" switch. In this case we need to move to the corresponding sub-menu, change the text annotations associated with the switches, and update the switches to reflect the currently active options. Note that nothing is happening here to affect the outside world.
- We're on the main screen and we press one of the mode (state) switches. The first thing we need to do is to check that we aren’t trying to disable the currently-selected mode -- if we are, then we need to turn that switch back on again and not do anything else (basically, we're making our switches emulate "Radio Buttons"). If we have selected another mode, then we need to place our pinValid
GPIO in its invalid state, wait a few milliseconds for the other MCU to see this, update the values on the pinsState
and pinsOptions
GPIOs, wait a few more milliseconds for the other MCU to catch up, then return the pinValid
GPIO to its active state.
- We're on one of the options screens and we press the "Main Menu" switch. In this case, all we need to do is re-draw the text annotations and update the switches to reflect the currently selected state. Once again, nothing is happening here to affect the outside world.
- We're on one of the options screens and we press one of the options switches. In that case, all we need to do is store the new value associated with this option in the appropriate location in our options[8][8]
array, and also to change the state on the corresponding pinsOptions
GPIO. Note that, in this case, we don’t need to play around with the pinValid
GPIO. This is because the way my Cunning Chronograph works is that for each main mode, it loops around performing that function, and it checks the state of the options pins each time around the loop.
Well, I think that's enough for now. Based on the descriptions above, I think you should be able to work out what's happening in my code. If you have any questions, or if you spot something you think I should have done differently (or better), please don’t hesitate to let me know in the comments below.
文章评论(0条评论)
登录后参与讨论