Multiple DS18B20 with Photon

@FiDel, in order to use loops with your variables try

float T[8]; // use arrays where possibe (T[index] -> T[0] .. T[7])
// instead of 
float T1, T2, T3, T4, T5, T6, T7, T8;

Hence this comment by @BulldogLowell a while back :wink:


@MORA, when you update your lib, could also provide a struct that takes the data returned by ow_search_sensors()?
This would make things easier to use with something like owSensorData sensors[10] and improves readability and maintainability if the format should change in future.
You might need to use __attribute__((packed)) for this.

1 Like

I use the code below to keep track of my sensors, since you are not guranteed that the sensors maintain the same position inside sensors[] when calling ow_search_sensors.

This uses a struct, is it something like that you are requesting @ScruffR ?

sensors.h

#ifndef SENSORSH
#define SENSORSH
#include "application.h"
#include "ds18x20/ds18x20.h"

#define MAXSENSORS 30
#define SENSORSIZE 11

class Sensors
{
    public:
        struct Sensor{
            uint8_t id[8];
            char sign;
            uint8_t cel;
            uint8_t cel_frac_bits;
            int frac() { return cel_frac_bits*DS18X20_FRACCONV; }
        };
        
        static Sensor get(uint8_t id);
        static Sensor *get(uint8_t* id, bool create=true);
};

#endif

sensors.cpp

#include "sensors.h"
#include "application.h"

Sensors::Sensor sensorArray[MAXSENSORS];

Sensors::Sensor *Sensors::get(uint8_t* address, bool create)
{
    for (uint8_t i=0; i<MAXSENSORS; i++)
    {
        if (sensorArray[i].id[0] == 0)
        {
            if (create)
            {
                sensorArray[i].id[0] = address[0];
                sensorArray[i].id[1] = address[1];
                sensorArray[i].id[2] = address[2];
                sensorArray[i].id[3] = address[3];
                sensorArray[i].id[4] = address[4];
                sensorArray[i].id[5] = address[5];
                sensorArray[i].id[6] = address[6];
                sensorArray[i].id[7] = address[7];
                return &sensorArray[i];
            } else {
                return NULL;
            }
        }
        if (
                sensorArray[i].id[0] == address[0] &&
                sensorArray[i].id[1] == address[1] &&
                sensorArray[i].id[2] == address[2] &&
                sensorArray[i].id[3] == address[3] &&
                sensorArray[i].id[4] == address[4] &&
                sensorArray[i].id[5] == address[5] &&
                sensorArray[i].id[6] == address[6] &&
                sensorArray[i].id[7] == address[7]
            )
            {
                return &sensorArray[i];
            }
    }
    return NULL;
}

Sensors::Sensor Sensors::get(uint8_t id)
{
    return sensorArray[id];
}

Pretty much, but since I see you use this

uint8_t sensors[80];
...
      sensors[(i*OW_ROMCODE_SIZE)+0],
      sensors[(i*OW_ROMCODE_SIZE)+1],
      sensors[(i*OW_ROMCODE_SIZE)+2],
      sensors[(i*OW_ROMCODE_SIZE)+3],
      sensors[(i*OW_ROMCODE_SIZE)+4],
      sensors[(i*OW_ROMCODE_SIZE)+5],
      sensors[(i*OW_ROMCODE_SIZE)+6],
      sensors[(i*OW_ROMCODE_SIZE)+7],

I just thought, why using an unstructured uint8_t[] and not directly read into an array of fitting structure?
But since struct Sensor { } is not packed

Sensors::Sensor mySensors[MAXSENSORS];
...
  int n = ow_search_sensors(MAXSENSORS, mySensors);
  ...

wouldnā€™t work, but might be desirable.

And if DS18X20_read_meas() would also have an overload that just takes a Sensors::Sensor* as single parameter, things would just be a bit neater (personal opinion).

Adding a const char* toString() which returns a formatted string into the struct would then be a nice little perk too :wink:

This is the code that I use:

#include "OneWire/OneWire.h"
#include "HttpClient/HttpClient.h"
#include "spark-dallas-temperature/spark-dallas-temperature.h"
...
...
#define ONE_WIRE_BUS D0  //data on pin d0
...
...
OneWire oneWire(tempSensor);             // Setup a oneWire instance 
DallasTemperature tankSensor(&oneWire);  // Pass oneWire reference to Dallas Temperature.

You have to use the libraries section of the IDE to copy the SPARK-DALLAS-TEMPERATURE & ONE WIRE libraries to the same cloud folder as your application code.

The reading routine is:

double readTankTemp()
    {
    double tempC;  
    
    int retries = 0;

    do
        {
        tankSensor.requestTemperatures();           
        tempC = tankSensor.getTempCByIndex(0);       //  0 refers to the first IC on the wire 
        retries++;
        }while(tempC == -127.0 && retries < 1000);
    
    return tempC;
    }

This prevents false readings distorting the returned data.

By the way, using a 1K0 resistor instead of the 4K7 reduces the chances of getting an equally false 85 degree value.

Rgds,

Neil.

Thanks @MORA, @ScruffR and @Neil_Mudford for these tips!

Sounds interesting! I know I should try out lower pull-up resistor values when issues with longer wires occur, but why exactly the "false 85 degree value"?

Concerning my "dream" :sunrise: to be able to use the temperature values in my loop(), I tried several ways, (including arrays) but I lack the experience you guys have... :smirk:

I want to keep the nice feature of this library intact: Automatic discovery of the sensors.

Question: Is it feasible to integrate one of the following procedures in my above collectTemp() function?

  1. For every sensor found, create a (floating?) variable with the name = sensorID and the value = temperature (including the sign)

Problem I see: As these variables are created in the function, they are local and hence not available in loop()

  1. Create a (global) array of floating variables before setup()

For every sensor found, insert the sensorID in the first position and it's temperature value in the next and so on for all sensors.

Or, a totally different approach:

From the loop, call for example collectTemp(28FF219F611503F9)
This is the function with the selected sensorID
The function would then return one (floating) variable with the temperature value...

I don't know which would be best...

:cold_sweat:

85C is the powerup value when the device cant yet produce a valid temperature, its very annoying that this value is within the valid range of the sensor and the CRC is correct, so its not possible to know that value is wrong.
One option is to just ignore 85.0000 :smiling_imp:

This is an example of how to use the sensor files I posted earlier, they are not a part of the DS lib, but rather a part of my (unpublished sofar) datalogger.

void prepareOW()
{
    DS18X20_start_meas( DS18X20_POWER_PARASITE, NULL );
}

void readOW(uint8_t pin)
{       
    uint8_t subzero, cel, cel_frac_bits;

    uint8_t numsensors = ow_search_sensors(10, owscan);

    for (uint8_t i=0; i<numsensors; i++)
    {
        if (owscan[i*OW_ROMCODE_SIZE+0] == 0x10 || owscan[i*OW_ROMCODE_SIZE+0] == 0x28) //0x10=DS18S20, 0x28=DS18B20
        {
            if ( DS18X20_read_meas( &owscan[i*OW_ROMCODE_SIZE], &subzero, &cel, &cel_frac_bits) == DS18X20_OK ) {
                Sensors::Sensor* sensor = Sensors::get(&owscan[i*OW_ROMCODE_SIZE]);
                char sign = (subzero) ? '-' : '+';
                sensor->sign = sign;
                sensor->cel = cel;
                sensor->cel_frac_bits = cel_frac_bits;
            }
        }
    }
}

You can then loop over them with

