BLE Cellbotics server

// .. Copyright (C) 2012-2020 Bryan A. Jones.
//
//  This file is part of the CellBotics system.
//
//  The CellBotics system 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.
//
//  The CellBotics system 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 the CellBotics system.  If not, see
//  <http://www.gnu.org/licenses/>.
//
// *********************
// BLE Cellbotics server
// *********************
// This is based on `Neil Kolban's example for IDF <https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleServer.cpp>`_. It was ported to the Arduino ESP32 by Evandro Copercini, with updates by chegewara, then extended by Bryan A. Jones.

// Includes
// ========
#include <string>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLECharacteristic.h>


// Macros
// ======
//
// .. _UUIDs:
//
// UUIDs
// -----
// See `here <https://www.uuidgenerator.net/>`_ for generating UUIDs.
//
// The UUID for the CellBot service.
#define SERVICE_UUID ("6c533793-9bd6-47d6-8d3b-c10a704b6b97")
// Characteristic for resetHardware.
#define RESET_HARDWARE_CHARACTERISTIC_UUID ("60cb180e-838d-4f65-aff4-20b609b453f3")
// Characteristic for pinMode.
#define PIN_MODE_CHARACTERISTIC_UUID ("6ea6d9b6-7b7e-451c-ab45-221298e43562")
// Characteristic for digitalWrite
#define DIGITAL_WRITE_CHARACTERISTIC_UUID ("d3423cf6-6da7-4dd8-a5ba-3c980c74bd6d")
// Characteristic for digitalRead
#define DIGITAL_READ_CHARACTERISTIC_UUID ("c370bc79-11c1-4530-9f69-ab9d961aa497")
// Characteristic for ledcSetup
#define LEDC_SETUP_CHARACTERISTIC_UUID ("6be57cea-3c46-4687-972b-03429d2acf9b")
// Characteristic for ledcAttachPin
#define LEDC_ATTACH_PIN_CHARACTERISTIC_UUID ("2cd63861-078f-436f-9ed9-79e57ec8b638")
// Characteristic for ledcDetachPin
#define LEDC_DETACH_PIN_CHARACTERISTIC_UUID ("b9b0cabe-25d8-4965-9259-7d3b6330e940")
// Characteristic for ledcWrite
#define LEDC_WRITE_CHARACTERISTIC_UUID ("40698030-a343-448f-a9ea-54b39b03bf81")

// Define this to return a textual description of each function executed to the Bluetooth client, and also print these descriptions to the serial port.
#define VERBOSE_RETURN

// Hardware
// ^^^^^^^^
// The only blue LED on the ESP32.
#define LED1 (2)
// The only pushbutton on the ESP32.
#define PB1 (0)


// Configure a pushbutton and LED to enable BLE pairing.
void configPairing() {
    // Configure the on-board pushbutton as the pairing enable button.
    pinMode(LED1, OUTPUT);
    // Configure the on-board LED controlled by the ESP32 to flash quickly while pairing.
    pinMode(PB1, INPUT);
}

// Variables
// =========
BLEServer *pServer = NULL;


// Reset
// =====
// Reset the device to a power-on like state in terms of the I/Os affected by the characteristics above. Also used as an e-stop when Bluetooth disconnects.
void reset_hardware() {

#ifdef VERBOSE_RETURN
    Serial.println("reset_hardware()");
#endif

    // TODO.
}


// Characteristic callbacks
// ========================
// Provide return values and checks used by all the following characteristics.
class InvokeArduinoCallback: public BLECharacteristicCallbacks {
    public:
    // A buffer for messages; a C string.
    char buf[100];
    // A string holding read data to return to the client.
    std::string ret;
    // The value read from a characteristic.
    std::string value;

    InvokeArduinoCallback() : ret(100, 0) {
        ret.assign("Not yet invoked.");
    };

    // On a read, return ``buf``.
    void onRead(BLECharacteristic* pCharacteristic) {
        pCharacteristic->setValue(ret);
    };

    // Get a write value and check its length.
    bool checkLength(size_t sz_expected_length, BLECharacteristic* pCharacteristic) {
        value = pCharacteristic->getValue();
        if (value.length() != sz_expected_length) {
            snprintf(buf, sizeof(buf), "Error: message length was %u, but expected %u.\n", value.length(), sz_expected_length);
            ret.assign(buf);
            return false;
        }

        // The default response is an empty string.
        ret.clear();
        return true;
    };
};


// Any write to this characteristic invokes ``reset_hardware``.
class ResetHardwareCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
            reset_hardware();
#ifdef VERBOSE_RETURN
            ret.assign("reset_hardware()");
#endif
    }
};


// A write to this characteristic invokes ``pinMode``; results are reported by a read of this characteristic.
class PinModeCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        ///                 value[0]    value[1]
        /// void pinMode(uint8_t pin, uint8_t mode)
        uint8_t u8_pin;
        uint8_t u8_mode;

        if (checkLength(2, pCharacteristic)) {
            u8_pin = static_cast<uint8_t>(value[0]);
            u8_mode = static_cast<uint8_t>(value[1]);
            pinMode(u8_pin, u8_mode);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "pinMode(%u, %u)", u8_pin, u8_mode);
            Serial.println(buf);
            ret.assign(buf);
#endif
        }
    }
};


// A write to this characteristic invokes ``digitalWrite``; results are reported by a read of this characteristic.
class DigitalWriteCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        ///                     value[0]     value[1]
        /// void digitalWrite(uint8_t pin, uint8_t val);
        uint8_t u8_pin;
        uint8_t u8_val;

        if (checkLength(2, pCharacteristic)) {
            u8_pin = static_cast<uint8_t>(value[0]);
            u8_val = static_cast<uint8_t>(value[1]);
            digitalWrite(u8_pin, u8_val);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "digitalWrite(%u, %u)", u8_pin, u8_val);
            Serial.println(buf);
            ret.assign(buf);
#endif
        }
    };
};


// A write to this characteristic invokes ``digitalWrite``; results are reported by a read of this characteristic.
class DigitalReadCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        /// ret[0]               value[0]
        /// int    digitalRead(uint8_t pin);
        uint8_t u8_pin;

        if (checkLength(1, pCharacteristic)) {
            u8_pin = static_cast<uint8_t>(value[0]);
            // Although ``digitialRead`` returns an ``int``, store it in a ``char``, since we assume it's a one-bit value.
            ret.resize(1);
            ret[0] = static_cast<char>(digitalRead(u8_pin));
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "%u = digitalRead(%u)", ret[0], u8_pin);
            Serial.println(buf);
            ret.replace(1, 99, buf);
#endif
        }
    };
};


// Same as above, for ``ledcSetup``.
class LedcSetupCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        // This wraps:
        /// ret[0:7]            value[0]      value[9:1]         value[10]
        /// double ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits)
        double d_ret;
        uint8_t u8_channel;
        double d_freq;
        uint8_t u8_resolution_bits;

        if (checkLength(11, pCharacteristic)) {
            // Extract function parameters.
            u8_channel = static_cast<uint8_t>(value[0]);
            memcpy(&d_freq, value.data() + 1, 8);
            u8_resolution_bits = static_cast<uint8_t>(value[10]);

            // Call the function.
            d_ret = ledcSetup(u8_channel, d_freq, u8_resolution_bits);
            ret.resize(8);
            ret.assign(reinterpret_cast<char*>(&d_ret), 8);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "%lf = ledcSetup(%u, %lf, %u)", d_ret, u8_channel, d_freq, u8_resolution_bits);
            Serial.println(buf);
            ret.replace(8, 92, buf);
#endif
        }
    };
};


// Same as above, for ``ledcAttachPin``.
class ledcAttachPinCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        ///                      value[0]     value[1]
        /// void ledcAttachPin(uint8_t pin, uint8_t channel)
        uint8_t u8_pin;
        uint8_t u8_channel;

        if (checkLength(2, pCharacteristic)) {
            u8_pin = static_cast<uint8_t>(value[0]);
            u8_channel = static_cast<uint8_t>(value[1]);
            ledcAttachPin(u8_pin, u8_channel);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "ledcAttachPin(%u, %u)", u8_pin, u8_channel);
            Serial.println(buf);
            ret.assign(buf);
#endif
        }
    };
};


// Same as above, for ``ledcAttachPin``.
class ledcDetachPinCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        ///                      value[0]
        /// void ledcDetachPin(uint8_t pin)
        uint8_t u8_pin;

        if (checkLength(2, pCharacteristic)) {
            u8_pin = static_cast<uint8_t>(value[0]);
            ledcDetachPin(u8_pin);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "ledcDetachPin(%u)", u8_pin);
            Serial.println(buf);
            ret.assign(buf);
#endif
        }
    };
};


// Same as above, for ``ledcWrite``.
class LedcWriteCallback: public InvokeArduinoCallback {
    void onWrite(BLECharacteristic* pCharacteristic) {
        // This wraps:
        ///                    value[0]      value[4:1]
        /// void ledcWrite(uint8_t channel, uint32_t duty)
        uint8_t u8_channel;
        uint32_t u32_duty;

        if (checkLength(5, pCharacteristic)) {
            // Extract function parameters.
            u8_channel = static_cast<uint8_t>(value[0]);
            // Since the data isn't aligned, use memcpy.
            memcpy(&u32_duty, value.data() + 1, 4);

            // Call the function.
            ledcWrite(u8_channel, u32_duty);
#ifdef VERBOSE_RETURN
            snprintf(buf, sizeof(buf), "ledcWrite(%u, %u)", u8_channel, u32_duty);
            Serial.println(buf);
            ret.assign(buf);
#endif
        }
    };
};


// On a server disconnect, reset and update paired status.
class CellBotServerCallback : public BLEServerCallbacks {
    virtual void onDisconnect(BLEServer* pServer) {
#ifdef VERBOSE_RETURN
        Serial.println("BLE disconnected.");
#endif
        reset_hardware();
        // Now that the board isn't connected, allow pairing.
        configPairing();
    };
};


// Functions
// =========
void setup() {
    Serial.begin(115200);
    Serial.println("Starting BLE work!");

    // Define the name visible when pairing this device.
    BLEDevice::init("CellBot");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new CellBotServerCallback());

    // Define the primary service for this server.
    BLEService *pService = pServer->createService(SERVICE_UUID);

    // Define characteristics for this server
    //***************************************
    BLECharacteristic *pCharacteristic = pService->createCharacteristic(
        RESET_HARDWARE_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new ResetHardwareCallback());

    pCharacteristic = pService->createCharacteristic(
        PIN_MODE_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new PinModeCallback());

    pCharacteristic = pService->createCharacteristic(
        DIGITAL_WRITE_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new DigitalWriteCallback());

    pCharacteristic = pService->createCharacteristic(
        DIGITAL_READ_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new DigitalReadCallback());

    pCharacteristic = pService->createCharacteristic(
        LEDC_SETUP_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new LedcSetupCallback());

    pCharacteristic = pService->createCharacteristic(
        LEDC_WRITE_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new LedcWriteCallback());

    pCharacteristic = pService->createCharacteristic(
        LEDC_ATTACH_PIN_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new ledcAttachPinCallback());

    pCharacteristic = pService->createCharacteristic(
        LEDC_DETACH_PIN_CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );
    pCharacteristic->setCallbacks(new ledcDetachPinCallback());

    // Complete Bluetooth config.
    pService->start();
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    // Functions that help with iPhone connections issue. Why is this done twice?
    pAdvertising->setMinPreferred(0x06);
    pAdvertising->setMinPreferred(0x12);
    Serial.println("Characteristic defined! Now you can read it in your phone!");

    configPairing();
}


void loop() {
    // Time in 0.1 s intervals since the pair pushbutton was pressed.
    static uint u_pairing_time_ds = 0;

    // Look for a paring pushbutton press only if we're not connected.
    if (!pServer->getConnectedCount()) {
        if (!digitalRead(PB1)) {
            // Set the timer to 30 s.
            u_pairing_time_ds = 300;
            BLEDevice::getAdvertising()->start();
        }
    }

    // Blink while pairing.
    if (u_pairing_time_ds) {
        --u_pairing_time_ds;
        // Turn the LED off and pairing off for the last blink.
        if (!u_pairing_time_ds) {
            digitalWrite(LED1, 0);
            BLEDevice::getAdvertising()->stop();
        } else {
            digitalWrite(LED1, !digitalRead(LED1));
        }
    }

    delay(100);
}