Photon SoftAP Inputs

If you can describe what exactly you need, I might find the time to put somethimg together for you.

1 Like

Iā€™m still prodding at your control_HTML, but fundamentally iā€™m just looking to take a string input (or possibly two) from the soft AP page to be saved and later used in the photon code. I imagine it will be some modification of your control code to change your slider bar to an input box, then parsing the URL for the string (or strings) and saving it to internal memory. A secondary goal would be to place it on the main page (or including a link to the control_HTML page on the main page) so little navigation is required.

On a side note, i find this SoftAP process to be absolutely fascinating, giving the average user the ability to tweak the innards of the system without having to futz about with actual code? Amazing.

This would be one way to add two extra inputs to the index.html (just replace the original with this)

const char index_html[] = 
"<!DOCTYPE html>"
"<html>"
"<head>"
"  <meta name='viewport' content='width=device-width, initial-scale=1'>"
"  <title>Setup your device</title>"
"  <link rel='stylesheet' type='text/css' href='style.css'>"
"</head>"
"<body>"
"  <h1>Connect me to your WiFi!</h1>"
"  <h3>My device ID:</h3>"
"  <input type=text id='device-id' size='25' value='' disabled />"
"  <button type='button' class='input-helper' id='copy-button'>Copy</button>"
"  <div id='scan-div'>"
"    <h3>Scan for visible WiFi networks</h3>"
"    <button id='scan-button' type='button'>Scan</button>"
"  </div>"
"  <div id='networks-div'></div>"
"  <div id='connect-div' style='display: none'>"
"    <p>Don't see your network? Move me closer to your router, then re-scan.</p>"
"    <form id='connect-form'>"
"      <input type='password' id='password' size='25' placeholder='password' />"
"      <button type='button' class='input-helper' id='show-button'>Show</button>"
"      <button type='submit' id='connect-button'>Connect</button>"
"    </form>"
"  </div>"
"<!-- vvvv  THIS IS THE NEW PART  vvvv -->"
"  <div id='extra-div' >"
"    <p>Add your own data here</p>"
"    <form id='extras' >"
"      <table width='100%'>"
"        <tr><td><input type='text' id='str1' name='str1' size='35' /></td></tr>"
"        <tr><td><input type='text' id='str2' name='str2' size='35' /></td></tr>"
"        <tr><td><button type='submit' id='store'>Store Data</button></td></tr>"
"      </table>"
"    </form>"
"  </div>"
"<!-- ^^^^  THIS IS THE NEW PART  ^^^^ -->"
"  <script src='rsa-utils/jsbn_1.js'></script>"
"  <script src='rsa-utils/jsbn_2.js'></script>"
"  <script src='rsa-utils/prng4.js'></script>"
"  <script src='rsa-utils/rng.js'></script>"
"  <script src='rsa-utils/rsa.js'></script>"
"  <script src='script.js'></script>"
"</body>"
"</html>"
;

and thats the code to parse the data

void myPage(const char* url, ResponseCallback* cb, void* cbArg, Reader* body, Writer* result, void* reserved)
{
    char  _url[strlen(url)+1];
    char* query;
    char* value;
    
    Log.trace("handling page %s", url);

    strcpy(_url, url);
    if (strtok_r(_url, "?&", &query)) 
    { // is it the control page [0]?
      Log.trace("pre for() - URL: <%s>, *query: <%s>", _url, query);
      while(query) 
      { // and has it got a query string
        Log.trace("in  for() - URL: <%s>, *query: <%s>", _url, query);
        char *token = strtok_r(query, "=", &value);
        Log.trace("*query: <%s>, *value: <%s>, *token: <%s>", query, value, token);

        if(strcmp(token, "str1") == 0) 
        { // with a key str1
          char* val = strtok_r(NULL, "?&=", &value);
          Log.trace("String1: <%s>", val);
        }
        else if(strcmp(token, "str2") == 0) 
        { // with a key str2
          char* val = strtok_r(NULL, "?&=", &value);
          Log.trace("String2: <%s>", val);
        }

        query = value;  // continue with the rest
      }
    }

    if (strcmp(url,"/index")==0) {
        Log.trace("sending redirect");
        Header h("Location: /index.html\r\n");
        cb(cbArg, 0, 301, "text/plain", &h);
        return;
    }

    int8_t idx = 0;
    for (;;idx++) {
        Page& p = myPages[idx];
        if (!p.url) {
            idx = -1;
            break;
        }
        else if (strcmp(_url, p.url)==0) {
            break;
        }
    }

    Log.trace("refresh page%d <%s>", idx, myPages[idx].url);

    if (idx==-1) {
      cb(cbArg, 0, 404, nullptr, nullptr);
    }
    else {
      cb(cbArg, 0, 200, myPages[idx].mime_type, nullptr);
      result->write(myPages[idx].data);
    }
}
1 Like

