Multiple DS18B20 with Photon

I know you understand that describing what you want to do as “I want to control a complex system of multiple sensors and actuators controlling the environment of two homes” cannot be programmed on that specification alone!

So before you can finish all that you have to organize what it is you want to do, in some pseudo-code style so that folks can understand, sort of like this:

loop()
{
  gatherTemperatures();
  analyzeData();
  makeDecisions();
  takeActions();
}

Compartmentalize your code solving the learning part is key, and it appears that you are doing that well.

The temperature sensor information in my code would fall into the gatherTemperatures() component of the functionality… X number of sensor Data stored in such a way that you can then analyzeData(). It doesn’t have to be “linear”, particularly if you have related devices (e.g. thermometers and control valves).

The question is, do you have a gatherTemperatures() module of programming that satisfies your need to gather the data?

PS: the DHT sensors are a relatively low quality sensor to rely on for controlling your HVAC system, IMHO. It does make a terrific hobby device, I would not personally rely on it for a robust control system. Use them for a while and you may learn the same, unreliable and not too accurate. :frowning:

2 Likes

Thanks for your response @BulldogLowell!
Of course, I can describe all that if you want, but it was not my intention to design the complete control system on this forum. If I have the “building blocks”, I believe I can do it.

Anyway, if you want it…
Here is my “schematic” description of the “HVAC_control.ino” sketch I want to develop, (only the heating system related to DS18B20 sensors is described):


Start

  1. Initialize variables, libraries, publish events, web variables…

  2. Create 2 arrays:

  • Array “tempReadings” (stores all temperature readings)
  • Array “sensorNames” (stores all corresponding sensor names)

setup()

  • whatever is needed here…

