Issues with event timing using millis()

Perhaps this topic is too specific, but I've run out of options. A vague description of what I'm doing: I've designed a behavioral apparatus for an ongoing experiment, wherein a triggering event activates of one of several relays and linked solenoid valves for different physical outputs. These outputs are then logged via a Processing sketch via the serial port. I'm simultaneously monitoring two behavioral apparatuses, so the timing of these events is controlled using the millis() function, and recording the initiation time of each event.

My setup:

  • Arduino ATMEGA 2560 (newest revision)
  • Sainsmart 16-channel relay (link)
  • Lee corp solenoids (link)

My issues / questions:

  1. All of the stated time intervals (airTime, waterTime, timeOut, etc.) appear to be doubled when I run the code. Interestingly, when I output the time differences between solenoid open and close, they read as expected (152-154ms, in the case of waterTime, for instance). Is there any obvious reason why this would be the case?

  2. I occasionally lose serial communication, but the arduino otherwise continues to run as expected (continues to activate relays, etc). Maybe I'm just eating up too much SRAM?

The code:

//set pins
const int nosePk1 = 2;     // nosepoke output pin (pin 1 from the nosepoke)
const int fill1 = 3;       //water purge button
const int irPin1 = 4;      // nosepoke IR beam
const int airRelay1 = 7; //relay 1 on sainsmart board, will dispense air
const int waterRelay1 = 6;
const int clickRelay1 = 5;
const int scheduleSwitch1 = 8;     // reward schedule switch
const int pressureSensor = A5; // MPX4250A

//Omron pins (obfuscated)
//Note: Omron IR LED is on by default, and reads LOW in event of poke


// used to monitor beam-breaking
int pokeState1 = LOW;
int prevPokeState1 = LOW;
int pokeState2 = HIGH; //note that Omron reads opposite
int prevPokeState2 = HIGH;

long lastWater1 = 0;
long lastAir1 = 0;
long lastClick1 = 0;
long lastWater2 = 0;
long lastAir2 = 0;
long lastClick2 = 0;
long rightNow;
 
//all times doubled. Unclear why.
const int waterTime = 150; //water solenoid open time (ms)
const int airTime = 250; //air solenoid
const int clickTime = 150; //click solenoid
const int timeOut = 1000; //time-out period for pokes
long lastPoke1 = 0; //holds time (in ms) of last nose-poke
long lastPoke2 = 0;

//for reward activation and recording (not important)
//schedules determined here

//for random clicking
long intrvl;
long lastClick = 0;
const long maxInterval = 600000;

void setup()
{
  Serial.begin(9600);
  // initialize the outputs
  pinMode(irPin1, OUTPUT);
  pinMode(airRelay1, OUTPUT);
  pinMode(waterRelay1, OUTPUT);
  pinMode(clickRelay1, OUTPUT);

  pinMode(airRelay2, OUTPUT);
  pinMode(waterRelay2, OUTPUT);
  pinMode(clickRelay2, OUTPUT);

  //intialize buttons/inputs
  //prevents signal leaking, other issues
  //uses pull-up resistor
  pinMode(scheduleSwitch1, INPUT_PULLUP);
  pinMode(scheduleSwitch2, INPUT_PULLUP);
  pinMode(fill1, INPUT_PULLUP);
  pinMode(fill2, INPUT_PULLUP);

  //close all of the relays
  digitalWrite(airRelay1, HIGH);
  digitalWrite(waterRelay1, HIGH);
  digitalWrite(clickRelay1, HIGH);

  digitalWrite(airRelay2, HIGH);
  digitalWrite(waterRelay2, HIGH);
  digitalWrite(clickRelay2, HIGH);

  // initialize the inputs (nosepokes)
  pinMode(nosePk1, INPUT);
  pinMode(nosePk2, INPUT);
  // IR LED left on continuously for monitoring
  digitalWrite(irPin1, HIGH);

  //set up random integer generation
  //A0 must be left empty for random seeding!
  randomSeed(analogRead(A0));
  intrvl = random(maxInterval);
}

