PDA

View Full Version : Disc1 Arduino Doser


disc1
05/05/2011, 07:22 PM
I promised over on the equipment forum to post this. This is a project still in development, so it looks ugly. But I think you can see how to utilize these pumps to build something cheap for yourself. I'll try to come back and post updates as I finish it.

I bought these cheap peristaltic pumps off e-bay for about $30 each. I wanted to use them to dose 2-part. To my surprise I found that they were quite precise. So I built a little Arduino sketch to run it.

These are the pumps that I got.

http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30459

These pumps run from about 6V to 12V dc. The flow rate of the two pumps did not match, one pumps 26ml/min at 12V and the other pumps 40ml/min. I think the slow one is defective, but there's really no sending it back and even though it is slow it is consistent. I use it for alk since I like to drip that slower anyway.

The flow rate is nice and linear with voltage, being about half at 6V. So I decided to use a MOSFET (RadioShack IRF510 was what I had on hand.) and PWM signal from the arduino. Power goes from positive to the pump, then from the pump to the drain pin, and then from the source pin to ground. The PWM signal goes to the gate pin. Since the pump doesn't run below 6V, use PWM values from 127 - 255 in the Arduino sketch.

Here is the full set-up...

http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30460

And a close-up on the breadboard part.

http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30463
http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30464

The knob and the two ic chips are part of a rotary encoder that was put together for an entirely different project and ended up getting absorbed into this when I got tired of pushing buttons over and over. It's a horribly inefficient use of a rotary encoder using two 555 timers and an 8-bit shift register, but it worked well for what it was originally built. When I build a permanent version to put on a board I will trim this whole part down. Besides, it doesn't work very well.

Aside from that there are two pushbuttons, both of which are connected to arduino, but only one is used. It is used to select menu items or values. The knob allows you to scroll through the menus and adjust values. There are a couple of capacitors to keep the power smooth and two diodes running backwards towards positive from the drain on the MOSFET's to handle any kick-back from the pumps. Without these two diodes, you arduino will reset everytime the pump runs.

Once I had the program working, I wanted to make it pretty so I could put it in a box and use it on my tank. So I added the LCD screen I had laying around and added some functionality to the program. There are options to calibrate the pumps, prime the pumps, and build a dosing schedule. The schedule is stored in a seperate class that has arrays full of times and volumes. The flow rate is the same for all doses. There is currently room for two schedules, Alk and Cal, but I am going to trim the program down a bit once I am satisfied and probably add functionality for more pumps. It also saves the schedule and the calibration in the EEPROM so you don't lose it on a reset.

Right now the schedule is built by selecting a start time, end time, total volume, and max volume per dose. Right now I dose 60ml per day of the bicarbonate recipe so I do 5ml every two hours and I do the calcium doses on the odd hours. It runs the schedule every day and starts over at midnight. There is absolutely no error checking to see if the schedule will fit or to make sure that two additions don't coincide. I want to add that, but as of right now you have to calculate things before you put it in.

That pretty much does it. If I can find somewhere to host the code I will post it. It uses several of my home-made libraries. I'll package them up and post them too if anybody wants it. I'm sure a real programmer would have a field day with my code, but it works and that's good enough for me. I'm not claiming any real skill here. Just didn't want to pay big bucks for a dosing pump.

disc1
05/05/2011, 07:28 PM
Oh and here's a couple pics of my 75gallon.

http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30457http://www.reefcentral.com/forums/picture.php?albumid=4418&pictureid=30458

shifty51008
05/06/2011, 06:27 AM
very nice job. I would be intrested in seeing the code and your lib.s:D

BeanAnimal
05/06/2011, 06:49 AM
Nice work :)

I built a fairly complex dosing controller using an Atmega, but the code is in BASCOM basic. Oddly, I used a grayhill rotary encoder for menu navigation.

In any case, the code allows for dosing windows to be set and the dose to be metered in many modes (all at once, in even increments, etc.). External input can also "hold" or "push" or "cancel" the current dose (for example if the system is in maintenance or feeding mode).

With all of the error checking and functionality to split the dose and fit it into the dosing window, calibration functions and other features, the code got very bulky. While I am pleased with the outcome, it was extreme overkill in the sense that I only use the basic function of dosing a fixed amount per day over a fixed time window :)

Nothing like beating a horse with a microcontroller when all you needed was a carrot to get the job done :)

disc1
05/06/2011, 02:54 PM
Nothing like beating a horse with a microcontroller when all you needed was a carrot to get the job done :)


Amen to that.

disc1
05/08/2011, 01:38 PM
Let's see if I can just upload the code here. You will need to move the libraries into you library folder. You will also need the latest version of the Time library from the arduino site.

For you programmers out there, take a look at my QuickPin library and tell me what you think. It make my life simpler in programming, but it's relatively new so I haven't really had a chance to run into any major problems with it. It's sort-of documented. You should be able to figure it out from the header.

disc1
05/08/2011, 05:24 PM
And here's a basic diagram of what's going on on that bread board. I've got it in an Eagle file if anyone wants it.

nrbelk
06/07/2011, 01:36 PM
How complicated is it to learn how to use something like arduino? I have basic coding knowledge and have done some soldering and working briefly with a bread board.

Would this be something I would be able to pick up quickly or is it a complicated process?

disc1
06/07/2011, 02:03 PM
The coding is as complicated as you want to make it. I learn computer languages fast, it took me a week or two with an old text book to get moving in C++. That's all the arduino language is, a fancied up C++. But for someone who's not familiar with programming, it has pretty much all the commands you need to write a basic program. If you understand loops and if/then type logic you can pick it up pretty quick. If you understand how to use pointers and arrays and referencing, and more hardcore C++ stuff, then you can get real dangerous. It all works. Except the feakin new operator. But somebody wrote one, it's actually commented out on one of the pages of code with this thread.

Go on the Arduino website and look at the reference section and the playground. If you get what's going on there, then Arduino will work for you. Go put C++ tutorials into google and check out some of that. If you get what's going on there then Arduino will be a breeze to master and you can do some really neat stuff with it.

nrbelk
06/07/2011, 02:27 PM
I did three college classes in C++, I know arrays, loops, if/then, but pointers and references I haven't had to ever use even though we were taught about them. Currently I write little scripts in perl to automate some menial tasks at work every once in a while. With this background, would you feel comfortable telling me to go for it?

I looked up arduino boards and I saw that there are various types/models. Which one is a common one to get for this type of project?

