Disc1 Arduino Doser

In case you think you might be loosing doses because interval is defined in minutes and over time you might skip a dose, a solution could be to track time using an unsigned int with "2 second" count since midnight.

You preserve the sketch size but increase time resolution. A day would have 43200 "2 second" intervals.

Snorkeler

Sent from Tapatalk
 
Here's the working simulation of the new schedule code. The full sketch is attached as a zip.

I did a few things in there. First of all, at each dose, the program checks the volume needed to finish the day and the number of intervals left and calculates a dose that will get you there. If it is the last dose of the day, then it doses everything it has left which can be at worst twice the average dose.

This also made it possible to allow a booster dose, spread out over days if needed simply by adding the amount of the booster to the target volume for the day.


The relevant variables are:

int last_d; // holds the last dose time
int volume_dosed; // keeps tally of how much dosed so far
int total_volume; // target volume for the window
boolean reset_flag; // useful flag

int booster_volume; // how much booster dose
int booster_days; // over how many days


int set_volume; // daily volume set by user
int start_time; // set by user to define window
int end_time; // set by user to define window
int interval; // set by user to define minimum dose interval




We also deal with another "variable" that's not really a variable. It's the argument to listSched() _c in the sketch. This is the check interval. It defines how often we will check the schedule. Since that was giving me problems, I was determined that it would work with anything. Now it works with anything smaller than the user set interval.



The function tryThese(int, int, int, int) simulates the user entering values for the four user defined variables.

Then the function listSched(int) simulates one week of dosing using the last set of variables tried. On day 3 at 1pm, a booster dose of 100ml over 3 days is added, to show what would happen then. listSched has some complicated parts, but that won't be part of the computer. It's just what we have to do to make a simulated user. For instance, since it always starts at midnight instead of start_time, it simulates that the doser had been running the day before and we are already in the middle of a schedule on the first day.


The loop function currently holds a set of these type of calls, showing several different simulations each with several different check times. Looking at the ones with larger check intervals, you can see how the schedule adapts to any doses that were missed by increasing the volume of the rest of the day's doses by a few ml. A little after the third time 720 comes around you can see the volume adjust itself for the booster dose.

The booster continues for two more days until 100ml has been added.

On the first day of the booster dose, if the schedule is more than half way through, the function will cut the first days dose in half to avoid shocking the system. The remainder will be made up over the rest of the days.




You can go into the loop and change any numbers you want to simulate your own dosing ideas. Anything where the check interval is less than the dosing interval should work out to give you the right volume at the end of the window.



The secret is in these three functions.

Code:
int lengthOfTime(int _start, int _end)
{
  //  Calculate the length of time between to time points 
  //  That rollover at m 

  if (_start < _end)
  {
    return (_end - _start);
  }
  else if (_start > _end)
  {
    return ((m - _start) + _end);
  }
  else return (m);
}



boolean isInRange(int _val)
{
  //  Returns true if the time is between start and end times
  //  Handles windows that wrap over midnight
  //  Always returns true if start_time = end_time

  if (start_time < end_time)
  {
    return ((_val >= start_time) && (_val <= end_time));
  }
  else if (start_time > end_time)
  {
    return ((_val >= start_time) || (_val <= end_time));
  }
  else return (1);  // If start = end the always in range
}




int calculateVolume(int _time)
{

  //  Calculates the dose volume needed to best finish the schedule


    int time_remain = lengthOfTime(_time, end_time);

  //  This if handles two different cases of identifying the last dose
  if ((time_remain < interval)  ||  ((start_time == end_time) && (time_remain <= interval)))
  {
    //  If this is the last dose, then dose everything that's left.
    return (total_volume - volume_dosed);
  }
  else
  {
    //  Calculate the volume needed to get there over time    
    return ((total_volume - volume_dosed) / (time_remain / interval));  
  }

}


Pretty simple huh?

Then there's this part. This handles variable resets, and identifies when it's ok to make a dose.

The argument _n would be the current time.

