Table of Contents

ChangeLog

28 April 2020

  • reset the device in case the wifi client cannot connect to the server (to avoid upstream bugs with wifi client)

27 April 2020

  • add OTA support
  • remove sleeping altogether since it does not look like a low energy project
  • implement WiFi reconnect

26 April 2020

  • move configurable parameters to the top of the file and add sections.
  • turn some configurable parameters into defines rather than variables.
  • change from DyanmicJsonDocument to StaticJsonDocument in order to suit arduinojson v6
  • allow the user to switch between SSL and plain HTTP (for private servers)
  • fix deep sleep support for ESP32 and ESP8266 (ESP8266 is now compatible)
  • remove yield() in the main loop since it is redundant
  • rename GROUP_NAME → FAMILY_NAME since that is what is implied (find3 terminology)
  • fix defines to include BLE only on ESP32
  • fix a startup crash on ESP8266 (wait for WiFI connection)
  • use proper NTP client to retrieve timestamp and remove getUnixTimestamp (now compatible with both ESP32 and ESP8266)
  • add the UTC offset as a parameter to the NTP client
  • shorten the chip ID and add "esp-" to make it clear what device is updating the find server
  • use toggle defines rather than "is defined" since it is more elegant for the user to deal with

About

Find3 is a framework that uses Wifi and Bluetooth signal levels (RSSI) from devices relative to each other and then applies statistical methods such as machine learning in order to track devices indoors. A0 rundown and tutorial of Find3 can be found in the IoT section.

This sketch is an alternative Find3 client for the ESP, forked from the official repository but with a few features added and some refactoring.

Code

/*
  This file is part of esp-find3-client by Sylwester aka DatanoiseTV.
  The original source can be found at https://github.com/DatanoiseTV/esp-find3-client.
 
  26/04/2020: Adjustments by Wizardry and Steamworks.
 
  esp-find3-client is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
 
  esp-find3-client is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License
  along with esp-find3-client.  If not, see <http://www.gnu.org/licenses/>.
*/
 
///////////////////////////////////////////////////////////////////////////
//                             CONFIGURATION                             //
///////////////////////////////////////////////////////////////////////////
 
// Set to the WiFi AP name.
#define WIFI_SSID ""
// Set to the WiFi AP password.
#define WIFI_PSK ""
 
// Set to 1 for learning mode.
#define MODE_LEARNING 1
#define LOCATION ""
 
// Family name.
#define FAMILY_NAME ""
 
// BLE requires large app partition or the sketch will not fit.
// Please choose:
// * Tools -> Partition scheme -> Minimal SPIFFS (1.9MB APP / 190KB SPIFFS)
// and set to 1 to enable BLE.
#define USE_BLE 1
#define BLE_SCANTIME 5
 
// Official server: cloud.internalpositioning.com
#define FIND_HOST "cloud.internalpositioning.com"
// Official port: 443 and SSL set to 1
#define FIND_PORT 443
// Whether to use SSL for the HTTP connection.
// Set to 1 for official cloud server.
#define USE_HTTP_SSL 1
// Timeout connecting to find3 server expressed in milliseconds.
#define HTTP_TIMEOUT 2500
 
// The NTP server to use for time updates.
#define NTP_HOST "pool.ntp.org"
// The offset in seconds from UTC, ie: 3600 for +1 Hour.
#define UTC_OFFSET 2 * 3600
 
// The password to use for OTA updates.
#define OTA_PASSWORD ""
 
// Set to 1 to enable. Used for verbose debugging.
#define DEBUG 1
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
#ifdef ARDUINO_ARCH_ESP32
#include <WiFiClientSecure.h>
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <ArduinoOTA.h>
 
#if defined(ARDUINO_ARCH_ESP8266)
#define GET_CHIP_ID() String(ESP.getChipId(), HEX)
#elif defined(ARDUINO_ARCH_ESP32)
#define GET_CHIP_ID() String(((uint16_t)(ESP.getEfuseMac()>>32)), HEX)
#endif
 
#define uS_TO_S_FACTOR 1000000  /* Conversion factor for micro seconds to seconds */
 
#define ARDUINOJSON_USE_LONG_LONG 1
#include <ArduinoJson.h>
 