@tsnealon, I have spent some time (and even fried one of my trusty Photons in the process :pensive:) on wrapping this up for you, so it would be nice to get some feedback too :sunglasses:
@RWB, how about you? Have you given this a whirl? Itā€™s free to use too.

Sorry for the delayed response, ScruffR. Gainful employment keeps me a little too busy during the week to spend much time playing with my electronics, but iā€™m sitting down now to take a look.

Ok, iā€™ve booted your code and taken it for a test spin. Itā€™s pretty much perfect. So it looks like the HTML of the index.html page takes the strings entered in the boxes and places them (with str tags) in the URL, which then gets parsed by the myPage. What i canā€™t quite tell is what it is doing with the strings once they are identified and parsed. I see these lines:

          char* val = strtok_r(NULL, "?&=", &value);
          Log.trace("String1: <%s>", val);

Best i can tell, the code parses out the string in the first line. I think the second line just sends the string to the log? (probably a stupid question). If i were to take that character pointer ā€˜valā€™, and assign it to a global string, i.e.:

std::String myString(val);

would this value persist through the rest of the code, after setup is completed/device is reset? (FYI iā€™m not sure the above snippet even works). If true, then this is a perfect solution to my problem.

Thanks again ScruffR. Sorry for taking so long to review the solution.

1 Like

Not a stupid question, but that is what my sample code does - not more.
But you can copy that string into a global variable to deal with it outside of myPage() although I'd go for a character array like this

char str[2][32];  // global
...

       if(strcmp(token, "str1") == 0) 
        { // with a key str1
          char* val = strtok_r(NULL, "?&=", &value);
          strncpy(str[0], val, sizeof(str[0]));
          Log.trace("String1: <%s>", str[0]);
        }
        else if(strcmp(token, "str2") == 0) 
        { // with a key str2
          char* val = strtok_r(NULL, "?&=", &value);
          strncpy(str[1], val, sizeof(str[1]));
          Log.trace("String2: <%s>", str[1]);
        }
        ...

@ScruffR

I just gave it a try and I like how itā€™s easily added to the main page.

This is a very powerful tool for projects that need setup but do not have an input interface.

For the current project Iā€™m working on I think the LCD screen with touch input is a better choice but I may change my mind about this so itā€™s nice to have a working backup plan if I want to ditch the LCD with touch input.

I need to order a few spare Photonā€™s myself, I have a project running at a higher voltage that sometimes back feeds that has caused a few Photons to fail.

Thanks for taking the time to code this so we have a better example of how this works.

2 Likes

Interesting. Why a character array and not a string? This particular aspect of C code is a bit nebulous to me. So if iā€™m reading this correctly, youā€™ve initialized an array with 2 entries up to 32 characters long? What sort of advantage does this have over using strings?

I may have to tackle conditioning of the HTML input, i.e. preventing someone from inputting too many characters or adding something that would otherwise confuse the code, but thatā€™s a hurdle for a later date.

Thanks again.

Iā€™ve gotten this particular permutation running, but it prompts two more interesting questions about listen mode and SoftAP in general:

  1. How is the SoftAP code exiting listen mode without losing the temporary data?

I managed to save the strings (or char arrays) to the photon, and have it echo out in a particle.publish, but i quickly realized that as soon as it resets, that inputted data is lost. Somehow, when pressing the ā€œConnectā€ button on the softAP index page, the photon is exiting listen mode without resetting, but i canā€™t parse out in the code how itā€™s happening. Iā€™d like to add an additional button that effectively does nothing but exit listen mode.

  1. Following up on #1, is it possible to save this data in a more permanent location?

