Particle.subscribe subscription behavior relative to private events , public events , product membership , and console publish tool

I’ve been trying to figure out how the publish and subscribe functionality works between Electrons as well as when published via the event stream “publish” button in Particle console (see screeshots at bottom of post).

I thought that since there appears to be no good documentation on this in the Particle reference documentation that I would post my findings for anyone else that is wondering how this stuff works. Also, I just want to know if I am correct, so please correct me if I’m wrong.

First off, I’m using an Electron 3G (Europe/Africa/Asia version) and am compiling using po-util and spark firmware branch release/v0.6.1

Here is my test program (note I have altered the product ID for security purposes) :

#include "Particle.h"

PRODUCT_ID(123456789);
PRODUCT_VERSION(1);

//Configure how the app runs within the Particle ecosystem
SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);



//Configure Particle system (runs this code before any Particle init code runs):
STARTUP(
   //Configure other Particle runtime parameters
   System.enableFeature(FEATURE_RETAINED_MEMORY);
   //Enable system reset information
   System.enableFeature(FEATURE_RESET_INFO);
   //Make sure that cloud DFU updates are enabled
   System.enableUpdates();
   //Set Zantel APN credentials
   cellular_credentials_set("znet", "", "", NULL);

);

SerialLogHandler logHandler(LOG_LEVEL_INFO,
   {
      {"app"                  , LOG_LEVEL_ALL   },
   }

);
const char* logName = "app.main";
static Logger myLog(logName);  //Logger object used in this "main.cpp" file

//Function forward declarations
void countDown(unsigned short seconds);
void checkSerialCommands();
void showProcessingChars();


void testSubscribeCallback(const char* eventName, const char* eventData){
   myLog.warn("EVENT RECEIVED");
   myLog.warn("Event name = \"%s\"", eventName);
   myLog.warn("Event data = \"%s\"", eventData);
}

void testSubscribeCallbackMyDevices(const char* eventName, const char* eventData){
   myLog.warn("EVENT RECEIVED");
   myLog.warn("Event name = \"%s\"", eventName);
   myLog.warn("Event data = \"%s\"", eventData);
}


void setup(){
   //Start the serial monitor, print some space... wait 3 seconds
   Serial.begin(115200);
   countDown(3);

   Particle.subscribe("test_event", testSubscribeCallback);

   Particle.subscribe("test_event_", testSubscribeCallback, MY_DEVICES);

   //Suspend hardware watchdog
   pinMode(B0, OUTPUT);
   digitalWrite(B0, HIGH);

   // Particle.connect();

   myLog.warn("Setup ending!");

}


bool firstConnection = true;

void loop(){

   if(Particle.connected() && firstConnection){
      firstConnection = false;
      Particle.keepAlive(70);
   }

   //Show a pinwheel demonstrating serial connection is live
   showProcessingChars();

   //Check for serial commands
   checkSerialCommands();


}



//Function that takes serial commands from serial monitor
//and executes code based on the content of command
char serCommand[65];

// //Debug
void processSerialCommand(){
   //Analyze incoming serial command
   myLog.warn("Serial command is: \"%s\"", serCommand);
   //Enter DFU mode
   if(strstr(serCommand, "dfu")){
      myLog.warn("Entering DFU mode!");
      Serial.flush();
      System.dfu();
   }
   else if(strstr(serCommand, "reset")){
      myLog.warn("Rebooting!");
      Serial.flush();
      System.reset();
   }
   else if(strstr(serCommand, "connect")){
      if(Particle.connected()){
         myLog.warn("Disconnecting from Particle servers");
         Particle.disconnect();
      }
      else{
         myLog.warn("Connecting to Particle servers");
         Particle.connect();
      }
   }
   else if(strstr(serCommand, "listen")){
      myLog.warn("Entering listening mode");
      Serial.flush();
      Cellular.listen();
   }
   else if(strstr(serCommand, "public")){
      myLog.warn("Publishing public event:");
      myLog.warn("event name = \"test_event_public\" | event data = \"public data\"");
      Particle.publish("test_event_public", "public data", 60, PUBLIC);
   }
   else if(strstr(serCommand, "private")){
      myLog.warn("Publishing private event");
      myLog.warn("event name = \"test_event_private\" | event data = \"private data\"");
      Particle.publish("test_event_private", "private data", 60, PRIVATE);
   }
}

void checkSerialCommands(){
   unsigned int numBytesAvailable = Serial.available();
   if(numBytesAvailable){
      //Allocate buffer to read serial command
      //Debug
      // printWarning(myLog, __LINE__, "INCOMING SERIAL COMMAND");
      for(unsigned int count = 0; count < numBytesAvailable; count++){
         //Read all incoming bytes
         int newByte = Serial.read();
         if(newByte >= 0){
            char newStr[2] = {(char)newByte, '\0'};
            strcat(serCommand, newStr);
            //Check if we got a carriage return
            if(newByte == 13){
               processSerialCommand();
               //Reset the serial command buffer
               serCommand[0] = '\0';
               return;
            }
         }
         else{
            break;
         }
      }//END for loop
   }//End if numBytesAvailable
}



/*=============================================>>>>>
= SHOW PROCESSING CHARS ("processing" animated icon) =
===============================================>>>>>*/

const unsigned int LINE_FEED_DEBUG_RATE_MS = 250;
unsigned int lastLineFeedDebugTimestamp = 0;
const uint8_t numProcessingChars = 4;
const char processingChars[numProcessingChars] = {
   '/',
   '|',
   '\\',
   '-',
};
uint8_t processCharCount = 0;

void showProcessingChars(){

   //Print spaces between chunks of debug messages that are spaced apart in time
   if( (millis() - lastLineFeedDebugTimestamp) > LINE_FEED_DEBUG_RATE_MS ){
      lastLineFeedDebugTimestamp = millis();
      Serial.write(8);  //Backspace character
      Serial.print( processingChars[processCharCount] ); //The processing animation char
      if(++processCharCount == numProcessingChars) processCharCount = 0;
   }

}

/*= End of SHOW PROCESSING CHARS ("thinking" animated icon) =*/
/*=============================================<<<<<*/


/*=============================================>>>>>
= Function that prints a countdown to the serial monitor and blocks app for X amount of seconds =
===============================================>>>>>*/
void countDown(unsigned short seconds){
   for(unsigned short count = 0; count < seconds; count++){
      myLog.warn("%u", (seconds - count));
      delay(1000);
   }
   myLog.warn("GO!!!");
}

##Observations:

It would appear that events published as PUBLIC can be seen by any device anywhere in the world, regardless of whether they are claimed via your Particle account or not (like the Twitter analogy used in the reference documentation).

It would appear that events published as PRIVATE can only be seen by another Electron if they are both claimed via the same Particle account.

Once a device is claimed via a Particle account, it would appear that it needs to reset and/or disconnect from the Particle servers in order to start instigating and receiving PRIVATE events. For example, if an Electron connects to the Particle servers, and then while that connection is active it gets claimed via Particle CLI, PRIVATE events that it publishes will not be received by other devices claimed to your account, and it will not receive PRIVATE events it is subscribed to either until it resets its connection to the Particle servers (not sure if this is a bug or not).

Additionally, it would appear that adding/removing devices from a product has no effect on whether or not they can view each others’ events.

Finally, when using the Particle console publish tool (see screenshot above), it would appear that the event that it publishes is published with the following contexts:

  • If published from device event stream (i.e. claimed devices event stream), the mock-device equivalent is a claimed device. In other words, all devices claimed via the corresponding account will receive PRIVATE publishes from this mock-device (see screenshot, below)

  • If published from product event stream (i.e. all claimed/unclaimed devices event stream within a given product), the mock-device equivalent is a claimed device that belongs to the product.

Edit Dec 14 2017: When you subscribe to an event using the MY_DEVICES flag, PUBLIC events that are published via the event stream publish tool do not appear to get received. In other words, if you include this line in your code:

Particle.subscribe("test_event_public", publicDataHandler, MY_DEVICES);

And then publish the following event via the publish tool in the devices event stream:

Your device will not receive the event.

End edit Dec 14, 2017

##What if you want to subscribe to private events within a Particle Product without claiming every device within that product via your Particle account?

It would seem there is no out-of-the-box way to do this. The best methodology I can think of for doing this is to make the publishes that you want your unclaimed devices within your product to receive have event names that are hard to guess and publish them as PUBLIC, as shown here:

Particle.publish("hardToGuessEventName381736#4$2*7" , "quasi-private data goes here" , 60 , PUBLIC );

The downside here is that if someone happens to guess the event name you are using (for example, perhaps they are subscribed to the event “h” and will therefore receive the above example event), then they could conceivably start peppering your device with publishes :sweat:

So, it appears that the only truly safe way to have your Particle device subscribe to an event that you would like to be PRIVATE is to actually make that event PRIVATE, and claim all of the devices within your product via your account.

This is not ideal, obviously, and it would be nice if there was a way to add a PRODUCT scope to a subscription to a public event, instead of simply a MY_DEVICES scope (which ignores the concept of a product, and for all intents and purposes interprets a PUBLIC publish from one of your claimed devices as PRIVATE). If such a product scope were available then you could subscribe to all PUBLIC events that were part of your Particle product without fear of a malicious 3rd party Particle device listening for your event and then regurgitating at infinitude the same event over and over again (now that I’ve typed that out it seems pretty unlikely, but still).

Does anyone know if a product scoped subscription flag is likely to be implemented, and if so, when?

3 Likes

Nice writeup :+1:
This is an extension of one of my (now closed) issues I had reported back in the early days :wink:
Especially the part about a device not being able to subscribe to PUBLIC events published by my device via a MY_DEVICES subscription.

https://github.com/spark/firmware/issues/645
Also related
https://github.com/spark/firmware/issues/1016
https://github.com/spark/docs/issues/560

2 Likes

Thanks for the props @ScruffR :smile:
just to be clear, what would be great is if I could subscribe with a signature something along the lines of:
Particle.subscribe("quasiPrivateEvent", quasiPrivateEventHandler, THIS_PRODUCT);

2 Likes