2019-01-07

ESP32 (more precisely: Heltec WiFi Kit 32 with onboard OLED display) WiFi WalkBy Signal Strength Plotter

Expanding on the previous post showing a restored out-of-the-box functionality, and inspired by the capabilities of the excellent u8g2 graphics library (all be it restricted by my limited imagination and capabilities), I have the pleasure to present Version 2 of the WiFi Scanner displayed onboard the ESP32.

This one only plots the top 3 WiFi Signals found (identified by router MAC address, not by name as you wouldn't believe the number of duplicate network names you'll find just walking around....) But, it stores "recent" signal strengths and uses that to plot a sort-of bar chart under the network of strength. I don't know how useful it is, but it's fairly nifty and I'm more proud than I ought to be about it.

Here's it running in a Generic Semi Public Location:

(image processed to disguise the terrible job I did of soldering on the pins)

Updated 2019-01-08 12:46: Changed the "replace with new network" code to zero-out stored and now invalid history for the old SSID.


/*
   A very heavily modified example of the built-in example WiFiSCAN project,
   aoriginally ugmented with OLED display code given in the example at: https://robotzero.one/heltec-wifi-kit-32/
   and now using actual history  to represent signal strength over time.
   Graphics library documented at https://github.com/olikraus/u8g2/wiki
   
   2019-01-07 MCE
   Target Board: Heltec_WIFI_Kit_32 (with onboard 128x64 OLED)

   NB: Uses U8g2 ver of library to get better font-size control.
   After investigation, uses Full-buffer mode not so much for speed as to make debugging
   output less repetitive (Page buffer mode repeats the bar display / debug loop 3x per
   iteration due to the reasons documented at: https://github.com/olikraus/u8glib/wiki/tpictureloop )

   Pretty much all the "raw data" is spewed out over the serial link for debugging purposes.

   No Warranty for operation or continued operation should be implied - need to do a fair amount
   of walking to test "handover" of top-3 networks and/or <3 available networks, and I haven't
   done that yet....

   For reference, on the Heltec_WIFI_Kit_32 board, Sketch uses 610046 bytes (46%) of program storage space.
   Global variables use 43600 bytes (13%) of dynamic memory, leaving 284080 bytes for local variables. 
*/


#include "WiFi.h"
#include <U8g2lib.h>

// the OLED used on the board maps to....
//PAGE LOOP mode:
//U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g2(U8G2_R0, /* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16);
//FULL BUFFER mode:
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16);

//Helpers for specific font chosen setting cursor position:
//Font u8g2_font_haxrcorp4089_tr is 8 wide, 10 high
//Font u8g2_font_baby_tf is 10 x 10
const int FW = 10;
const int FH = 11; //Actual height is 10 but this gives a little space

// Arrays used to hold history info
// NOTE: We only show the "top 3" SSIDs 'cos that's all we can fit onto the display
// Adjust max. history in HISTSIZE to fit the width of the display (hint: 128 / 12 = 10 mod 8)
const int SSIDSIZE = 3 ; //Maximum number of SSIDs for which we'll store data
const int HISTSIZE = 12; //Noting that our index arithmetic needs to be 0->7....
const int SSIDLEN  = 20; //Max. SSID name length we'll use (truncate if less)
int PTR = 0; //the initial setting
char SSIDNames[SSIDSIZE][SSIDLEN] = {"DummySSID1", "DummySSID2", "DummySSID3"};
char BSSIDidx[SSIDSIZE][18]; //We need to match against Router BSSIDs to cope with multiple access points
//This is the signal strength history array. If SSIDSIZE is ever changed, this'll need updating too....
int SSIDStrength[SSIDSIZE][HISTSIZE] = { {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0} };

//Max dimensions of the bounding box we'll draw our bar segments into
const int BW = 128 / HISTSIZE;
const int BH = 10;

//Refresh interval (ms). In practice the refresh rate is this PLUS the wifi scan interval
//(which will dominate in the real world)
const int REFRESHINT = 2000;

