Handling Multiple Events/ Sequential Event Reactions


#1

Hi All,

New to the community here, and to particle in general so forgive me if I ask something simplistic or obvious. I searched the forum but didn’t quite find what I am looking for.

Anyways I’m hoping someone can give me pointers to implementing a few ideas for my system.
I currently have about 6 photons running, 5 are connected to sensors/lights and are publishing events, while the last one is receiving all these events and controlling additional lighting.
The published events are set to poll status from each sensor every 60 seconds and say whether it’s high or low or to publish the current analog value of sensor.

One of my end goal here is to use these events to do predictive lighting.
I would like to say if sensor 1 has activated followed by sensor 2 then activate sensor 3.
So ideally if I move from sensor 1 to 2 then 3 will activate early but it won’t trigger 4.
but once I’m in range of sensor 3 (moving from 2) then sensor 4 will trigger.
Hopefully you guys get the idea, basically Im looking for a delay aspect that will wait a defined time and if that event occurs it will instantly react.

As the system stands I feel like it may not be ideal to publish each event from the sensors and send it to a hub.
Perhaps having sensor 1 publish to sensor 2 and 2 to 3 would be a better method?

I also feel like I’m not using the publish event status effectively as I am simply polling every 60 seconds on each sensor which leaves my console event tab quite clustered and it seems like fairly inconsistant polling with this method.

Let me know your opinions on this type of implementation and what steps you would take.
Any help is appreciated!


#2

All your “sensor” devices can subscribe to eachothers events and events are best published only when something interesting happens (e.g. state change, presence detect, threshold hit, …)


#3

Personally, I think I would send all the data to the last Photon, so you can keep the predictive logic all in one place. Publishing can have a bit of a lag sometimes, so depending on how fast you need to react, you might consider another option. Are all these Photons connected to the same WiFi? If so, you could directly communicate among them using a TCP connection. As @ScruffR wrote, you should only publish to this Photon when something relevant happens; I don’t think there’s any need to do polling.


#4

Thanks for the replies @ScruffR and @Ric. Ill try changing the logic to only publish with state change. Hopefully that will increase reliability in events. All the photons are on the same wifi. I went with cloud events as opposed to TCP as most of the older posts I read said it was an easier route to take. I’m not opposed to TCP connection, however I currently lack the knowledge to implement it.

My initial thought was the same as yours Ric to keep the predictive logic all on one device for simplicity however I’m finding it difficult to develop predictive code with this route.


#5

I’m not sure how spreading the logic out will make it simpler. If you could be more explicit about what logic you’re trying to implement, then perhaps we can help.


#6

BTW, this seems like a good case for using (as @peekay123 is fond of pointing out) an FSM (a finite state machine). If I understand your project correctly, each Photon would need to send the state of its sensor, the state of the light(s) it controls, and the time at which the sensor was activated.


#7

Is there a simple way to determine the time of an event trigger and make it into a usable variable @Ric ? I do agree that an FSM wouldn’t be a bad method to developing this code but for a group of only 3 sensor I don’t think its too necessary. Although I definitely will look more into how to develop these in particle.

You seem to have a decent understanding of what I am trying to accomplish but just to give some additional/simplified info:
-I have 3 photons each with sensor and a light.
-When any sensor detects movement it should turn on its corresponding light.
-I would like to have when sensor 1 is triggered and next sensor 2 is triggered then photon 3’s light will turn on early before sensor 3 is triggered.
-In most cases only one sensor will be triggered at a time.


#8

Yes, you can just use Time.now() which will give you the number of seconds that have elapsed since 1/1/1970. I assume that you would use an interrupt to catch the motion event.

volatile int triggerTime; // this would be at the top of your code
volatile bool motionDetected;
char timeString[12];

void motionDetected() { // the ISR
    triggerTime = Time.now(); 
    motionDetected = true;
}

In loop(), you would check the status of the motionDetected variable, and publish either to the next Photon in line or to the last Photon depending on how you decide to proceed. If you go with using the last Photon as a master device, then Photons 1 and 2 could both have this same code (except for the Publish name), and the master Photon would compare the times to see if Photon 2 triggered after Photon 1, and then act accordingly.

void loop() {
    if (motionDetected == true) {
        motionDetected = false;
        snprintf(timeString, 12, %d, triggerTime);
        Particle.publish("motionDetected_1", timeString); // 1 for Photon 1, 2 for Photon 2, etc.
    }
}