Somewhere that would survive a reset? My rudimentary understanding of IOT boards seems to indicate a limited amount of R/W cycles, but this data would only rarely be adjusted so thatā€™s not an enormous issue.

Any thoughts?

Because String objects store data on the heap via dynamic memory allocation which - when used a lot - lead to heap fragmentation which will eventually cause the device to crash, since heap is small and unliker bigger systems there is no garbage collection/defragmentation.

strncpy() only takes a limited number of characters, but you may want to add str[0][31] = '\0' to make sure the string is teeminated after a truncated copy action.
But sure, adding the maxlength="31" attribute to the <TEXT> tag makes this even safer.

What temporary data? Global variables are not temporary. But a reset does "clear" global variables.
Dropping out of Listening Mode is just a change of mode and happens in the system part of the code you don't see, but it's not resetting the device.
To do the same thing from your code, you'd use WiFi.listen(false)

Yes, you can ether use Backup RAM based variables via the retained keyword or go for non-volatile EEPROM

1 Like

Ahh this is very helpful. I hadnā€™t realized that Strings were handled so differently than character arrays. Iā€™ll have to adjust some of my code to make better use of this. It certainly makes me miss the silent conversion capabilities of Python :sweat_smile:

Based on the two possible non(or less) volatile memory types, it seems like EEprom is more what iā€™m looking for. Iā€™ll have to play with it a bit and see if i can store (and retrieve) the two character arrays across resets/power cycles. Thanks ScruffR - if they invent a way to ship beer digitally, youā€™ll find a cold one in your inbox. Cheers! :beers:

1 Like

Hello!

I thought the concept is something like: When SYSTEM_THREAD(ENABLED) is used in LM, loop() cannot run until it exits LM! But setup() could run!

Nope, thatā€™s the whole point. LM will be running on the system thread, keeping the application thread free to run your loop() code.
Just be aware that some resources - in particular the WiFi block - will be occupied by LM.
I havenā€™t been digging into that too deep but straight forward tests indicated that you canā€™t (easily) use TCPClient features in your code while LM is active.

Maybe @rickkas7 has some advice here.

Thanks for showing the right sight to my eyes! :relieved:

And this:

It brings another issue that I may face in my application! And yes, some advice will be helpful!

I guess till Particle implements the long promised hybrid ā€œConnected SoftAPā€ mode you need to implement a SoftAP part for initial setup and a seperate ā€œnormalā€ webserver for connected tasks.
For the latter you may need to assign a static IP during setup and/or add something like mDNS or Bonjour to allow for device discovery.

Yeah, right. That is the way til then! Iā€™m gonna work on this with @AJ7 (As Youā€™ve explained in his post!)

1 Like

Hello @ScruffR,

Iā€™ve been printing the WiFi credentials(in setup()) like IP, SSID, gateway on the serial monitor. It worked fine, but as soon as I added SYSTEM_THREAD(ENABLED) on the top of my code, serial monitor showed 0.0.0.0.
Then I tried printing them in loop(), and it works fine whether system thread is enabled or not!
My system was running on Automatic mode.

AFAIK, setup() will run immediately when system thread is enabled. So,my question is that Can I not use Networking functions (e.g. WiFi) in setup() at this time? Are there any other functions and variables too that I need to take care of?

Thank You,
djs07

Thatā€™s to be expected since your code starts running immediately - even without having received a valid IP address - with SYSTEM_THREAD(ENABLED)
If you want to print the IP you need to check for WiFi.ready() first and to be sure the IP has been stored correctly add a Particle.process() after the check too.

I've done the same thing as you're saying!

  Serial.begin(9600);
  waitUntil(WiFi.ready);
  Particle.process();

Still facing the same issue!

@ScruffR , Iā€™ve used mDNS library as you suggested. But Iā€™m not able to access the webserver by using something like http://abc.local (which I can access using local/dynamic IP). Iā€™m working on Windows OS. Any Idea What could be the issue here?
Thanks.

EDIT: Iā€™ve hosted server locally.