void loop()
{

  //close solenoids, as necessary. Will perform periodically even without input.
  rightNow = millis();
  if ((rightNow - lastWater1) >= waterTime)
  {
    digitalWrite(waterRelay1, HIGH);
  }
  if ((rightNow - lastAir1) >= airTime)
  {
    digitalWrite(airRelay1, HIGH);
  }
  if ((rightNow - lastClick1) >= clickTime)
  {
    digitalWrite(clickRelay1, HIGH);
  }
  if ((rightNow - lastWater2) >= waterTime)
  {
    digitalWrite(waterRelay2, HIGH);
  }

  //get the current time
  rightNow = millis();
  pokeState1 = digitalRead(nosePk1);
  pokeState2 = digitalRead(nosePk2);

  // Island motion monitoring
  // Reads HIGH during poke, else LOW
  if (pokeState1 != prevPokeState1)
  {
    //if the beam is broken, deliver reward, etc.
    if (pokeState1 == HIGH && (rightNow - lastPoke1) >= timeOut)
    {
      cage = 1;
      //set last poke to current time
      toFailOrNotToFail = random(100) + 1; //returns number between 1 & 100, thereby making response probabilistic
      //theoretically runs "failRate" times in 100 trials
      if (toFailOrNotToFail <= failRate)
      {
        dispensation1 = dispense(failSchedule1, cage); //logs failure, dispenses click
      }
      //theoretically runs (100-failRate) times in 100 trials
      else
      {
        dispensation1 = dispense(curSchedule1, cage); //otherwise: dispense standard schedule, log it
      }
      prevPokeState1 = HIGH;
      dispensation1 = dispensation1 + String(cage); //include cage number of analysis
    }
    // if the beam has been restored, collect remaining data
    else if(pokeState1==LOW)
    {
      lastPoke1 = millis();
      dispensation1 = dispensation1 + "," + String(holdPressure) + "," + String(minPressure);
      Serial.println(dispensation1);
      dispensation1 = "";
      minPressure = 700000;
      prevPokeState1 = LOW;
      holdPressure = (getPressure() + getPressure() + getPressure()) / 3;
    }
  }

  // Omron monitoring
  // Reads LOW during poke, else HIGH
  //otherwise identical to above

  //if interval has completed, send a random click
  if ((rightNow - lastClick) >= intrvl)
  {
    dispense(randSchedule,1); //cage number doesn't matter in this case
    intrvl = random(maxInterval);
    lastClick = millis();
    Serial.println("RC");
  }

  delay(10); //absolutely necessary for normal operation
}

String dispense(boolean valves[6], int cageNumber)
{
  //for recording rewards dispensed
  String airDispensed = "FALSE";
  String waterDispensed = "FALSE";
  String clickDispensed = "FALSE";
  float holdPressure = 0; //all will read as 0 for Omron cage
  float minPressure = 0;
  String dispensed;


  //runs if valves[0] (air valve) is true
  //There will never be simualtaneous cage activation, so redundancy is OK
  if (valves[0])
  {
    //activate valve 1 (island motion air valve)
    digitalWrite(airRelay1, LOW);
    lastAir1 = millis();
    airDispensed = "TRUE";
  }
  if (valves[1])
  {
    //activate valve 2 (water valve)
    digitalWrite(waterRelay1, LOW);
    lastWater1 = millis();
    waterDispensed = "TRUE";
  }
  if (valves[2])
  {
    //activate valve 3 (empty valve)
    digitalWrite(clickRelay1, LOW);
    lastClick1 = millis();
    clickDispensed = "TRUE";
  }
  if (valves[3])
  {
    digitalWrite(airRelay2, LOW);
    lastAir2 = millis();
    airDispensed = "TRUE";
  }
  //you get the idea

  dispensed = airDispensed + "," + waterDispensed + "," + clickDispensed + ",";

  if (cageNumber == 1)
  {
    dispensation1 = dispensed;
  }
  else
  {
    dispensation2 = dispensed;
  }
  return dispensed;
}

I'm somewhat of a novice to this, so I'm happy to take any feedback you may have to offer on style, etc.! Any help you may have to offer would be greatly appreciated.

No time now, I have not checked the sketch completely but these should be 'unsigned long'

long lastWater1 = 0;
long lastAir1 = 0;
long lastClick1 = 0;
long lastWater2 = 0;
long lastAir2 = 0;
long lastClick2 = 0;
long rightNow;

Also once a timer interval passes, you may want to reinitializing
.

Ah yes - meant to change that a while ago. That said, experiments only run for a maximum of ~7days, so I wouldn't expect to max out any of those variables in a "normal" run. In any case, these are things I need to learn.

You have a few versions of this

  if ((rightNow - lastWater1) >= waterTime)
  {
    digitalWrite(waterRelay1, HIGH);
  }

but there seems to be no code to update the variable lastWater1

If the idea was to repeat every waterTime interval would expect it to be like this

  if ((rightNow - lastWater1) >= waterTime)
  {
    lastWater1 += waterTime;
    digitalWrite(waterRelay1, HIGH);
  }

If that is not appropriate you need to explain how you want it to work.

...R

All of those variables are updated using the dispense() function

Also worth noting: the Arduino board still reads as available in the device manager, in processing, and in the Arduino IDE, but does not send serial data through to processing or the IDE serial monitor. The system continues to run without issue, otherwise, and continues to "dispense". Resetting the Arduino has no effect, nor does restarting the logging computer. All that seems to work is switching the USB port.

I have the USB power settings on the living computer set to always deliver power. Interestingly, this was not an issue with a previous iteration using half the number of relays and a delay-driven sketch.

ragman922:
All of those variables are updated using the dispense() function

OK, I see that now - I'm not sure how I missed it.

You seem to be using a variable called dispensation1 which does not seem to be defined anywhere.

You seem to be using Strings (capital S) all over the place and they can cause memory corruption in the small memory of an Arduino. Use strings (small s) which are arrays of char terminated with a 0.

It seems silly to use

String airDispensed = "FALSE";

when all you really need is

boolean airDispensed = false;

The use of Strings may account for some of your troubles.

Can you provide an overview of how the program is intended to work - variables like pokeState1 are not very illuminating.

...R

Thanks, Robin. I didn't know about "string", so I'll make that adjustment. As for the "FALSE", etc, I'm analyzing the data with R, and that's how boolean values unfortunately have to be. I guess I could iterate through and change them all, but that's not ideal.

The issues with missing variables are probably from removing some of the code...I ran over the character limit and had to remove some "nonessential" parts. All of the logging works well when I still have a serial connection.

The goal of the system is to have an animal break an IR beam paired to a phototransistor and receive some array of "rewards", ie water, air, etc. The pokeState is therefore the state of the paired phototransistor for a given cage. I can explain more if necessary, but I don't want to make too much of the system public before we've published. Everything has been approved by an IACUC, FYI.

ragman922:
Thanks, Robin. I didn't know about "string", so I'll make that adjustment. As for the "FALSE", etc, I'm analyzing the data with R, and that's how boolean values unfortunately have to be. I guess I could iterate through and change them all, but that's not ideal.

I think you're confusing internal with external representation. If you have to send data serially to another device, you can convert any kind of values to character strings (including booleans). I would hope, at least, that your reporting is centralized and not scattered all through the code. If it is, it should be very straightforward to translate internal states to serial output. This is a very common, routine approach.

Sorry, I don't think I was clear. This is all output to a csv, which I eventually import to R. R reads every value in and then assigns it a data type. 1 or 0 would be stored as an int, as would even "false". "FALSE ", on the other hand, is treated as a boolean. No way around this except to change all the values manually from integers.

As for the serial output, the output string is collected from various methods, concatenated, and then output as a single string. I don't have a choice on the matter, as I have to log the reward schedule and then a pressure drop from another some milliseconds later. I removed that code due to length constraints in my post.

Maybe I wasn't clear. :slight_smile:

It is easy to translate a boolean variable into a string. For example:

char * boolString(byte val)
{
  if (val == true)
    return "TRUE";
  else
    return "FALSE";
}

Ah I see what you mean now. I'll give that a shot, thanks.

In fact, R recognizes "T" as true (forgot about this), so I may just use a char.

In case anyone happens to be following: the issue was apparently just a result of a bad USB cable. Swapping the cable has prevented any further serial interruptions. In any case, this was a nice lesson in best practice.