You might need some “debouncing” of your motion sensors to make sure they don’t trigger multiple times for one event.


#9

I did something like this for a project we built. Devices looked for events published from other devices; we queued them to evaluate what actions to take based on who was in the queue.

We solved the problem using a vector to build a list of events with the originator and a timestamp stored (similar to what @Ric described) each time an event was detected (using publish/subscribe paradigm).

I adapted what I did for your problem. The trigger for the LED to illuminate is either 1) a sensor (which then publishes an event to its peers) or 2) the evaluation of all the events, looking for the particular sequence of events and timing you wish to employ.

publish two events sequentially like this:

and then this:

will trigger the led to light…

code (it looks scarier than it is):


#define makeString(s) str(s)
#define str(s) #s

#include <vector>
#include <algorithm>

#define THIS_DEVICE_ID 1  // This is used as a string and a number in the code below

#define LED_ON_INTERVAL       15*1000   // 15 seconds
#define EVENT_KEEP_ALIVE_TIME 60        // one minutes
#define EVENT_SPACING         10        // 10 seconds

enum SensorState{
  NOT_TRIPPED,
  TRIPPED,
};

struct ActiveCaller{
  int deviceId;
  uint32_t timeStamp;

  bool operator == (const ActiveCaller& ac) {
    return ac.deviceId == deviceId;
  }
};

std::vector<ActiveCaller> activeCallers;

const int sensorPin = D6;
const int outputPin = D7;

Timer ledTimer(LED_ON_INTERVAL, [](){
    digitalWrite(outputPin, LOW);
  }, true);

void setup()
{
  Serial.begin(9600);
  pinMode(sensorPin, INPUT_PULLUP);
  pinMode(outputPin, OUTPUT);
  Particle.subscribe("myEventName", handleMessages, MY_DEVICES);
}

void loop()
{
  if (sensorTripped())
  {
    digitalWrite(outputPin, HIGH);
    ledTimer.start();
    notifyAll();
  }

  removeOldEvents();

  if (activeCallers.size())
  {
    if (checkEvents())
    {
      digitalWrite(outputPin, HIGH);
      ledTimer.start();
      notifyAll();
      activeCallers.clear();  // OPTIONAL: you trapped the event you were looking for so clear the vector
    }
  }

  static uint32_t lastMillis = 0;
  if (millis() - lastMillis > 5000)
  {
    lastMillis += 5000;
    Particle.publish("active events", String(activeCallers.size()), 60, PRIVATE);
  }
}

/*
Trigger events...
1 -> 2, 3 // a 2 is followed by a three in the vector
2 -> 3, 4
3 -> 2, 1
4 -> 3, 2
*/

bool checkEvents()
{
  switch (THIS_DEVICE_ID)
  {
    case 1:
      if(checkForSequence(2, 3) < EVENT_SPACING)
      {
        return true;
      }
      break;
    case 2:
      if(checkForSequence(3, 4) < EVENT_SPACING)
      {
        return true;
      }
      break;
    case 3:
      if(checkForSequence(2, 1) < EVENT_SPACING)
      {
        return true;
      }
      break;
    case 4:
      if(checkForSequence(2, 3) < EVENT_SPACING)
      {
        return true;
      }
      break;
  }
  return false;
}

uint32_t checkForSequence(int x, int y)
{
  ActiveCaller a;
  a.deviceId = x;
  ActiveCaller b;
  b.deviceId = y;

  // look for x in the vector
  std::vector<ActiveCaller>::iterator itrX = std::find(activeCallers.begin(), activeCallers.end(), a);
  if (itrX == activeCallers.end())
  {
    return EVENT_SPACING + 1;
  }

  // then look for y following x in the vector
  std::vector<ActiveCaller>::iterator itrY = std::find(itrX, activeCallers.end(), b);
  if(itrY == activeCallers.end())
  {
    return EVENT_SPACING + 1;
  }
  
  //return the elapsed time between the events
  uint32_t timeBetweenEvents = (*itrX).timeStamp - (*itrY).timeStamp;
  return timeBetweenEvents;
}

void removeOldEvents()
{
  if(activeCallers.size())
  {
    if (Time.now() - activeCallers.back().timeStamp > EVENT_KEEP_ALIVE_TIME)
    {
      activeCallers.pop_back();
    }
  }
}