loop()

  1. Refresh temperature data:
  • Run function collectTemp()
    => Fills the array “tempReadings”
  1. Retrieve (with “retrieveTemp()”) all temperatures in variables with the sensor names

  2. Publish all temperatures as one json string
    => Double purpose:

    1. Monitor temperatures in a Google sheet
    2. Display each temperature with a Neopixel on my “Status frame”
      (controlled by another Photon)
  3. Energy buffer management:

    • How full are the 3 buffers?
    • How is the temperature distribution inside? (Stratification)

    For this, we must:

  • Calculate buffer capacities: Use the 5 temperatures for each “layer” to calculate an energy value (in kWh).
  • Publish the 5 buffer temperatures and calculated energy as one json string per buffer: Te-buff, Tw-buff, Ts-buff
    => Purpose: Display the temperatures and stored energy with Neopixels and a mini-OLED display on my “Status frame” (controlled by another Photon)
  1. Control pumps with temperature data (using hysteresis) and buffer energy levels:
  • If (conditions1), then operate pump P1
  • If (conditions2), then operate pump P2
  • If (conditions3), then operate pump P3
  • If (conditions4), then operate pump P4
  • If (conditions5), then operate pump P5
  • If (conditions6), then operate pump P6
  • If (conditions7), then operate pump P7
  • If (conditions8), then operate pump P8
  • Publish the “status” messages of all pumps as one json string for all pumps (To display on my “Status frame” (controlled by another Photon)
  • Publish “alert” messages, if something is “wrong” with a pump
    (eg: 10s after a pump is started, the temperature after the pump remains the same as before)
  1. Control valves with temperature data (using hysteresis):
  • If (conditions9), then operate valve Stage1-2
  • If (conditions10), then operate valve High-Low-1
  • If (conditions11), then operate valve High-Low-2
  • If (conditions12), then operate valve Solar1-2
  • If (conditions13), then operate valve Buffer-Direct
  • Publish the “status” messages of all valves as one json string for all pumps (To display on my “Status frame” (controlled by another Photon)
  • Publish “alert” messages, if something is “wrong” with a valve
    (eg: 10s after a valve is opened, the temperature after the valve remains the same as before)
  1. Control heating resistors in 2 buffers with temperature data + solar PV data (using hysteresis):
  • If (conditions14), then operate Resistor R-buf-w (or R1)
  • If (conditions15), then operate Resistor R-buf-s (or R2)
  1. Publish alert messages, if something is “wrong” with one of the temperatures (MIN/MAX values)

collectTemp()

  • Collect the temperatures of Bus1 (Blue), Bus2 (Green), Bus3 (Purple)
  • Store all temperatures in array “tempReadings” => Array data available for the loop()

retrieveTemp()

  • Retrieve the temperature of a sensor with a particular name
  • Return this value (integer?) to the calling command in the loop()

I hope that clarifies my “dream” for you… :rainbow:

Most of the programming in loop() I feel capable of doing myself, except for how to call the temperature from a particular sensor by using it’s name, so that the IF / CASE structures will use the correct temperatures.
I suppose we should create a function retrieveTemp() maybe?

I will certainly need some help from specialists like you guys, to create the function collectTemp() so that the array will be filled with temperature data (floating? including + & - sign)

PS: For the ventilation system, it’s another story, I believe outside of the scope for this discussion.
Probably I’ll come back later to that, as I planned to share my project with CO2 sensors as this may interest some Particle users…

Concerning your remark about the DHT22 sensors, I don’t agree with your remark:

Since 6 months I have now used 10 of these in our current home, to monitor temperature and humidity (in a google sheet) and they perform very well.
Of course they must be calibrated to an accurate reference first!

Also, I don’t use the (slow!) traditional library which exists for them but a function I made: It allows much faster sampling of these sensors…

Food for another discussion thread later maybe!
:hand: :older_man:

1 Like

and continuously...

Great project, I cannot wait to see the final version posted to the forum.

1 Like

OK, glad you like it!
I will need some help of course…

Here’s an image of where I need help and what I can probably do myself:
Red = :wrench: (Specialist needed)
Green = :older_man: can do it…

This is why I posted my first message in this thread…

:bow:

Actually no, whats taking time in that function is asking all sensors to start the conversion and then asking one sensor for its value, only one sensor will reply to that.

Ideally you should have a function that calls start_meas on all the pins you use for onewire.
Then one second later you read the sensors you want, no need to call start_meas multiple times on same pin unless you want a new reading.

Looks like a very nice project, the onewire lines are maybe a bit long, but if you can make it work, great!
Otherwise it would be feasible to add a (few) more photon(s) that collects temperature data and sends it to the master one, either locally or cloud based :smile:

This is my functions for collecting temperature data and storing it in the sensors.h array that I posted earlier.

void prepareOW()
{
    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 );
}

void readOW(uint8_t pin)
{
    ow_setPin(pin);
    
    uint8_t subzero, cel, cel_frac_bits;
    char msg[100];

    uint8_t numsensors = ow_search_sensors(10, owscan);
/*
    sprintf(msg, "Found %i sensors", numsensors);
    log(msg);
*/
    
    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;
			}
        }
    }
}

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

You can then call Sensors::get(uint8_t* addr) to get a specificsensor, it would be trivial to make a function that takes a string and chops it up into 8 bytes, but would it make anything easyier ?

To get double instead of cel, frac and subzero theres a function posted earlier in the thread also.

1 Like

Since you will have a lot of logic to control the valves and such, it would seem best to move the code to get the temperatures to another file. I think something like below should give you close to what you want. The temperatures are put into 3 arrays, one for each bus, rather than one, but you probably could get them in to one array if you really want that. I only have 3 sensors, so I’m simulating 3 sets of sensors. Anyway, here’s what I came up with.

The Dallas.h file,

#include "Particle.h"

void getTemperatures(float temps[], int tempsCount, int pin, int select);

The Dallas.cpp file,

#include "Dallas.h"
#include "OneWire/OneWire.h"

double celsius, fahrenheit;