// Automagically disable BLE on ESP8266
#if defined(ARDUINO_ARCH_ESP8266) && !defined(ARDUINO_ARCH_ESP32)
#define USE_BLE 0
#endif
 
#if defined(ARDUINO_ARCH_ESP32) && (USE_BLE == 1)
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
 
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      // Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    }
};
#endif
 
#ifdef ARDUINO_ARCH_ESP32
RTC_DATA_ATTR int bootCount = 0;
#endif
 
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, NTP_HOST, UTC_OFFSET, 60000);
 
// Retrieves the WiFi MAC address.
String getWiFiMAC() {
  String result;
  byte mac[6];
  WiFi.macAddress(mac);
  for (int i = 5; i > -1; --i) {
    result += String(mac[i], HEX);
    if (i != 0) {
      result += ":";
    }
  }
  return result;
}
 
void SubmitWiFi(void)
{
  Serial.println("[ INFO ]\tWiFi MAC: " + getWiFiMAC());
 
  String request;
 
  StaticJsonDocument<256> jsonBuffer;
  JsonObject root = jsonBuffer.to<JsonObject>();
 
  root["d"] = "esp-" + GET_CHIP_ID();
  root["f"] = FAMILY_NAME;
  root["t"] = timeClient.getEpochTime();
#if (MODE_LEARNING == 1)
  Serial.println("[ iNFO ]\tLearning enabled, sending learning data.");
  root["l"] = LOCATION;
#endif
 
  JsonObject data = root.createNestedObject("s");
 
  Serial.println("[ INFO ]\tWiFi scan starting..");
  int n = WiFi.scanNetworks(false, true);
  Serial.println("[ INFO ]\tWiFi Scan finished.");
  if (n == 0) {
    Serial.println("[ ERROR ]\tNo networks found");
  } else {
    Serial.print("[ INFO ]\t");
    Serial.print(n);
    Serial.println(" WiFi networks found.");
    JsonObject wifi_network = data.createNestedObject("wifi");
    for (int i = 0; i < n; ++i) {
      wifi_network[WiFi.BSSIDstr(i)] = WiFi.RSSI(i);
    }
 
#if (USE_BLE == 1)
    Serial.println("[ INFO ]\tBLE scan starting..");
    BLEDevice::init("");
    BLEAddress btMAC = BLEDevice::getAddress();
    Serial.println("[ INFO ]\tBT MAC: " + String(btMAC.toString().c_str()));
    BLEScan* pBLEScan = BLEDevice::getScan(); // create new scan
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    pBLEScan->setActiveScan(true); // active scan uses more power, but get results faster
    BLEScanResults foundDevices = pBLEScan->start(BLE_SCANTIME);
 
    Serial.print("[ INFO ]\t");
    Serial.print(foundDevices.getCount());
    Serial.println(" BLE devices found.");
 
    JsonObject bt_network = data.createNestedObject("bluetooth");
    for (int i = 0; i < foundDevices.getCount(); i++)
    {
      std::string mac = foundDevices.getDevice(i).getAddress().toString();
      bt_network[(String)mac.c_str()] = (int)foundDevices.getDevice(i).getRSSI();
    }
#else
    Serial.println("[ INFO ]\tBLE scan skipped (BLE disabled)..");
#endif // USE_BLE
 
    serializeJson(root, request);
 
#if (DEBUG == 1)
    Serial.println("[ DEBUG ]\t" + request);
#endif
 
#if (USE_HTTP_SSL == 1)
    WiFiClientSecure client;
#else
    WiFiClient client;
#endif
    if (!client.connect(FIND_HOST, FIND_PORT)) {
      Serial.println("[ WARN ]\tConnection to server failed, restarting in 5s...");
      delay(5000);
      ESP.restart();
    }
 
    // We now create a URI for the request
    String url = "/data";
 
    Serial.print("[ INFO ]\tRequesting URL: ");
    Serial.println(url);
 
    // This will send the request to the server
    client.print(String("POST ") + url + " HTTP/1.1\r\n" +
                 "Host: " + FIND_HOST + "\r\n" +
                 "Content-Type: application/json\r\n" +
                 "Content-Length: " + request.length() + "\r\n\r\n" +
                 request +
                 "\r\n\r\n"
                );
 
    client.flush();
 
    unsigned long timeout = millis();
    while (client.available() == 0) {
      if (millis() - timeout > HTTP_TIMEOUT) {
        Serial.println("[ ERROR ]\tHTTP Client Timeout!");
        client.stop();
        return;
      }
    }
 
    // Check HTTP status
    char status[60] = {0};
    client.readBytesUntil('\r', status, sizeof(status));
    if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
      Serial.print(F("[ ERROR ]\tUnexpected Response: "));
      Serial.println(status);
      return;
    }
    else
    {
      Serial.println(F("[ INFO ]\tGot a 200 OK."));
    }
 
    char endOfHeaders[] = "\r\n\r\n";
    if (!client.find(endOfHeaders)) {
      Serial.println(F("[ ERROR ]\t Invalid Response"));
      return;
    }
    else
    {
      Serial.println("[ INFO ]\tLooks like a valid response.");
    }
 
    Serial.println("[ INFO ]\tClosing connection.");
    Serial.println("=============================================================");
  }
}
 
void setup() {
  Serial.begin(115200);
  delay(1000);
 
#if defined(ARDUINO_ARCH_ESP32) && (USE_BLE == 1)
  Serial.println("Find3 ESP client by DatanoiseTV (WiFi + BLE support.)");
#else
  Serial.println("Find3 ESP client by DatanoiseTV (WiFi support WITHOUT BLE.)");
#endif
 
  Serial.print("[ INFO ]\tESP ID is: ");
  Serial.println("esp-" + GET_CHIP_ID());
 
  // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setHostname(String("esp-" + GET_CHIP_ID()).c_str());
  // Set the OTA password
  ArduinoOTA.setPassword(OTA_PASSWORD);
  ArduinoOTA.onStart([]() {
    Serial.println("=============================================================");
    switch (ArduinoOTA.getCommand()) {
      case U_FLASH: // Sketch
        Serial.println("[ INFO ]\tOTA start updating sketch.");
        break;
#if defined(ARDUINO_ARCH_ESP8266)
      case U_FS:
#elif defined(ARDUINO_ARCH_ESP32)
      case U_SPIFFS:
#endif
        Serial.println("[ INFO ]\tOTA start updating filesystem.");
        break;
      default:
        Serial.println("[ WARN ]\tUnknown OTA update type.");
        break;
    }
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("[ INFO ]\tOTA update complete.");
    Serial.println("=============================================================");
    ESP.restart();
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("[ INFO ]\tOTA update progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("[ ERROR ]\tOTA update error [%u]: ", error);
    switch (error) {
      case OTA_AUTH_ERROR:
        Serial.println("OTA authentication failed");
        break;
      case OTA_BEGIN_ERROR:
        Serial.println("OTA begin failed");
        break;
      case OTA_CONNECT_ERROR:
        Serial.println("OTA connect failed");
        break;
      case OTA_RECEIVE_ERROR:
        Serial.println("OTA receive failed");
        break;
      case OTA_END_ERROR:
        Serial.println("OTA end failed");
        break;
      default:
        Serial.println("Unknown OTA failure");
        break;
    }
    ESP.restart();
  });
}
 
void loop() {
  // If WiFi is not connected, attempt to reconnect and if that fails then restart.
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[ WARN ]\tWiFi not connected, retrying...");
    WiFi.disconnect();
#if defined(ARDUINO_ARCH_ESP32)
    WiFi.setHostname(String("esp-" + GET_CHIP_ID()).c_str());
#elif defined(ARDUINO_ARCH_ESP8266)
    WiFi.hostname(String("esp-" + GET_CHIP_ID()).c_str());
#endif
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PSK);
    while (WiFi.waitForConnectResult() != WL_CONNECTED) {
      Serial.println("[ ERROR ]\tFailed to connect to Wifi, rebooting in 5s...");
      delay(5000);
      ESP.restart();
    }
    Serial.println("[ INFO ]\tStarting OTA...");
    ArduinoOTA.begin();
 
    Serial.println("[ INFO ]\tStarting NTP...");
    timeClient.begin();
  }
  ArduinoOTA.handle();
  timeClient.update();
  SubmitWiFi();
}