disc1
06/07/2011, 02:42 PM
Oh yeah. You should be good to go, ahead of the curve even. Anything you don't know how to do google it. It's just the nature of the web to have great info on writing code. Go figure.

There are basically two types of arduino to start out with. The others are variations on the theme. There's the ATmega328 chip like in the UNO. That's what the one in this thread is, an UNO. There's also the ATmega2560 which has more inputs / ouputs and more memory. That's the Arduino Mega. It's more the scale for like a full on controller for the whole tank. Then there's all sorts of versions that are flexible, or have built in blue-tooth, or are the size of a button, whatever you can imagine.

Start out with an UNO or a mega (or one of the clones) since they are pretty much plug and play. Get a kit or something with some little leds and caps and diodes and resistors and stuff to play with. Spend a few days just writing programs around an led, make it come on, make it blink, make it fade, fade based on a pot. By the time you have written a few of those, you will know what's up.

nrbelk
06/07/2011, 02:46 PM
Thanks! This is great, I'm getting way excited!

To do all that, do I need a bread board?

Also, with what you said about the differences of the two, uno and mega, I think I will go with mega so that perhaps I can eventually make a full on aquarium controller. When I get proficient that is :)

disc1
06/07/2011, 02:53 PM
It will make it a whole lot easier to build circuits. When things are trial and error, you don't want to have to unsolder things to hook it up a different way.

Check out this site. www.sparkfun.com
I don't know if it is the absolute best prices, but it's reasonable and it will have just about everything you need to get started. digikey.com and mouser.com are two more that I use a lot, but they are more the type of thing where you need to know what you are looking for. Sparkfun has all sorts of good tutorials and how-to's on all of their products. I can get lost for days in there.

nrbelk
06/07/2011, 04:12 PM
thanks for the site! I found kits that come with mini breadboards, I think it would be perfect for me at this time!

Ok, I think I have one last question before I pull the trigger and buy a mega instead of an uno. Would you be able to do things like dim LEDs? I'm guessing that turning high power LEDs on and off would be similar to the pumps, but what about dimming them? and how do I find how many inputs/outputs the arduino has? Also, are there ways to add more inputs/outputs?

Ok, I guess it was more than one question..... sorry.. but now I'm so excited! :)

disc1
06/07/2011, 10:22 PM
To dim LEDs with an arduino, you are going to be using pulse width modulation to control your LED driver. It has to be a dimmable driver that works with pwm. The ATmega328 boards have 14 IO lines with 6 that can be pwm outputs. The mega has 54 IO lines, 14 of them with pwm. There are other chips you can get that can extend the IO capabilities of either board.

http://arduino.cc/en/Main/Hardware
Go to that site and look all over it. At the top of the page getting started, reference, learning. And check out at the very very top of the page, the playground.

You feed the pwm signal to the driver on it's dimmer line and viola, all you gotta do is write code. Well there's a little more to it, but nothing you can't do. There are TONS of arduino LED threads on here. And lots of ways to get the same thing done.

nrbelk
06/08/2011, 10:05 AM
Thanks! I bought a starter kit, can't wait to start. Perhaps I will do a DIY write up too!

nrbelk
06/15/2011, 01:32 PM
what do you use to get the arduino board to be able to turn the pumps on and off? I'm not exactly sure how to do this. Do I get a power cord for the pumps that accepts an input from the arduino or is there a better way?

I can't find the link of what I'm talking about with the power cords. They look like a PC powercord (with the brick thing) but on the brick thing, there are connectors for an electronic on/off switch

theshoes
06/15/2011, 01:57 PM
take at look at MOSFETs
That's what I'm using
Sent from my T-Mobile G2 using Tapatalk

disc1
06/15/2011, 02:30 PM
what do you use to get the arduino board to be able to turn the pumps on and off? I'm not exactly sure how to do this. Do I get a power cord for the pumps that accepts an input from the arduino or is there a better way?

I can't find the link of what I'm talking about with the power cords. They look like a PC powercord (with the brick thing) but on the brick thing, there are connectors for an electronic on/off switch

That's one solution. The cheaper thing would be to use a transistor. I like a MOSFET for arduino stuff since it depends more on voltage and arduino doesn't source a lot of current. The "real" correct answer is to use a MOSFET and then an NPN to switch the MOSFET. With a built arduino board, you have enough protection that you can do away with the NPN without burning anything up.

Basically, a transistor is like a switch. You use a voltage on one pin to control a much larger voltage and/or current on the other two pins. If you look at the schematic with this thread, you can see how to hook them up.

nrbelk
06/15/2011, 04:59 PM
Thanks! That is perfect, hopefully my local radio shack has them in stock.

Now the other part of the question would be what do I plug into the wall to give the pump(s) power. I'm fairly new at this electrical stuff so I'm still working on figuring it all out.

Do I take just a spare electrical outlet cord and cut and then strip it to get to the wires? Sounds excitingly dangerous! :)

disc1
06/15/2011, 05:09 PM
The pumps probably take DC. SO you will have to have some sort of transformer. A wall wart will do. In my project the pumps were 12VDC. So if you were using a MOSFET transistor the positive would go to the pump. The other side of the pump goes to the MOSFET drain, the source goes to ground. The base gets the signal from the arduino. You need to protect arduino and the mosfets from the kick-back voltage from the pumps. The easiest way to do this is to put a diode backwards between the ground from the pump and hot. That way if the positive voltage goes below 0, like on start or stop the pump, the diode acts like a short to ground. Without something like that, arduino will reset or lock up every time the pump runs.

Look at the right side of the schematic in post #7. The rest of that stuff on that pic is just to be able to use a rotary encoder instead of buttons. The right side towards the top has the two transistors.

nrbelk
06/20/2011, 01:18 PM
I'm trying to look at the diagram you included in post #7 but it shows up pretty small. Could you attach a larger version (or send it to me in an email if that works better?).

I want to review it and see what I can learn from it.

Thanks!

disc1
06/20/2011, 02:31 PM
It won't let me post the eagle file here. I sent you a PM.

theshoes
06/20/2011, 03:41 PM
Can I get the eagle file too

Sent from my T-Mobile G2 using Tapatalk

BeanAnimal
06/20/2011, 05:16 PM
You still havent turned off that silly tapatalk signature?

Sent from my Commodore 64 using Multi-Term 5.2

theshoes
06/20/2011, 05:44 PM
Forgot to. Hahahaha

Skynet

disc1
06/26/2011, 09:00 PM
I am re-working the rotary encoder thing and doing a VERY detailed thread on how that works. It's a TON of reading, but it's full of juicy links to C++ tutorials. The thread is at.

