An oxygen control system based on an optical oxygen sensor

++++UPDATED, SEE MY LATEST POST++++
Dear community,

I would like to present to you my first draft of an oxygen control system based on an optical oxygen sensor. So far, I couldn't find any ressources about a comparable setup, so I thought, this may be of interest for you. Also, I'm a biologist and thus not a trained coder. Please, if you may, have a look at my code and help me to clean it up and make it stable.

For a university project, I wanted to develop an arduino based system that:
i) measures dissolved oxygen in fish tanks
ii) logs the measurements (for now on SD card)
iii) controls the oxygen concentration by operating a magnetic valve through a relay.
iiii) can run autonomously for several weeks

I was able to borrow an optical oxygen sensor from Pyro-Science (http://www.pyro-science.com/piccolo2-oem-optical-oxygen-meter.html) that uses 5V/3.3V serial communication.
The dissolved oxygen measurement is triggered by sending a measurement command via the TX of the Arduino. Measurements are read equally by sending a read command. The sensor echoes the command and appends the measured oxygen concentration. If the measured oxygen concentration is above a certain threshold (say 60% air saturation), a magnetic valve should be opened which will bubble nitrogen gas into the water.
I did not expect to run into a lot of difficulties as the principle of the system is quite straightforward:
trigger measure -> trigger read -> extract oxygen values from serial data -> save to SD -> trigger relay output -> repeat

I use an Arduino Uno in combination with the W5100 Ethernet shield, the PyroScience Piccolo2 oxygen sensor and a 5V 2-channel relay module for Arduino that controls a 24V magnetic valve.
After some troubleshooting, I can now run a first overnight test.

However, as this system should be implemented in a big fish holding facility, it should be super stable and safe against disturbances. As I'm not very experienced in i) coding and ii) kybernetics etc. I'd like to hear from you, what I could improve. Especially the SD-card logger needs improvement. Also the relay-control (I skripted a very simple proportional control function, tried to implement the PID library but that did somehow not trigger the relay at all).

Thank you in advance and I hope that you may find something interesting in this project.
This is the sketch that I am currently working with

#include <SD.h>
#include <SoftwareSerial.h>

//### SD card stuff ###
const int chipSelect = 4;     // chip pin for SD card (UNO: 4; MEGA: 53)
int a = 0;                    // temporary counter index for SD card data

//### Oxygen optode Stuff ###
SoftwareSerial mySer(6, 7);   // RX TX
const byte numChars = 17;     // array length for incoming serial data
char receivedChars[numChars]; // array to store the incoming serial data
char airSatStr[6];            // array to store only air saturation readings from receivedChars
float airSat;                 // variable for air saturation readings
boolean newData = false;      // logical operators 
boolean emptyBuffer = false;

//### Relay stuff ###
float airSatThreshold = 60.00;// threshold for relay operation
float difference;             // variable for difference between air saturation and threshold
float onTime;                 // time window during which magnetic valve is open based on difference between air saturation and threshold
float offTime;                // time window during which magnetic valve is closed
int delayOn;                  // onTime converted to int for delay
int delayOff;                 // offTime converted to int for delay
int relayPin = 12;            // control pin for relay operation


//### Subfunctions for void loop() ###
//### Sensor specific commands ###
void toggleMeasurement(){
  mySer.write("MSR 1");
  mySer.write('\r');
  emptyBuffer = false;
}

void toggleRead(){
  mySer.write("REA 1 3 4");
  mySer.write('\r');
}

//### Receiving serial data ###
void recvWithEndMarker() {      //receives serial data into array until endmarker is received
  static byte ndx = 0;
  char endMarker = '\r';
  char rc;
  while (mySer.available() > 0 && newData == false && emptyBuffer == true) {
    rc = mySer.read();
    if (rc != endMarker) {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0'; // terminate the string
      ndx = 0;
      newData = true;
    }
  }
}

void clearBuffer(){               // similar to recvWithEndMarker(), clears serial buffer
  static byte ndx = 0; 
  char endMarker = '\r';
  char rc;
  while (mySer.available() > 0 && emptyBuffer == false){
    rc = mySer.read();
    if (rc != endMarker) {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0';  // terminate the string
      ndx = 0;
      emptyBuffer = true;
    }
  }
}


void extractAirSat(){             // extracts the last 7 characters from serial data array and transforms to float
  static byte i = 0;
  while(i < 7) {
    airSatStr[i] = receivedChars[(10+i)];
    i++;
  }
  i = 0;
  airSat = atol(airSatStr);
  airSat = airSat/1000;
  difference = airSat - airSatThreshold;
}


void showNewData() {                // send data to serial monitor
  if (newData == true && emptyBuffer == true) {
    Serial.println(airSat);
    newData = false;
  }
}

//### Toggle relay based on measured airSat values ###
void toggleRelay(){
  if(difference >= 1.00){
    onTime = 1.00 - (1/(difference));
    offTime = (1.00 - onTime);
    offTime = 1.00-(difference/(difference+1.00));
    onTime = 1.00 - offTime;
    onTime = onTime * 5000.00;
    offTime = offTime * 5000.00;
    delayOn = (int)onTime;
    delayOff = (int)offTime;
    digitalWrite(relayPin, LOW);
    delay(delayOn);
    digitalWrite(relayPin, HIGH);
    delay(delayOff);
  }
  else digitalWrite(relayPin, HIGH);
  delay(5000);
}


//### Log dissolved oxygen measurements to SD card ###
void writeToSD(){
  File dataFile = SD.open("DOlog.csv", FILE_WRITE); //create excel file with air saturation values
  a=a+1;                                            // increase a by one for each measurement
  dataFile.print(a);                                // print counting index a
  dataFile.print(";");                              // print a semicolon to separate values
  dataFile.println(airSat);                         // save air saturation value
  dataFile.close();                                 // close file temporarily
}

boolean startSDCard() {                             // check whether the SD card is ready
  boolean result = false;
  pinMode(4, OUTPUT); // UNO: 4, MEGA: 53

  if (!SD.begin(chipSelect)) //Can the SD card be read?
  { 
    result = false;
  } 

  else //create file like in the loop
  {  
  File dataFile = SD.open("datalog.csv", FILE_WRITE);
   if (dataFile) 
    {
      dataFile.close();
      result = true;
    }
  }  
  return result;
}

//### Setup ###
void setup() {
// if(startSDCard() == true){
 Serial.begin(19200);
 mySer.begin(19200);
 clearBuffer();
 pinMode(relayPin, OUTPUT);
// }
}

//### Loop ###
void loop() {
  delay(1000);
  toggleMeasurement();
  delay(100);
  clearBuffer();
  toggleRead();
  delay(100);
  recvWithEndMarker();
  delay(100);
  extractAirSat();
  delay(100);
  writeToSD();
  delay(100);
  showNewData();
  delay(1000);
  toggleRelay();
//### test of relay functionality ###
//  digitalWrite(relayPin, LOW);
//  delay(1000);
//  digitalWrite(relayPin, HIGH);
  delay(52500);
}

New and improved version:
Major change: extended the first version of my setup to read out a 4-channel optical oxygen sensor (FireStingO2, Pyro Science).

Further improvements:

  • changed to adafruits datalogger shield with rtc to add timestamps to log-file (link)
  • adapted relay operation algorithm - still needs improvement. I just can't get the PID library to work...
  • changed to arduino mega (-> larger memory and more pins)

Remaining issues:

  • sometimes, all measurements are "shifted" - e.g.: I measure 3 times per channel and average the values. If I would save those measurements in an array, I would expect something like this for channel 1: [chan1_measurement1, chan1_measurement2, chan1_measurement3].
    However, sometimes I get this for the first measurement: [0, chan1_measurement1, chan1_measurement2] followed by this for the second channel: [chan1_measurement3, chan2_measurement1, chan2_measurement2]. In the following measurements, all indices are off by 1. What weirds me out is that this only happens sometimes...
  • implement the PID library for better relay operation
  • implement LCD display of measurements

As the code has grown too large to post it here, I attached the .ino sketch

4_channel_oxygen_control.ino (12.6 KB)

Hi StefanM,

I am planning to make project on atmospheric oxygen control system. So decided to go with the Grove O2 sensor. https://store.arduino.cc/usa/grove-gas-sensor-o2

I have to decide the control mechanism to release O2. Any suggestions from you?

Hi Shaakir,

I guess that you use compressed O2 gas - in that case the cheapest solution would be a solenoid valve - you can get one for around 20$ or cheaper (I use the buerkert 6011 valve, link). You can control it with the arduino using relays (high voltage) or field effect transistors (low voltage, wiki).

However, solenoids can only operate in a binary way - open or closed. If you want to control your O2 values very precisely or steadily, I would suggest you use either of the following two:

a) a mass flow controller can adjust the gas flow and thus adjust O2 concentration more smoothly and precisely. But the ones that I know start at 1000$

b) a gas mixing pump (e.g. by Woesthoff), also rather expensive stuff.

Another option to get a smooth, continuous O2 concentration could be to pre-mix the gases in a reservoir before they enter the experimental chamber.

It really depends on your application and your financial frame what is best for you and there might be more options available that I don't know of but I hope this helped a bit.

Beware of any excess oxygen in a non vented room/enclosure. Just take a spark to cause a flammable material begin to burn.

Paul

Update: The sketch now successfully measures and controls DO in 8 tanks. However, in long term use, there is an issue with the communication between sensor and arduino as it reports erroneous data. My workaround so far is to reboot the arduino every time there is a communication mismatch.

I plan to raise a topic with this issue in another part of this forum so please don't reply here or send me a pm.

You can find the most recent version of the sketch plus a very brief description in my github repository: GitHub - muchaste/ArdOxy: An Arduino controlled system for automated oxygen control in fish tanks with FireStingO2 optical oxygen sensors

Hello!

I am doing an aqua culture project which uses an Arduino mega to monitor and control a fish pond. I monitor pH,Temperature and Dissolved Oxygen. My dissolved oxygen sensor uses a filling solution which requires timing changing which in my case is very hard as the site is in a remote location and doing is quite a burden, Can you suggest me with a good Dissolved oxygen sensor to be used continuously and which is compatible with Arduino mega. Hoping to hear from you soon.Thanks in advance.

Cheers!
Moh

RE_MAK:
Hello!

I am doing an aqua culture project which uses an Arduino mega to monitor and control a fish pond. I monitor pH,Temperature and Dissolved Oxygen. My dissolved oxygen sensor uses a filling solution which requires timing changing which in my case is very hard as the site is in a remote location and doing is quite a burden, Can you suggest me with a good Dissolved oxygen sensor to be used continuously and which is compatible with Arduino mega. Hoping to hear from you soon.Thanks in advance.

Cheers!
Moh

There are other dissolved oxygen sensors out there that don't require filling solution such as this one