Code:
void checkSched(int _n)
{

  //  This function simulates the call that would check and run the pump
  //  Instead, it prints a simulated dose to the serial monitor
  //  anytime one would be triggered


  
  // This reset function deals with many many possible scenarios
  if ((!(isInRange(_n)))  ||  ((start_time == 0) && (_n < interval) && (last_d > interval))  ||  ((start_time == end_time) && (((_n >= start_time) && ((last_d - start_time) < 0 ))))) 
  {    
    if (volume_dosed != 0)  // if it hasn't already reset
    {
      Serial.println();  // put a break in the schedule
      volume_dosed = 0;  // reset tally 
      reset_flag = true;
    }
    
  }



  //  This is the core logic.  
  if ( (isInRange(_n)) && (lengthOfTime(last_d , _n) >= interval)  && (volume_dosed < total_volume) )
  {
    // make a dose
    
    
    if (reset_flag == true)  //  If this is the first dose of the schedule, then handle the target volume resets
    {
      total_volume = set_volume;
      if(booster_volume > 0) addBooster();
      
      reset_flag = false;
    }
     

      int vol = calculateVolume(_n);  // get the volume


    volume_dosed += vol;  // update the tally
    last_d = _n;   //  adjusts dose time back to correct rounding errors or late doses.

The very next thing would be to submit that volume vol to the function that runs the pump.

Ignore that very last comment, it shouldn't be there. That line just set's last_d = the current time.



Let's look at this line:

if ( (isInRange(_n)) && (lengthOfTime(last_d , _n) >= interval) && (volume_dosed < total_volume) )


That's the heart of the operation. it reads:

if were in the window between start an finish AND it's been long enough since the last dose AND there's still some volume left to dose

THEN and only then make a dose.

Pretty simple, and it can't go wrong. Everything else get's recalculated on the fly. Leading to a completely adaptive dosing schedule.


Note that NONE of the complicated reset checks are needed if you check the schedule once per minute. In that case, you can handle the reset all at once right when the time equals the start_time. But I thought, what if the controller is busy for five or ten minutes and then comes back to the schedule. This way, it can still handle things and the right volume ends up being dosed every single day. That's what I mean by volume is more important than time, knock my times off if you need to, get my volume right.



Load this sketch up on your board and open the serial monitor. The board will reset and you will see a whole bunch of simulated days of dosing. At the beginning of each is a big space and then a list of the variables used for that simulation. Afterwards are 7 days worth of times in minutes-past-midnight format. Each time represents a dose that would be made. It also gives you a volume for the dose, and a running tally of how much has been dosed over each window. Notice that they each end up exactly at either the prescribed dose by the end_time, or that plus some portion of the booster. There is a blank line everywhere the volume reset happens, it marks the begining of each dosing window.
 

Attachments

  • schedule_test_5.zip
    2.7 KB · Views: 4
This is the other bit that makes the booster doses work. Two functions, one to create a booster dose, and another that gets called whenever we reset the total_volume target for the day to see if we should add part of a booster that day.


The first function takes a volume and a number of days over which to break up the booster. In the simulation, it also has to be passed the current time as an argument, but in the real computer it will be able to check its own time. In the simulation, time is local to the listSched function, so I have to keep passing it around as an argument. It's n_count in listSched(), it's _n in checkSched(), it's _time in calculateVolume(), and it's _t here.

Code:
void createBooster(int _vol, int _days, int _t)
{
  //  puts booster amounts into the variables
  //  to let the program know a booster is
  //  requested.  booster_volume serves
  //  as it's own flag


  booster_volume = _vol;
  booster_days = _days;
  
  int day1_vol;

  if (isInRange(_t))
  {
    day1_vol = (booster_volume / booster_days);
    if ((lengthOfTime(start_time, _t)) >= (lengthOfTime(_t, end_time)))
    {
      day1_vol = (day1_vol / 2);
    }

    booster_days -= 1;
    booster_volume -= day1_vol;

    total_volume += day1_vol;
  }

}

This function also checks the current time to see if we are inside of a dosing window. If not, then we will handle the adding the booster to the target when we reset the target with the first dose. But if we are already in the middle of the schedule, we can go ahead and add part of the booster dose to the current schedule. I added a little if statement that figures whether we are more than halfway through the scheduled window and cuts the first day's booster dose in half if so. That avoids a huge shock if you put in a booster late in the day. In the real deal, I might make this a little more complicated and have it acually figure something appropriate. Either way, the rest of the booster gets spread out over the number of days specified.



The next bit here get's called right after we reset the target volume for the day. If there is booster to add, then it will add the right amount to the target volume. This results in every dose being increased by just enough to reach the new target by the end of the window.


Code:
void addBooster()
{
  if (booster_days > 0)  //  That would break things
  {
    int day_vol = booster_volume / booster_days;   // on day = 1 it should do everything left

    total_volume += day_vol;   // add it to the days target, and the calculateVolume function handles the rest
    
    booster_volume -= day_vol;
    booster_days -= 1;
  }
}





The other neat thing about handling things this way is that it protects us from large single doses of anything. Say we're dosing sodium carbonate for alk and we have the pump turned of for several hours and we have a large dose scheduled anyway. We could end up dumping a lot of carbonate into the tank fast and get a pH spike and a big precipitation event.

But in the real computer, it would be no problem to have a check after you calculate the volume but before you make the dose. And if the dose is larger than some preset maximum value, it rounds it down and adds the remainder as a booster onto the next day.






Wow, I already see how to break it. These lines.

booster_volume = _vol;
booster_days = _days;


what if the new booster is set while there are still days left on an old booster. FAIL. The rest of the old one would be lost.

I should use += for the volume.

And I should set the days to the larger of the old or new values.


booster_volume += _vol;
if (_days > booster_days) booster_days = _days;


That would fix it.
 
I added variables to the Dose_Schedule class to keep track of how much you have left in your dosing container. You can input the size, and there are entries on the menu to add to the container, or reset the container to full. It's just a neat option that naturally flowed from the new way of thinking about the doses. Each time we update the tally for a dose, we also take the same amount out of the container.



I'm working on fixing up some of the display functions and running tests on the new UNO board I just got. Also cleaning up and commenting out the code to make it easier to read.




Expect new code soon!!!!
 
I'm waiting for someone to respond to me on the Arduino forum.

I got a minute to explain how the menu works. For those that are new to arduino, this is a very simple example of a sometimes very complex problem.


The goal of this menu function was simple. I want to be able to add to it without having to change any code. Only move or add code.



I don't want it taking a lot of memory, so I started by putting all of the items on the menus into arrays. The enum that goes along with it is basically a way to write a whole bunch of "#define"s in one line. That way I can keep up with names of things instead of having to remember numbers. Also, if I add in a new item to the menu, I can put it in the middle without changing anything. Just add the name of it to the enum in the middle in the same place and I'm good to go.



There's a line missing here, that should say:

typedef char* menu_t;

It allows me to use "menu_t" (or anything else I want) to mean the same thing as char*.


Code:
//  ENTRIES ON BASE LEVEL MENU
menu_t base_level_menu[] = {"Set Time", "Schedule", "Single Dose", "Pump", "Container" , "Run", NULL};
enum { SET_TIME, SCHEDULE, SINGLE_DOSE, PUMP, CONTAINER, RUN};

// ENTRIES ON SCHEDULE MENU
menu_t schedule_menu[] = {"Set Schedule", "Show Schedule" , "List Schedule", "Save Schedule", "Get Saved", "Clear Saved", "Exit", NULL};
enum { SET_SCHEDULE, SHOW_SCHEDULE , LIST_SCHEDULE, SAVE_SCHEDULE, GET_SAVED, CLEAR_SAVED};

// ENTRIES ON PUMP MENU
menu_t pump_menu[] = {"Prime Pump" , "Calibrate Pump" , "Save Calibration" , "Exit" , NULL  };
enum { PRIME_PUMP , CALIBRATE_PUMP , SAVE_CALIBRATION};

// ENTRIES ON CONTAINER MENU
menu_t container_menu[] = {"Reset Cont" , "Add to Cont" , "Set Cont Size" , "Exit" , NULL};
enum { RESET_CONTAINER , ADD_TO_CONTAINER , SET_CONTAINER_SIZE};


Notice that each list ends in NULL. That's so we can find the end. Also notice that Exit never shows up in an enum statement. That's because Exit might be a different number in two different menus. In fact it is. So we handle Exit a little differently.




Then names for the menus themselves, and pointers in the same order that point to the lists we just made. Now we can use a single name to refer to the whole menu, and a different set of names for the menu items. And all of the names in capital letters can be used as indexes for the arrays to get the thing they name.

Code:
//  THE MENU NAMES
enum {BASE_MENU, SCHEDULE_MENU, PUMP_MENU, CONTAINER_MENU};
menu_t* MENUS[] = {base_level_menu, schedule_menu, pump_menu, container_menu};



Pretty self explanatory how that works. You can already see how the menu will be laid out to the user.



Next is the complicated part. What to do when you choose something on the menu. In order to keep all that simple stuff up there, we're going to need something elegant. While we scroll through the menu, we have to use a variable to hold the index of the current item on the menu. We need a function that we can pass that number to and it can figure out what to do. Each menu will need a different function, because they all have an entry 0, 1, 2, ... So we need to know which menu called the function, and that's easiest to do if each menu has its own associated branch function.


So next we set up an array of pointers to the branch functions. Yes you can have a pointer to a function. I'll start the name of each with the word branch to make them easier to keep up with.

Code:
//  THE BRANCH FUNCTIONS    MUST MATCH THE ORDER OF THE MENU NAMES ABOVE  ONE BRANCH PER MENU
boolean (*BRANCHES[])(int) = {&branch_Base, &branch_Schedule, &branch_Pump, &branch_Container};

The way the pointers are set up, we have to make the branch function take exactly one int argument, and it must return a boolean. That is on purpose, I could have done it any wya I wanted, but you'll see in a minute where I was going with the boolean.




Next we have the branch functions. One per menu. There are two examples. A menu that returns you to the menu one level up after you make a selection, or another type that always returns you back into the same menu after you handle the selected thing and therefore needs an exit choice.


This first example is the main menu. It always returns to itself unless you select "Run" or "Single Dose". Run will dump you straight out of the menu and return you to the main program. Single Dose will run a single dose of the pump and then put you back to the main program.


Each case breaks so that it runs into the return false at the end.
Any case that you want to exit should say return true.

Code:
boolean branch_Base(int _item)
{
    switch (_item)
    {
      case SET_TIME:
      {
        timeSetup();
        break;
      }
      
      case SCHEDULE:
      {
        menu(SCHEDULE_MENU);
        break;
      }
      
      case SINGLE_DOSE:
      {
        singleDose((chooseSchedule()).pump);
        return true;   // Escapes out of the menu
      }
      
      case PUMP:
      {
        menu(PUMP_MENU);
      }
      
      case CONTAINER:
      {
        menu(CONTAINER_MENU);
      }
      
      case RUN:
      {
        return true;   // Escape choice
      }
      
    }
    
    return false;   // Everything else breaks and returns to the menu
}


Notice how true and false are used to determine where we go after the menu.


In the next example, the schedule menu, every item returns to the schedule menu after it runs whatever it has to do. So we need an Exit choice. And it has to be at the end of the list. We will return false in each case instead of break. Serves the same purpose, but it allows us to put return true at the end, which catches anything past the last thing in the enum. In our case, that is "Exit".


Most all of the menus after the main menu will use this form. Other cases can return true if you want them to bounce you up one level on the menu (ie back to the main menu in this case, since the branch_Base is the one that called this one.)

Code:
boolean branch_Schedule(int _item)
{
    switch (_item)
    {
      case SET_SCHEDULE:
      {
        (chooseSchedule()).inputSchedule();
        return false;
      }
      
      case SHOW_SCHEDULE:
      {
        (chooseSchedule()).printSchedule();
        return false;
      }
      
      case LIST_SCHEDULE:
      {
        (chooseSchedule()).listSchedule();
        return false;
      }
      
      case SAVE_SCHEDULE:
      { 
        Alk_Schedule.saveSchedule(EA_ALK_SCHEDULE);
        Ca_Schedule.saveSchedule(EA_CA_SCHEDULE);
        return false;
      }
      
      case GET_SAVED:
      {
        Alk_Schedule.getSchedule(EA_ALK_SCHEDULE);
        Ca_Schedule.getSchedule(EA_CA_SCHEDULE);
        return false;
      }
      
      case CLEAR_SAVED:
      {
        byte _flag = 0; 
        writeToEEPROM(EA_ALK_SCHEDULE +10, _flag);
        writeToEEPROM(EA_CA_SCHEDULE +10, _flag);
        return false;
      }
      
    }
    
    return true;   // This would be the choice "Exit"
}

In this one, there are no breaks. Each case returns you where you want to go. If you don't choose one of the cases (choose "Exit") then you fall all the way through to the return true. If you forget to put "Exit" in your list of items, you end up trapped in this menu.




The last bit we need is a function to run through the choices and let us pick one. Everything else is already done. All we need to do is list it on the screen and rotate through.

And to add a new item, we only need to add it to one of the lists and add a case for it in the branch function. To add a whole new menu, we need only write a list of items, and name them with another enum. Then we add a name for the menu and write a branch for it. The only change we need to make is to have one of the other menus have an option that sends you to the new menu. And adding options is easy as the first sentence of this paragraph.


Here's the bit that scrolls through and selects. When you press a button in the main loop of the dosing computer, it calls menu(0) to get the base level or main menu.

Code:
void menu(int _menu)
{
  
  //  This is the main menu function.  It handles scrolling and button press for choice
  //  and runs the branch associated with the appropriate menu
  
    int current_item = 0;
    encoderOn();
    
    do
    {
      useRotaryEncodersingle(current_item);
      
      if (current_item < 0)   //  If we're rolling over the menu backwards
      {
        for (int i=0; ;i++)    //  Will take us to the last item in menu
        {
          if (MENUS[_menu][i] == NULL)
          {
            current_item = i-1;
            break;
          }
          else continue;
        }
      }
      
      
      if (MENUS[_menu][current_item]== NULL) current_item = 0;  // Handle rolling over the menu forwards
            
      LCD.clear();
      LCD.print("->");   // A little arrow to let us know what we're picking will always be top line of display
      LCD.print(MENUS[_menu][current_item]);   //  Print the menu item to the screen
      LCD.setCursor(0,1);  
      LCD.print("  ");     
      
      //  Print the next choice under it
      if (MENUS[_menu][current_item +1] == NULL) LCD.print(MENUS[_menu][0]);  // only have to handle forward rollover
      else LCD.print(MENUS[_menu][current_item + 1]);
      
      delay(250);  // display delay
      
    } while (button1 == HIGH);   // keep scrolling through menu until button press
    
    while (button1 == LOW);   // wait for button release
    
    encoderOff();
    
    if (!((*BRANCHES[_menu])(current_item))) menu(_menu);  // return false(in branch) to run this menu again
    else return;  // return true(in branch) to escape this menu to the one that called it
    //  YOU MUST RETURN TRUE IN AT LEAST ONE CASE OF THE BRANCH
    //  if a menu must return to itself in every case, include an exit option
    //  on the menu (BUT DO NOT put it in the Enum) and put a return true at the 
    //  very end of the branch.  (See branch_Schedule) 
            
}

NOTE my buttons are active low. That means they are LOW when you press, and HIGH when not pressed.


The encoder functions just turn the interrupt on and off and the useRotaryEncoder function updates current_item to reflect any clicks since the last loop through.

And that's it. You need some way to call menu(0) from somewhere in your code, and the function above handles everything else.

Notice that this function calls itself.

if (!((*BRANCHES[_menu])(current_item))) menu(_menu);

That's a recursive call. Kinda.

Since we call menu from a branch to go to deeper levels, we need to keep things short and sweet or we'll run out of memory fast. We're stuck in the if statement at the end of every menu level above the one we're on. I haven't gone past three leels with this on an arduino, I don't really know that I would.

For a larger menu, we would do the same thing, but we would let each menu exit and keep track of things with global variables and flags.




There's a little more on that tab, but it's just functions to choose schedules or pumps or containers. If you're interested in digging further, the new and MUCH improved code will be up real soon.
 
Last edited:
For example, I just added the booster dose to the menu with three small changes.

I changed the menu to this.

Code:
//  ENTRIES ON BASE LEVEL MENU
menu_t base_level_menu[] = {"Set Time", "Schedule", "Single Dose", "Booster" , "Pump", "Container" , "Run", NULL};
enum { SET_TIME, SCHEDULE, SINGLE_DOSE, BOOSTER, PUMP, CONTAINER, RUN};

See what I did there? Just stuck it right in.


And then I added one new case to the branch_Base(int item) function.

Code:
case BOOSTER:
      {
        chooseSchedule().setBooster();
        break;
      }


Done and done. Added to the menu. Nothing to hunt down, no variables to change, nothing to sweat but writing the functions to actually do the stuff.
 
Well Here it is. The new Disco Doser Code.

Give me a few days to get it installed and looking pretty and I'll put up schematics. They have major changes.

A big thanks to everyone that has had input on this thread


I am much much happier with what I have now. There are still a few minor tweaks I would like to make ( A warning when a container is running low) but I really want to get my tank back to an automated state so I'll finish up later.





There are two zip files. One contains the entire program with headers, and the other contains some libraries that are used by the program that are not included with the normal Arduino software.

YOU NEED BOTH zip files. DOSING_COMPUTER_6.zip goes in your sketch folder and extracts a folder containing the program and headers. DOSING_COMPUTER_LIBRARIES.zip goes into your libraries folder and extracts four folders.

DosingPump is just a small class that holds all the parameters for the pump and has the functions that convert volumes and rates into PWM rates and time.

LiquidCrystal_SPI_8Bit is a slightly re-written copy of the normal LiquidCrystal library included with Arduino. The only change in this version is the write function which uses SPI instead of seperate pins on the Arduino. If you have your LCD connected the old fashioned way, you can replace this include with the standard LiquidCrystal library and change the declaration of LCD (single line of code in DOSE_head.h) in the program and everything should work fine.

QuickPin is my own deal. That allows me to use operators to program with pins and also gives me the pin reading speed needed to handle things like rotary encoders. Regular old digitalRead just isn't fast enough to catch the pulse train. It has a big read me file and it has it's own thread here on RC.

This time the code was written with other people reading it in mind. It should be much easier to follow. It also broken up a little better, in case one needs to borrow one part or another for use in a different project.

In order to make the thing Arduino 2.2 compatable, I had to move everything off into header files. These have to stay in the folder with the main program and if you open the main program in the Arduino software, you should see the headers on the other tabs. There are instructions in the read me file on how to copy and paste the whole thing into one long program so you can read through it in a linear fashion if you want to, but that will not compile with Arduino 2.2.

So far the only known bug is in the way the year is displayed while setting the time. It's just not worth fixing right now. I never set the date on it anyway.


Below is the text of the Read Me 2 file that explains how to use the doser. In the near future we can talk about how any particular features work. Please ask any questions you have about the code.

















*******************************************************************************************************************

****************************************** FEATURES ****************************************************************

*******************************************************************************************************************




This application includes a few new features. The dosing is controlled by s very simple logic that ensures the entire volume will be dosed each day and also ensures that dosing intervals are respected. This is accomplished by allowing the schedule to be adaptive. With each dose, the computer will calculate the time left in the window and the volume left to dose and will break the volume up to fit the rest of the window. If a dose is missed, this results in a small increase in each subsequent dose in the window to make up for it. The last dose of the window will always complete the dosing target. It will also adapt for time. If a dose is late, each subsequent dose will also be late so that the interval is respected. When using 24 hour dosing (start time = end time) this offset will continue into the next day. If the dosing window is less than 24 hours, then the offset will not carry over and the computer will remove the last dose time of the current window if needed to maintain the interval. No volume will be lost by this process since the volume is also adaptive.


Another new feature is the container. A container is associated with each schedule. This represents the bottle youare dosing from. Set the size and the volume in it, and the computer will subtract from this any time it makes a dose. Future versions will allow for warnings when you are running out of suppliment. If you choose not to use the containers, it will not affect the performance in any way.


The last new feature is booster dosing. With conventional dosing, when a level gets low due to a water change or running out of suppliment for a week or whatever reason, we will usually add some amount of suppliment to boost the level back up to where it should be. This is in addition to the amount we normally dose just to maintain levels. But this has to be done by hand.

With booster dosing, you can set a volume and number of days and the program will add the additional amount neccessary each day until it is all dosed. For example, I dose 60mL per day into a tank. Let's say something happens to the tank and the alkalinity is low by 2dkH. I need to add 2dkH, and I don't want to raise by more than 0.5dkH per day to avoid shocking things. Assume that it takes 100ml of alkalinity suppliment to raise the level by 1dkH. So 200mL needs to be added over four days in addition to the 60mL per day that is normally dosed. Simply enter a booster dose of 200mL and 4 days and the computer will add an additional 50mL each day for four days for a daily total of 110ml per day during the boost.




*******************************************************************************************************************

****************************************** SCHEDULE RESETS ******************************************************

*******************************************************************************************************************


There are several conditions that will require the schedule to be reset. Due to the way the schedule keeps track of volume and time, setting the time or entering a new schedule could cause unexpected results for the first dose if things aren't reset.

When the schedule is reset, the program first looks to see if the current time is within the dosing window. If not, then it simply sets all everything to where it would be had the previous window been run using the new settings. The time of the last dose will be the end time of the new window. This causes few problems. Resetting the schedule while outside the dosing window is the safest option.

If the current time is inside the dosing window, then the computer will calculate how many doses would have been made so far if the current window had been run using the new settings. First it calculates how many intervals would have passed, and sets the time of the last dose to the time that the last dose would have occurred. It then sets the running voume tally for the day to reflect the doses that would have been made so far. This means that the volume total may not accurately reflect the actual volume dosed on the day that the schedule is reset. It also means that any booster doses that were included with the day's target will be lost, but subsequent days will not.

Setting a new schedule or getting a schedule from EEPROM will always cause a schedule reset.

When setting the time, you will be given the option to reset the schedules. It is recommended, but may not be necessary in all situations.









*******************************************************************************************************************

****************************************** MENUS ****************************************************************

*******************************************************************************************************************



When you first fire up the dosing computer, you see the word Starting for 2 seconds. It will then check a byte in EEPROM to see if flags are set indicating a schedule is saved in EEPROM. If so, it will download it.

Press the button and you get the top level menu. The choices are:

Set Time , Schedule , Single Dose , Booster , Pump , Container , Run

Set Time has the only known bug. It has to do with how the year gets displayed while you set the time. I haven't been setting the date and that one sneaked through and just wasn't worth going back in to fix.

Set each number by turning the knob and lock in your choice with a button press. Once you set the seconds, you will be asked whether or not to reset the schedules. If you don't reset, then the schedules will think that they have been turned off for the whole time missed between the old time and the new time you set. For small forward changes this can be handy, but since time only goes forward to the schedules, if you set the clock back the schedules will think they missed almost a whole day and dump in doses to catch up. This is handy if it's what you intend to do, but if not resetting the schedules will simulate the conditions that would be present at the new time if the pumps had been running all along. It will cause you to lose the days booster volume, so go easy on time resets if you are using boosters. I'm not using a RTC of any kind, and I allow the time to wander by a few seconds per day. It's not really a big deal, but I definitely don't look at the dosing computer to get the real world time. Every couple of months I think I will do a hard reset of the Arduino to avoid any millis() rollover issues that may crop up.

Booster allows you to set a booster dose. First is will ask you which schedule to boost. Then it asks for the total volume of the booster dose. Then it asks for the number of days over which to break up the booster. The program will look at the current time and determine if it is within the dosing window. If so, it will calculate an appropriate volume to add to the current days schedule and that will count as the first day of boost. If the current time is outside the dosing window, or is too close to the end of the dosing window, then the program will simply hold the volume and number of days and will add the appropriate amount to the target volume each day to finish the booster in the given number of days.

Schedule , Pump , and Container take you to other menus. Run exits the menu and Single Dose allows you to choose a pump and will ask you for a volume and rate. It will then run the pump for that volume and then exits the menu.


The Schedule menu has the following options:

Set Schedule , Adjust Volume , Show Schedule , List Schedule , Match Schedule , Save Schedule , Get Saved , Clear Saved , Exit

Set Schedule allows you to set the dosing window. If you set the start time = to the end time, then dosing will run continuously. You can use the start times to stagger the dosing of alkalinity and calcium. If you set the end time at least one interval short of the start time, then dosing will run and complete within the window every day. From here you also set the interval (in Hours : Minutes format) between consecutive doses of the same suppliment. It is recommended to set the intervals the same for alkalinity and calcium and set the start time one half interval apart. This will stagger the doses to avoid dosing them together and causing a precipitation. The Match Schedule option will do this for you, setting the calcium schedule to match the alkalinity schedule with the appropriate stagger.

After the schedule is set by the user, it will immediately be reset. This means any booster volume that day will be lost, but subsequent days will still get their boosters. The reset will simulate conditions that would be present had the computer been running on the new schedule in the past. The running tally for the day will reflect that simulation and may not reflect the actual amounts dosed. The container volumes will not be affected by this discrepancy.

Finally you will set the volume and rate for the doses. Rate is never matched between schedules. You can always have two different rates if you want.

Adjust Volume does exactly what it says. It allows you to adjust the dose for a either schedule.

Show Schedule displays the parameters set for a schedule.

List Schedule will simulate dosing and print the times, volumes, and a daily running tally to the screen. It will ask for a number of days to run the schedule. The first day will start at the current time and will include any boosters present at the time. The rest of the days will run the entire window and will not include any booster volumes that may be set. At the end of each simulated day, the running tally will be shown with the target volume. They should match.

Match Schedule will set the calcium schedule off the alkalinity schedule. The alkalinity schedule is always the master. The intervals will be set to match and the start and end times will be staggered. You will be given the option of matching the volumes.

Save Schedule , Get Saved , Clear Saved all deal with schedules saved in EEPROM. Get saved will reset the schedule once the new schedule is loaded. If this happens during the dosing window, any booster dose for the day may be lost and the days volume dosed tally may not reflect the actual volume dosed.

Exit returns you to the main menu.



The Pump menu has the following options:

Prime Pump , Calibrate Pump , Save Calibration , Exit

Prime Pump allows you to use prime either of the dosing pumps. First you are asked which pump to prime. Then the word PRIME or QUIT is displayed. Pressing the button while PRIME is displayed will run the pump at full speed until you release the button. QUIT exits to the main menu.

Calibrate Pump will start the calibration sequence to set the minimum PWM rate, and the minumum and maximum flow rates. You will first set the minimum PWM rate. The pump will run at a low speed. Use the knob to adjust the rate until you find the slowest speed at which the pump will reliably run. If you do not want to use PWM, set this to full speed.

You will then set the minimum and maximum flow rates. The procedure is the same for both. The screen will promt you for a button press to start. Put the output tube from the pump into a graduated cylinder or other container and press the button. The pump will run for 30 seconds. When it stops, you will be asked to input the volume that was produced.

Exit returns you to the main menu.



The Container menu has the following options:

Reset Container , Add to Cont , Set Cont Vol , Set Cont Size , Exit

With any of these choices, you will be asked to choose a container to act on. The container choice will list both the name of the schedule it is associated with and the current volume and container size. There is also an exit option, so you can use any of these to check up on the current volume in your containers.

Reset Container will reset the volume to the size. Use this when you refil your container.

Add to Cont allows you to add to the current volume. Use this when you add suppliment, but don't fill the container completely.

Set Cont Vol and Set Cont Size allow you to set the size of the container (the total volume you want to use it for) and the volume currently in the container.


Exit returns you to the main menu.


*****************************************************************************************************************************




Hope you find this code helpful. Please feel free to take it apart and do whatever you want with it. I only ask that you let me know about any improvements that you make that I might enjoy.







:)
 

Attachments

  • DOSING_COMPUTER_6.zip
    19.5 KB · Views: 4
  • DOSING_COMPUTER_LIBRARIES.zip
    44.5 KB · Views: 4
I have not looked at your code, but the possible problem I see is with the "match schedule" and "boost".

If you set ALK and CA to dose opposite but boost one, then you may end up with overlap unless each interval is calculated on the fly and in mind of the associated interval for the other mathcing fluid....

I struggled with this on my dosing project. While the on-the-fly scheduleing is easy using a 3g language and cpu like vb.net, it takes up a LOT of CPU in the uC. I found it much easier to cut an hour into time slices for products that can not be dosed together. In that way on-the-fly calcs are limited, even when the volumes don't match. The number of slices per hour is calculated to meet the needs of the product that needs the most time to dose. Once that is done, then the second product will fit in between without overlap, as it takes less time total but 50% of the time is left. Confused? I am after reading what I just wrote :)

Not sure how you tackled the problem, but I get a headache reading C :)
 