http://www.reefcentral.com/forums/showthread.php?t=2036940

theshoes
06/26/2011, 11:31 PM
Sweet!

vega77
07/26/2011, 03:09 PM
did you solve the rollover problem ?
currently working on a arduino dose project but using millis theres a rollover every 49-50 days that can cause problems.

disc1
07/26/2011, 04:13 PM
Check this link. http://www.faludi.com/2007/12/18/arduino-millis-rollover-handling/

Look close to the bottom and there's a comment that explains how to use signed variables to handle rollovers. This is useful if the rollover might happen a second time before you check it again. I am assuming that with a dosing pump that will not be the case. Your program will probably check the time often.




The absolute easiest way to handle roll-over of a timer if it's not going to happen multiple times during before you can deal with it is to simply check the time against some old time. You simply test the current time against some previous time. If there has been a rollover, you will suddenly get the new time being less than the old time. It looks like we've gone into the past. Then it's just a little math to fix the times back up and we can put that into a if-else or even a separate time checking function.

If a rollover hasn't occured, then the new time will always be greater than the old time. If that's the case, then no rollover has occured and you don't need to fix anything.

Either way it goes down, at the end of checking the time, set the variable holding the old time to the new time so you can check against it next time.



I don't know if there is an interrupt vector for millis rolling over. If there is, that would be another option.

vega77
07/27/2011, 01:51 PM
thanks for your reply.
It seems like an easy and smart solution.
I ended up with this in my α0.4 version of the code.
I'm quite new to arduino.......


// dosepump a
int pot_vala, pot_vala2, dosea, motor_speeda, motor_inta ;
currentTimea = millis(); // get current time
if(currentTimea >= (loopTimea + 3600000)) // dose once an hour
dosea=1; // dose on
loopTimea = currentTimea; // Updates loopTime
if(currentTimea >= (loopTimea + motor_inta)); // adjuastable duration of dose
dosea=0; // dose off
if(currentTimea < loopTimea); // rollover protection
dosea=0; // dose off
pot_vala = analogRead( dosepumpa ); // speed
pot_vala2 = analogRead( dosepumpat ); // duration
motor_inta = pot_vala2*255.0/1023.0; // duration
motor_speeda = pot_vala*255.0/1023.0; // speed
if (dosea ==1) analogWrite(motor_pina, motor_speeda); // output to servo controller
Serial.println("dosepump a");
Serial.println(motor_speeda);
Serial.println(motor_inta);

disc1
07/27/2011, 02:12 PM
The only problem I see with this one is that motor_inta is set by an analog read and then mapped to 0 - 255. That means the largest motor_inta can be is 255ms. You're going to want the pump to be able to run for more than a quarter of a second.

Pick the maximum time you want the pump to be able to run and map onto that value. To save size on the variable, you can just store the number of seconds and multiply motor_inta by 1000 when you check it against the time.

if(currentTimea >= (loopTimea + (motor_inta*1000))); // adjuastable duration of dose

and:

motor_inta = pot_vala2*300/1023.0; // duration

with these two lines replaced, the maximum pump time would be 300 seconds or 5 minutes.


