Peristaltic Doser - Doser set amount over 24hr period

So a while back I started with the original core from kickstarter. With the help of a nice gentleman on this forum I was able to get programming down for my project (with a simple website), which is a four channel peristaltic dosing pump.

Currently I can set each pump to run for a set period of time at an exact time. I would be curious about changing that programming so that I could put in a set amount of milliliters (knowing how many mL/hr) and it would be divided out to 12 doses over a 24hr period.

I’m also no opposed to purchasing a new Photon if that makes more sense.

Here is the original programming:

// This #include statement was automatically added by the Spark IDE.
#include "SparkTime.h"

UDP UDPClient;
SparkTime rtc;

int relayPin[4] = {D0,D1,D2,D3};

//By relays number
bool    isSet[4] = {false,false,false,false};
uint8_t startHours[4]   = {0,0,0,0};
uint8_t startMinutes[4] = {0,0,0,0};
uint8_t startSeconds[4] = {0,0,0,0};
unsigned int duration[4] = {0,0,0,0};

unsigned long stopTime[4] = {0,0,0,0}; 

#define NCHARS 32
char relayStr[4][NCHARS];

void setup()
{
   for(int relay=0;relay<4;relay++) {
       pinMode(relayPin[relay], OUTPUT);
       digitalWrite(relayPin[relay], LOW);
   }

   Spark.function("setRelay1", setRelay1);
   Spark.function("setRelay2", setRelay2);
   Spark.function("setRelay3", setRelay3);
   Spark.function("setRelay4", setRelay4);

   Spark.variable("getRelay1", relayStr[0], STRING);
   Spark.variable("getRelay2", relayStr[1], STRING);
   Spark.variable("getRelay3", relayStr[2], STRING);
   Spark.variable("getRelay4", relayStr[3], STRING);

rtc.begin(&UDPClient, "pool.ntp.org");
rtc.setTimeZone(-5); // gmt offset
rtc.setUseDST(true);

}

void loop() {

unsigned long currentTime = rtc.now();

for(int relay=0;relay<4;relay++) {
    if (TRUE==isSet[relay]) {
        if (rtc.hour(currentTime)==startHours[relay] &&
            rtc.minute(currentTime)==startMinutes[relay] &&
            rtc.second(currentTime)==startSeconds[relay]) {
                digitalWrite(relayPin[relay],HIGH);
                stopTime[relay] = currentTime + duration[relay];
            } // start time
    } // is set

    if (currentTime >= stopTime[relay]) {
        digitalWrite(relayPin[relay],LOW);
    }

}

delay(100);

}

// Parse the format: 08:56:05*6000 or 18:59:00*10
//  hh:mm:ss*duration
int parseTimeDuration(String command, int relay) {
    char copyStr[33];
    command.toCharArray(copyStr,33);
    char *p = strtok(copyStr, ":");

    startHours[relay]   = (uint8_t)atoi(p);
    p = strtok(NULL,":");
    startMinutes[relay] = (uint8_t)atoi(p);
    p = strtok(NULL,":");
    startSeconds[relay] = (uint8_t)atoi(p);
    p += 3;
    duration[relay]     = atoi(p);
    isSet[relay] = true;
    sprintf(relayStr[relay], "%02d:%02d:%02d*%d",startHours[relay],startMinutes[relay],startSeconds[relay],duration[relay]);
    return 1;
}

int setRelay1(String command) {
    return parseTimeDuration(command, 0);
}
int setRelay2(String command) {
    return parseTimeDuration(command, 1);
}
int setRelay3(String command) {
    return parseTimeDuration(command, 2);
}
int setRelay4(String command) {
    return parseTimeDuration(command, 3);
}
1 Like

@james211, not sure what you’re asking for. You seem to know exactly what needs to be done so why don’t you try and when you hit a bump, ask for help. It’s the best way to learn! :smiley:

3 Likes

There certainly isn’t any need to use SparkTime any more, since the Time functions can be used to get time now. It’s also not clear to me why you have those Spark variables; it looks like they just give you back the values that you’re sending in the function command string. It would help to know what your parameters are for what you would like to do. Do you always want the doses spread over 12 portions evenly spaced two hours apart? Do you need them to start at an exact time (to what resolution)? If you want to express the dose in amount, rather than a time, is seems you’ll have to make changes to your front end (the web page). You should at least try to write something yourself, then come back with more specific questions.

4 Likes

It has SparkTime because I wrote it for him before the current Time class existed!

The set/get methods were so that the web page could read back what exactly was set using a periodic refresh, just for safety. It was written like my servo example with user feedback to see what the current setting is.