for (uint8_t i=0; i<MAXSENSORS; i++)
{
    Sensors::Sensor sensor = Sensors::get(i);
    if (sensor.id[0] != 0)
    {
        String::format("&%02X%02X%02X%02X%02X%02X%02X%02X=%c%d.%04d",
        sensor.id[0],
        sensor.id[1],
        sensor.id[2],
        sensor.id[3],
        sensor.id[4],
        sensor.id[5],
        sensor.id[6],
        sensor.id[7],
        sensor.sign,
        sensor.cel,
        sensor.frac()
        );
    }
}

Or get a specific sensor with

uint8_t addr[8] = {0x20,0x11,0x22,etc};
Sensors::get(addr);
1 Like

OK, good to know about this default start-up value! Iā€™ll filter it out before it does any damageā€¦ :japanese_ogre:

Thanks for the snippets of code @MORA
I donā€™t see yet what it does but will try to digest it and see if I can knit it into the rest to create those variablesā€¦
:hand: :older_man:

I don't want to print or publish my temperatures, but I want to use them to control water flow etc...
Probably "Hardcoded sensor IDs" is the right approach to my application, so that I am always sure which temperature I'm using to control a pump or a valve.

These are for example the ROM codes of 3 of my sensors:

28FF219F611503F9
10E96B0A030800AC
10444E0B0308001F

Is there a way to read their temperature by using these codes or do I always need their HEX codes?

This is really what I would like to achieve:

    #include "ds18x20.h"
    #include "onewire.h" // Note: Make sure also crc8.h library is present!
    
    float T1, T, T3;
    define Sensor1 = 28FF219F611503F9;
    define Sensor2 = 10E96B0A030800AC;
    define Sensor3 = 10444E0B0308001F;
    
    void setup()
    {
      ow_setPin(D2);
    }
    
    

    void loop()
    {
     T1 = collectTemp(Sensor1);
     T2 = collectTemp(Sensor2);
     T3 = collectTemp(Sensor3);
    }
    
    
    void collectTemp(int temperature)
    {
      ...
       // Here read the temperature of one sensor with the ROM code used in the loop()
       Sensors::get(sensorID);
      ...
      return temperature;
      ...
    }

Is this feasible?
Even if I can only use the HEX codes for sensors, that's also fine...

I donā€™t know why the device delivers a spurious 85 degree return, but it is prone to doing so and nothing short of depowering will get it to report the correct value in my experience. I suspect that asking for a reading too frequently is a probable cause.

Rgds,

Neil.

1 Like

Those are the HEX values, just in a string representation.

int res;
uint8_t addr[8] = {0x28,0xFF,0x21,0x9F,0x61,0x15,0x03,0xF9};

ATOMIC_BLOCK() {    
    res = DS18X20_read_meas(addr, &subzero, &cel, &cel_frac_bits);
}
if (res  == DS18X20_OK ) {
    char sign = (subzero) ? '-' : '+';
    int frac = cel_frac_bits*DS18X20_FRACCONV;
    sprintf(msg, "Sensor : %c%d.%04d \r\n", sign, cel, frac);
    log(msg);
}  

Its still not quite a float, my code does not include a float method yet.
cel is the whole celcius, frac is the decimal part, and subzero is true/false to indicate if its a value below 0, it can be converted to a float or similar with some code though.

And that snippet can be made into a function so it works like you described.

1 Like

OK, thanks for confirming Iā€™m on the right track @MORA!

Thereā€™s some more work to do, I will try to achieve my goal using your valuable tips.
I learn so much from you guys, every dayā€¦

Thanks also to @Ric, @ScruffR and @Neil_Mudford for all help!

1 Like

OK, here is my next step code:

    // Libraries for off-line Particle Dev use. Make sure also crc8.h library is present!
    #include "ds18x20.h"
    #include "onewire.h"
    
    
    void setup()
    {
    }
    
    
    void loop()
    {
     collectTemp(D2);
     delay(1000);// Was 5000
    }
    
    
    void collectTemp(pin_t pulsePin)
    {
      ow_setPin(pulsePin);
      uint8_t sensors[80];
      uint8_t subzero, cel, cel_frac_bits, numsensors;
      char msg[100];
      int res;
    
      // List with sensor addresses & names:
      uint8_t T1[8] = {0x28,0xFF,0x0D,0x4C,0x05,0x16,0x03,0xC7}; // DS18B20 Waterproof wired sensor #1
      uint8_t T2[8] = {0x28,0xFF,0x25,0x1A,0x01,0x16,0x04,0xCD}; // DS18B20 Waterproof wired sensor #2
      uint8_t T3[8] = {0x28,0xFF,0x89,0x19,0x01,0x16,0x04,0x57}; // DS18B20 Waterproof wired sensor #3
      uint8_t T4[8] = {0x28,0xFF,0x21,0x9F,0x61,0x15,0x03,0xF9}; // DS18B20 Waterproof wired sensor #4
      uint8_t T5[8] = {0x28,0xFF,0x16,0x6B,0x00,0x16,0x03,0x08}; // DS18B20 Waterproof wired sensor #5
      uint8_t T6[8] = {0x28,0xFF,0x90,0xA2,0x00,0x16,0x04,0x76}; // DS18B20 Waterproof wired sensor #6
      uint8_t T7[8] = {0x10,0xE9,0x6B,0x0A,0x03,0x08,0x00,0xAC}; // DS18S20 TO92 #1
      uint8_t T8[8] = {0x10,0x44,0x4E,0x0B,0x03,0x08,0x00,0x1F}; // DS18S20 TO92 #2
    
      ATOMIC_BLOCK()
      {
       DS18X20_start_meas( DS18X20_POWER_EXTERN, NULL ); //(Was DS18X20_POWER_PARASITE ) Asks all DS18x20 devices to start temperature measurement, takes up to 750ms at max resolution
      }
      delay(1000);                                         //(was 1000) If your code has other tasks, you can store the timestamp instead and return when a second has passed.
    
      ATOMIC_BLOCK()
      {
       res = DS18X20_read_meas(T7, &subzero, &cel, &cel_frac_bits);
      }
    
      if (res  == DS18X20_OK )
      {
       char sign = (subzero) ? '-' : '+';
       int frac = cel_frac_bits*DS18X20_FRACCONV;
       sprintf(msg, "Sensor : %c%d.%04d \r\n", sign, cel, frac);
       log(msg);
      }
    }
    
    
    void log(char* msg)
    {
      Spark.publish("log", msg);
      delay(500);// Was 500
    }

This works well, but I have to edit the name of the sensor (T1, T2, T3...) in this command:

Now, in the loop(), I want to adapt the command to read the temperature of a specific sensor from the list of sensors.
The sensor name should be used as parameter in the function call from the loop().

eg: collectTemp(D2,T6);
= Pin D2, Sensor T2

I suppose that must be possible?

you can create an array of your sensors and save the values into an array of thermometer readings

something like this (un-tested but compiles):

#include "ds18x20.h"
#include "onewire.h"

uint8_t tempSensor[][8] = {                         // Array of sensor ID's
  {0x28, 0xFF, 0x0D, 0x4C, 0x05, 0x16, 0x03, 0xC7},
  {0x28, 0xFF, 0x25, 0x1A, 0x01, 0x16, 0x04, 0xCD},
  {0x28, 0xFF, 0x89, 0x19, 0x01, 0x16, 0x04, 0x57},
  {0x28, 0xFF, 0x21, 0x9F, 0x61, 0x15, 0x03, 0xF9},
  {0x28, 0xFF, 0x16, 0x6B, 0x00, 0x16, 0x03, 0x08},
  {0x28, 0xFF, 0x90, 0xA2, 0x00, 0x16, 0x04, 0x76},
  {0x10, 0xE9, 0x6B, 0x0A, 0x03, 0x08, 0x00, 0xAC},
  {0x10, 0x44, 0x4E, 0x0B, 0x03, 0x08, 0x00, 0x1F}
};