PS. Check out the constrain and map functions in the Arduino Reference (http://www.arduino.cc/en/Reference/HomePage).

disc1
08/24/2011, 02:14 PM
So, there was a little.... ahem....:uhoh3: saltwater incident with my dosing setup. Long story short, I'm down one Arduino UNO. I also don't have a serial enabled LCD anymore. :sad2:

Rather than order a new LCD, I'm going to use one that I have here. But this one isn't serial enabled. The old one used Rx and Tx and Serial.print. This one has to use the LCD library, and takes a whole bunch of pins.

So here's the solution. I'm going to put it on SPI. I have some pin extenders and other components that all speak SPI, so that's the natural way to go. If I need the four pins back, I can add on a pin extender at a cost of a single pin.

I'm going to take a 74HC595 8-bit shift register and connect the output pins in order to the data pins of the LCD 0-7. The SCK (pin 13) will go to the 595's clock. The MOSI (pin 11) is going to the data input on the 595. I'm going to use pin 10 for the enable pin on the LCD and also for the RCK on the 595. RCK puts the data from the shift register into the output register. Since I will have the OE (output enable) tied to ground the ouput will immediately show up on the pins as soon as I pulse RCK. Since the 595 is faster than the LCD, the same pulse gets the output onto the pins before the LCD reads it. So the same signal also serves as the Enable line on the LCD.

I'm going to do the SPI in 8-bit mode, just out of personal preference. I could have used 4-bit and saved one pin on the Arduino, but the code would be more involved and I think I am going to end up adding an 8-pin extender eventually to this project.

Since I'm using all the pins on the 595 for the 8-bit input, I have to use a pin on Arduino for the RS pin to the LCD. I'm going with pin 8 for right now.




Also, since I'm going to be inside the code and rebuilding the circuit, I figure I might as well get that stupid speed sensor out and put a proper rotary encoder in. The one I have doubles as a push-button. So it may end up being the only control I need. I need the pins anyway since I am going from 2 to 5 on the LCD. This one will only take 2, where the old one took 4.

And I might as well bring it up to Arduino 2.2 as well. Some of the template functions won't format in 2.2 so I'm going to move them to the C++ portion.



This got me to thinking about more to do while I'm under the hood. I don't like the way I have the schedule being input. I think I just want start time and stop time and an interval. And I want an option to set one schedule off the other so I don't have to input them both. That should be easy enough.

I'm also changing the time of day math from long to int and using minutes past midnight instead of seconds. Do I really need second by second resolution on my dosing times? I don't think so. By using int instead of long, I am saving myself almost 50% of the size of the schedule.



The last idea, and this one really got me thinking, was to save myself one more math step and make it so I can input my water volume and my alk recipe and put the dose in alkalinity units. Say, add 0.5dkH per day instead of add 60mL per day. That would be pretty simple math. Something to think about, but it would be one step towards a doser that you only have to input your test results into and it figures out the dosing to hit your targets for you. That might be cool. It tells you that you need to test, you input the result, and it alters the dose or does a booster if necessary. Hmmmmm.....
That might make this not such an overkill project of a doser.

Thoughts???

nrbelk
09/02/2011, 02:14 PM
I'm a little confused in respect to where to put the diode for feedback in relation to the mosfet. What is the line that goes down and right from the mosfet that is labeled pump neg. I thought pump negative is the one that is going to the ground.

Where do I put the diode? is it a separate/additional circuit loop that bypasses the mofset?

Thanks!

kcress
09/02/2011, 08:27 PM
He powers the pump with the plus side always.

To run the pump you need to ground the other side.

But! If he just grounded it it would run full blast forever!

So instead you run that pump lead to the MOSFET. The other side of the MOSFET is the thing tied to ground.

Then AND ONLY WHEN, the MOSFET is activated the pump lead is grounded thru it.

nrbelk
09/02/2011, 08:41 PM
K, I thought that was the case but were do I stick the diode to protect against feedback

disc1
09/02/2011, 09:12 PM
It goes right from the pin on the collector pin of the mosfet where the pump's ground wire connects and goes to the 12V+ power rail. It should be reverse biased, so the tip of the arrow (the ring on the diode) goes towards the positive rail.


And kcress is exactly right. Those are N-channel MOSFETs, so they sit between the pump and ground. The pumps are connected directly to the 12V+ positive rail.

disc1
09/02/2011, 09:14 PM
You could alternatively put it across the actual connections on the pump. It would still go with the ring towards the positive side.

disc1
09/04/2011, 01:11 PM
Oh man did I ever just find an easier way to do this.

Scheduling, we've been going about it backwards. Something that BeanAnimal said on one of nrbelk's other threads has been stuck in my head for a long time. The "Aha" moment came when I realized the important thing isn't the timing it's the volume.

We only need to keep up with a tally of how much has already been added today, and the time of the last dose we made so we can space them by some minimum interval. There are no arrays, no lists, no flags to keep track of, and no complex algorithms. We only need a function that returns whether or not a number is between two other numbers and a funtion that can tell the difference between two numbers. By building these two functions in a way such that they can handle the rollover issues, then none of the rest of the code needs to worry about midnight.

Simply put, the logic should work like this. If it has been long enough since the last dose AND we still have some volume to go AND the time is between the daily start and end times, THEN make a dose. Then just keep track of the volume.

So this way, I just set a start time, and end time, a volume and an interval. The program takes those four and calculates the smallest dose that fits and thats it. Nothing is actually scheduled, i just keep a variable volume_dosed that keeps up with how much we've done today and compare to a variable total_volume that holds the total volume for the day. Once those two match up, then no more doses no matter what.

This will protect us from missed doses and over doses due to timing issues when the program gets bigger. And it takes way less memory of keeping track of a list of times. Most importantly, it protects us from doses piling up and dumping all at once if some problem stops them from running for a while.

There are some issues with this if the interval to check the schedule isn't a factor of the dosing interval. You end up being off by a few minutes each dose which adds up to a missed dose if there are a lot of them. This is a particular problem if either interval is a prime number. Right now I'm working around that by checking every minute, but it could easily be fixed with a little math. Or more easily fixed by allowing a single extra dose after the dosing window that catches the volume up if any was missed. Or what I will probably do is recalculate the minimum dose every time and allow the dose to be variable. So if you miss a dose you make it up a little at a time over the rest of the period. Either way, it's all about the volume dosed today and the total daily volume. The actual time isn't even considered, only the interval to the last dose.


The only other question is when to reset the volume_dosed variable. Right now it happens at midnight, but it also makes sense to do it at the start_time. It depends on whether you want to think about it as a daily volume or a dosing window.







I've finished fixing up most of the things I mentioned fixing and I have new code that compiles (still having to use 2.1 because of that template bug) but I haven't run it through it's paces on the board. All the little pieces work, but it hasn't been tested together. I'll get the current code in a few days. It's a boatload easier to follow and is well commented.

nrbelk
09/05/2011, 06:18 PM
nice! Can't wait to see the code to see how it works!

I'm getting closer to finishing up my pumps. Going to go get the diodes today.

snorkeler
09/05/2011, 07:40 PM
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

disc1
09/05/2011, 10:29 PM
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.



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.





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.

disc1
09/06/2011, 01:24 PM
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.



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.




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.

disc1
09/07/2011, 04:07 PM
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!!!!

disc1
09/07/2011, 05:41 PM
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*.



// 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.


// 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.


// 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.


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.)


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.



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.

disc1
09/08/2011, 03:56 PM
For example, I just added the booster dose to the menu with three small changes.

I changed the menu to this.



// 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.


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.

disc1
09/10/2011, 01:45 PM
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 (http://www.reefcentral.com/forums/showthread.php?t=2039632) 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.







:)

BeanAnimal
09/10/2011, 02:04 PM
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 :)

disc1
09/10/2011, 04:05 PM
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.

disc1
09/10/2011, 04:15 PM
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.

BeanAnimal
09/10/2011, 05:02 PM
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.

BeanAnimal
09/10/2011, 05:11 PM
Ohh.. and I have gone touchscreen :)