I have not looked at your code, but the possible problem I see is with the "match schedule" and "boost".

If you set ALK and CA to dose opposite but boost one, then you may end up with overlap unless each interval is calculated on the fly and in mind of the associated interval for the other mathcing fluid....


That gets taken care of by, and is the reason for, the adaptive volume component. The times and intervals will never change, so there can never be any overlap. The start times need to be staggered if the doses are to be staggered, and that's what Match does.

There is also a boolean in the main sketch called lock_out that is set to true. It would have to be hard coded to false, there is no option to set it. lock_out enforces a rule that each calcium dose must be at least half an interval away from an alkalinity dose and vice versa. So if one gets off schedule, it will take the other off schedule to maintain their distance. The volume would be adapted to make up for the missed dose in that scenario.


In the case that the dose gets so large that one pump actually runs long enough to put the other schedule off, the volume calculation will again catch the problem and fix it.


I tested things by setting the timeOfDay function so that the computer would run at 1 second = 1 minute. Then in one test I created a loop that would tie the processor up and cause every other dose to be missed. In that scenario, the first dose of each suppliment was normal, and each subsequent dose increased in volume each time to make up for the doses being missed. This results in the majority of the dose being delivered towards the end of the window, but it is also a condition that shouldn't be seen in the real world.



I'll do a more detailed post about how the adaptive volume works. It's basically just a whole bunch of taking advantage of the limitations of int math.
 
Another test condition was to set the calcium and alkalinity intervals to numbers that were not going to work out with the lock_out thing. Not multiples of the window, nor each other.

In this case, lock_out was enforcing the minimum distance. This caused the schedule with the shorter interval to progressively miss more and more doses as the day went on. BUT, the volume of the doses slowly increased over the course of the day to eventually catch back up with the target volume.




That's the new paradigm. I don't care one little bit about time, so long as my intervals are respected. And I don't really even care about the dose being broken up exactly evenly. What I care about is the volume at the end of the day. And that is guaranteed to be right. By a simple check. If there is less than an interval left in the day, then this is the last dose that will fit in the window, so dump in everything you got left.

In the near future, I'll give it a maximum size for a single dose and let it add anything over that to a booster to go on the next day. But that might get complicated and my alkalinity is running low because I myself am not a very reliable doser.
 
That is basically the way i started (and ended up on the first software version) but felt that the logic was a bit too complex to be comfortable with.

In the current build, I can choose the number of intervals per fluid and different volumes and windows for each fluid. There is no need for the user to choose (or consider) the "offset" as each pump can only run in its own time slices.

The user can choose to spread the doses out over the window or weight them to one end, or the other by choosing the number of intervals (MAX, MIN, or anything in between).

I don't think my current method is better by any means, It just felt easier to code and debug.

One concern when I was doing adaptive logic, was that if doses grew to large per internval, precipitation or PH problems could arise.
 
Last edited:
Touchscreen? Cool. Not even ready to go there yet.

Here's the schematic I worked up to connect this thing up. It's kinda crazy with lots of lines crossing. Every way I arranged it, same problem. So I suggest downloading the pic and looking at it with paint or something where you can zoom it in and make it bigger. It's really high resolution to begin with.


I'm open to any suggestions. I'm not the greatest at sizing components. I already know that I need to switch the 1K resistors for 220 ohm since I made the transistor change.

I'm using 12K pull-up resistors for the button and the encoder because I have a big box full of about a million of them. You can choose any size you like, I think 10K is the norm.

I'm using the 2N3904 NPN to drive a IRF510 N-channel enhanced MOSFET to turn the punps on. The IRF510 takes less than 5V G-S to turn fully on, so I'm driving the 2N3904 from the 5V rail.

There is a 100uF cap smoothing the 12V and a 10uF cap on the 5V. Both are electolytic. The 10nF and 100nF caps are the regular little ceramic ones. The 100nF value on the encoder came from much experimentation with that particular encoder and a scope. Your encoder may behave differently. The 10nF is just a standard sort of value for smoothing power to an IC.



The LCD in the picture is NOT the one I used. It was just the closest match I could find on my Eagle software. The last two connections that say NC are really the ground and power for the LED backlight. 15 ground and 16 is 5V through a 220 ohm resistor.

There's an L7805 linear regulator to get 5V to the 5V components. And I should probably add a cap to the point where the Arduino power comes out.

The shift register takes SPI and turns it into parallel for the LCD screen. It is a standard 74HC595N (NPX I think it came from www.sparkfun.com).

The LCD is this one from Mouser.

The rotary encoder / push button is COM-09117 from sparkfun.com.




All in all, much simpler this time.
 

Attachments

  • bb_img_w_bjt.jpg
    bb_img_w_bjt.jpg
    53.7 KB · Views: 4
Since I have 12V and 5V both available to me, which do you think I should feed to Arduino? Should I give it 12V at Vin and let the onboard regulator do it's thing? Or should I feed it 5V from the L7805 to the 5V pin?

If I feed it 12V, can I ditch the L7805 and just use the Arduino 5V pin as the source for my 5V?
 
Never looked at the arduino boards to see what the PSU section looks like. A 7805 regulator has to dissipate the voltage drop as heat so it should be well heatsinked if you feed it with 12V or more. You can likely bypass the onboard regulator if you wish.

My original design used hall sensors to count revolutions, but I found it to be somehat pointless. My second design was going to use steppers to count doses, but I found that to be too much trouble and have since gone back to a simple ml/sec calibration and timed intervals. No PWM or step counts, etc.

As I mentioned I started with adaptive dosing and all sorts of "options" for splitting the dose up throughout the dosing window. I fiddled with code to create parallel and sequential doses, etc. In the end I realized I was being somewhat silly and creating options that would NEVER be used.

The current (and last) revision is much more simple:

The doser is setup to handle (6) channels. An HOUR is divided into (48) time slices that are 75 seconds each (1.25 minutes). So each pump can utilize up to 6 time slices per hour.

Because each channel has its own time slice, no intervals will ever overlap.

The only user choices are the volume, window lenght and start time, and the NUMBER of intervals to use during the window. Each user choice is constrained to only values that will work. The maximum dosing window for ANY option is 22 hours. This allows appended doses and schedule holds (feeding, maintenance) to push into those two hours.

I built a simple spreadsheet to build and display the logic and smoke test it. I am MUCH happier with this new approach!
 
So, this thread just popped up on the Arduino forum.

http://arduino.cc/forum/index.php?PHPSESSID=90d6f699fa1a444a1187b77682455f36&topic=72000.0

After reading it, and seeing as I am in the middle of this same issue, I went back to the data sheet to see what he was talking about.

http://www.irf.com/product-info/datasheets/data/irf510.pdf


I think I am going to hook those collector pins on the 2N3904 's to 12V+ instead of 5V+.



Minor change in plans. Going to put 12V+ on the collector of the two 2N3904 's and deliver 12V to the gate of the IRF510 instead of 5V.
 
Here's a new picture with the MOSFETs hooked up right and a zip with the Eagle file.

I'm pretty new at this Eagle thing, so if I did something wrong, let me know. I want it to be readable.

I'd like to get a PCB design going with just a ATMEGA328p and the other components. There's a new service going through sparkfun, you can get boards made for dirt dirt cheap if you don't mind waiting for a while to get them. Pay by the square inch. So if anyone knows what they're doing with this Eagle thing and can help me out, I'd be much appreciative.
 

Attachments

  • bb_img_w_bjt.jpg
    bb_img_w_bjt.jpg
    56.8 KB · Views: 4
  • DOSING_COMPUTER_EAGLE.zip
    17 KB · Views: 4
Word of note and warning. That L7805 that steps the 12V down to 5V absolutely positively has to have a heat sink. It will get hot enough to burn you and then the whole thing quits. You can't even fire it up for a few minutes to check the circuit.
 
OK we have a major problem. With those 2N3904 NPN transistors in there, once the pumps turn on they won't turn off. Even though the signal from the arduino goes to 0V. When I toke the 3904's out and ran the Arduino signal directly through the 220 ohm resistor to the gate of the IRF510 MOSFET, then things work as they should.

What am I missing here???? Why does the transistor not want to turn back off?
 
Back
Top