Interactive, Bidirectional Communications

Things you need:

  • Arduino, 5V tolerant
  • 1 or 2 Tentacle Shield(s)
  • 1-8 Atlas Scientific circuits
  • Computer with Arduino IDE installed

Difficulty: easy

You can find all code mentioned on this page, including a special Arduino Yún version on GitHub.

All circuits in UART mode

  1. Set all your circuits to UART mode at 38400 baud.
  2. Upload the code below to your Arduino
  3. Open the Arduino IDE serial monitor @9600 baud
  4. Type the channel number, a colon and the command into the serial monitor like so:
    0:r
// WhiteBox Labs -- Tentacle Shield --  UART interactive example
// https://www.whiteboxes.ch/tentacle
//
// This code is based on https://www.atlas-scientific.com/_files/code/4-port-board.pdf
//
// How to use 4 (or 8 if using 2 Tentacle shields) Atlas Scientivic devices in serial mode
// and interact with them via the serial monitor.
// This sketch assumes all of your devices are either older serial devices or EZO circuits
// in serial mode. You can use the tentacle_setup.ino to autodetect and setup your devices.
//
// This code is intended to work on all 5V-tolerant Arduinos. If using the Arduino Yun, connect
// to it's serial port. If you want to work with the Yun wirelessly, check out the respective
// Yun version of this example.
//
// USAGE:
//---------------------------------------------------------------------------------------------
// - Set all your EZO circuits to UART @38400 baud before using this sketch.
//    - You can use the "tentacle-steup.ino" sketch to do so)
//
// - Set host serial terminal to 9600 baud
//
//  - Serial channel numbers are 0-3
//    ( Channels 4-7 are also available, if you're using two stacked Tentacle shields)
//    in UART mode, the channel number is defined by physical location on the shield
//
// - To open a channel send the number of the channel, a colon and the command ending with a carriage return.   
//
// 0:r<CR>
// 1:i<CR>
// 2:c<CR>
// 3:r<CR>
//
//---------------------------------------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
//---------------------------------------------------------------------------------------------

#include <SoftwareSerial.h>         //Include the software serial library  

SoftwareSerial sSerial(11, 10);     // RX, TX  - Name the software serial library sftSerial (this cannot be omitted)
                                    // assigned to pins 10 and 11 for maximum compatibility
int s0 = 7;                         // Tentacle uses pin 7 for multiplexer control S0
int s1 = 6;                         // Tentacle uses pin 6 for multiplexer control S1
int enable_1 = 5;	            // Tentacle uses pin 5 to control pin E on shield 1
int enable_2 = 4;	            // Tentacle uses pin 4 to control pin E on shield 2

char computerdata[20];              // A 20 byte character array to hold incoming data from a pc/mac/other
char sensordata[30];                // A 30 byte character array to hold incoming data from the sensors
byte computer_bytes_received = 0;   // We need to know how many characters bytes have been received
byte sensor_bytes_received = 0;     // We need to know how many characters bytes have been received

char *channel;                      // Char pointer used in string parsing
char *cmd;                          // Char pointer used in string parsing



void setup() {
  pinMode(s0, OUTPUT);             // set the digital output pins for the serial multiplexer
  pinMode(s1, OUTPUT);
  pinMode(enable_1, OUTPUT);
  pinMode(enable_2, OUTPUT);

  Serial.begin(9600);              // Set the hardware serial port to 9600
  sSerial.begin(38400);             // Set the soft serial port to 9600 (change if all your devices use another baudrate)
  intro();                         // display startup message
}


void serialEvent() {               //This interrupt will trigger when the data coming from the serial monitor(pc/mac/other) is received
  computer_bytes_received = Serial.readBytesUntil(13, computerdata, 20); //We read the data sent from the serial monitor(pc/mac/other) until we see a <CR>. We also count how many characters have been received
  computerdata[computer_bytes_received] = 0;      //We add a 0 to the spot in the array just after the last character we received.. This will stop us from transmitting incorrect data that may have been left in the buffer
}


void loop() {

  if (computer_bytes_received != 0) {             // If computer_bytes_received does not equal zero
    channel = strtok(computerdata, ":");          // Let's parse the string at each colon
    cmd = strtok(NULL, ":");
    open_channel();                               // Call the function "open_channel" to open the correct data path
    if (cmd != 0) {                               // if no command has been sent, send nothing
      sSerial.print(cmd);                         // Send the command from the computer to the Atlas Scientific device using the softserial port
      sSerial.print("\r");                        // <CR> carriage return to terminate message
    }
    computer_bytes_received = 0;                  // Reset the var computer_bytes_received
  }

  if (sSerial.available() > 0) {                 // If data has been transmitted from an Atlas Scientific device
    sensor_bytes_received = sSerial.readBytesUntil(13, sensordata, 30); //we read the data sent from the Atlas Scientific device until we see a <CR>. We also count how many character have been received
    sensordata[sensor_bytes_received] = 0;       // we add a 0 to the spot in the array just after the last character we received. This will stop us from transmitting incorrect data that may have been left in the buffer
    Serial.println(sensordata);                  // let’s transmit the data received from the Atlas Scientific device to the serial monitor
  }
}


// Open a channel via the Tentacle serial multiplexer
void open_channel() {

  switch (*channel) {

    case '0':                                // if channel==0 then we open channel 0
      digitalWrite(enable_1, LOW);           // setting enable_1 to low activates primary channels: 0,1,2,3
      digitalWrite(enable_2, HIGH);          // setting enable_2 to high deactivates secondary channels: 4,5,6,7
      digitalWrite(s0, LOW);                 // S0 and S1 control what channel opens
      digitalWrite(s1, LOW);                 // S0 and S1 control what channel opens
      break;

    case '1':
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, LOW);
      break;

    case '2':
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, LOW);
      digitalWrite(s1, HIGH);
      break;

    case '3':
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, HIGH);
      break;

    case '4':
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, LOW);
      digitalWrite(s1, LOW);
      break;

    case '5':
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, LOW);
      break;

    case '6':
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, LOW);
      digitalWrite(s1, HIGH);
      break;

    case '7':
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, HIGH);
      break;
  }
}


// Print intro
void intro() {
  Serial.flush();
  Serial.println(" ");
  Serial.println("READY_");
}

All circuits in I2C mode

  1. Set all your I2C circuits to a unique ID (they are by default – unless you have multiple of the same type, e.g. 2x pH EZO)
  2. Upload the code below to your Arduino
  3. Open the Arduino IDE serial monitor @9600 baud
  4. Type the channel number, a colon and the command into the serial monitor like so:
    99:r
// WhiteBox Labs -- Tentacle Shield -- I2C interactive example
// https://www.whiteboxes.ch/tentacle
//
// How to use 4 (or 8 if using 2 Tentacle shields) Atlas Scientivic devices in I2C mode
// and interact with them via the serial monitor.
//
// This code is intended to work on all Arduinos. If using the Arduino Yun, connect
// to it's serial port. If you want to work with the Yun wirelessly, check out the respective
// Yun version of this example.
//
// USAGE:
//---------------------------------------------------------------------------------------------
// - Set all your EZO circuits to I2C before using this sketch.
//    - You can use the "tentacle-steup.ino" sketch to do so)
//    - Make sure each circuit has a unique I2C ID set 
// - Set host serial terminal to 9600 baud
//
// - To send a command, send the number of the i2c address, a colon and the command ending with a carriage return.
//
// - To issue a command, enter it directly to the console.
//
// 102:r<CR>
// 99:i<CR>
// 100:c<CR>
// 99:r<CR>
// 110:cal,mid,7.00<CR>
//
//---------------------------------------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
//---------------------------------------------------------------------------------------------

#include <Wire.h>                   // enable I2C.

unsigned long serial_host  = 9600;  // set baud rate for host serial monitor(pc/mac/other)

char sensordata[30];                // A 30 byte character array to hold incoming data from the sensors
byte computer_bytes_received = 0;   // We need to know how many characters bytes have been received
byte sensor_bytes_received = 0;     // We need to know how many characters bytes have been received
int channel;                        // INT pointer for channel switching - 0-7 serial, 8-127 I2C addresses
char *cmd;                          // Char pointer used in string parsing

char computerdata[48];              // we make a 20 byte character array to hold incoming data from a pc/mac/other.
byte code = 0;                      // used to hold the I2C response code.
byte in_char = 0;                   // used as a 1 byte buffer to store in bound bytes from the I2C Circuit.
int time;                   	    // used to change the dynamic polling delay needed for I2C read operations.


void setup() {                      // startup function
  Serial.begin(serial_host);	    // Set the hardware serial port.
  Wire.begin();			    // enable I2C port.
  intro();			    // display startup message
}


void serialEvent() {               						// This interrupt will trigger when the data coming from the serial monitor(pc/mac/other) is received
  computer_bytes_received = Serial.readBytesUntil(13, computerdata, 20); 	// We read the data sent from the serial monitor(pc/mac/other) until we see a <CR>. We also count how many characters have been received
  computerdata[computer_bytes_received] = 0; 				        // We add a 0 to the spot in the array just after the last character we received.. This will stop us from transmitting incorrect data that may have been left in the buffer
}


void loop() {                                 	// main loop

  if (computer_bytes_received != 0) {           // If computer_bytes_received does not equal zero
    
    channel = atoi(strtok(computerdata, ":"));  // Let's parse the string at each colon
    cmd = strtok(NULL, ":");                    // Let's parse the string at each colon

    I2C_call();		                        // send to I2C

    computer_bytes_received = 0;                // Reset the var computer_bytes_received to equal 0
  }

}

void intro() {                                  // print intro
  Serial.flush();
  Serial.println(" ");
  Serial.println("READY_");
}


void I2C_call() {  			        // function to parse and call I2C commands
  sensor_bytes_received = 0;                    // reset data counter
  memset(sensordata, 0, sizeof(sensordata));    // clear sensordata array;

  if (cmd[0] == 'c' || cmd[0] == 'r')time = 1400;
  else time = 300;                              //if a command has been sent to calibrate or take a reading we
  //wait 1400ms so that the circuit has time to take the reading.
  //if any other command has been sent we wait only 300ms.
  
  Wire.beginTransmission(channel); 	// call the circuit by its ID number.
  Wire.write(cmd);        		// transmit the command that was sent through the serial port.
  Wire.endTransmission();          	// end the I2C data transmission.

  delay(time);

  code = 254;				// init code value

  while (code == 254) {                 // in case the cammand takes longer to process, we keep looping here until we get a success or an error

    Wire.requestFrom(channel, 48, 1);   // call the circuit and request 48 bytes (this is more then we need).
    code = Wire.read();

    while (Wire.available()) {          // are there bytes to receive.
      in_char = Wire.read();            // receive a byte.

      if (in_char == 0) {               // if we see that we have been sent a null command.
        Wire.endTransmission();         // end the I2C data transmission.
        break;                          // exit the while loop.
      }
      else {
        sensordata[sensor_bytes_received] = in_char;  // load this byte into our array.
        sensor_bytes_received++;
      }
    }


    switch (code) {                  	// switch case based on what the response code is.
      case 1:                       	// decimal 1.
        Serial.println("Success");  	// means the command was successful.
        break;                        	// exits the switch case.

      case 2:                        	// decimal 2.
        Serial.println("< command failed");    	// means the command has failed.
        break;                         	// exits the switch case.

      case 254:                      	// decimal 254.
        Serial.println("< command pending");   	// means the command has not yet been finished calculating.
        delay(200);                     // we wait for 200ms and give the circuit some time to complete the command
        break;                         	// exits the switch case.

      case 255:                      	// decimal 255.
        Serial.println("No Data");   	// means there is no further data to send.
        break;                         	// exits the switch case.
    }

  }

  Serial.println(sensordata);	        // print the data.
}

Mixed UART and I2C mode

  1. Set all your I2C circuits to a unique ID (they are by default – unless you have multiple of the same type, e.g. 2x pH EZO)
  2. Set all your UART circuits to 38400 Baud
  3. Upload the code below to your Arduino
  4. Open the Arduino IDE serial monitor @9600 baud
  5. Type the channel number, a colon and the command into the serial monitor like so:
    0:r
    99:r
// WhiteBox Labs -- Tentacle Shield -- Mixed I2C and UART interactive example
// https://www.whiteboxes.ch/tentacle
//
// How to use 4 (or 8 if using 2 Tentacle shields) Atlas Scientivic devices in I2C mode and
// UART mode mixed and interact with them via the serial monitor.
//
// This code is intended to work on all Arduinos. If using the Arduino Yun, connect
// to it's serial port. If you want to work with the Yun wirelessly, check out the respective
// Yun version of this example.
//
// USAGE:
//---------------------------------------------------------------------------------------------
// - Set host serial terminal to 9600 baud
// - Set the Atlas Scientific devices to 38400 baud or use them I2C mode
//
// - To send a command, send the number of the channel, a colon and the command ending with a carriage return.
//   - serial channels are  numbered 0 - 7
//   - i2c addresses are numbered 8 - 127
// 
// - To issue a command, enter it directly to the console.
//
// 1:r<CR>
// 2:i<CR>
// 3:c<CR>
// 99:r<CR>
// 110:cal,mid,7.00<CR>
//
//---------------------------------------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
//---------------------------------------------------------------------------------------------


#include <SoftwareSerial.h>      // Include the software serial library  
#include <Wire.h>                // enable I2C.

SoftwareSerial sSerial(11, 10);  // RX, TX  - Name the software serial library sSerial (this cannot be omitted)
//assigned to pins 10 and 11 for maximum compatibility

int s0 = 7;                         // Arduino pin 7 to control pin S0
int s1 = 6;                         // Arduino pin 6 to control pin S1
int enable_1 = 5;	            // Arduino pin 5 to control pin E on board 1
int enable_2 = 4;                   // Arduino pin 4 to control pin E on board 2

char sensordata[30];                // A 30 byte character array to hold incoming data from the sensors
byte computer_bytes_received = 0;   // We need to know how many characters bytes have been received
byte sensor_bytes_received = 0;     // We need to know how many characters bytes have been received
int channel;                        // INT pointer for channel switching - 0-7 serial, 8-127 I2C addresses
char *cmd;                          //Char pointer used in string parsing

char computerdata[48];              // we make a 20 byte character array to hold incoming data from a pc/mac/other.

byte code = 0;                      // used to hold the I2C response code.
byte in_char = 0;                   // used as a 1 byte buffer to store in bound bytes from the I2C Circuit.
int time;                   	    // used to change the dynamic polling delay needed for I2C read operations.

boolean I2C_mode = false;	    // bool switch for serial/I2C


void setup() {                      // startup function
  pinMode(s1, OUTPUT);		    // Set the digital pin as output.
  pinMode(s0, OUTPUT);	            // Set the digital pin as output.
  pinMode(enable_1, OUTPUT);	    // Set the digital pin as output.
  pinMode(enable_2, OUTPUT);	    // Set the digital pin as output.
  Serial.begin(9600);	    // Set the hardware serial port.
  sSerial.begin(38400);	    // Set the soft serial port to rate of default channel (0).
  Wire.begin();			    // enable I2C port.
  intro();			    // display startup message
}


void serialEvent() {               						// This interrupt will trigger when the data coming from the serial monitor(pc/mac/other) is received
  computer_bytes_received = Serial.readBytesUntil(13, computerdata, 20); 	// We read the data sent from the serial monitor(pc/mac/other) until we see a <CR>. We also count how many characters have been received
  computerdata[computer_bytes_received] = 0; 				        // We add a 0 to the spot in the array just after the last character we received.. This will stop us from transmitting incorrect data that may have been left in the buffer
}


void loop() {                                 	// main loop

  if (computer_bytes_received != 0) {           // If computer_bytes_received does not equal zero
    channel = atoi(strtok(computerdata, ":"));  // Let's parse the string at each colon
    cmd = strtok(NULL, ":");                    // Let's parse the string at each colon
    open_channel();                             // Call the function "open_channel" to open the correct data path

    if (I2C_mode == false) {	  // if serial channel selected
      sSerial.print(cmd);         // Send the command from the computer to the Atlas Scientific device using the softserial port
      sSerial.print("\r");        // After we send the command we send a carriage return <CR>
    } else {			  // if I2C address selected
      I2C_call();		  // send to I2C
    }

    computer_bytes_received = 0;  // Reset the var computer_bytes_received to equal 0
  }

  if (sSerial.available() > 0) {                   				// If data has been transmitted from an Atlas Scientific device
    sensor_bytes_received = sSerial.readBytesUntil(13, sensordata, 30); 	// we read the data sent from the Atlas Scientific device until we see a <CR>. We also count how many character have been received
    sensordata[sensor_bytes_received] = 0;           				// we add a 0 to the spot in the array just after the last character we received. This will stop us from transmitting incorrect data that may have been left in the buffer
    Serial.println(sensordata);                    				// let’s transmit the data received from the Atlas Scientific device to the serial monitor
  }
}

void intro() {                                // print intro
  Serial.flush();
  Serial.println(" ");
  Serial.println("READY_");
}


void open_channel() {                         // function controls which UART/I2C port is opened.
  I2C_mode = false;			      // false for serial, true for I2C
  switch (channel) {                          // Looking to see what channel to open

    case 0:                                   // If channel==0 then we open channel 0
      digitalWrite(enable_1, LOW);            // Setting enable_1 to low activates primary channels: 0,1,2,3
      digitalWrite(enable_2, HIGH);           // Setting enable_2 to high deactivates secondary channels: 4,5,6,7
      digitalWrite(s0, LOW);                  // S0 and S1 control what channel opens
      digitalWrite(s1, LOW);                  // S0 and S1 control what channel opens
      break;                                  // Exit switch case

    case 1:
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, LOW);
      break;

    case 2:
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, LOW);
      digitalWrite(s1, HIGH);
      break;

    case 3:
      digitalWrite(enable_1, LOW);
      digitalWrite(enable_2, HIGH);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, HIGH);
      break;

    case 4:
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, LOW);
      digitalWrite(s1, LOW);
      break;

    case 5:
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, LOW);
      break;

    case 6:
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, LOW);
      digitalWrite(s1, HIGH);
      break;

    case 7:
      digitalWrite(enable_1, HIGH);
      digitalWrite(enable_2, LOW);
      digitalWrite(s0, HIGH);
      digitalWrite(s1, HIGH);
      break;

    default:					// I2C mode
      digitalWrite(enable_1, HIGH);		// disable soft serial
      digitalWrite(enable_2, HIGH);		// disable soft serial

      if (channel <= 127) {
        I2C_mode = true;			// 0 for serial, 1 for I2C
        return;
      }

  }
}


void I2C_call() {  			        // function to parse and call I2C commands
  sensor_bytes_received = 0;                    // reset data counter
  memset(sensordata, 0, sizeof(sensordata));    // clear sensordata array;

  if (cmd[0] == 'c' || cmd[0] == 'r')time = 1400;
  else time = 300;                              //if a command has been sent to calibrate or take a reading we
  //wait 1400ms so that the circuit has time to take the reading.
  //if any other command has been sent we wait only 300ms.
  
  Wire.beginTransmission(channel); 	// call the circuit by its ID number.
  Wire.write(cmd);        		// transmit the command that was sent through the serial port.
  Wire.endTransmission();          	// end the I2C data transmission.

  delay(time);

  code = 254;				// init code value

  while (code == 254) {                 // in case the cammand takes longer to process, we keep looping here until we get a success or an error

    Wire.requestFrom(channel, 48, 1);   // call the circuit and request 48 bytes (this is more then we need).
    code = Wire.read();

    while (Wire.available()) {          // are there bytes to receive.
      in_char = Wire.read();            // receive a byte.

      if (in_char == 0) {               // if we see that we have been sent a null command.
        Wire.endTransmission();         // end the I2C data transmission.
        break;                          // exit the while loop.
      }
      else {
        sensordata[sensor_bytes_received] = in_char;  // load this byte into our array.
        sensor_bytes_received++;
      }
    }


    switch (code) {                  	// switch case based on what the response code is.
      case 1:                       	// decimal 1.
        //Serial.println("Success");  	// means the command was successful.
        break;                        	// exits the switch case.

      case 2:                        	// decimal 2.
        Serial.println("< command failed");    	// means the command has failed.
        break;                         	// exits the switch case.

      case 254:                      	// decimal 254.
        //Serial.println("< command pending");   	// means the command has not yet been finished calculating.
        delay(200);                     // we wait for 200ms and give the circuit some time to complete the command
        break;                         	// exits the switch case.

      case 255:                      	// decimal 255.
        Serial.println("No Data");   	// means there is no further data to send.
        break;                         	// exits the switch case.
    }

  }

  Serial.println(sensordata);	// print the data.
}

Found a bug?

Something working not just right? Help us keeping the docs & examples up to date – leave a comment if you find any problems. Thank You.

Leave a Reply

Your email address will not be published. Required fields are marked *