Register / Login  |  Desktop view  |  Jump to bottom of page

tcMenu Arduinio library » Help with integrating tcmenu in custom library instead of main.cpp

Author: Rockclimber399
29/01/2021 22:07:36
Hello,

I'm trying to build a somewhat simple application on an Arduino. I have a pneumatic ram that I'll be controlling push and pull force with via two switching solenoids. It's for a testing apparatus, so I'm trying to build out a menu system that lets me predefine things like, cycle frequency, number of cycles, total run time etc. then hit start and have it run. While I could probably do this as one big main.cpp file, I really like the practice of breaking up my code into custom libraries then including them (though admittedly my knowledge of the "how's" of this can be limited).

My whole main.cpp:
#include <Arduino.h>
#include <REPTestOS.h>

REPTestOS OS; //create now Operating System object

void setup()
{
  OS.bootOS();
}

void loop()
{
  OS.runOS();
}


I have three libraries I'm building. PnumaControl to manage actuators, REPTestOS_menus this is the tcmenu, and REPTestOS to control everything, imports both other libraries.

This is the main library that I'm trying to join everything together with. It imports PnumaControl.h and REPTestOS_menus.h

#include "REPTestOS.h"
#include "Util.h"

//global variable definitions
Util util;
PnumaControl pnuma1(9, 10);

REPTestOS::REPTestOS(){};  //constructor
REPTestOS::~REPTestOS(){}; //destructor

void REPTestOS::bootOS() //boot operating system function. To run in startup
{
  Wire.begin();              //Start wire
  setupMenu();               //Initialize menu system
  Serial.begin(9600);        // initialize serial communication at 9600 bits per second:
  Serial.println("Booting"); //small serial indicator during boot phase

  //Start remaining items
}

void REPTestOS::runOS() //constantly runs in loop
{
  taskManager.runLoop(); //handler for menu system task management
}

/*----------------------------------------------------------
------------------------------------------------------------
----------------- Menu System Handlers ---------------------
------------------------------------------------------------
----------------------------------------------------------*/

void CALLBACK_FUNCTION cycleFrequency(int id)
{
  pnuma1.cyclesPerSecond = int(menuTestSettingsCyclesPerSecond.getAsFloatingPointValue()); //update pnuma1 with value
}

void CALLBACK_FUNCTION pullControl(int id)
{
  pnuma1.setMode(menuTestSettingsPush.getBoolean(), menuTestSettingsPull.getBoolean()); //update pnuma1 with mode
}

void CALLBACK_FUNCTION pushControl(int id)
{
  pnuma1.setMode(menuTestSettingsPush.getBoolean(), menuTestSettingsPull.getBoolean()); //update pnuma1 with mode
}

void CALLBACK_FUNCTION stopTest(int id)
{
  pnuma1.running = false;
}

void CALLBACK_FUNCTION startTest(int id)
{
  pnuma1.running = true;
}


and here is the beginning of PnumaControl.cpp, she's rather long:
PnumaControl::PnumaControl(int pushIn, int pullIn)
{
    pushPin = pushIn; //constructor requires push and pull actuator pin #'s, set to private
    pullPin = pullIn;
}

PnumaControl::~PnumaControl()
{
}

void PnumaControl::setup()
{
    pinMode(pushPin, OUTPUT);
    pinMode(pullPin, OUTPUT);
    digitalWrite(pushPin, LOW);
    digitalWrite(pullPin, LOW);
}
void PnumaControl::control()
{
    if (running)
    {
        baseClock = millis();                                //update clock
        unsigned long timeDelta = baseClock - lastActuation; //find time difference between last actuation

        int freq = 1000 / cyclesPerSecond; //compare to desired frequency

        if (timeDelta > freq)
        {
            setNextCycle();           //update state based off of mode
            actuate();                //fire updated actuation state
            lastActuation = millis(); //reset timer
        }
    }
};


I'm currently running into an issue with the callback functions. An issue which I'm certain is my approach, not a limitation of the way tcmenu is built. If I define a global instance of PnumaControl, within REPTestOS, I have access to it within the callback functions which I've moved to REPTestOS, but I then do not have access to it within public member functions of REPTestOS, this is the current code, and from my reading, bad practice. On the contrary, if I create an instance of PnumaControl at main.cpp, (where I'm also creating the REPTestOS instance) and pass it to the REPTestOS instance, I then have access to it from within the public member functions of REPTestOS, but I don't have the ability to access it from the callback functions.

I'd like to have the ability to call "pnuma1.control()" in conjunction with "taskmanager.runLoop()" so that as the state of my actuator is manipulated, it is managed.

From the reading I have done it appears to be a scoping issue because the callback functions are "static member functions" meaning they don't play nice with one another. I'm not familiar enough with them to work around this. Would love some assistance if you could!

I've linked the project so you can see everything if I've not been complete enough with my snippets.

Author: davetcc
30/01/2021 09:52:01
So here's a few starter points, quite long so it may take me a few iterations to get through it.

Instead of your control method that currently needs to do the timing loop, why not create a taskManager event or task that does this instead. TcMenu is built on top of taskManager, so it's there for you to use by default. Essentially, you could either extend BaseEvent and register an event, or you could schedule work to be done (once or fixed rate). Take a look at the task manager docs: https://www.thecoderscorner.com/products/arduino-libraries/taskmanager-io/ and there are many examples packaged with it too, including demonstrating multithreaded use with ESP32 and mbed.

On the design front, we can't do much in the short term to change where tcMenu puts the callback functions, but I would recommend something more like:

void CALLBACK_FUNCTION cycleFrequency(int id)
{
  auto cycles = int(menuTestSettingsCyclesPerSecond.getAsFloatingPointValue()); //update pnuma1 with value
  pnuma1.onCycleFrequencyChanged(cycles); 
}

The callback functions are actually global, they are scoped using extern so that they are available everywhere. You just need to include the projectName_menu.h in any file you want to use them. But that should not be of interest to users, as they probably shouldn't be called directly.

If I define a global instance of PnumaControl, within REPTestOS, I have access to it within the callback functions which I've moved to REPTestOS


If I understand this correctly, you want to access your global instance in different places, this is a bit beyond to scope of this forum to be honest, it's more a C++ question. But hey let's give it a go here:

If I had class Abc that I wanted to access in various places.


Abc.h
class Abc {
private:
  int a;
  int b;
  int c;

public:

  void onAChanged(int val) {
     a = val
  }
};

// forward declare Abc here, so it can be used anywhere
extern Abc myGlobalAbc;


abc.cpp
// declare it global here
Abc myGlobalAbc;


main.cpp
#include <abc.h>
// use myGlobalAbc here


Also, are you confusing classes with libraries, or are you putting each class in it's own Arduino / PIO library?

Author: davetcc
30/01/2021 10:17:13
At the moment you can't move the callbacks out of main when using the designer, it will just put them back into the main file again on the next pass, thinking that they are missing. Has tcMenu designer detected your main class with that name?

The designer comes with a few limitations on how the menu software is put together, these make it trivial to build most applications, but if you don't follow the same pattern, you just make life difficult for yourself.

The path of least resistance is to keep the callbacks in your project main, and refer to the objects in their various places. You can still keep all your other code in separate class files.

Author: Rockclimber399
31/01/2021 18:14:49
 
davetcc wrote:Instead of your control method that currently needs to do the timing loop, why not create a taskManager event or task that does this instead. TcMenu is built on top of taskManager, so it's there for you to use by default.


After I posted my question, I started exploring taskManager a bit more for this exact reason. Seems like the right tool for the job. Just means I need to approach handling the timing differently. And I'm still not 100% on how to implement taskManager after the examples.

davetcc wrote:If I understand this correctly, you want to access your global instance in different places, this is a bit beyond to scope of this forum to be honest, it's more a C++ question. But hey let's give it a go here:

If I had class Abc that I wanted to access in various places.


Abc.h
class Abc {
private:
  int a;
  int b;
  int c;

public:

  void onAChanged(int val) {
     a = val
  }
};

// forward declare Abc here, so it can be used anywhere
extern Abc myGlobalAbc;


abc.cpp
// declare it global here
Abc myGlobalAbc;


main.cpp
#include <abc.h>
// use myGlobalAbc here


You're completely right...it was a c++ question more than anything, and you clarified it perfectly. This resolved my issue instantly. That said, as I understand it, globals are bad practice and should be avoided...but not sure how much it really matters for this project for the sake of simplicity. Thank you for this super concise example.

davetcc wrote:Also, are you confusing classes with libraries, or are you putting each class in it's own Arduino / PIO library?


Once again, yep, you're totally right. I do a good bit of stuff in JavaScript and it seems pretty common people refer to them as libraries. But yea, I'm making classes in separate files.


davetcc wrote:At the moment you can't move the callbacks out of main when using the designer, it will just put them back into the main file again on the next pass, thinking that they are missing. Has tcMenu designer detected your main class with that name?


It will generate them in the main menu file each time, but I just comment and uncomment them out if I need to regenerate the menu system. I much prefer having them organized in my os class. it would be cool if you could point the designer to look for those functions in a specific file. But its really not a big deal to handle it as I am.

Thank you again for your response! Absolutely ecstatic about what this has to offer, and am enjoying the process of building something with the next tier of complexity.

Author: davetcc
31/01/2021 19:43:06
 
That said, as I understand it, globals are bad practice and should be avoided


In the embedded world runtime memory allocation tends to be minimised, you normally see globals used a bit more often.

In the future we may make it possible to have menu callbacks that is based on an implementation of an interface as an option, similar to taskManager and switches. But I would hazard a guess that the vast majority would want that statically allocated at compile time.

TcMenu itself is a hybrid, it does do some dynamic memory allocation during setup, but we try to avoid using new after setup completes. Otherwise we could limit the audience who would use it.

Don’t forget that even the boards that sound huge has less available memory than you may expect after RTOS has started.

Author: Rockclimber399
01/02/2021 04:32:07
 
davetcc wrote:Don’t forget that even the boards that sound huge has less available memory than you may expect after RTOS has started.


This piqued my interest. I'm quite curious if there is a good way to figure out what this actually is? I did notice that all the required support tcmenu needs takes up a substantial amount of room. I'm currently at 29400bytes Flash, and 1065 Ram, which seems fine on the Mega, but was pushing it on the Uno.

I was under the impression that the compiler would look at all variables, functions and objects, and when they are called in say, the setup() function, account for that in the ram usage output shown. Obviously, if you have software that allows someone to add and remove menu items or something to that idea you're going to have memory allocations that the compiler could never estimate. How much of this is happening from tcMenu? I can't imagine much with it--as you just stated--primarily statically allocated.

I'd like to heed your warning here, but I'm trying to figure out if maybe I've been looking at this wrong!


Author: davetcc
01/02/2021 07:56:45
TcMenu is mainly static for the reasons discussed earlier, there's a few small allocations at runtime, with things that are generally not known upfront, but they are pretty small, maybe adding 500 or 600 bytes extra on an AVR. Menu Items structures are completely static, it is task-manager, switches and possibly the rendering that may need to allocate at runtime - they do so once and don't let go of the memory.

I did notice that all the required support tcmenu needs takes up a substantial amount of room. I'm currently at 29400bytes Flash, and 1065 Ram


For what it's doing that is pretty small, an Uno is on the edge of what we can support, and in some ways, it's been holding back what we can do on bigger boards. Don't forget that includes all the parts of Arduino we're using, your display code, any Wire, Serial libraries etc. Only the smallest menu will work on an Uno. You'd struggle to write a browser-side javascript menu program that would fit in 30K, let alone a full program with all runtime included smilie

I was under the impression that the compiler would look at all variables, functions and objects, and when they are called in say, the setup() function,


Linking and memory allocation in C++ is a huge topic, we cannot cover it here. The basic rule is stick to statically allocated memory when you can, especially on smaller boards.




Register / Login  |  Desktop view  |  Jump to top of page