Embedded Systems – Implementing a Simple Controller in C

cembedded-systemsmvc

Is there a known method or pattern to implement a simple controller for an MVC design in pure C or the switch case approach is the standard?

Background :

I have an embedded application and I'm pleased with how the business logic turned out. I also finished the UI which boils down to basically 2 functions:

uint8_t getInput(void);
void display(logger toDestination, char* message, ...)

Inputs can come from multiple sources and outputs can be sent to several destinations. All of that is taken care of so the application has no idea if its run by a PC on on the device. It also has no idea if the events come from humans or are faked by unit tests.

Where I struggle is with the application logic. My views are in charge of where and how to display the outputs. My drivers, regrouped into behavioral models providing clear features, know how to do their jobs very well but not when.

Here's my functional rough draft, which in my opinion violates principles like DRY, cohesion, flexibility and may present scalability problems:

Rough draft : (I faked the names and reduced the size but you get the idea)

menu.h

void goToMainMenu(void); // This is the entry point of the application after everything was setup correctly

    void goToFirstSubMenu(void);
        void firstFunctionality(void); //The last levels are the ones doing the calls to the business logic
        void secondFunctionality(void);
        //Other functionality or nested sub-sub-menus

    void goToSecondSubMenu(void);
        void thirdFunctionality(void);

    int doSomethingWithTheBusinessLogic(int arg1, char arg2, void* arg3);

    //Other sub-menus and options

    /* And so on, the function prototype are indented to reflect the logical level of the menus.*/

Then most of the functions will roughly look like this:

menu.c

void goToMainMenu(void)
{
  uint8_t choice = 0;
  resetDefaultState(); 

  do
  {
    display(toTerminal, "Press 1 to go to sub-menu 1" ENDL);
    display(toTerminal, "Press 2 to go to sub-menu 2" ENDL);
    display(toTerminal, "Press 3 to do something with the drivers" ENDL);
    display(toTerminal, "Press z to go back." ENDL);

    choice = getChar(); /* Blocking thread waiting for events to pop from the other threads */

    if     (choice == '1'){ goToFirstSubMenu(); }
    else if(choice == '2'){ goToSecondSubMenu(); }
    else if(choice == '3'){ doSomethingWithTheBusinessLogic(); }

  }while(choice != 'z');

  resetDefaultState(); 
  /* When a submenu returns we're taken to the parent menu's do-while loop */
}
  • Is it a good way to do things for small-medium systems?
  • How could this be improved considering the limits of C and embedded environnements?

What has been tried :

I tried a n-ary tree approach where nodes are menus and leaves are functionality but without polymorphism it's hard to define elegently how both should behave to inputs. The strongly typed system also makes it hard to do a simple callbacks system because I end up needing tons of wrappers around the drivers to unifiy a single callback signature (void myCallback(void*)).

It also seem to be overkill for this simple state-flow; I feel like I'm working against KISS and productivity only because I've been hammered with "chains of if-elses is the devil".

What I would like :

With higher level languages usually you can bind events and callbacks. Something along the lines of bind(myMenu, thisInput, theCallback) so whenever myMenu is focused and an event thisInput it raised theCallback is called.

I'm unsure how to do this in C without going way overboard.

Best Answer

There is a development process question which should be understood and answered before worrying too much about the code.

IMHO, any organisation which employes more than one software developer, and especially one using interns to develop code, should have sane, rational, understood, shared development processes. There is a tremendous amount of evidence that reviewing code helps a lot. They help all developers to get onto the same page about coding techniques, style, libraries, tools, etc.

Recommendation 1: Ask your supervisor about doing a review of your code or design as soon as practical. It'll give you some feedback from people who's views are much more relevant than ours. Also it'll provide a way for your supervisor to understand your code well in advance of you leaving, with a clear invitation to direct you to fix things they don't like.

Let's try to look at it from some other persons position. They have to change the code, without breaking it, or fix a bug. They might be the next intern, with less knowledge than you. They want life to be straightforward. Ideally they will look at your code and say to their supervisor "this code is great, I can understand it really easily, and I am confident the set of test cases they built will catch most of the types of errors that I might make."

When I have to read other peoples code, I like it 'as simple as possible, but no simpler'.

The less code I have to read, the fewer ideas, and the less 'stacking' or 'cross-referencing' (I wonder how that works, I'll have to go and read that before I can progress), the better. So callbacks are unwelcome unless they reduce the total amount of effort a lot. Big libraries, with lots of extra features mainly obscure the code. Good unit tests and test cases help me to understand what the developer intended.

Your code seems like a model of how this might be done. My only slight criticism is:

    display(toTerminal, "Press 2 to go to sub-menu 2" ENDL);

then later

    else if(choice == '2'){ goToSecondSubMenu(); }

requires someone to ensure the text of the terminal message, and the value in the if test matches. If the program really is that level of complexity. I wouldn't worry. Hoever if it is significantly larger, then I might be tempted to create some named constants, and write something like:

#define SUB_MENU_2 '2'  
...

    display(toTerminal, "Press " SUB_MENU_2 " to go to sub-menu 2" ENDL);

    else if(choice == SUB_MENU_2){ goToSecondSubMenu(); }

You might be tempted to put the message and choice character into an array of structs, so that the choice value and message are kept together. That is making the reader of your code have to think a bit harder, but you might find it helps if there are some other requirements. For example, it might help to identify values explicitly, so that more ways of navigation can be made to work, e.g. driving up and down the menu with cursor keys, or pressing the digit. However, resist the temptation to write 'clever' code; it causes a new reader to have to move around the code, rather than simply read it in sequence.

MVC is useful when you need to separate concerns when one or more parts are complex, or there is a lot of value in reusing solutions to part of the overall problem.

For example, the code to manage a view, menus, mouse clicks, and keyboard input in a windowing system is complex to write and test. It is worth reusing that. Your 'view library' is display() and getchar(). That's it. The rest is your code to do more view stuff.

There seems to be very little control state. In the example you've shown, the view isn't changing what is offered to the user depending on the state of the model or rules managed by the controller. Further, the actions seem to be straightforward too. So the controller seems very simple, and the model encapsulated.

If that changes, and the view must dynamically respond to reflect the state of the model, or their are several ways to get at the model, or more 'input validation' of data, their may be a need for a more explicit implementation of MVC.

The stuff you have been taught is typically designed for very large systems (or is just another new fad). Unfortunately, when it is taught, the reasoning or rationale might be poorly explained.

IMHO, some of that complexity comes from people trying to implement things in a language which is not a good fit. For example, SmallTalk, the original home of MVC, makes some things easy because it is so dynamic and rich, however some of its mechanisms are ugly to implement in other languages. Hence we end up with a lot of complication, to address complex and very general situations.

Also, there is quite a lot of money, marketing and ego tied up in offering very clever solutions designed for extremely complex problems, then selling the skills capable of applying those clever solutions. It is unlikely you'll hear a large system integrator say "no, you don't need anything this clever and expensive, you just need that very simple, straightforward approach that any competent developer will use successfully". Just my $0.02.

Related Topic