//Main function to gather WiFi signal strengths and store into the data structures
void gatherWiFiData(int PTR)
{
  //Gather WiFi data
  digitalWrite(LED_BUILTIN, HIGH);
  int foundSSIDCount = WiFi.scanNetworks();
  Serial.print(foundSSIDCount); Serial.println(" total SSIDs found");
  digitalWrite(LED_BUILTIN, LOW);

  //Pass one: Did we get data that matches known BSSIDs?
  int foundSSID[3] = { -1, -1, -1 }; //Record index of matching BSSIDs for use in Pass two
  for (int i = 0; i < foundSSIDCount; ++i) {
    char thisSSID[SSIDLEN]; WiFi.SSID(i).toCharArray(thisSSID, sizeof(thisSSID));
    Serial.print(i); Serial.print(" Found SSID: |"); Serial.print(thisSSID); Serial.print("| BSSID:");
    //NOTE: BSSIDstr isn't documented but is a string-format version of the BSSID connected to... oy vey
    Serial.print(WiFi.BSSIDstr(i)); Serial.println(".");
    for (int j = 0; j < SSIDSIZE; j++) {
      char bssidStr[18]; WiFi.BSSIDstr(i).toCharArray(bssidStr, sizeof(bssidStr));
      if (strcmp(BSSIDidx[j], bssidStr) == 0) {
        //Matches! Mark as found, update signal strength
        foundSSID[j] = i;
        SSIDStrength[j][PTR] = WiFi.RSSI(i); //Set current sig. strength
        //Debug output:
        Serial.print("MATCH SSID Idx "); Serial.print(i); Serial.print(" against hist. SSID Idx "); Serial.print(j);
        Serial.print("("); Serial.print(SSIDNames[j]);
        Serial.print(") setting RSSI["); Serial.print(PTR); Serial.print("] = "); Serial.println(SSIDStrength[j][PTR]);
      }
    }
  }
  
  //Pass two: If we DIDN'T find the SSID, replace with the N'th one...
  for (int j = 0; j < SSIDSIZE; j++) {
    if (foundSSID[j] == -1) {
      //entry wasn't found -> replace with n'th WiFi result (where we cap to max num of SSIds found if less)
      int toUse = (j < foundSSIDCount) ? j : foundSSIDCount;
      foundSSID[j] = toUse;
      //char thisSSID[SSIDLEN]; WiFi.SSID(toUse).toCharArray(thisSSID,sizeof(thisSSID));
      //Convert both SSID and BSSIDstr (Strings) to relevant char* size and store:
      WiFi.SSID(toUse).toCharArray(SSIDNames[j], sizeof(SSIDNames[j]));
      WiFi.BSSIDstr(toUse).toCharArray(BSSIDidx[j], sizeof(BSSIDidx[j]));
      //Zero-out the existing RSSI data as we're starting with a new set:
      for ( int x = 0; x < HISTSIZE; x++) { SSIDStrength[j][x] = 0; }
      //Replace with current value:
      SSIDStrength[j][PTR] = WiFi.RSSI(toUse);
      //Debug output
      Serial.print("NEW SSID Idx "); Serial.print(toUse); Serial.print(" set to Hist. SSID Idx "); Serial.print(j);
      Serial.print("("); Serial.print(SSIDNames[j]); Serial.print(") BSSID: "); Serial.print(BSSIDidx[j]);
      Serial.print(" setting RSSI["); Serial.print(PTR); Serial.print("] = "); Serial.println(SSIDStrength[j][PTR]);
    }
  }

}

//Main function to output stored WiFi SSIDs and their associated strength histories.
void showWiFiStrength(int PTR)
{
  //Clear the screen, setup for write
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_baby_tf);
  u8g2.setFontPosTop();
  //Iterate over all stored SSIDs:
  for (int i = 0; i < SSIDSIZE; i++) {
    //Print SSID on "lines" 0, 2, 4...
    u8g2.drawStr(4, 2 * i * FH, SSIDNames[i]);
    //Debug output:
    Serial.print(i); Serial.print(" "); Serial.println(SSIDNames[i]);
    //Walk through sig history using modulus arithmetic from PTR -> PTR via HISTSIZE
    //Walk from OLDEST (PTR+1) to NEWEST (PTR)
    int j = (PTR + 1) % HISTSIZE;
    int cnt = 0; //Absolute counter, used for bar output positioning
    int barTop = ((2 * i) + 1) * FH; //Starting Y-position for all bars in this output
    u8g2.setCursor(0, barTop); //Not, strictly, needed as we direct-plot bars anyway
    do  {
      //Call our procedure to plot the bar
      drawSigStrengthBar(cnt, barTop, SSIDStrength[i][j]);
      //Debug: output index and strength
      Serial.print(j); Serial.print(":"); Serial.print(SSIDStrength[i][j]); Serial.print(" ");
      //Update the counter & pointer:
      j = (j + 1) % HISTSIZE;
      cnt++;
    } while ( j != PTR );
    //Debug: close the per-SSID history line off
    Serial.println("!");
  }
  //All elements written, actually paint the display:
  u8g2.sendBuffer();
}

//Graphical function to plot a single bar in given index position
//with height proportional to observed signal strength
void drawSigStrengthBar(int xIdx, int yTop, int sigStrength)
{
  //Scale sigStrength (expected range -40ish to -99) to a value 0 - (BH/10)
  //NB: we draw top to bottom, so output is INVERTED (strong signal, big bar = low offset)
  int barHeight = BH;
  //these magic numbers should really be consts at the top, but we're fiddling:
  if (sigStrength == 0 ) {
    //NaN / no value available
    barHeight = BH;
  } else if (sigStrength > -40) {    // Have never observed RSSI > -40 dBM, take that as full strength
    //V.strong signal, max bar height
    barHeight = 0;
  } else {
    //Normal, do the calcs
    sigStrength = sigStrength + 40;    // normalise by observed full-strength value
    barHeight = -1 * sigStrength / 6;  //Range is about -40 to -99, or 60 for short, which gives 6 ints per height
    barHeight = (barHeight >= BH) ? BH - 1 : barHeight; //Catch out-of-range values, coerce to 1-pixel
  }
  //Our "box" for this output is calculated as TOP-LEFT:
  // TOP-LEFT: x=xIdx*BW, y = yTop + barHeight
  // BOTTOM-RIGHT:   x=xIdx*BW+BW, y=yTop+BH
  //(note that drawBox takes TOP-LEFT, then WIDTH and HEIGHT though...)
  u8g2.drawBox(xIdx * BW, yTop + barHeight, BW, BH - barHeight);
}

//
// Standard Arduino setup() / loop() functions:
//
void setup()
{
  // Set WiFi to station mode and disconnect from an AP if it was previously connected
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  //Initialise the graphics library
  u8g2.begin();

  //Setup onboard LED for output:
  //Used by the WiFi gatherer process only to show scanning state.
  pinMode(LED_BUILTIN, OUTPUT);

  //Support debugging....
  Serial.begin(115200);
}

// the Master Control Loop is relatively simple......
// (if you proceduralise everything, of course....)
void loop()
{
  //Debug output marking our place:
  Serial.println("--------------------------------------------");
  Serial.print("Iterating. Loop Index = "); Serial.println(PTR);
  //Get the latest WiFi strength data, populating the PTR indexed element
  gatherWiFiData(PTR);
  //Display the current SSIDs and Strength charts at the PTR indexed element
  showWiFiStrength(PTR);
  //Increment the Loop Pointer using modulus arithmetic
  PTR = (PTR + 1) % HISTSIZE;
  //And sleep until we do it all again...
  delay(REFRESHINT);
}

1 comment:

  1. I only see two bar graphs lines on my v2.0 heltec. just saying.
    good work!

    ReplyDelete