byte addrs0[3][8] = {{0x28, 0x1B, 0x1C, 0xE3, 0x03, 0x0, 0x0, 0xC5}, {0x28, 0x8, 0x56, 0xE3, 0x3, 0x0, 0x0, 0x93}, {0x28, 0xD, 0xD3, 0xE2, 0x3, 0x0, 0x0, 0xEE}};
byte addrs1[3][8] = {{0x28, 0x1B, 0x1C, 0xE3, 0x03, 0x0, 0x0, 0xC5}, {0x28, 0x8, 0x56, 0xE3, 0x3, 0x0, 0x0, 0x93}, {0x28, 0xD, 0xD3, 0xE2, 0x3, 0x0, 0x0, 0xEE}};
byte addrs2[3][8] = {{0x28, 0x1B, 0x1C, 0xE3, 0x03, 0x0, 0x0, 0xC5}, {0x28, 0x8, 0x56, 0xE3, 0x3, 0x0, 0x0, 0x93}, {0x28, 0xD, 0xD3, 0xE2, 0x3, 0x0, 0x0, 0xEE}};


void getTemperatures(float temps[], int tempsCount, int pin, int select) {
    OneWire ds = OneWire(pin);
    ds.reset();            
    ds.skip();          // Make all devices start the temperature conversion
    ds.write(0x44, 1);  // tell it to start a conversion, with parasite power on at the end (pass 0 for second argument if using powered mode)

    delay(1000);       //  wait 1 sec for conversion
    ds.reset();
  
    for (int i=0; i<tempsCount; i++) {
        switch (select) {
            case 0:
                ds.select(addrs0[i]);
                break;
            case 1:
                ds.select(addrs1[i]);
                break;
            case 2:
                ds.select(addrs2[i]);
                break;
        }
        
        ds.write(0xBE,0);   
    
        byte data0 = ds.read();
        byte data1 = ds.read();
        ds.reset();
      
        int16_t raw = (data1 << 8) | data0;
    
        celsius = (float)raw * 0.0625;
        fahrenheit = celsius * 1.8 + 32.0;
        temps[i] = fahrenheit;
  }
}

The main .ino file,

// This #include statement was automatically added by the Particle IDE.
#include "Dallas.h"

float T1, T2, T3, T4, T5, T6, T7, T8, T9;
float* temps1[] = {&T1, &T2, &T3}; // group sensors by which pin they're connected to
float* temps2[] = {&T4, &T5, &T6};
float* temps3[] = {&T7, &T8, &T9};

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


void loop() {
    getTemperatures(*temps1, 3, D4, 0); // the last argument is used to select which array of addresses is used in the Dallas.cpp file
    getTemperatures(*temps2, 3, D4, 1);
    getTemperatures(*temps3, 3, D4, 2);
    
   if (T7>50) {
       Serial.printf(" T1 = %.1f  T2 = %.1f  T3 = %.1f", T1, T2, T3);
       Serial.println();
       Serial.printf(" T4 = %.1f  T5 = %.1f  T6 = %.1f", T4, T5, T6);
       Serial.println();
       Serial.printf(" T7 = %.1f  T8 = %.1f  T9 = %.1f", T7, T8, T9);
       Serial.println();
       Serial.println();
   }
   
    delay(10000);
}

The if(T7>50) block is just there to test whether I was getting the correct readings, and to show that you can use your variable names directly to get the temperatures.

1 Like

Thanks @MORA and @Ric for these encouraging contributions, I’m sure we’ll get there!
Lots of good ideas for me to digest :+1:

Unfortunately no time till monday to start testing… To be continued!

So, as pointed out in the other thread, try this:

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

const byte 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 Sensors {
  float T;
  byte address[8];
};

Sensors mySensors[8];

Sensors* bus1[] = {&mySensors[0], &mySensors[1], &mySensors[2], &mySensors[3]}; // group sensors by which pin they're connected to
Sensors* bus2[] = {&mySensors[4], &mySensors[5], &mySensors[6]};
Sensors* bus3[] = {&mySensors[7]};

Sensors** bus[] = {bus1, bus2, bus3};

int busPin[3] = {2,3,4};
void setup()
{
  unsigned long startMillis = millis();
  Serial.begin(9600);
  for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      mySensors[i].address[j] = tempSensor[i][j];
    }
  }
}