void handleMessages(const char* event, const char* data)
{
  if(strstr(data, "device")) // {"device":2}
  {
    char mssg[strlen(data) + 1];
    strcpy(mssg, data);
    strtok(mssg, ":");
    int peerId = atoi(strtok(NULL,":"));
    if(peerId == THIS_DEVICE_ID)
      return;
    ActiveCaller newCaller;
    newCaller.deviceId = peerId;
    newCaller.timeStamp = Time.now();
    activeCallers.insert(activeCallers.begin(), newCaller);
    Particle.publish("debug", String(newCaller.deviceId ) + ": " + String(newCaller.timeStamp), 60, PRIVATE);
  }
}

void notifyAll()
{
  char message[32];
  strcpy(message, "{\"device\":");
  strcat(message, makeString(THIS_DEVICE_ID));
  strcat(message, "}");
  Particle.publish("myEventName", message, 60, PRIVATE);
}


bool sensorTripped()
{
  static SensorState lastState = TRIPPED;
  static uint32_t lastTripped = 0;  // timeout
  SensorState currentState = static_cast<SensorState>(digitalRead(sensorPin));
  if (lastState != currentState and currentState == TRIPPED and millis() - lastTripped > 200)
  {
    lastState = currentState;
    lastTripped = millis();
    return true;
  }
  lastState = currentState;
  return false;
}

this was written for 4 devices and you identify them with a unique DEVICE_ID of 1 to 4


#10

Thanks for the example codes @Ric and @BulldogLowell. Your code is fairly in depth Lowell, took me quite a few hours to understand everything happening. One thing I wasn’t sure of is how the sensorPin is controlling the SenorState states. It seems that the pin reacts when changing from low to high but I’m not seeing this called out in code. I’m trying to adapt this to an analog input.


#11

In essence it is accumulating the events (indicated by a device id and a timestamp) into a vector (array). It then looks at that array for a specific sequence of device events, compares the times that those events happened and does something if a defined event occurred.

I used a simple motion sensor to test this. The loop() function calls sensorTripped() and returns true for a state change from NOT_TRIPPED to TRIPPED.

For your analog input, you would need to create/modify a/the function that defines what TRIPPED means for your sensor.

What type of sensor did you decide to use?

This part is the function that looks for the TRIPPED state of my motion sensor:

bool sensorTripped()
{
  static SensorState lastState = TRIPPED;
  static uint32_t lastTripped = 0;  // timeout
  SensorState currentState = static_cast&lt;SensorState&gt;(digitalRead(sensorPin));
  if (lastState != currentState and currentState == TRIPPED and millis() - lastTripped &gt; 200)
  {
    lastState = currentState;
    lastTripped = millis();
    return true;
  }
  lastState = currentState;
  return false;
}

#12

@BulldogLowell, I have a general question about vectors after seeing your code. I don’t have any experience with vectors on Particle devices. My understanding is that they are allocated on the heap. Is there any problem with heap fragmentation when using them? I’ve been avoiding using any kind of dynamically allocated arrays because of a (perceived) problem with heap fragmentation. Am I being overly cautious?


#13

That is a great question and also very much a concern of mine.

We tested devices which use something similar to the above code; one using a vector of just int types and another with a much bigger vector object that looks something like the struct I have below. Each test had 10-20 devices broadcasting in a similar fashion both directly and through our server.

We have yet to experience any issues with over 6 months of testing but I can tell you that we do set a limit on the size of the vectors (in both cases) in the code.

struct ActiveCaller{
  char name[21];
  uint32_t colorID;
  const unsigned char* imagePtr;
  uint32_t mssgTime;  // basic timestamp
  char message[65]; //
};

If need be, you can certainly accomplish this with a circular buffer fixed-size arrays (or a mix of a lighter vector and arrays) but it was so easily programmed using vectors that once we tested we never really looked back. :wink:

I can add too that in both cases we are using the vectors with a considerably large code footprint managing an lcd display, analog sensors, motion sensors, servos and an array of RGB LEDs. I cannot share that code. :frowning:


#14

How does that code limit the size of the vectors? Doesn’t that just set the size if an individual element of the internal array?


#15

That bit obviously doesn’t, that was just an example of the object we containerize.

Size of the container is managed elsewhere in the code; max is 10, we pop_back() before inserting a new message.