Read temperature using a Timer

Hi @ScruffR

I’m more used to C# with thread pools, background workers and delegates, although I mainly do web these days so haven’t had the need for sometime, but I suppose this is a totally different kettle of fish.

So I’ve included my code below this is now working as expected, I can log motion interrupts from the PIR on a minimum defined interval. I have my led blinking away and temperature readings are coming through on thee timed interval I specified. Tbh I haven’t tried the particle variables and functions, but I’m sure they are fine.

This is all entirely hypothetical at the moment, what I have wired to get has no real world function for me, but its really great to get your feedback on how I can do these types of tasks better or differently, which may be to keep things simple or try more complex things. I’ve been told a thousand times I over complicate things, but I really do like to try more complex things to help understand the other options available and how I can better do things. So if you don’t mind and you have the time can you give me some pointers on how to make my code better (can be simpler). Also on the flip side, are there any more advanced techniques I can employ?

// This #include statement was automatically added by the Particle IDE.
#include "DS18B20/Particle-OneWire.h"
#include "DS18B20/DS18B20.h"

// Globals
DS18B20 ds18b20 = DS18B20(D2); //Sets Pin D2 for Water Temp Sensor

int LED = D7; // pin for LED
int PIR = D3; //pin for PIR motion sensor

float pubTemp;
double celsius;
double fahrenheit;
unsigned int Metric_Publish_Rate = 30000;
unsigned int MetricnextPublishTime;
int DS18B20nextSampleTime;
int DS18B20_SAMPLE_INTERVAL = 2500;
int dsAttempts = 0;
int PIRnextSampleTime;
int PIR_SAMPLE_INTERVAL = 3000;
int alarmStatus;

Timer ledTimer(1000, blink_led);

int alarm(String command);

int alarm(String command)
{
    if(command == "reset")
    {
        alarmStatus = 0;
        return 0;
    }
    if(command == "panic")
    {
        alarmStatus = 1;
        return 1;
    }
    else 
    {
        alarmStatus = -1;
        return -1;
    }
}

void setup()
{
    Time.zone(-5);
    Particle.syncTime();
    Particle.function("alarm", alarm);
    Particle.variable("tempCelsius", &celsius, DOUBLE);
    Particle.variable("tempFahrenheit", &fahrenheit, DOUBLE);
    pinMode(LED, OUTPUT); // Use for a simple test of the led on or off by subscribing to a topical called led
    pinMode(PIR, INPUT_PULLUP); // Initialize pin as input with an internal pull-up resistor
    ledTimer.start();
    attachInterrupt(PIR, motion_detected, RISING);
    Serial.begin(9600);
}

void loop() 
{
    if (millis() > DS18B20nextSampleTime)
    {
        getTemp();
    }

    if (millis() > MetricnextPublishTime)
    {
        Serial.println("Publishing now.");
        publishData();
    }
    
    if (millis() > PIRnextSampleTime)
    {
        if (alarmStatus == 1)
        {
            raiseAlarm();
        }
    }
}

void blink_led()
{
    digitalWrite(LED, !digitalRead(LED));
}

void motion_detected()
{
    alarmStatus = 1;
}

void resetAlarm()
{
    alarmStatus = 0;
}

void raiseAlarm()
{
    Particle.publish("photon/motion/detected", "1");
    resetAlarm();
    PIRnextSampleTime = millis() + PIR_SAMPLE_INTERVAL;
}

void publishData() 
{
    if(!ds18b20.crcCheck())
    {
        return;
    }
    Particle.publish("photon/temperature/celsius", String(celsius));
    Particle.publish("photon/temperature/fahrenheit", String(fahrenheit));
    MetricnextPublishTime = millis() + Metric_Publish_Rate;
}

void getTemp()
{
    if(!ds18b20.search())
    {
        ds18b20.resetsearch();
        celsius = ds18b20.getTemperature();
        Serial.println(celsius);
        while (!ds18b20.crcCheck() && dsAttempts < 4)
        {
            Serial.println("Caught bad value.");
            dsAttempts++;
            Serial.print("Attempts to Read: ");
            Serial.println(dsAttempts);
            if (dsAttempts == 3)
            {
                delay(1000);
            }
            ds18b20.resetsearch();
            celsius = ds18b20.getTemperature();
            continue;
        }
        dsAttempts = 0;
        fahrenheit = ds18b20.convertToFahrenheit(celsius);
        DS18B20nextSampleTime = millis() + DS18B20_SAMPLE_INTERVAL;
        Serial.println(fahrenheit);
    }
}

For example (another hypothetical here…)

Aquarium controller:

Pump speed can be controlled 0%-100% by Particle.function
Pump speed can be scheduled by Particlie.function from an array [ { time, speed }, { time, speed} ]
Pump schedule will persist and continue if connectivity is lost from the cloud.
Pump schedule will default if restarted and cloud connectivity is unavailable.
Pump speed can be read by Particle.variable.
When pump stops, heater is turned off.
Temperature can be set by Particle.function.
Temperature is persisted if connectivity is lost.
Temperature defaults if restarted and connectivity is unavailable. (Would be interesting to know if user defined value can persist a restart?)
Temperature is logged every minute.
Temperature must also be readable on demand Particle.variable.
Temperature change can be scheduled throughout the day by Particle.function.
Temperature schedule persists if connectivity is lost.
Ph readings are taken once an hour and logged.
Ph read can manually be triggered using a Particle.function and 2 Particle.variables (phValue and phLastRead)
Ph threshold can be set using Particle.function (max/min)
If Ph exceeds upper limit dose with 1 unit of acid and log dose time.
If Ph drops below lower limit dose with 1 unit of alkali and log dose time.
If connectivity is lost or it is reset then maintain this threshold.
Light intensity can be controlled by Particle.function.
Light intensity can be read by Particle.variable.
Light schedule can be set by Particle.function.
Light schedule persists reset and connectivity lost.
Feeding schedule is set by Particle.function.
Feeding can be triggered by Particle.function.
Last feed time is available as a Particle.variable.
Feeding can be triggered manually this will skip the next scheduled event.
Feed routine will bring the lights up full, stop the pump and consequently stop the heaters for 3 minutes.

This is a bigger example, there is some repetition of scenarios but I think it is a good illustration that perhaps people can relate to (if they keep fish) and it has an element of critical or reliable functioning.

Let say for a more realistic scenario that this is all controlled (in terms of Particle.function calls) by a web interface, so the UI makes the Particle.function call. Things like schedules and user defined settings like temperature, ph, etc have a factory default but are also set by the UI. When the user sets a parameter or schedule in the UI they are stored in a database and these values can be retrieved through a webhook which the particle will do when a schedule function is called or if the particle has lost connectivity or has been restarted.

So we have a fair bit going on in this scenario as I’m sure you can appreciate; we have 6 parameters or functions to control (pump, heating, ph, dosing pump, lighting and feeding), 5 schedules, 10 functions, 6 variables, 7 timers and 6 interrupts (I think).

We also want pin state on startup to be managed so pump and heater are on, lights off, feed off, dosing pump off.

I’m not expecting you to write this code for me as this is a learning exercise, but I would really appreicate if can you give me some pointers on where to begin, how to structure something as complex as this (in terms of managing all those timers, etc), best practices, patterns that could be used, etc?

Many thanks

Andy

Actually, I don't think what you are trying to achieve requires this much complexity. You could just build a timeout into your ISR as easy as this:

void motion_detected()
{
  static volatile unsigned long lastFlag= 0;
  if(millis() - lastFlag > 5000)
  {
    isrFlag = true;
    lastFlag = millis();
  }
}

While the millis() function won't advance during the ISR, you can call it, so you have a pretty robust timeout method for the interrupt directed motion sensor and you don't need any funky multithreading or Class development overhead (for this quite trivial application).

Side note... I would caution you about unsigned addition.... your timers may have the dreaded rollover problem.

You should consider using unsigned subtraction as so eloquently described here.

2 Likes

Also keep in mind you can only register 4 Particle.functions(). There is a multifunction implementation out there somewhere and you can override the max but they are costly in terms of resources from what I understand.

2 Likes

Uhh, that’s some use case there :wink:

I’d be happy to provide ideas, but I’m definetly not in the position to say this is right or that is wrong, so whatever I say is merely my personal opinion :wink:

Just some minor pointers (which took longer, so others got in quicker ;-)):

  • If you write const int LED = D7;, your variable will be placed in flash and safe RAM as well as initialisation code.
  • If you declare a function befor its first use, you don’t need to provide a prototype (int alarm(String command);).
  • I’ve learnt to never trust myself, so I always add a final unconditional return xxx; statement in my typed functions, to avoid errors if I’d happen to change an else into an else if() and hence might drop out of the function without properly returning something.
  • If you are living somewhere with daylight saving time, you might need to add some extra logic to Time.zone(-5);
  • You could use other Timers instead of your if (millis() ...) constructs, or even the TimeAlarms library for timed events
  • The commonly used practice for if (millis() ...) constructs is if (millis() - msLastEvent > msDelayTime) - the reasons are widely discussed on Arduino forums (as @BulldogLowell has already said)
  • If you want to achieve a dead-time for your PIR detector, you’d need to observe this inside the ISR rather than in loop(), otherwise the flag will still be set immediately and the alarm will still fire then after the dead-time (as @BulldogLowell has already said)
  • Since you have several independet timed actions running, you might need to take precautions that your code does never offend the Particle.publish() limit of avg 1/sec with a burst rate of 4, otherwise the last event(s) will not be sent and your device be muted for at least 4 sec (especially immediately after start all your timed events will fire, due to initial millis() > 0, and two system events are published upon cloud connect)
  • Mark variables that get set/altered by ISR or other out of order instructions (e.g. other threads) as volatile!
  • To care for loss of cloud connection have a look at non-AUTOMATIC SYSTEM_MODES() and SYSTEM_THREAD(ENABLED)
  • For persisting schedules have a look at the EEPROM “library”

If you still want to dive into multithreading with the Photon, have a read here
https://github.com/spark/firmware/pull/614

There are some posts (which I can’t find just now) how to create a user thread, but it’s rather tricky and very alpha :wink:

2 Likes

Just wanted to say thanks for all the replies, I will spend a little time tonight digesting the advice and see if I can apply it to my bloated use case. An aquarium controller is on my backlog of diy projects but it makes an interesting learning scenario due to the number of control parameters etc.