void loop()
{
  getTemp(bus1, sizeof(bus1) / sizeof(bus1[0]), busPin[0]);
  getTemp(bus2, sizeof(bus2) / sizeof(bus2[0]), busPin[1]);
  getTemp(bus3, sizeof(bus3) / sizeof(bus3[0]), busPin[2]);
  unsigned long endMillis = millis();
  for(int i = 0; i < sizeof(mySensors)/sizeof(mySensors[0]); i++)
  {
    Serial.print("T");
    Serial.print(i);
    Serial.print("=");
    Serial.print(mySensors[i].T);
    Serial.print("; ");
  }
  Serial.println();
  Serial.print("Sensor readings took ");
  Serial.print(endMillis - startMillis);
  Serial.println("milliseconds total.");
  Serial.println();
  delay(5000);
}

void getTemp(struct Sensors** ptrArray, size_t size, int pin)  // pointer to array of pointers
{
  ow_setPin(pin);
  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);
  for(int i = 0; i < size; i++)
  {
    byte sensorAddress[8];
    memcpy(sensorAddress, ptrArray[i]->address, 8);
    ATOMIC_BLOCK()
    {
      res = DS18X20_read_meas(sensorAddress, &subzero, &cel, &cel_frac_bits);
    }
    if(res == DS18X20_OK)
    {
      char floatString[100];
      int frac = cel_frac_bits * DS18X20_FRACCONV;
      snprintf(floatString, sizeof(floatString), "%c%d.%04d", (subzero) ? "-" : "", cel, frac);
      ptrArray[i]->T = atof(floatString);
    }
  }
}

can you see how to:

  • set the array for number of sensors? select which sensors are on
  • determine which bus?

If you need it revised for alternative sensors and bus config, let me know.

:smile:

2 Likes

From the "side-track" by @Ric :

I want to thank all of you again for the many useful contributions to help me reach my goal(s) in controlling this system.
I'm confident that this "dream" is coming through... :rainbow:
Could never do this alone with my limited experience in C/C++!

STATUS of the project:

For simulating the situation, I created 2 buses:

Bus1: 6x waterproof sensors connected in "star" and with a long UTP cable between
Bus2: 2x TO-92 sensors on the breadboard

Right now, I feel the excellent work done by @Ric gives me (almost) everything I need.
I have just tested his combination of 3 files (above) and it (almost) works flawlessly.

It gives us the following great features:

  1. It's very simple and light software: Low memory usage I suppose.
    It keeps all the work behind the screens in an external file. Results:
    => Makes especially the main file (.ino) very simple and light
    => Easy to keep focus on the most important part: Controlling the pumps, valves and heating systems with simpler IF/CASE functions.

  2. It does not need another complex external library, (except for "OneWire.h")
    Remark: The library introduced by @MORA is of course great, but as discussed it has strong and weak points not fully compatible with what we want to achieve: It is great in discovering all sensors automatically, but the disadvantage is linked: If one sensor gets disconnected, others take his place... We're never sure we are reading a particular sensor. Another weak point is the complex assembling needed to get one floating variable.

  3. It addresses the sensors directly with their fixed HEX "ROM code": Always sure which one it is!

  4. It stores all results of readings in one array per bus of sensors, but in the loop() I can address all sensors with their "Nickname", as if I were using separate global floating variables. That is most familiar for me.

At this moment, I have only one problem remaining with Ric's software. Or is it in my hardware?

One of the buses gives all temperature values correctly while the second bus gives strange values.
This is the output in serial monitor:

D1 BUS: T1 = 22.2 T2 = 22.0 T3 = 22.1 T4 = 24.2 T5 = 22.2 T6 = 22.0
D2 BUS: T7 = 3.0 T8 = 3.0

I or @Ric may come back later after trying out a few more things. Maybe it's something trivial...

PS: The accuracy of these sensors that I will be using mainly with water is indeed in another league compared to the DHT22 sensors I am using for the airco & ventilation system. But both are adequate for their purposes. Very happy!!!

:+1: :older_man:

Glad you got it working, however saying my lib will mix up sensors, is not really being completely honest.
You compare the solution to a list of hardcoded sensor ids, if you give my lib a specific hex code, it will return that sensor everytime if its there and otherwise an error.

If you blindly read from a search result of whats currently on the line and expect them to keep the same array index despite sensors coming and going, then yes, its going to be a bit hit and miss :wink:

The lib was posted because many people had trouble getting the cleaner Wire based system working, while my lib is quick and dirty, it usually works, and allows projects to get going, and maybe later convert to the Wire system.

2 Likes

Building on that, usually it is preferred to use the standard library and adapt a function to your uses. Going in and modifying the class is OK, but it may cause you issues of compatibility over time.

I would think that @Ric can eventually work that out so you don't have to worry, assuming you want to take advantage of a Particle firmware update or Library update later on. You will have to keep the 2nd library and modify that as you go.

all that said, if it works well, and you are happy then what else do you need?!

:+1:

1 Like

Sorry @MORA I probably expressed unclearly: I didn’t mean to say it mixes up sensors, but I hope you understand what I mean: When one sensor disappears, the next sensor will get the name of the disappeared one etc… This is due to the “auto discovery” characteristic, which is useful in many cases, but not when conrolling hot fluid…

I have successfully tested your library, and it’s is fantastic! I’ll certainly use it for some other taks but probably not this one.
It’s always good to have a choice! :relaxed: :ok_hand:

To be continued…

I didnt modify the wire lib, I ported the code I have used in my earlier data loggers, which was 8bit AVR in C, hence the not so pretty code, partly of frustration that the Wire based solution did not work at the time (it does now), partly because I haved used the other approach for more than 5years, connecting a sensor to the internet is not new :smile:

No, not what I meant.

See code above.

Well no, if a sensor gets removed on the next search, the sensor after that will get the INDEX of the missing one, the ID will be the same as always.
Its one of the main selling points of onewire sensors, that they have a factory set unique ID, even if you move it to another board it will be the same ID.

If you dont need auto discovery, you can use a static address with both libs, so "We're never sure we are reading a particular sensor." is false, you are always reading exactly the sensorID you asked for.
Only one sensor can reply at a time, although if theres only 1 sensor on a bus you can ask for its temperature without knowing its ID, which some projects use for simplicity.

Its all good, you just triggered me a bit by posting false statements about my lib :slight_smile:

I dont mind using your lib if you can give me an example which does the same as @Ric 's code… :wink:

for (int i=0; i<tempsCount; i++) {
  if (DS18X20_read_meas(addrs0[i], &subzero, &cel, &cel_frac_bits) == DS18X20_OK)
  save sensor value etc...
}

I am not going to replicate the entire function just for fun, but the central point is there, the first argument to read_meas is a sensorID.
In my example included with the lib its being used inside a loop over the searched values, but it could just as well be from a static array of addresses.

In response to the code you posted above, and on my thread about the “T9” problem, unless I missed something in your code, it looks like you have to get at the temperatures with something like mySensors[1].T which is what I think @FiDel was trying to avoid when he implements logic to control valves. As I understand his wishes, he would like to be able to use meaningful names in those statements. His concern seems to be a about readability of the code. Something like,

if (Tank1>70 && Tank2<60) then open some valve

This is the concern I was addressing with my code. The “T9” problem is just a weird side issue that I’ve managed to fix, though not explain.

Given the complexity of his system, I think that code readability is something he’s right to be concerned about. I don’t know enough about his logic to know what overall architecture would be best. If the temperatures at various points in the system all have set points that would trigger an action, then maybe a FSM would be a way to go.

1 Like

@FiDel, @MORA, @BulldogLowell, I just wanted to be clear that the only purpose of my code was to show how you could get the temperatures into meaningful variable names using arrays (I assume FiDel will use better names than T1, T2, etc.). I’m agnostic with regard to which library to use, I only used the OneWire library because that’s the one I’d been using in my own code, and had code snippets that I could easily grab.

1 Like

Indeed @Ric, that was indeed one of my important criteria.
After testing your code, I feel very comfortable in creating the control structures in a very clean way.
Congratulations for having made this in such a short time!!!

of course, I learned a lot from all contributions and I hope to use some of these tips in other sketches.
Thank you all for participating in this search for the holy grail...


Now only my "Bus2" issue must be solved...
:fearful: