DISCLAIMER::::
I am only posting this code for a few who might be following along with the process. THIS CODE HAS KNOWN BUGS. Some real nasty ones that can possibly cause a pump to stick on or to not run. I'll try to note as many of them as I can here. The main bug is in the attempt to allow the user to disable the PWM control. As written the order of operations (Change PWM enabled, Then recalibrate, Then reset the schedule) is critical. If you enable PWM after the calibration you may not have a minimum flow rate set and the consequences are indeterminate.
However, this is what is currently running on my tank (with PWM disabled). It has been running for a few months now and hasn't let me down. I had another Arduino monitoring it and recording its dosing activity and everything was spot on timing wise. Still, the code is not complete. Work has been crazy and I haven't had time to fix this up.
The code has gone through a major rearrangement. All the same pieces are still there, but they work together differently now. This has finally evolved into a true finite state machine. It is now not only 1.0 compliant, but I am pretty sure that my prodigious use of the F() macro means that you need to have Arduino1.0 to run this.
The first major difference you'll notice is the loop function.
Code:
void loop()
{
(*State_Functions[current_state])();
doColor();
}
Simple stuff huh? There are three different possibilities for the loop, menu state, dosing state, and free running state. Each one does different things, so I put them into different functions. That first line calls the appropriate function from an array of function pointers. Here are the other relevant code pieces.
From DOSE_head.h
Code:
typedef void (*S_Function)();
Code:
S_Function State_Functions[] = {
&doRunState, &doMenuState, &doDoseState};
Code:
#define NUM_STATES 6
enum state_vars {
RUN_STATE, MENU_STATE, DOSE_STATE}
current_state;
Then the three functions doDoseState, doRunState, and doMenuState are all defined in DOSE_head.h.
The second line of the loop is new. The LCD I am using has an RGB backlight. So I thought it would be neat to have it change colors for warnings and such. Right now it will change yellow or red based on how much supplement is remaining in the container.
The Menu has changed radically. I am trying to set up to integrate this with a larger controller idea and the old menu wasn't going to do that. This also eliminates that recursion in my old menu that could potentially cause problems down the road if the menu structure got any deeper. I've also moved all of the strings into PROGMEM. This has made the written code a lot longer and bulkier, but makes the compiled code fit much better on the Arduino.
Also many of the functions used by the menu have changed. I am setting up to allow for the seamless integration of more pumps or even other items on timers. I'm not quite there yet because the code gets caught in some of these functions if the user doesn't input. I need to bring them out into a state of their own and let the state machine handle them. But that's for way down the road. That's another one of those bugs I was talking about. While you are using menus, the rest of the program is frozen. So always make sure you exit out of the menus and don't try to access them while a pump is running. If a dose is due while you are in the menu, it will be made up as soon as you exit. If you leave it in there too long, it could ramp up some huge dose. I will add something to handle that, but I haven't got to that yet.
The last major change is the button. I liked the way the encoder worked so much, that I put the button on the other hardware interrupt. It handles its own debounce and automatically updates a flag that indicates a button push-and-release as well as a second flag that indicates the button is currently pressed down. A couple of simple macros allow the rest of the program to access the flags which are packed into an int type to be accessed by names. This will change soon as I am going to do a struct with a union to bit pack a bunch more flags into an unsigned long. That way I can implement the methods to access and twiddle the flags right there in the same struct they are held in. That way I will be able to pull that module out of this program and use it somewhere else if I want.
Here are the code bits I'm talking about. Notice that it still uses the QuickPin library, but it won't for long. This is the last place in the code that uses it, so I will switch this and the encoder to direct port manipulation and cut QuickPin out to save a little memory.
Code:
void ISR_button_handler()
{
if (button1 == LOW) // FALLING button PRESSED
{
SETF(BUTTONMADE_FLAG);
if (!BUTTON_PRESSED) press_detect_time = micros();
}
else // RISING button RELEASED
{
CLRF(BUTTONMADE_FLAG);
if (press_detect_time && (micros() - press_detect_time > 50000)) // 50ms DEBOUNCE
{
SETF(BUTTONPRESS_FLAG);
}
press_detect_time = 0;
}
}
And the flag stuff. This is a neat way to do it, but I think I'm going to go for a struct with a union holding a bitfield.
Code:
/*********** FLAGS ***********/
#define SETF(_flag) (PROGRAM_FLAGS |= (_flag))
#define CLRF(_flag) (PROGRAM_FLAGS &= ~(_flag))
#define CHKF(_flag) (PROGRAM_FLAGS & (_flag))
volatile byte PROGRAM_FLAGS;
// Set if button enabled
#define BUTTON_FLAG 0x01
#define BUTTON_ON CHKF(BUTTON_FLAG)
// Not Debouned Set when button is made low (contact made)
#define BUTTONMADE_FLAG 0x02
#define BUTTON_MADE CHKF(BUTTONMADE_FLAG)
// Requires a debounced press and release to set
#define BUTTONPRESS_FLAG 0x04
#define BUTTON_PRESSED CHKF(BUTTONPRESS_FLAG)
// Set if Encoder Enabled
#define ENCODER_FLAG 0x08
#define ENCODER_ON CHKF(ENCODER_FLAG)
// Set if PWM enabled
#define PWM_FLAG 0x10
#define PWM_ENABLED CHKF(PWM_FLAG)
I'm trying to allow for the user to select whether to run the pumps with PWM or not. I noticed that I always ran mine at full blast since they run better that way so I wanted to turn it off but also wanted to leave the ability to use it later for something else. Like I said before, that is real buggy. That's what I was working on when I left off.
Most of the other changes involve cleaning up functions. I reduced a bunch of overloads with default arguments. There are a lot of new things at the bottom of DOSE_head.h. Mostly those have to do with running the state machine. All of that is probably going to move to a different file soon.
I'm also working out some ways to make the interface a little easier. I used to have a problem hitting the button and updating the menu at the same time and selecting something I didn't mean to. So now with the button being interrupt and flag driven, I can clear any button press and ignore it if the display is updating at the same time.
There is a few new arrays besides the one for the state machine. One of them holds the schedules, and another with the same index holds name strings for the schedules. That way functions from the menu like chooseSchedule() don't have to be re-written if you add more pumps.
In the future, I want to make this truly turn into the basis of a more functional controller based on the Arduino MEGA2560. I want to turn the skeleton of the Dose_S class into a general scheduler base class that Dose_S can then derive from in addition to any number of other classes for other equipment that might need a timer.
I also need to finish working out the whole global flag system so the program can keep up with whether pumps are calibrated to match their modes and whether schedules have been set up before you run them. That's really where I left off with this code.
This code compiles and runs and does exactly what I expect it to. But then again I wrote it. It might not do exactly what you expect all the time so I wouldn't base a project on it just yet unless you plan to fix some things in the code yourself. As this is a work in progress I am always open to suggestions and criticism. But as I am a busy man these days you'll have to forgive me if I don't get back to this for a while.
If any of you are running a design like mine and would like to load up my code and try to break it, I always appreciate that.