Managing WiFi and Cloud With optional Wifi Support on Device


#21

Just did a quick test. Even when in SYSTEM_MODE(MANUAL) - if a connection is lost after a successful connection is made the Photon will attempt to auto reconnect indefinitely.


#22

With SYSTEM_THREAD(ENABLED) you shouldn’t need to bother too much in your own code to keep the connection.

Because in most cases you’d be expecting more than one result, but for the compiler there isn’t much difference between an array of one type and a pointer to the same type. &array[0] (address of a single field in the array) and array (base address of the array) is essentially the same thing and consequently a pointer to one single entity is just another way to achieve the same.

Since the above reasoning can be considered C basics I don’t think that should be necessary.

That’s only true when already WiFi.ready() but since you can call Particle.connect() even when not, it’s always good to be prepared.

Too bad - would have been an easy fix :pensive: On the other hand, if there would only be a short drop which could be quickly cured it does make sense.


#23

I am not being crass - I have spent a lot of time trying to get a product working and out in the field that works exactly as I understand you want your products to work. The answers to your questions are in the topics but not always as precise solutions. Additionally, the documentation isn’t as clear as it could be.

I tried a lot of combinations settings and learned to not try and go against the flow. SEMI_AUTOMATIC is the way to go for my products. @scruffr has suggest MANUAL - the thing I guess you need to decide is whether the product is normally connected or normally disconnected to WiFi?

how are you keeping the Photon from automatically retrying to connect if your connection fails after a successful connection was made

If the WAP is out of range - no problem - LED flashes green, application is not blocked (as long as you are checking WiFi.ready() and Particle.connected(). WAP back in range then it will automatically reconnect - no need to call WiFi.connect(). If you want to operate not connected then turn off WiFi module.

I have some timed regular functions that check the connection status and decide what to do. I separate the testing for WiFi.ready and Particle.connected and also call WiFi.connect and Particle.connect separately and do a waitFor() after each. Clearly if you have some user interface (buttons/led) and want to ensure responsiveness then you would approach this using a millis() based timeout.


#24

Again, thanks so much for the guidance. I am sure when this is working and acceptable it will help others building products using the Photon and want a similar setup where their device should function with/without its companion app.

Is this looking better? I started a new firmware to just focus on this wifi connection logic. I’m still doing some testing but this seems to cover the bases.

The firmware uses a button to control the D7 led.

Single click: Blink LED for 5 seconds slow
Double click: Blink LED for 5 seconds normal
Long click: Clear WiFi credentials and enter listening mode, flashing LED while in listening mode
Particle Function to trigger the LED

I’ve tried to incorporate the suggestions to clean using the pointer instead of the array. I cleaned up the function that checks for an available network. I also incorporated the retry logic for the Particle.connect in the loop as well as WiFI connection retry logic with a max attempt of 10

This sample supports the idea that the physical device should function without WiFi using its on hardware button. The user can invoke setup by holding down the button for 10 seconds which will enter listening mode (timeout after 180 seconds) to provide credentials. The Photon will auto attempt to connect once the credentials are received so we need to reset our state when that occurs. If the user does not provide credentials in the timeout the device will leave listening mode and reset the should listen state.

On startup if there are no creds it will not try to connect. Nothing else needs to be done here b/c once creds are provided it will auto attempt to connect. User would enter setup via the long click on the button or just use the device without it being connected. Loop retry wifi connection logic wouldn’t run here either b/c no creds.

On startup if there are creds and avail network it will attempt to connect. If that fails for some reason the Particle enters its own listening mode which we do not want so a timer is in place to kill listen mode if the user did not invoke it. The connection attempt could fail. We should retry here bc there are a number of reasons it could have failed that are resolved with a retry. If it does fail the loop retry logic will try every 60 seconds up to 10 times to see if it can try a reconnection. The max attempts here is to prevent the indefinite retries.

On startup if there are creds but no avail network it will not try to connect. It should retry as its possible the ap is just not in range etc. The loop retry logic will check every 60 seconds for a max of 10 times in case the ap comes in range. However, the user could be using their device in a different location so I wanted the max attempts here so we dont try indefinitely.

If connection is successful and connection is lost later, Photon will auto attempt to reconnect so nothing to do for that case.

#include <clickButton.h>

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

//Startup Setup
STARTUP(WiFi.selectAntenna(ANT_EXTERNAL));

//LED Speeds
int slow = 500;
int normal = 250;
int fast = 100;

//Inputs
int userBtnPin = D4;
ClickButton userBtn(userBtnPin, HIGH, CLICKBTN_PULLUP);

//Outputs
int userFeedbackLed = D7;

//State
bool shouldSignal = false;
bool shouldRemoteSignal = false;
bool shouldWiFiListen = false;
bool hasWiFiConnected = false;
int signalSpeed = normal;

//Used for loop particle connect retry
int msRetryConnect   = 30000; 
int msOngoingConnect = 0;
      
//Used for wifi connect retry
int msWiFiRetryConnect   = 60000; 
int msWiFiOngoingConnect = 0;
int wiFiRetryCheckAttempts = 0;
int maxWiFiRetryCheckAttempts = 10;

//Button click result
int userBtnFunction = 0;

//Logger
SerialLogHandler logHandler;

//Timers
Timer stopInvalidWifiListeningTimer(1000, stopInvalidWifiListening);
Timer stopSignalTimer(5000, stopSignal, true);
Timer stopValidWifiListeningTimer(180000, stopValidWifiListening, true);

void setup(){
    //Register remote particle functions
    Particle.function("signal", remoteSignal);

    //Btn setup
    userBtn.debounceTime  = 20;   // Debounce timer in ms
    userBtn.multiclickTime = 250;  // Time limit for multi clicks
    userBtn.longClickTime  = 10000; // time until "held-down clicks" register

    //Pin setup
    pinMode(userFeedbackLed, OUTPUT);
    digitalWrite(userFeedbackLed, LOW);

    //Open serial for logging
    Serial.begin(9600);
    
    //Gives you some time to connect to Serial to see the output of the logger
    waitFor(Serial.isConnected, 10000);
    
    //Small delay needed here as initial log entries may not make it to serial even after connected
    delay(1000);

    //Log device info
    Log.info("System version: %s", (const char*)System.version());
    Log.info("Device id: %s", (const char*)System.deviceID());

    //Stop wifi from listening if user didn't invoke it, if WiFi connect below fails (bad creds etc) Photon will attempt listening mode.  This will kill it
    stopInvalidWifiListeningTimer.start();

    //If user entered listening mode and provided credentials we need to reset our states and set timer as the Photon will try to auto connect
    System.on(network_credentials, handleNetworkCredentialsEvent);
    
    //If we ever get a successful connection we can rely on the Photon to try the reconnect on lost connection. This state will prevent the retry in the loop
    System.on(network_status, handleNetworkStatusEvent);

    WiFi.on();
    delay(1000); //Small delay as network scan returns 0 if done immediately after turning on wifi
    
    //Try wifi connection if there are creds and available network to connect to.
    if(WiFi.hasCredentials()){
        msWiFiOngoingConnect = millis(); // putting this here so loop initial check is delayed 
        if(networkFromCredsAvailable()){
            tryWiFiConnection();
        } else {
            //We have creds but no AP here so will retry in loop in case AP comes in range
            Log.info("Did not find a matching network so will try in loop later");
        }
    } else {
        Log.info("Did not find any credentials so not going to try to connect to WiFi at all");
    }
}

bool networkFromCredsAvailable(){
    bool foundNetwork = false;
    WiFiAccessPoint credAP;
    
    if(WiFi.getCredentials(&credAP, 1)){
        const char* ssidFromCreds = (const char*)credAP.ssid;
        Log.info("Located SSID from credentials: %s", ssidFromCreds);
    
        WiFiAccessPoint availableAPS[20];
        Log.info("About to scan for available networks");
        
        int foundAvailableAps = WiFi.scan(availableAPS, 20);
        Log.info("Found %d available networks", foundAvailableAps);
    
        if(foundAvailableAps){
            for (int i = 0; i < foundAvailableAps; i++) {
                if(strcmp(availableAPS[i].ssid, ssidFromCreds) == 0){
                    Log.info("Found network SSID from credentials: %s", ssidFromCreds);
                    foundNetwork = true;
                    break;
                }
            }
            if(!foundNetwork){
                Log.info("No networks match");
            }
        } else {
            Log.info("No networks available");
        }
    } else {
        Log.info("No credentials retrieved");
    }        

    return foundNetwork;
}

void stopSignal(){
    Log.info("Stop signaling");
    shouldSignal = false;
    shouldRemoteSignal = false;
}

void stopInvalidWifiListening(){
    if(!shouldWiFiListen && WiFi.listening()){
        WiFi.listen(false);
    }
}

void stopValidWifiListening(){
    if(shouldWiFiListen && WiFi.listening()){
        shouldWiFiListen = false;
        WiFi.listen(false);    
    }
}

void toggleLed(int lPin, int speed) {
    digitalWrite(lPin, HIGH);
    delay(speed);
    digitalWrite(lPin, LOW);
    delay(speed);
}

int remoteSignal(String paramStr){
    Log.info("Running remote signal: %s", paramStr);
    switch(paramStr.toInt()){
        case 0:
            shouldRemoteSignal = false;
            stopSignalTimer.stop();
            break;
        case 1:
            shouldRemoteSignal = true;
            stopSignalTimer.start();
            break;
    }
    return 1;
}

void doSignaling(int speed){
    shouldSignal = true;
    signalSpeed = speed;
    stopSignalTimer.start();
}

void handleNetworkCredentialsEvent(system_event_t event, int param){
    if(param == network_credentials_added){
        Log.info("Network credentials added");
        msWiFiOngoingConnect = millis(); //set timeout base here b/c photon auto connects after getting credentials so we dont want loop trying
        shouldWiFiListen = false;
    }
}

void handleNetworkStatusEvent(system_event_t event, int param){
    if(param == network_status_connected){
        Log.info("Network connection established");
        hasWiFiConnected = true;
    }
}

bool shouldRetryWiFiConnection(){
    Log.info("Retry connection attempt: %d", wiFiRetryCheckAttempts);
    return !WiFi.ready() &&                //Check just incase system event wasn't fired yet
            !WiFi.connecting() &&           //Not current connecting
            networkFromCredsAvailable();    //Has available AP from creds
}

void tryWiFiConnection(){
    Log.info("Attempting wifi connection");
    WiFi.connect();                     // initiate wifi (re)connect
};

bool shouldRetryParticleConnection(){
    return WiFi.ready() && !Particle.connected(); 
}

bool okTimeToCheckWifiRetry(){
    return ((millis() - msWiFiOngoingConnect > msWiFiRetryConnect) || !msWiFiOngoingConnect); //has enough time passed or first time
};

bool okTimeToCheckParticleConnectRetry(){
    return ((millis() - msOngoingConnect > msRetryConnect) || !msOngoingConnect); //has enough time passed or first time
}

void resetBtnState(){
    userBtnFunction = 0;
}

void loop(){
    userBtn.Update();

    if(userBtn.clicks != 0){
        userBtnFunction = userBtn.clicks;
    }     
    
    //If wifi connection wasn't made in setup lets retry
    if(!hasWiFiConnected && (wiFiRetryCheckAttempts < maxWiFiRetryCheckAttempts) && okTimeToCheckWifiRetry() && WiFi.hasCredentials()){
        msWiFiOngoingConnect = millis();
        wiFiRetryCheckAttempts++;
        if(shouldRetryWiFiConnection()){
            tryWiFiConnection();    
        }
    }
    
    // when particle not connected lets retry
    if (okTimeToCheckParticleConnectRetry()) {
        if(shouldRetryParticleConnection()){
            msOngoingConnect = millis(); // set the timeout base
            Log.info("Particle not connected... Lets attempt to connect");
            Particle.connect();                  // initiate (re)connect
        }
    }

    if(shouldSignal){
        Log.info("In Local Signal Mode, Toggle user feedback led and exit loop.");
        toggleLed(userFeedbackLed, signalSpeed);
        resetBtnState();
        return;
    }

    if(shouldRemoteSignal){
        Log.info("In Remote Signal Mode, Toggle user feedback led and exit loop.");
        toggleLed(userFeedbackLed, slow);
        resetBtnState();
        return;
    }

    if(shouldWiFiListen && WiFi.listening()){
        Log.info("In WiFi Listen Mode, Toggle user feedback led and exit loop.");
        toggleLed(userFeedbackLed, fast);
        resetBtnState();
        return;
    }

    switch (userBtnFunction) {
      case -1:
        Log.info("Handle Btn Long Click");
        shouldWiFiListen = true;
        stopValidWifiListeningTimer.start();
        WiFi.clearCredentials();
        WiFi.listen();
        break;
      case 1:
        Log.info("Handle Btn Selector Click");
        doSignaling(slow);
        break;
      case 2:
        Log.info("Handle Btn Selector Double Click");
        doSignaling(normal);
        break;        
    }

    resetBtnState();

    delay(5);
}


#25

One thing I have noticed is that if you do a scan for WAPs in a very crowded (wireless networks place) you can lock up the device because of array overflow. I guess in testing in your workshop you won’t notice this. It might be that the bug that ignored the scan limit has been fixed - I haven’t regression tested for a while as I have not had any issues reported with an array size of 20 so you should be good. Alternatively, you can use the callback mechanism.