struct Thermo{               // Struct for each thermometer
  uint8_t subzero;
  uint8_t cel;
  uint8_t cel_frac_bits;
};

Thermo thermo[8];            // Array of thermometer objects

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  for (int i = 0; i < 8; i++)
  {
    if(collectTemp(D2, tempSensor[i], i))
    {
      char msg[100];
      int frac = thermo[i].cel_frac_bits * DS18X20_FRACCONV;
      sprintf(msg, "Sensor : %c%d.%04d \r\n", (thermo[i].subzero) ? '-' : '+', thermo[i].cel, frac);
      log(msg);
      delay(5000);
    }
  }
}

bool collectTemp(int pulsePin, uint8_t sensor[], int num)
{
  ow_setPin(pulsePin);
  int res;
  ATOMIC_BLOCK()
  {
    DS18X20_start_meas( DS18X20_POWER_EXTERN, NULL ); //(Was DS18X20_POWER_PARASITE ) Asks all DS18x20 devices to start temperature measurement, takes up to 750ms at max resolution
  }
  //delay(1000);                                         //(was 1000) If your code has other tasks, you can store the timestamp instead and return when a second has passed.
  ATOMIC_BLOCK()
  {
    res = DS18X20_read_meas(sensor, &thermo[num].subzero, &thermo[num].cel, &thermo[num].cel_frac_bits);
  }
  return res == DS18X20_OK;
}

void log(char* msg)
{
  Particle.publish("log", msg);
}

4 Likes

That's very nice @BulldogLowell, but that's not really what I want to achieve... :relaxed:

In the loop() of the HVAC system control sketch will be many other functions besides checking temperatures of sensors. That's why I want to put as much as possible in a function.
In the loop, I want to call that function with a pin number and a sensor name as parameters and then the function should return a value (floating?) which I can use for calculations and controlling relays (pumps, valves...)

I do not want to publish or print anything...

Anyway, thanks to all of you for helping!

It sort of does, but it now stores those names in an array, allowing you to iterate through them more easily. The principle should be the same though.
Rather than using the name, you use an array position which contains the name (or ID), though you can insert a name (ID) manually if thatā€™s what you want to do :slight_smile:
The prints and publishes can be commented out quite easily, though theyā€™re useful for debugging.

1 Like

You could easily reshape your perception of how your sensor names should look.
Just use T[2] instead of T2 (you can ignore T[0] if you have them named starting with 1 already).

But if you had two arrays or a struct that held the designated pin and sensor ID with the same index, your function would only one patameter -the joint index.

1 Like

We publish and print to demonstrate what's happening.

Your solution is in the code, I only had it outputting the same way you output the data., Perhaps you didn't see your way over the goal line. :wink:

The key is to get the function to return a double, which is a trivial excercise.

Here is an example (sorry it prints, but you can edit that out if you want) that uses an array of sensors, passed to a function, that returns a double that is stored into an array for future evaluation/analysis/etc.

I obviously cannot test it, the ID's are from your code.

#include "ds18x20.h"
#include "onewire.h"
#include "math.h"

uint8_t tempSensor[][8] = {                         // Array of sensor ID's
  {0x28, 0xFF, 0x0D, 0x4C, 0x05, 0x16, 0x03, 0xC7},
  {0x28, 0xFF, 0x25, 0x1A, 0x01, 0x16, 0x04, 0xCD},
  {0x28, 0xFF, 0x89, 0x19, 0x01, 0x16, 0x04, 0x57},
  {0x28, 0xFF, 0x21, 0x9F, 0x61, 0x15, 0x03, 0xF9},
  {0x28, 0xFF, 0x16, 0x6B, 0x00, 0x16, 0x03, 0x08},
  {0x28, 0xFF, 0x90, 0xA2, 0x00, 0x16, 0x04, 0x76},
  {0x10, 0xE9, 0x6B, 0x0A, 0x03, 0x08, 0x00, 0xAC},
  {0x10, 0x44, 0x4E, 0x0B, 0x03, 0x08, 0x00, 0x1F}
};

double thermo[8];  // array of floating point numbers

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  for (int i = 0; i < 8; i++)
  {
    double t = (getTemp(2, tempSensor[i], i));
    if(isfinite(t))
    {
      thermo[i] = t;
    }
  }
  for (int i = 0; i < 8; i++)
  {
    Serial.print("Temperature");
    Serial.print(i);
    Serial.print(" = ");
    Serial.println((isnan(thermo[i])? "NaN" : String(thermo[i])));
  }
  delay(5000);
}

double getTemp(int pulsePin, uint8_t sensor[], int num)
{
  ow_setPin(pulsePin);
  int res;
  uint8_t subzero, cel, cel_frac_bits;
  ATOMIC_BLOCK()
  {
    DS18X20_start_meas( DS18X20_POWER_EXTERN, NULL ); //(Was DS18X20_POWER_PARASITE ) Asks all DS18x20 devices to start temperature measurement, takes up to 750ms at max resolution
  }
  delay(750);
  ATOMIC_BLOCK()
  {
    res = DS18X20_read_meas(sensor, &subzero, &cel, &cel_frac_bits);
  }
  if(res == DS18X20_OK)
  {
    char msg[100];
    int frac = cel_frac_bits * DS18X20_FRACCONV;
    sprintf(msg, "%c%d.%04d", (subzero) ? "-" : "", cel, frac);
    return atof(msg);
  }
  else
  {
    return sqrt(-1);  //NaN
  }
}

I don't use a lot of work with double data types, so maybe others out there can comment on recognizing/screening for a bad sensor read.

have fun with it!

1 Like

You need a delay after calling start_meas and before calling read_meas, at full resolution it needs to be atleast 750ms, maybe its not clear in the comment, but start_meas returns near instantly, but the sensors need the time to measure/convert the temperature.

However start_meas only needs to be called once per output pin, per reading cycle, not once per device per pin.
In my data logger I call a function every 2 seconds and use a variable to check if its time to read or measure.
That way the rest of the program does not need to standstill waiting.

void updateOW()
{
    if (OWconvert)
    {
        readOW(D4);
        readOW(D5);
        readOW(D6);
        readOW(D7);
        pinMode(D7, OUTPUT);
        digitalWrite(D7, LOW);   //D7 has onboard LED, since we use external pullup, this keeps the LED off        
        OWconvert = false;
    } else {
        ow_setPin(D4);
        DS18X20_start_meas( DS18X20_POWER_PARASITE, NULL );
        ow_setPin(D5);
        DS18X20_start_meas( DS18X20_POWER_PARASITE, NULL );
        ow_setPin(D6);
        DS18X20_start_meas( DS18X20_POWER_PARASITE, NULL );
        ow_setPin(D7);
        DS18X20_start_meas( DS18X20_POWER_PARASITE, NULL );

        pinMode(D7, OUTPUT);
        digitalWrite(D7, LOW);  //D7 has onboard LED, since we use external pullup, this keeps the LED off        
        OWconvert = true;
    }
}

Btw I updated the lib in particle build to have atomic_block built in now, so refresh and it should make the code a bit nicer :slight_smile:

2 Likes

I see.

Maybe we should write a template/method that does that and returns a double/int when passed an array of sensors.

your library is great, making it more extensible would be fun!

I edited back in the call to delay()

1 Like

Yes, I understand all of your comments, but I donā€™t see why the ā€œarchitectureā€ ( :wink: ) that I proposed would not workā€¦
Probably itā€™s good that I show what I want to do with those sensors. Iā€™ll make a drawing right now and be back tomorrow!
:clap: :older_man: