• 1
    Building the Antenna

    I mostly copied the design from Here.

    Using the calculator for a frequency of 2440 MHz (Bluetooth goes from 2400 to 2483) and 13 turns to get a beam angle of 30 degrees.

    I fixed the PVC tube to the center of the PCB copper clad plate with a few tie wraps. The copper side is on the same side as the tube, but I only had double sided plate so that's what I used.

    I put a mark on the tube at 1-2 mm from the plate near the hole for the cable, then every 27.6 mm.

    I recomment coiling the #12 AWG wire on a slightly smaller tube to shape it before putting it over the PCV tube.

    I placed the wire to approximately the correct spacing on the tube, then glued the bottom of the wire near the plate, leaving a small bit to be soldered on the coax UF.L cable. The cable core is soldered to the helix, the cable shield is soldered to the copper plate.

    After this I aligned the wire on the marks and put some electrical tape and hot glue to hold it in place. Then drilled two holes in the plate to fix it to a piece of 1.5"x1.5" wood.

  • 2
    Selecting the external antenna on the Esp32 board

    This step is a bit tricky, some boards already come with an external antenna, but most Esp32 board are by default connected to the small onboard pcb antenna. You need to remove the small resistor (it's a 0 ohm resistor) and solder the two other traces instead.

    I used a very small copper wire and a magnifying glass to get it done, solder alone didn't want to connect the two dots.

    There are some Esp32-CAM kits that already have the correct external antenna connection.

  • 3
    Code for single Esp32 Board

    This is the code for everything on a single Esp32 Board. 

    Since I didn't have the correct board, I needed to use two of them, and they communicate through the serial port.

    The Bluetooth packets address and raw data get printed to the Arduino Serial Monitor.

    The starting point was the BLE_Beacon_Scanner example code.

    #include <Arduino.h>
    #include <BLEDevice.h>
    #include <BLEUtils.h>
    #include <BLEScan.h>
    #include <BLEAdvertisedDevice.h>
    #include <BLEEddystoneURL.h>
    #include <BLEEddystoneTLM.h>
    #include <BLEBeacon.h>
    
    #include <MCUFRIEND_kbv.h>
    
    // Connections between 3.5" TFT and ESP32-WROOM
    /*#define LCD_RD  2  //LED
    #define LCD_WR  4
    #define LCD_RS 15  //hard-wired to A2 (GPIO35) 
    #define LCD_CS 33  //hard-wired to A3 (GPIO34)
    #define LCD_RESET 32 //hard-wired to A4 (GPIO36)
    
    #define LCD_D0 12
    #define LCD_D1 13
    #define LCD_D2 26
    #define LCD_D3 25
    #define LCD_D4 17
    #define LCD_D5 16
    #define LCD_D6 27
    #define LCD_D7 14*/
    
    #define BLACK   0x0000
    #define BLUE    0x001F
    #define RED     0xF800
    #define GREEN   0x07E0
    #define CYAN    0x07FF
    #define MAGENTA 0xF81F
    #define YELLOW  0xFFE0
    #define WHITE   0xFFFF
    
    #define MinRSSI -100 // Ignore devices below this signal strength, -80 is better if there's too many signals
    #define MaxRSSI -30 // Full length rssi bar will be displayed at this rssi value
    #define RangeRSSI (MaxRSSI-MinRSSI)
    #define MaxBarLength 100 // 100 pixel rssi bar
    #define BarCol  210 // Bar x position on screen
    
    #define MaxAge 20 // Forget device after some time of inactivity = MaxAge*LoopTime
    #define NTrackMax 30 // Max number of devices displayed depending on screen and text size
    #define LoopTime 500 // display refresh period in milliseconds
    
    MCUFRIEND_kbv tft;
    
    SemaphoreHandle_t dataSemaphore; // For data exchange between Bluetooth and Display threads
    
    uint32_t previousT, currentT;
    
    #define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00) >> 8) + (((x)&0xFF) << 8))
    
    int scanTime = 10; //In seconds, not important since it's in a loop anyway
    BLEScan *pBLEScan;
    
    struct Device{
      char Address[18];
      int rssi;
      uint8_t V;
      uint8_t age;
      bool allocated;
      bool updated;
    //  uint8_t mData[10];
    //  uint8_t Nbytes;
    };
    
    Device DeviceList[NTrackMax]; // List of displayed devices
    Device DeviceList2[NTrackMax];
    
    class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
    {
      void onResult(BLEAdvertisedDevice advertisedDevice) // Executed every time a BLE device is detected
      {
        char Address[18];
        int rssi = -200;
        uint8_t mData[100];
        uint8_t V = 0;
        
        sprintf(Address, "%s", advertisedDevice.getAddress().toString().c_str());
        Serial.print(Address);
        Serial.print(" ");
        
        if(advertisedDevice.haveRSSI() == true){
          rssi = advertisedDevice.getRSSI();
          Serial.printf("RSSI: %d ", rssi);
        }
        
        uint8_t *payLoad = advertisedDevice.getPayload();
        size_t payLoadLen = advertisedDevice.getPayloadLength();
        Serial.print("DATA: ");
        for (int idx = 0; idx < payLoadLen; idx++)
        {
          Serial.printf("%X ", payLoad[idx]);
        }
    
      /*  if(advertisedDevice.haveServiceUUID()){
          BLEUUID devUUID = advertisedDevice.getServiceUUID();
          Serial.print("UUID ");
          Serial.print(devUUID.toString().c_str());
          Serial.print(" ");
        }
        if(advertisedDevice.haveName()){
          Serial.println(advertisedDevice.getName().c_str());
          Serial.print(" ");
        } */
        if(advertisedDevice.haveManufacturerData() == true){ // Manufacturer data is the same as the payload but without the first two bytes
          std::string strManufacturerData = advertisedDevice.getManufacturerData();
          int Nbytes = strManufacturerData.length();
          strManufacturerData.copy((char *)mData, Nbytes, 0);
         /* if(Nbytes == 25 && mData[0] == 0x4C && mData[1] == 0x00){
            Serial.print("iBeacon ");
          } 
          for (int i = 0; i < Nbytes; i++){
            Serial.printf("%X ", mData[i]);
          } */
          if(Nbytes <= 10 && mData[0] == 0x4C){ // Might depends on country
            V = 1;
          }
        }
        Serial.println();
        
        if(rssi >= MinRSSI){ // && V == 1){ // If true, add device to the list, if it's not full already, && V to only display V
          xSemaphoreTake(dataSemaphore, portMAX_DELAY);
          bool found = false;
          for(int i=0; i<NTrackMax; i++){
            if(strcmp(DeviceList[i].Address, Address) == 0){ // If address found in list
              found = true;
              DeviceList[i].rssi = rssi;
              DeviceList[i].V = V;
              DeviceList[i].age = 0;
              DeviceList[i].allocated = true; // In case we are in a deleted slot of the same device
              DeviceList[i].updated = true;
            }
          }
          if(!found){ // New device not found in the list placed in first unallocated slot
            for(int i=0; i<NTrackMax; i++){
              if(DeviceList[i].allocated == 0){
                strcpy(DeviceList[i].Address, Address);
                DeviceList[i].rssi = rssi;
                DeviceList[i].V = V;
                DeviceList[i].age = 0;
                DeviceList[i].allocated = true;
                DeviceList[i].updated = true;
                break;
              }
            }
          }
          xSemaphoreGive(dataSemaphore);
        }
      }
    };
    
    void setup()
    {
      Serial.begin(115200);
    
      BLEDevice::init("");
      pBLEScan = BLEDevice::getScan(); //create new scan
      pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true); // true = don't ignore duplicate device packet within a scan
      pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
      pBLEScan->setInterval(100);
      pBLEScan->setWindow(99); // less or equal setInterval value
    
      uint16_t ID = tft.readID();
      tft.begin(ID);
      tft.fillScreen(BLACK);
      tft.setTextColor(WHITE, BLACK);
      tft.setTextSize(2);
      tft.setTextWrap(false);
    
      dataSemaphore = xSemaphoreCreateMutex();
      xSemaphoreGive(dataSemaphore);
      previousT = millis();
      xTaskCreatePinnedToCore(DisplayTask, "DisplayTask", 10000, NULL, 2, NULL, 0); // Create a task on core 0 for the display
      delay(500);
    }
    
    void loop()
    {
      BLEScanResults foundDevices = pBLEScan->start(scanTime, false); // flase = clear previously scanned devices
    }
    
    void DisplayTask( void * pvParameters ){
      while(true){
     //   do{ // Maintain constant Loop Time
      //    currentT = millis();
       // }while(currentT-previousT < LoopTime);
       // previousT = currentT;
        delay(500);
        // Copy the data to be displayed, Bluetooth thread can write in DeviceList anytime.
        xSemaphoreTake(dataSemaphore, portMAX_DELAY);
        memcpy(DeviceList2, DeviceList, sizeof(DeviceList));
        
        for(int i=0; i<NTrackMax; i++){
          if(DeviceList[i].allocated){
            DeviceList[i].age++;
            if(DeviceList[i].age > MaxAge){
              DeviceList[i].allocated = false; // Delete devices who didn't emit in past 10 second
            }
            DeviceList[i].updated = false;
          }
        }
        xSemaphoreGive(dataSemaphore);
    
        uint16_t row, barLength, color;
      
        for(int i = 0; i < NTrackMax; i++){ // Concaténer des string à la place
          row = i*16;
          if(DeviceList2[i].allocated){
            if(DeviceList2[i].updated){
              tft.setCursor(0, row);
              tft.print(DeviceList2[i].Address);
              barLength = (DeviceList2[i].rssi-MinRSSI)*MaxBarLength;
              barLength = constrain(barLength/RangeRSSI, 1, MaxBarLength);
              if(DeviceList2[i].V == 0){
                tft.fillRect(BarCol, row+2, barLength, 12, GREEN);
              }else{
                tft.fillRect(BarCol, row+2, barLength, 12, RED);
              }
              tft.fillRect(BarCol+barLength, row+2, MaxBarLength-barLength, 12, BLACK);
            }
          }else{
            tft.fillRect(0, row, tft.width(), 16, BLACK); // Erase line
          }
        }
      }
    }