If you want to change the dosing, you just need to know

  • The user-set volume of liquid you want in mL
  • The constant for the pumps in mL/hr
  • The assumed period of 24 hours

Then you take the total volume, divide by 24 to get mL per dose and then divide that by the pump constant of mL/hr to get the pump run time in hours. Convert to minutes or seconds and away you go.

1 Like

See the math aspect I understand, translating that into code is where I get stuck. I’ll look everything over according to what I currently have and see what I can come up with. I like the learning aspect of it, but I get stuck so often. Having an example that I can run and see how it performs and then tear that apart often times works for me. But that doesn’t mean it will help me to understand the programming aspect, terminology and such.

Out of curiosity, would anyone have any recommendations of a good place to start for tutorials or reading for a better understanding of this programming language?

So I’m looking through the code, and this seems like the appropriate code that needs to be changed for telling the doser how much total volume, and how many times to dose that over a 24hr period.

}

void loop() {

unsigned long currentTime = rtc.now();

for(int relay=0;relay<4;relay++) {
    if (TRUE==isSet[relay]) {
        if (rtc.hour(currentTime)==startHours[relay] &&
            rtc.minute(currentTime)==startMinutes[relay] &&
            rtc.second(currentTime)==startSeconds[relay]) {
                digitalWrite(relayPin[relay],HIGH);
                stopTime[relay] = currentTime + duration[relay];
            } // start time
    } // is set

    if (currentTime >= stopTime[relay]) {
        digitalWrite(relayPin[relay],LOW);
    }

}

delay(100);

}

Now here’s where I get lost. Some where in the code I know I need to declare what the constant is for the mL/hr, which I believe is in this section of the code. Am I on the right track here? I guess I need to find the list of commands for figuring this stuff out.

//By relays number
bool    isSet[4] = {false,false,false,false};
uint8_t startHours[4]   = {0,0,0,0};
uint8_t startMinutes[4] = {0,0,0,0};
uint8_t startSeconds[4] = {0,0,0,0};
unsigned int duration[4] = {0,0,0,0};

unsigned long stopTime[4] = {0,0,0,0};

I was thinking, it might be a bit easier to change this to a method where I simply tell it to dose X amount of times for X period of time. The time aspect is already there, I just need to tell the code to repeat that X amount of times.

Would that be written as a loop function?

I also read that there is an alarm (alarm.timerRepeat) function. But I don’t see that working with the current code that’s been written.

It often helps to write pseudo-code first, and work from that. It’ll give to a high-level view of what needs to be done, which can then be broken down into manageable chunks.

If (check-time()){
  dose();
}

There’s a part which need to check whether it’s time to dose, and there’s a part that actually does the dosing. Those can be handled separately and might make things easier. You can store the last time it dosed and compare that to the current time. If the difference is bigger than the interval you’ve specified, then dose. Using deep sleep, you could have it dose, sleep, and when it wakes up, it’ll repeat itself. The device won’t be accessible during sleep though.

For the dosing, figure out how much time the pump needs for your specified amount, and have it run for that long.

Start off simple, one relay for example, and if that works, expand. The same goes for controlling it; a Web interface is very nice to have, but might complicate things during development. See if it works with hardcoded times, and if it does, you can start on the remote control.

I get what you’re saying, and that’s good advice. I’m pretty sure that this is the chunk that tells the system to stop dosing, or don’t dose:

if (currentTime >= stopTime[relay]) {
        digitalWrite(relayPin[relay],LOW);
    }

And I’m pretty sure this is the section that is the variable for when, or if to dose:

for(int relay=0;relay<4;relay++) {
    if (TRUE==isSet[relay]) {
        if (rtc.hour(currentTime)==startHours[relay] &&
            rtc.minute(currentTime)==startMinutes[relay] &&
            rtc.second(currentTime)==startSeconds[relay]) {
                digitalWrite(relayPin[relay],HIGH);
                stopTime[relay] = currentTime + duration[relay];
            } // start time
    } // is set

Are my assumptions correct?

Also, I presume using a delay function wouldn’t be the correct way to do this using this code. I guess if I really wanted to simplify things and change it up completely I could just do something like this(below) knowing that the pumps dose 1.26mL/Min | 38mL/30Min | 77mL/hr. The only thing I don’t know, is how to convert a delay in milliseconds into hours, as the delay off time would be 2 hours.

In general I’m not as worried about a start time, so much as making sure it runs for X-amount of time X-amount of times per day.

void loop() {
  digitalWrite(Relay1, HIGH);
  digitalWrite(lRelay2, HIGH);

delay(476000);

  digitalWrite(Relay1, LOW);
  digitalWrite(lRelay2, LOW);

What are the 4 relays used for?
Do you need to trigger each relay 12 times during the day, or one of them, etc ?

Well I have four relays, but currently only use two pumps. I have another four pumps here and I’d like to build an additional setup. I need to trigger each of the relays at separate times throughout the day.

So I guess if that was the case I’d need to setup individual delays for each relay. Again this is why I think there is a better way to do this.

Something like this perhaps ?

struct relayEvent {
 int pin = -1;
 int hour;
 int min;
 int state;
};

relayEvent events[50];

void setup() {
    pinMode(D7, OUTPUT);
    
    events[0].pin = D7;
    events[0].hour = 19;
    events[0].min = 45;
    events[0].state = HIGH;

    events[1].pin = D7;
    events[1].hour = 19;
    events[1].min = 50;
    events[1].state = LOW;
}

void loop() {
    int hour = Time.hour();
    int min = Time.minute();
    
    for (int i=0; i<50; i++)
    {
        if (events[i].pin >= 0 && events[i].hour == hour && events[i].min == min)
        {
            //Particle.publish("log", String(i), 60, PRIVATE);
            digitalWrite(events[i].pin, events[i].state);
        }
    }
    
    //To make sure we get triggered once every minute
    while (Time.minute() == min)
    {
        delay(1000);
    }
}

I was thinking along the same lines as @Mora. Create an object ( a struct in my example) to hold the values for each channel, and update them in loop. Something like this,

typedef struct {
    int startTime;
    int onTime;
    int offTime;
    float totalDose = 0;
    float doseRate = 10; // mL/min
    int connectedPin;
} Channel;

 void updateChannel(Channel c);
 float now;

Channel channels[4];


void setup() {
    channels[0].connectedPin = D0;
    channels[1].connectedPin = D1;
    channels[2].connectedPin = D2;
    channels[3].connectedPin = D3;
    Serial.begin(9600);
    Time.zone(-5);
    delay(3000);
    Particle.function("setupChannel", setupChannel);
}



void loop() {
    now = Time.hour() * 3600 + Time.minute() * 60 + Time.second();
    for (int i=0;i<4;i++) {
        updateChannel(channels[i]);
    }
    
    delay(500);
}



void updateChannel(Channel c) {
    
    if (c.onTime >= now) { 
        digitalWrite(c.connectedPin, HIGH);
        c.onTime += 7200; // move on time 2 hours forward
    }

    if (c.offTime >= now) {
        digitalWrite(c.connectedPin, LOW);
        c.offTime += 7200;
    }
}


int setupChannel(String cmd) { // cmd syntax: "channel#,startTime,dose" (startTime is sent as hour*3600 + minute*60 + second)
    char *rmdr;
    char *stringArgs = (char*)cmd.c_str();
    int index = atoi(strtok_r(stringArgs, ",", &rmdr));
    Channel c;
    c.startTime = atof(strtok_r(NULL, ",", &rmdr));
    c.onTime = c.startTime;
    c.totalDose = atoi(rmdr);
    float dosePerActuation = c.totalDose/12;
    float durationPerActuation = (dosePerActuation/c.doseRate) * 60; // assuming doseRate is in volume/minute
    c.offTime = c.onTime + durationPerActuation;
    return 1;
} 

If the dose rates are different for each channel, then you would need to incorporate that into the command string also.

1 Like

Ok, this seems to be going in the right direction, to help my brain, can anyone explain what the following events do?

float totalDose = 0; Does this refer to how much will be dosed each time?

void updateChannel(Channel c); What does Channel c represent, as I see you referred to the channels as 1,2,3,4 below.

void loop() {
    now = Time.hour() * 3600 + Time.minute() * 60 + Time.second();
    for (int i=0;i<4;i++) {
        updateChannel(channels[i]);
    }
    
    delay(500);
}

I presume the first part gets the time from the server, but what is this below…I assume thats some C++ code…?

for (int i=0;i<4;i++) {
            updateChannel(channels[i]);

Does this part tell the program to wait 2 hours until the next dose based on the status of the connectedPin being HIGH or LOW?

void updateChannel(Channel c) {
    
    if (c.onTime >= now) { 
        digitalWrite(c.connectedPin, HIGH);
        c.onTime += 7200; // move on time 2 hours forward
    }

    if (c.offTime >= now) {
        digitalWrite(c.connectedPin, LOW);
        c.offTime += 7200;
    }
}

This part, the only part I understand is totalDose and how long to dose for, in this case 60 seconds?

int setupChannel(String cmd) { // cmd syntax: "channel#,startTime,dose" (startTime is sent as hour*3600 + minute*60 + second)
    char *rmdr;
    char *stringArgs = (char*)cmd.c_str();
    int index = atoi(strtok_r(stringArgs, ",", &rmdr));
    Channel c;
    c.startTime = atof(strtok_r(NULL, ",", &rmdr));
    c.onTime = c.startTime;
    c.totalDose = atoi(rmdr);
    float dosePerActuation = c.totalDose/12;
    float durationPerActuation = dosePerActuation/(c.doseRate*60); // assuming doseRate is in volume/minute
    c.offTime = c.onTime + durationPerActuation;
    return 1;
}

I’m asking questions because I’m trying to get a grasp on what does what for starters.
Depending on the setup and pump quality, each pump may have different flow rates. Cheap pumps typically vary a bit, expensive pumps will be more accurate.

The way I set this up, that is the total dose that you want to give over 1 day (but divided in to 12 equal portions).

c is the argument of type Channel that is passed in to the function. In loop, this function is called once for each channel that you have (4 in my example). So, c will be channels[0] the first time it's called, then channels[1], etc.

No, that's not c++, it's just a for loop that calls a function 4 times.

No, it sets the pin HIGH when any channel's onTime is equal to now, then updates the onTime variable to point to a time 2 hours in the future. The relay is then set to LOW when the offTime is equal to now.

No, the dose time is, as it shows in the code, calculated from the total dose, divided by 12, then divided by the rate that's expressed in volume/min; the 60 is there because we're converting from a volume/minute to a time that's in seconds. There was a mistake in that code, which I corrected; the parentheses were in the wrong place. So for instance, if you had a total dose of 100 mL, and a rate of 1.26 ml/min, the code gives you an offTime that is 396 seconds later than the onTime: ((100/12)/ 1.26) * 60

Well…I guess you can see how ignorant I am with this stuff. I’ll with play with this a bit this week to see how it all works out. I’m sure I’ll have more questions.

I kind of just want to pay someone to do this… :confused:

BTW, when you reply, you should click the reply arrow that’s right under the post you’re replying to so that that person who wrote that answer gets notified. Don’t use the reply arrow that’s at the bottom of the page.

Good to know! That might explain my lack of replies.

As for adding the dose rate for each pump, I presume that would go into typedef struct chunk?

The doseRate is already a member of the struct, I just gave it a default value of 10. To use a different doseRate for different channels, you would need to send that data as part of the Particle function command. So, instead of the command syntax being, “channel#,startTime,dose”, you could send “channel#,startTime,doseRate,dose” and change the setupChannel function to,

int setupChannel(String cmd) { // cmd syntax: "channel#,startTime,doseRate,dose" (startTime is sent as hour*3600 + minute*60 + second)
    char *rmdr;
    char *stringArgs = (char*)cmd.c_str();
    int index = atoi(strtok_r(stringArgs, ",", &rmdr));
    Channel c;
    c.startTime = atof(strtok_r(NULL, ",", &rmdr));
    c.onTime = c.startTime;
    c.doseRate = atof(strtok_r(NULL, ",", &rmdr));
    c.totalDose = atoi(rmdr);
    float dosePerActuation = c.totalDose/12;
    float durationPerActuation = (dosePerActuation/c.doseRate) * 60; // assuming doseRate is in volume/minute
    c.offTime = c.onTime + durationPerActuation;
    return 1;
}

there are a couple of other changes that should be made. I would change the declaration of the struct to,

typedef struct {
    int startTime;
    int onTime;
    int offTime;
    float totalDose;
    float doseRate;
    int connectedPin;
} Channel;

When the struct is initialized, all the member variables will be set to zero. If you don’t set up all for channels, the un-setup ones would try to fire at midnight ( onTime = 0) even though the dose is 0. The onTime and offTime would be the same so it might try to cycle the relay really fast – that might not be such a good thing. So, it would be better to check for a non-zero dose when you update the channels,

void updateChannel(Channel c) {
    
    if (c.onTime >= now && c.dose > 0) { 
        digitalWrite(c.connectedPin, HIGH);
        c.onTime += 7200; // move on time 2 hours forward
    }

    if (c.offTime >= now && c.dose > 0) {
        digitalWrite(c.connectedPin, LOW);
        c.offTime += 7200;
    }
}

With these changes, any channels that you haven’t setup with a call to the Particle function, will be ignored.