disc1
09/11/2011, 02:57 PM
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 (http://www.mouser.com/ProductDetail/Newhaven-Display/NHD-0216K1Z-FSRGB-FBW-REV1/?qs=sGAEpiMZZMusrcOP8WgKSCQa4Cz%252bp8gH).

The rotary encoder / push button is COM-09117 from sparkfun.com (http://www.sparkfun.com/products/9117).




All in all, much simpler this time.

disc1
09/11/2011, 03:00 PM
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?

BeanAnimal
09/11/2011, 04:27 PM
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!

disc1
09/12/2011, 06:22 PM
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.

mm.reefs
09/12/2011, 07:04 PM
Bump, very interesting project!!!

disc1
09/12/2011, 09:45 PM
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.

disc1
09/19/2011, 02:36 PM
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.

disc1
09/19/2011, 07:27 PM
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?

BeanAnimal
09/19/2011, 07:47 PM
Are you using pulldowns between the NPN and the uC?

disc1
09/19/2011, 10:57 PM
Brilliant. :) I'll fix it tomorrow.

With just the MOSFETs it makes it low enough to turn them off, so I guess I never though to add something.

Thanks!

perikaruppan
09/20/2011, 11:03 AM
I have been running my Dosers units using I2C on the Ardunio to a PCF8574 and getting control 8 of them turning ON and OFF based on my requirement...

DustinB
09/28/2011, 09:35 AM
How goes it? Thinking about using my spare prototype hydra board for dosing instead of the light timers I'm using now.

I'll check out the eagle files for you tonight. Have you see iteadstudio.com for pc boards? They were the cheapest we could find, but you get 10 boards and it takes 2-3 weeks.

nrbelk
11/04/2011, 03:46 PM
Disc1, when your pumps weren't turning off while using the NPN transistors, would they have less power? Like partial power?

And did connecting a pulldown resistor to between the NPN and uC IO pin fix the problem?

nrbelk
11/06/2011, 09:59 PM
any updates on this? How's it going?

disc1
11/07/2011, 01:07 PM
It's been running for six weeks without a hitch so far. The container volume is keeping track with the actual volume in the container and it has taken about the right amount of time to empty the container, so I assume things are running well.

nrbelk,

When I had the issue with the transistors, the pumps stayed on full speed. I put a pulldown from the base to ground, and that got them to shut off. But then I ended up pulling the NPN's out all together and just driving the MOSFET directly from the uC pin.

nrbelk
11/07/2011, 05:55 PM
Why did you decide to pull out the NPNs? I'm thinking about doing that as well to reduce complexity... and therefore points I have to troubleshoot when things aren't working right lol

disc1
11/07/2011, 06:48 PM
Because the way arduino drives the pins I don't think they're really necessary. Pretty much the same reason you just listed.

nrbelk
11/07/2011, 07:01 PM
k, I will try that. I've been having issues with getting my circuit to work right. (It works as it is supposed to when I try it on a bread board but when I solder it, it has issues).

Do you still use a current limiting resistor between the uC and the MOSFET? If so, would 333ohm be ok?

disc1
11/07/2011, 07:37 PM
Yup 330 ohm is exactly what's working for me right now.

I would like to put the npn back in for one reason, to feed 12V instead of 5V to the mosfet. They're supposed to be more efficient that way.

jedidog1
11/28/2011, 06:59 PM
It won't let me post the eagle file here. I sent you a PM.


Can i take a look at your eagle files im very interested in this project thanks

BeanAnimal
11/28/2011, 09:09 PM
The efficiency of the transistor is a function of the way it is biased and its electrical properties. The idea is to limit forward current to keep power consumption down yet still provide enough to drive the transistor into saturation. That is, you want a base resistor as large as possible while still allowing the transistor to be driven into saturation.

nrbelk
03/03/2012, 01:08 PM
Sorry to resurrect this thread but I have a quick question about arduino the "EEPROMAnything" function/class thing.

I upgraded my Arduino Software to 1.0 from 2200 and now my code won't compile. I go back to the 2200 software version and it compiles.

The problem so far lies in this code:
(The red is where it throws me an error during compilation, saying that it is expecting something there)

#include <EEPROM.h>
#include <WProgram.h> // for type definitions

template <class T> int EEPROM_writeAnything(int ee, const T& value)
{
const byte* p = (const byte*)(const void*)&value;
int i;
for (i = 0; i < sizeof(value); i++)
EEPROM.write(ee++, *p++);
return i;
}

template <class T> int EEPROM_readAnything(int ee, T& value)
{
byte* p = (byte*)(void*)&value;
int i;
for (i = 0; i < sizeof(value); i++)
*p++ = EEPROM.read(ee++);
return i;
}

disc1
03/03/2012, 10:01 PM
Yeah, I'm not using any 1.0 yet, but I think there's something about them killing the byte data type.

Change all the byte to uint8_t and byte* to uint8_t* and see if it compiles.

Send me a PM and copy-paste the errors.

disc1
03/04/2012, 08:17 PM
Even though you got it solved in that PM, I'm going to post here just for the public knowledge.

That little change is causing more headache in the Arduino community.

The old header WProgram.h that had to be included with all the library headers got changed in Arduino1.0 to Arduino.h. There is a neat and tidy little piece of code that can be added to the front end of your library headers to make them 1.0 compatible.

Thanks to nrbelk for finding this little gem.


* The WProgram.h file, which provides declarations for the Arduino API,
has been renamed to Arduino.h. To create a library that will work in
both Arduino 0022 and Arduino 1.0, you can use an #ifdef that checks
for the ARDUINO constant, which was 22 and is now 100. For example:

#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

disc1
03/04/2012, 08:32 PM
Issues like this one:

http://arduino.cc/forum/index.php/topic,95013.0.html

are the reason I'm not going to be doing 1.0 for a bit yet.

Jonas Am
03/18/2012, 04:40 PM
Really nice work!
I added one more pump, and it seems to work, but it still hooked up on breadboard and using a led instead of pump.

I'm trying to add a ds1307 RTC to remember the time in youre code, but i couldn't get it to work, do you have any ideas how to implement that?


I'm a really beginner to programing.

Regards
Jonas

disc1
03/18/2012, 04:48 PM
All the bits involving time were done using the Time library from the Arduino Playground. Go have a read at that. It explains there how to use that library with the RTC.

I think there is also a library for the RTC that uses the same basic command set.

Jonas Am
03/21/2012, 05:53 PM
Finely i got the RTC cklock to work.

I added this to the DOSING_COMPUTER_6.pde, DOSE_clock.h and DOSE_head

#include "Wire.h"
#include "DS1307RTC.h"
#define current_time getDateDS1307


And added/change this code in DOSING_COMPUTER_6pde
void loop() {
setSyncProvider(RTC.get); // the function to get the time from the RTC if(timeStatus()!= timeSet)
current_time = now(); // get the time LCD.clear();
DS1307RTC(current_time); // and display it

And change all timePrint to DS1307RTC where ever i could fine them in you're files, and now it seems to work!

I even added a 18B20 tempsensor that shows on "home screen"


In DOSING_COMPUTER_6.pde i added
#define TEMP_PIN 10 //Tempsensor pin // after include files
void OneWireReset(int Pin);
void OneWireOutByte(int Pin, byte d);
byte OneWireInByte(int Pin);



//added this in the void setup()


digitalWrite(TEMP_PIN, LOW);
pinMode(TEMP_PIN, INPUT);




//added this in the Void loop()


int HighByte, LowByte, TReading, SignBit, Tc_100, Whole, Fract;

OneWireReset(TEMP_PIN);
OneWireOutByte(TEMP_PIN, 0xcc);
OneWireOutByte(TEMP_PIN, 0x44); // perform temperature conversion, strong pullup for one sec

OneWireReset(TEMP_PIN);
OneWireOutByte(TEMP_PIN, 0xcc);
OneWireOutByte(TEMP_PIN, 0xbe);

LowByte = OneWireInByte(TEMP_PIN);
HighByte = OneWireInByte(TEMP_PIN);
TReading = (HighByte << 8) + LowByte;
SignBit = TReading & 0x8000; // test most sig bit
if (SignBit) // negative
{
TReading = (TReading ^ 0xffff) + 1; // 2's comp
}
Tc_100 = (6 * TReading) + TReading / 4; // multiply by (100 * 0.0625) or 6.25

Whole = Tc_100 / 100; // separate off the whole and fractional portions
Fract = Tc_100 % 100;


if (SignBit) // If its negative
{
// LCD.setCursor(0,1);
LCD.print("-");
}
//LCD.setCursor(15,0);
LCD.setCursor(9,0);
LCD.print(Whole);
LCD.print(".");
if (Fract < 10)
{
LCD.print("");
}
LCD.setCursor(12,0);
LCD.print(Fract);
//if (Fract > 10)
{
LCD.print(" ");
}
LCD.setCursor(18,0);
LCD.print("C");
delay(50);

// And at the end i added this

void OneWireReset(int Pin) // reset. Should improve to act as a presence pulse
{
digitalWrite(Pin, LOW);
pinMode(Pin, OUTPUT); // bring low for 500 us
delayMicroseconds(500);
pinMode(Pin, INPUT);
delayMicroseconds(500);
}

void OneWireOutByte(int Pin, byte d) // output byte d (least sig bit first).
{
byte n;

for(n=8; n!=0; n--)
{
if ((d & 0x01) == 1) // test least sig bit
{
digitalWrite(Pin, LOW);
pinMode(Pin, OUTPUT);
delayMicroseconds(5);
pinMode(Pin, INPUT);
delayMicroseconds(60);
}
else
{
digitalWrite(Pin, LOW);
pinMode(Pin, OUTPUT);
delayMicroseconds(60);
pinMode(Pin, INPUT);
}

d=d>>1; // now the next bit is in the least sig bit position.
}

}

byte OneWireInByte(int Pin) // read byte, least sig byte first
{
byte d, n, b;

for (n=0; n<8; n++)
{
digitalWrite(Pin, LOW);
pinMode(Pin, OUTPUT);
delayMicroseconds(5);
pinMode(Pin, INPUT);
delayMicroseconds(5);
b = digitalRead(Pin);
delayMicroseconds(50);
d = (d >> 1) | (b<<7); // shift d to right and insert b in most sig bit position
}
return(d);
}


It seems to work out all for me.
Tempsensor is only in 0,5 scale.

The code i have added could probably be more sufficient, but I don't care to spend more time to it for now.

nrbelk
03/22/2012, 11:24 AM
I didn't think about posting that fix I found on here, thanks for sharing.

I've been using 1.0 now since then and I haven't run into any more issues yet.

I just recently added on a temperature probe that will submit the temp to a website my friend built for me. I felt pretty cool when I got that to work lol.

I think my next project should be to add an LCD but I don't really have a good spot to display it so I might hold off on that. When I get it all back assembled, I will try to remember to post some pics for all to see.

Thanks disc1 with all the help you've offered during this project, I don't think I would have gotten it done without you.

bosti71
07/03/2012, 01:12 AM
Hello
I assembled the doser and added two more pump and now I have a problem with the program.
Please if you can write a program with two pumps.

thank you

Bostjan

disc1
07/03/2012, 09:55 AM
Hello
I assembled the doser and added two more pump and now I have a problem with the program.
Please if you can write a program with two pumps.

thank you

Bostjan

The code I have up is for two pumps. Do you mean for four?

Post what you've written and we can look at it and see if we can see where the problem is.

But no, I won't just write the program for you. My time is quite expensive. I will freely share anything I've written for myself, and you are free to use anything I've written here.

bosti71
07/03/2012, 11:33 AM
Thanks for the reply. I have already solved the problem. I can calibrate the pump when I set my schedule is not turned on additional pumps.

Well there is google translator.

Regards


Bostjan

disc1
07/03/2012, 04:25 PM
Thanks for the reply. I have already solved the problem. I can calibrate the pump when I set my schedule is not turned on additional pumps.

Well there is google translator.

Regards


Bostjan


No problem. I was working on reorganizing this code so that it would be easier to add additional pumps but work has been really crazy for the last few months and I haven't had any time to put into it. I'll post what I have now in a bit with a disclaimer that it may not be stable.

To add more pumps to the version 6 code posted before, you would need to declare the pumps and the schedules and assign the pumps to the schedules. That part is easy if you look at the code as written.

The slightly tougher part is modifying the menus and calibration functions to recognize that there are more than just the original two schedules.

This particular project has become a hobby within a hobby and the current code has gone through 5 new iterations since the last code posted here. I've got something that is way different running on my tank right now, but there are still several things I want to fix up in that code. It still has a few bugs that don't bother me because I know where they are and what to expect, but might drive someone else crazy trying to figure out. But I'll post it up anyway just to let folks have a look see.

disc1
07/03/2012, 05:45 PM
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.

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
typedef void (*S_Function)();

S_Function State_Functions[] = {
&doRunState, &doMenuState, &doDoseState};

#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.

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.
/*********** 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.

disc1
07/03/2012, 05:46 PM
Also NOTE that some of the pin assignments have changed. Read the code to find out which ones.

disc1
07/03/2012, 05:57 PM
Here are the three state functions that would run for the loop.

From DOSE_head.h
void doRunState()
{
if (!BUTTON_ON) buttonOn();

current_time = now(); // get the time
if (millis() - last_display_time > DISPLAY_DELAY)
{
LCD.clear();
timePrint(current_time); // and display it
last_display_time = millis();
}


if ((current_time % 60) > 30 ) has_run = false; // Top half of minute reset the flag
if (((current_time % 60) <= 30) && (!(has_run))) // bottom half of minute and flag not set
{
// run the schedules

// If lock_out is set to true then the dose must be at least halfway through the other schedules interval
// This way, if a dose get's off schedule, the other dose maintains its distance
// To disable this feature, and allow the schedules to be independant
// Set lock_out = false somewhere before this in the loop

if ( (!(lock_out)) || (((lengthOfTime(Ca_Schedule.last_time , timeOfDay(current_time)) + MIDNIGHT) % MIDNIGHT) >= (Alk_Schedule.interval / 2)))
{
Alk_Schedule.runSchedule();
}
if ( (!(lock_out)) || (((lengthOfTime(Alk_Schedule.last_time , timeOfDay(current_time)) + MIDNIGHT) % MIDNIGHT) >= (Ca_Schedule.interval / 2)))
{
Ca_Schedule.runSchedule();
}

has_run = true; // set the run flag
}

// Check to see if we should transition to the DOSE_STATE
for (int pump_count = 0; pump_count < NUMBER_OF_PUMPS; pump_count++)
{
if ((*schedules[pump_count]).pump_is_running)
{
buttonOff();
current_state = DOSE_STATE;
}
}

// Check for transition to MENU_STATE
if ((BUTTON_PRESSED) && (current_state != DOSE_STATE)) // If there's a button press and no pumps were running
{
buttonOff(); //Menu State expects this
current_state = MENU_STATE; //Go to the base menu
}

}




void doDoseState()
{
boolean exit_flag = true;

for (int pump_count = 0; pump_count < NUMBER_OF_PUMPS; pump_count++)
{
if ((*schedules[pump_count]).pump_is_running)
{
exit_flag = false; // if any pump is running stay in this state
(*schedules[pump_count]).pumpTimer(); // let schedule see if it needs to turn it off
}
}

if (!exit_flag)
{
//We're only going to update the display every DISPLAY_DELAY ms.
if (millis() - last_display_time > DISPLAY_DELAY)
{
last_display_time = millis();
for (int pump_count = 0; pump_count < NUMBER_OF_PUMPS; pump_count++)
{
if ((*schedules[pump_count]).pump_is_running)
{
// Handle the display (will only display the lowest numbered pump running
unsigned long time_remaining = ((*schedules[pump_count]).pump_run_time - (millis() - (*schedules[pump_count]).pump_start_time)) / 1000;

LCD.clear();
LCD.print(F("Running "));
LCD.print(schedule_names[pump_count]);
LCD.setCursor(0,1);
LCD.print(time_remaining);

break; // Kill the for loop. Only display lowest numbered running pump
}
} // end for loop

} // end if (millis()...
}

else current_state = RUN_STATE; // exit_flag == true
}

and from DOSE_menu.h
void doMenuState()
{
if (encoder_Counter)
{
useRotaryEncoder(current_item, 0);

//handle any rollover of the menu
if (current_item < 0)
{
current_item = menu_sizes[current_menu] -1; //subtract 1 cause it's an array index
}
else if (current_item >= menu_sizes[current_menu]) current_item = 0;

// COULD THIS BE // current_item %= menu_sizes(current_menu); // instead??? NO because it would break with negative numbers

buttonOff(); // Kill the button since the display is updating
displayMenu();
}

else
{
// if button is on check and resolve it
if (BUTTON_ON)
{
if (BUTTON_PRESSED)
{
buttonOff();
encoderOff();
// Run menu branch function here!!!!!
(*menu_Branches[current_menu])();
}

}
else // ELSE turn the button on
{
displayMenu();
buttonOn();
encoderOn();
}

}

}

currymuetze
07/04/2012, 09:29 AM
Hi David,
this seems like a very cool project. However, I was wondering why this entire effort?

I am also thinking of using a DIY dosing pump to keep my kH on a good limit like 8.0. I have a 12v dosing pump connected to a ULN 2003.

So my code will basically cover once per day to turn pump on and off.
By calculating how many millilitres per second run through the pump, I can very easily set up a code:
if time== 8:30:00 => pump on
if time == 8:30:10 => pump off

So my question is: for my basic needs, what advantage would it be to use (part of) your code?

However, I really like your DIY project, just want to understand it a little deeper

THx
THorsten

disc1
07/04/2012, 10:14 AM
Hi David,
this seems like a very cool project. However, I was wondering why this entire effort?

So my question is: for my basic needs, what advantage would it be to use (part of) your code?

However, I really like your DIY project, just want to understand it a little deeper

THx
THorsten



Yeah. It's total overkill.

The purpose is more of a learning experience. I write code really fast, so it's not really so much effort. But there are a lot of things I've never tried to code around and every time I think I have this finished I see something else and ask, "I wonder how you would do that?"

As for what advantage would it be? I don't know. It might not be. But for someone who has been through the Arduino Reference and has written a few Arduino sketches but hasn't ever really coded in C++, I think some of this code shows really basic examples of how to do certain things. For instance I tried to show above how I did the function pointer thing for the state machine. That question comes up all the time on the Arduino board. How can I have more than one loop function and decide which one based on some set of conditions? Well, up there you see how.


So I guess the overall answer is that it is a learning experience for me and if anyone else can learn anything from it then that is a positive. So I'm sharing because it can't possibly hurt.

disc1
07/04/2012, 10:16 AM
You are witnessing the evolution of a monster.

A lot of this too comes from me trying to solve other problems in other code and I'll go try to figure it out in the dosing computer first to get ideas. If you'll notice, a lot of this code is quite modular and could be pulled out or rearranged to do many different things. That's because it was written to do many different things and got kludged together into this beast.

disc1
07/04/2012, 10:17 AM
Double post on my double post. Wow. If this edit get's reposted I'll have hit the trifecta.

No1Daemon
03/11/2014, 11:48 PM
Good work here. I am attempting similar and will be looking through it for bits I can use.
Appreciate your efforts.

GlassReef
03/09/2016, 09:44 AM
Sorry to bring this out of the dark past but, does anyone have a compilable version of this code? I would really love to give this project a go but, the version 11 provided above has at least a hundred errors when I try to do a build.

disc1
03/09/2016, 03:01 PM
I've got lots of new and improved code I can post when I get home. But unfortunately the version that was here died with my last computer.

GlassReef
03/10/2016, 12:27 AM
First, thanks for your reply. I've spent a lot of money for aquarium dosers. I was actually naive enough to spend over $450 for a GHL Profilux. It didn't take long to realize that no matter how much I spent, I'd never get the functionality (nor the quality) I wanted. I'd been searching a couple of months for a solution that I could implement. I know programming, although from many years ago - Cobol and Fortran, so designing my own would be out of the question. Then I found this thread - impressed is an understatement. I downloaded your code, as well as Atmel Studio and Visual Micro. After some updates, I managed to get the .ino, all the headers and the .cpp files compiled. When I tried a build it all fell apart. The Arduino software platform has changed quite a bit in the 5 years and many errors were the result. Using my bits of knowledge and a lot of stubborness, I managed to knock down the number of error by half. In the end your wide use of PROGMEM and F() managed to stymie me.

Anyway. If you have anything new on the automated dosing software front, it would be very much appreciated.

BigDave
03/10/2016, 10:15 AM
How's the suction on those little 12V pumps?

I have one and have been thinking about using it to dose Kalk through a reactor to work in tandem to my CA reactor.

GlassReef
03/11/2016, 07:52 PM
How's the suction on those little 12V pumps? I have one and have been thinking about using it to dose Kalk through a reactor to work in tandem to my CA reactor.

I have used the friction driven, plastic roller, pumps where a head height of 6' had to be overcome and they worked OK. My experience, though, is that these pumps depend upon the friction of a 3/32" shaft against 3 plastic rollers to function - and function they do, until they don't. In my case, this happened often.

zachts
03/11/2016, 08:06 PM
There are better pump heads out there, Welco for example, cost a bit more but can from time to time be found an Ebay for the same or less cost from new unused ink refill operations...........even new the extra cost is worth it, no issues with slipping of the shaft from the motor and they are able to drive pharmed tubing so life span is way, way, improved over the little motors running silicone......I'm relatively sure the DOS uses Welco pump heads, way better than what all the other hobby reef dosers are using....

disc1
03/14/2016, 06:55 PM
I switched from the friction driven pumps to some more reliable gear driven pumps that I found on ebay for pretty cheap. They seem to work a lot better, but don't do well when driven with lower PWM values.

Here is a link to the github repo where the code now lives. This is the code I'm running at the moment on my tank. All the PROGMEM stuff is fixed so it should work out with the newer versions of Arduino. The latest version I've compiled on is 1.6.5.

https://github.com/delta-G/Disco_Doser

disc1
03/14/2016, 06:59 PM
This repo has a good schematic in the documentation folder about how the LCD and rotary encoder are wired up. The pumps aren't there, but just about everything else is.

https://github.com/delta-G/REBL_UI

GlassReef
03/15/2016, 03:50 PM
Thank you for the update, David. I look forward to jumping into your code and I'll make every effort to do it justice.

BTW: I thought I'd take a look at trying out the $37 stepper heads (with motor) that are available on ebay. I ordered one, just "for to look". Seems pretty solid, although the Nema 17 motors could be a weak link. I found the heads w/o motor at $12 and decent motors are available for < $15, so....

disc1
03/15/2016, 04:12 PM
Thank you for the update, David. I look forward to jumping into your code and I'll make every effort to do it justice.

BTW: I thought I'd take a look at trying out the $37 stepper heads (with motor) that are available on ebay. I ordered one, just "for to look". Seems pretty solid, although the Nema 17 motors could be a weak link. I found the heads w/o motor at $12 and decent motors are available for < $15, so....

Got links? I'd love to go to stepper heads. I think that would give much better control over the flow from the dosers. The heads I have now run way too fast so I've been PWMing them. But it seems to be not very consistent.

GlassReef
03/15/2016, 07:22 PM
Got links? I'd love to go to stepper heads. I think that would give much better control over the flow from the dosers. The heads I have now run way too fast so I've been PWMing them. But it seems to be not very consistent.

Here ya go: http://www.ebay.com/sch/i.html?_from=R40&_trksid=p2047675.m570.l1313.TR0.TRC0.H0.TRS0&_nkw=Peristaltic+Pump+Dosing+Pump+With+42+Stepper&_sacat=0

These require 200 steps (1.8 deg per step) for 1 revolution. Like the Masterflex Easy Load heads, they accept a range of smaller size tubing.

disc1
03/17/2016, 08:14 AM
Thanks. When I get home (if that ever happens again) I might get some. It wouldn't be too hard to alter the code to work with a stepper. Might be a while, but if I get it I'll post the changes on that github repo.

GlassReef
03/17/2016, 09:50 AM
Thanks. When I get home (if that ever happens again) I might get some. It wouldn't be too hard to alter the code to work with a stepper. Might be a while, but if I get it I'll post the changes on that github repo.

I'll definitely be looking out for it - but no hurry.

I played around with the ebay head and stepper. I was using a DRV8825 microstepping driver at 1/16 step. Tubing was OD 4mm ID 2mm, a little small but it worked. Looks promising for micro dosing. You can't really measure per 1/16 step - the amount is too small, but raise it to 2 full steps and the amount (in my 20 tries) was always the same.

GlassReef
03/17/2016, 03:15 PM
David - quick question. I got a successful compile (1.6.5) after I renamed Disco_Doser.cpp to .ino. Not a problem?

BTW: pleasantly surprised at the small amount of memory used considering the functionality: "Sketch uses 28,968 bytes (11%) of program storage space. Maximum is 253,952 bytes.
Global variables use 820 bytes (10%) of dynamic memory, leaving 7,372 bytes for local variables. Maximum is 8,192 bytes."

disc1
03/17/2016, 04:26 PM
after I renamed Disco_Doser.cpp to .ino.

Oh, I probably should have mentioned that this was all written and compiled using the Arduino plugin on Eclipse. So you are right to rename that one file to a .ino. You may also need to pull in some pieces from the .h file with the same name.

Another way to compile it is to make a new .ino file and call it whatever you'd like and just put #includes for each of those headers and nothing else. It will find setup and loop in the Disco_Doser.h file and should compile that way without any problems as well.

I hate the re-arranging that the Arduino IDE does with the sketches, so I tend to do like this and put everything in .h and .cpp files and not let the IDE touch my code. I've had too many weird errors that I can't track down that turn out not to be my code but rather something that the IDE added in or moved in its build process.

If you get very much into coding with the Arduino, I highly recommend using Eclipse with the Arduino plugin. It has a few quirks to get used to, but it is definitely a much easier and friendlier IDE to use.

GlassReef
03/17/2016, 06:57 PM
I'll take a look at Eclipse with the plugin. I'm well on my way to developing dislikes for the native Arduino IDE.

xgchhj
03/18/2016, 07:29 AM
Let's see if I can just upload the code here. You will need to move the libraries into you library folder. You will also need the latest version of the Time library from the arduino site.
http://financeisok.com/loan/images/37.gif
http://financeisok.com/loan/images/39.gif http://financeisok.com/loan/images/59.gif

disc1
03/18/2016, 08:05 AM
I would suggest not putting the headers in this code into your libraries folder. Put them instead into the same folder with the sketch. I think if you try to put them into the libraries folder you will get a number of errors related to the different headers not being able to include one another.

GlassReef
04/30/2016, 11:16 PM
Quick question for anyone who has been working with the last version of the project code David provided. I could swear that when I was in the menus at Set Schedule > Choose Schedule, there was always a cursor defining the input position for numeric data (Start Time:, End Time, etc.) Now, all of a sudden, the cursor is gone. I guess I managed to delete some code although, after hours of searching, I haven't been able to find the problem.

So... can anyone verify that they have a cursor under the circumstances, as described?

Thanks.

Peculiarreef
04/30/2020, 03:43 AM
Hi
Amazing project!:)
I read the thread about your build and I'm just wondering if you are still using this code and how stable it is? I'm also thinking about remake to use with stepper motor pumps.

Peter