// WhiteBox Labs -- Tentacle Shield -- Circuit Setup // https://www.whiteboxes.ch/tentacle // // Tool to help you setup multiple sensor circuits from Atlas Scientific // It will allow you to control up to 8 Atlas Scientific devices through 1 soft serial RX/TX line or more through the I2C bus // For serial stamps (legacy or EZO-stamps in serial mode), the baudrate is detected automatically. // // THIS IS A TOOL TO SETUP YOUR CIRCUITS INTERACTIVELY. THIS CODE IS NOT INTENDED AS A BOILERPLATE FOR YOUR PROJECT. // // 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 // - To open a serial channel (numbered 0 - 7), send the number of the channel // - To open a I2C address (between 8 - 127), send the number of the address // - To issue a command, enter it directly to the console. // //--------------------------------------------------------------------------------------------- // // 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 . // //--------------------------------------------------------------------------------------------- #include //enable I2C. #if defined (ARDUINO_SAM_DUE) // On Arduino Due the standard I2C pins are Wire1, not Wire. #define WIRE Wire1 #else #define WIRE Wire #endif #if defined (ARDUINO_SAM_DUE) || defined (ARDUINO_SAMD_ZERO) #define I2C_ONLY 1 #endif #if !defined(I2C_ONLY) #include //Include the software serial library 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 const int s0 = 7; //Arduino pin 7 to control pin S0 const int s1 = 6; //Arduino pin 6 to control pin S1 const int enable_1 = 5; //Arduino pin 5 to control pin E on shield 1 const int enable_2 = 4; //Arduino pin 4 to control pin E on shield 2 #endif char sensordata[32]; //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 int retries; // com-check functions store number of retries here boolean answerReceived; // com-functions store here if a connection-attempt was successful byte error; // error-byte to store result of Wire.transmissionEnd() String stamp_type; // hold the name / type of the stamp char stamp_version[4]; // hold the version of the stamp char computerdata[20]; //we make a 20 byte character array to hold incoming data from a pc/mac/other. int computer_in_byte; boolean computer_msg_complete = false; byte i2c_response_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 an I2C stamp. #if !defined (I2C_ONLY) const long validBaudrates[] = { // list of baudrates to try when connecting to a stamp (they're ordered by probability to speed up things a bit) 38400, 19200, 9600, 115200, 57600 }; long channelBaudrate[] = { // store for the determined baudrates for every stamp 0, 0, 0, 0, 0, 0, 0, 0 }; #endif boolean I2C_mode = false; //bool switch for serial/I2C void setup() { #if !defined (I2C_ONLY) 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. #endif Serial.begin(9600); // Set the hardware serial port to 9600 while (!Serial) ; // Leonardo-type arduinos need this to be able to write to the serial port in setup() #if !defined (I2C_ONLY) sSerial.begin(38400); // Set the soft serial port to 38400 #endif WIRE.begin(); // enable I2C port. stamp_type.reserve(16); // reserve string buffer to save some SRAM intro(); // display startup message } void loop() { while (Serial.available() > 0) { // While there is serial data from the computer computer_in_byte = Serial.read(); // read a byte if (computer_in_byte == '\n' || computer_in_byte == '\r') { // if a newline character arrives, we assume a complete command has been received computerdata[computer_bytes_received] = 0; computer_msg_complete = true; computer_bytes_received = 0; } else { // command not complete yet, so just ad the byte to data array computerdata[computer_bytes_received] = computer_in_byte; computer_bytes_received++; } } if (computer_msg_complete) { // if there is a complete command from the computer cmd = computerdata; // Set cmd with incoming serial data if (String(cmd) == F("help")) { //if help entered... help(); //call help dialogue computer_msg_complete = false; //Reset the var computer_bytes_received to equal 0 return; } else if (String(cmd) == F("scan")) { // if scan requested scan(true); computer_msg_complete = false; // Reset the var computer_bytes_received to equal 0 return; } else if (String(cmd) == F("scani2c")) { // if i2c-scan requested scan(false); computer_msg_complete = false; // Reset the var computer_bytes_received to equal 0 return; } else { // it's not a known command, so probaby it's a channel number // TODO: without loop? for (int x = 0; x <= 127; x++) { //loop through input searching for a channel change request (integer between 1 and 127) if (String(cmd) == String(x)) { Serial.print(F("changing channel to ")); Serial.println( cmd); channel = atoi(cmd); //set channel variable to number 0-127 if (change_channel()) { //set MUX switches or I2C address Serial.println( F("-------------------------------------")); Serial.print( F("ACTIVE channel : ")); Serial.println( channel ); Serial.print( F("Type: ")); Serial.print( stamp_type); Serial.print( F(", Version: ")); Serial.print( stamp_version); Serial.print( F(", COM: ")); #if !defined (I2C_ONLY) if (channel > 8) { Serial.println(F("I2C")); } else { Serial.print( F("UART (")); Serial.print( String(channelBaudrate[channel])); Serial.println(F(" baud)")); } #else Serial.println(F("I2C")); #endif } else { Serial.println(F("CHANNEL NOT AVAILABLE! Empty slot? Different COM-mode?")); Serial.println(F("Try 'scan' or set baudrate manually (see 'help').")); } computer_msg_complete = false; //Reset the var computer_bytes_received to equal 0 return; } } #if !defined (I2C_ONLY) for ( int x = 0; x < 5; x++) { // check if a baudrate was entered manually if (String(cmd) == String(validBaudrates[x])) { Serial.print(F("Setting baudrate manually to ")); Serial.println(validBaudrates[x]); channelBaudrate[channel] = validBaudrates[x]; String(channel).toCharArray(cmd, 4); // do a "fake" command for the next loop-run, as if the channel number was entered again (will refresh info with the manually set baudrate) return; // do not set computer_bytes_received to 0, so the channel-command gets handled right away } } #endif if (String(cmd).startsWith(F("serial,")) || String(cmd).startsWith(F("i2c,"))) { Serial.println(F("! when switching from i2c to serial or vice-versa,")); Serial.println(F("! don't forget to switch the hardware jumpers accordingly.")); } } Serial.print(F("> ")); // echo to the serial console Serial.println(cmd); if (I2C_mode == false) { //if serial channel selected #if !defined (I2C_ONLY) 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 #endif } else { //if I2C address selected I2C_call(); // send i2c command and wait for answer if (sensor_bytes_received > 0) { Serial.print(F("< ")); Serial.println(sensordata); //print the data. } } computer_msg_complete = false; //Reset the var computer_bytes_received to equal 0 } #if !defined (I2C_ONLY) 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 . 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.print(F("< ")); Serial.println(sensordata); //let’s transmit the data received from the Atlas Scientific device to the serial monitor } #endif } boolean change_channel() { //function controls which UART/I2C port is opened. returns true if channel could be changed. I2C_mode = false; //0 for serial, 1 for I2C stamp_type = ""; memset(stamp_version, 0, sizeof(stamp_version)); // clear stamp info #if !defined (I2C_ONLY) change_serial_mux_channel(); // configure serial muxer(s) (or disable if we're in I2C mode) if (channel < 8) { // serial? if (!check_serial_connection()) { // determine and set the correct baudrate for this stamp return false; } return true; } else { // TODO: without loop? for (int x = 8; x <= 127; x++) { if (channel == x) { I2C_mode = true; if ( !check_i2c_connection() ) { // check if this I2C port can be opened return false; } return true; } } } #else for (int x = 1; x <= 127; x++) { if (channel == x) { I2C_mode = true; if ( !check_i2c_connection() ) { // check if this I2C port can be opened return false; } return true; } } #endif } byte I2C_call() { //function to parse and call I2C commands. sensor_bytes_received = 0; // reset data counter memset(sensordata, 0, sizeof(sensordata)); // clear sensordata array; //Serial.print("beginning transmission... "); WIRE.beginTransmission(channel); //call the circuit by its ID number. //Serial.print("transmit command: "); //Serial.print(cmd); WIRE.write(cmd); //transmit the command that was sent through the serial port. //Serial.println("... ending transmission"); error = WIRE.endTransmission(); //end the I2C data transmission. //Serial.println("command sent"); if (error != 0) { //Serial.print("i2c error "); //Serial.println(error); } i2c_response_code = 254; while (i2c_response_code == 254) { // in case the cammand takes longer to process, we keep looping here until we get a success or an error if (String(cmd).startsWith(F("cal")) || String(cmd).startsWith(F("Cal")) ) { delay(1400); // cal-commands take 1300ms or more } else if (String(cmd) == F("r") || String(cmd) == F("R")) { delay(1000); // reading command takes about a second } else { delay(300); // all other commands: wait 300ms } //Serial.println("requesting data from device"); WIRE.requestFrom(channel, 32); //call the circuit and request 48 bytes (this is more then we need). i2c_response_code = WIRE.read(); //the first byte is the response code, we read this separately. //Serial.print("response code: "); //Serial.println(i2c_response_code); 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. while (WIRE.available()) { WIRE.read(); // some arduinos (e.g. ZERO) put padding zeroes in the receiving buffer (up to the number of requested bytes) } break; //exit the while loop. } else { sensordata[sensor_bytes_received] = in_char; //load this byte into our array. sensor_bytes_received++; } } switch (i2c_response_code) { //switch case based on what the response code is. case 1: //decimal 1. Serial.println( F("< success")); //means the command was successful. break; //exits the switch case. case 2: //decimal 2. Serial.println( F("< command failed")); //means the command has failed. break; //exits the switch case. case 254: //decimal 254. Serial.println( F("< command pending")); //means the command has not yet been finished calculating. break; //exits the switch case. case 255: //decimal 255. Serial.println( F("< no data")); //means there is no further data to send. break; //exits the switch case. } } } #if !defined (I2C_ONLY) void change_serial_mux_channel() { // configures the serial muxers depending on channel. 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: digitalWrite(enable_1, HIGH); //disable soft serial digitalWrite(enable_2, HIGH); //disable soft serial } } boolean check_serial_connection() { // check the selected serial port. find and set baudrate, request info from the stamp // will return true if there is a stamp on this serial channel, false otherwise answerReceived = true; // will hold if we received any answer. also true, if no "correct" baudrate has been found, but still something answered. retries = 0; if (channelBaudrate[channel] > 0) { sSerial.begin(channelBaudrate[channel]); while (retries < 3 && answerReceived == true) { answerReceived = false; if (request_serial_info()) { return true; } } } answerReceived = true; // will hold if we received any answer. also true, if no "correct" baudrate has been found, but still something answered. while (retries < 3 && answerReceived == true) { // we don't seem to know the correct baudrate yet. try it 3 times (in case it doesn't work right away) answerReceived = false; // we'll toggle this to know if we received an answer, even if no baudrate matched. probably a com-error, so we'll just retry. if (scan_baudrates()) { return true; } retries++; } return false; // no stamp was found at this channel } boolean scan_baudrates() { // scans baudrates to auto-detect the right one for this uart channel. if one is found, it is saved globally in channelBaudrate[] for (int j = 0; j < 5; j++) { // TODO: make this work for legacy stuff and EZO in uart / continuous mode sSerial.begin(validBaudrates[j]); // open soft-serial port with a baudrate sSerial.print(F("\r")); sSerial.flush(); // buffers are full of junk, clean up sSerial.print(F("c,0\r")); // switch off continuous mode for new ezo-style stamps delay(150); //clearIncomingBuffer(); // buffers are full of junk, clean up sSerial.print(F("e\r")); // switch off continous mode for legacy stamps delay(150); // give the stamp some time to burp an answer clearIncomingBuffer(); int r_retries = 0; answerReceived = true; while (r_retries < 3 && answerReceived == true) { answerReceived = false; if (request_serial_info()) { // check baudrate for correctness by parsing the answer to "i"-command channelBaudrate[channel] = validBaudrates[j]; // we found the correct baudrate! return true; } r_retries++; } } return false; // we could not determine a correct baudrate } boolean request_serial_info() { // helper to request info from a uart stamp and parse the answer into the global stamp_ variables clearIncomingBuffer(); sSerial.write("i"); // send "i" which returns info on all versions of the stamps sSerial.write("\r"); delay(150); // give it some time to send an answer sensor_bytes_received = sSerial.readBytesUntil(13, sensordata, 9); //we read the data sent from the Atlas Scientific device until we see a . We also count how many character have been received if (sensor_bytes_received > 0) { // there's an answer answerReceived = true; // so we can globally know if there was an answer on this channel if ( parseInfo() ) { // try to parse the answer string delay(100); clearIncomingBuffer(); // some stamps burp more info (*OK or something). we're not interested yet. return true; } } return false; // it was not possible to get info from the stamp } #endif boolean check_i2c_connection() { // check selected i2c channel/address. verify that it's working by requesting info about the stamp retries = 0; while (retries < 3) { retries++; WIRE.beginTransmission(channel); // just do a short connection attempt without command to scan i2c for devices //WIRE.write((uint8_t)0); error = WIRE.endTransmission(); if (error == 0) // if error is 0, there's a device { int r_retries = 0; while (r_retries < 3) { r_retries++; //Serial.print("retry: "); //Serial.println(r_retries); cmd = "i"; // set cmd to request info (in I2C_call()) I2C_call(); if (parseInfo()) { return true; } } return false; } else { return false; // no device at this address } } } boolean parseInfo() { // parses the answer to a "i" command. returns true if answer was parseable, false if not. // example: // PH EZO -> '?I,pH,1.1' // ORP EZO -> '?I,OR,1.0' (-> wrong in documentation 'OR' instead of 'ORP') // DO EZO -> '?I,D.O.,1.0' || '?I,DO,1.7' (-> exists in D.O. and DO form) // EC EZO -> '?I,EC,1.0 ' // TEMP EZO-> '?I,RTD,1.2' // Legazy PH -> 'P,V5.0,5/13' // Legazy ORP -> 'O,V4.4,2/13' // Legazy DO -> 'D,V5.0,1/13' // Legazy EC -> 'E,V3.1,5/13' if (sensordata[0] == '?' && sensordata[1] == 'I') { // seems to be an EZO stamp // PH EZO if (sensordata[3] == 'p' && sensordata[4] == 'H') { stamp_type = F("EZO pH"); stamp_version[0] = sensordata[6]; stamp_version[1] = sensordata[7]; stamp_version[2] = sensordata[8]; return true; // ORP EZO } else if (sensordata[3] == 'O' && sensordata[4] == 'R') { stamp_type = F("EZO ORP"); stamp_version[0] = sensordata[6]; stamp_version[1] = sensordata[7]; stamp_version[2] = sensordata[8]; return true; // DO EZO } else if (sensordata[3] == 'D' && sensordata[4] == 'O') { stamp_type = F("EZO DO"); stamp_version[0] = sensordata[6]; stamp_version[1] = sensordata[7]; stamp_version[2] = sensordata[8]; return true; // D.O. EZO } else if (sensordata[3] == 'D' && sensordata[4] == '.' && sensordata[5] == 'O' && sensordata[6] == '.') { stamp_type = F("EZO DO"); stamp_version[0] = sensordata[8]; stamp_version[1] = sensordata[9]; stamp_version[2] = sensordata[10]; return true; // EC EZO } else if (sensordata[3] == 'E' && sensordata[4] == 'C') { stamp_type = F("EZO EC"); stamp_version[0] = sensordata[6]; stamp_version[1] = sensordata[7]; stamp_version[2] = sensordata[8]; return true; // RTD EZO } else if (sensordata[3] == 'R' && sensordata[4] == 'T' && sensordata[5] == 'D') { stamp_type = F("EZO RTD"); stamp_version[0] = sensordata[7]; stamp_version[1] = sensordata[8]; stamp_version[2] = sensordata[9]; return true; // unknown EZO stamp } else { stamp_type = F("unknown EZO stamp"); return true; } } // it's a legacy stamp (non-EZO) else { // Legacy pH if ( sensordata[0] == 'P') { stamp_type = F("pH (legacy)"); stamp_version[0] = sensordata[3]; stamp_version[1] = sensordata[4]; stamp_version[2] = sensordata[5]; stamp_version[3] = 0; return true; // legacy ORP } else if ( sensordata[0] == 'O') { stamp_type = F("ORP (legacy)"); stamp_version[0] = sensordata[3]; stamp_version[1] = sensordata[4]; stamp_version[2] = sensordata[5]; stamp_version[3] = 0; return true; // Legacy D.O. } else if ( sensordata[0] == 'D') { stamp_type = F("D.O. (legacy)"); stamp_version[0] = sensordata[3]; stamp_version[1] = sensordata[4]; stamp_version[2] = sensordata[5]; stamp_version[3] = 0; return true; // Lecagy EC } else if ( sensordata[0] == 'E') { stamp_type = F("EC (legacy)"); stamp_version[0] = sensordata[3]; stamp_version[1] = sensordata[4]; stamp_version[2] = sensordata[5]; stamp_version[3] = 0; return true; } } /* Serial.println("can not parse data: "); Serial.print("'"); Serial.print(sensordata); Serial.println("'"); */ return false; // can not parse this info-string } #if !defined (I2C_ONLY) void clearIncomingBuffer() { // "clears" the incoming soft-serial buffer while (sSerial.available() ) { //Serial.print((char)sSerial.read()); sSerial.read(); } } #endif void scan(boolean scanserial) { // Scan for all devices. Set scanserial to false to scan I2C only (much faster!) #if !defined (I2C_ONLY) if (scanserial) { Serial.println(F("Starting scan... this might take up to a minute.")); Serial.println(F("(if only using i2c mode, use 'scani2c' to scan faster)")); } else { Serial.println(F("Starting I2C scan...")); } #endif int stamp_amount = 0; #if !defined (I2C_ONLY) for (channel = 8; channel < 127; channel++ ) #else for (channel = 1; channel < 127; channel++ ) #endif { //Serial.println(channel); delay(5); if (change_channel()) { stamp_amount++; serialPrintDivider(); Serial.print( F("-- I2C CHANNEL ")); Serial.println( channel); Serial.println( F("--")); Serial.print( F("-- Type: ")); Serial.println( stamp_type); } } #if !defined (I2C_ONLY) if (scanserial) { for (channel = 0; channel < 8; channel++) { if (change_channel()) { stamp_amount++; serialPrintDivider(); Serial.print( F("-- SERIAL CHANNEL ")); Serial.println( channel); Serial.println( F("--")); Serial.print( F("-- Type: ")); Serial.println( stamp_type); Serial.print( F("-- Baudrate: ")); Serial.println( channelBaudrate[channel]); } } } #endif Serial.println( F("\r\r")); Serial.println( F("SCAN COMPLETE")); Serial.print( stamp_amount); Serial.println( F(" stamps found")); } void intro() { //print intro serialPrintDivider(); Serial.println( F("Whitebox Labs -- Tentacle Shield - Stamp Setup")); Serial.println( F("For info type 'help'")); Serial.println( F("To read current config from attached stamps type 'scan'")); Serial.println( F(" (e.g. if you've changed baudrates)")); Serial.println( F("To read current config from attached I2C stamps only, type 'scani2c'")); #if !defined (I2C_ONLY) Serial.println( F("TYPE CHANNEL NUMBER (Serial: 0-7, I2C: 8-127):")); #else Serial.println( F("TYPE CHANNEL NUMBER (I2C: 0-127):")); #endif } void help() { //print help dialogue serialPrintDivider(); Serial.println( F("To open a serial channel (numbered 0 - 7), send the number of the channel")); Serial.println( F("To open a I2C address (between 8 - 127), send the number of the address")); Serial.println( F("To issue a command, enter it directly to the console.")); Serial.println( F("To update information about connected stamps, type 'scan'.")); Serial.println( F(" -> This might take a while, it will scan all ports (serial and I2C)")); Serial.println( F(" -> and determine correct baudrates.")); Serial.println( F("To set baudrate manually, type one of 38400,19200,9600,115200,57600")); Serial.println( F("==========")); } void serialPrintDivider() { Serial.println( F("------------------")); }