diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cb2c2b0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "app/QBluetoothLeUart"] + path = app/QBluetoothLeUart + url = https://itsblue.dev/itsblue-development/QBluetoothLeUart.git diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..4a0b530 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,74 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/app/BirdLamp.pro b/app/BirdLamp.pro new file mode 100644 index 0000000..1e4fef3 --- /dev/null +++ b/app/BirdLamp.pro @@ -0,0 +1,24 @@ +QT += quick + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +RESOURCES += qml.qrc + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH = + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +CONFIG += QBluetoothLeUart_QML +include($$PWD/QBluetoothLeUart/QBluetoothLeUart.pri) diff --git a/app/BluetoothOffPage.qml b/app/BluetoothOffPage.qml new file mode 100644 index 0000000..2ab39e3 --- /dev/null +++ b/app/BluetoothOffPage.qml @@ -0,0 +1,10 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Page { + id: root + + Label { + text: "Please turn Bluetooth on" + } +} diff --git a/app/ColorPicker.qml b/app/ColorPicker.qml new file mode 100644 index 0000000..9c36e13 --- /dev/null +++ b/app/ColorPicker.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 + +Item { + +} diff --git a/app/ControllerPage.qml b/app/ControllerPage.qml new file mode 100644 index 0000000..055a536 --- /dev/null +++ b/app/ControllerPage.qml @@ -0,0 +1,73 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.15 + +Page { + id: root + + anchors.fill: parent + + property int lampState: 0 + + onLampStateChanged: { + console.log(lampState.toString(16)) + sendBufferTimer.start() + } + + Timer { + id: sendBufferTimer + interval: 500 + running: false + repeat: false + onTriggered: { + ble.sendData("s:" + lampState.toString(16)) + } + } + + Column { + anchors.centerIn: parent + Switch { + checked: app.remoteState >> 24 === 1 + onCheckedChanged: { + let newState = lampState & 16777215 + lampState = newState | ((checked ? 1:0) << 24) + } + } + + Slider { + id: red + Material.accent: Material.Red + from: 0 + to: 255 + value: app.remoteState >> 16 & 255 + onValueChanged: { + let newColor = lampState & 16842751 + lampState = newColor | (value << 16) + } + } + + Slider { + id: green + Material.accent: Material.Green + from: 0 + to: 255 + value: app.remoteState >> 8 & 255 + onValueChanged: { + let newColor = lampState & 33489151 + lampState = newColor | (value << 8) + } + } + + Slider { + id: blue + Material.accent: Material.Blue + from: 0 + to: 255 + value: app.remoteState & 255 + onValueChanged: { + let newColor = lampState & 33554176 + lampState = newColor | (value) + } + } + } +} diff --git a/app/PermissionPage.qml b/app/PermissionPage.qml new file mode 100644 index 0000000..b80455a --- /dev/null +++ b/app/PermissionPage.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Page { + id: root + + Column { + Label { + text: "Please grant bluetooth permission" + } + Button { + onClicked: ble.requestLocationPermission() + } + } +} diff --git a/app/QBluetoothLeUart b/app/QBluetoothLeUart new file mode 160000 index 0000000..b6ff6d1 --- /dev/null +++ b/app/QBluetoothLeUart @@ -0,0 +1 @@ +Subproject commit b6ff6d1e0ce6d05761761daf27343a3dd13fb1d0 diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..bda1d32 --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include + + +int main(int argc, char *argv[]) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QGuiApplication app(argc, argv); + + QBluetoothLeUartClient::init(); + QQuickStyle::setStyle("Material"); + + + QQmlApplicationEngine engine; + const QUrl url(QStringLiteral("qrc:/main.qml")); + QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, + &app, [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, Qt::QueuedConnection); + engine.load(url); + + return app.exec(); +} diff --git a/app/main.qml b/app/main.qml new file mode 100644 index 0000000..95a2ef3 --- /dev/null +++ b/app/main.qml @@ -0,0 +1,101 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import de.itsblue.bluetoothleuart 1.0 + +Window { + width: 640 + height: 480 + visible: true + title: qsTr("Hello World") + + Page { + id: app + + anchors.fill: parent + + property int remoteState: -1 + property bool foundLamp: false + + QBluetoothLeUart { + id: ble + Component.onCompleted: { + ble.startScanningForDevices() + } + + onFoundNewDevice: { + console.log("Found a device: name: " + device.name + " address: " + device.address) + if(device.address === "08:B6:1F:38:17:16") { + app.foundLamp = true + ble.stopScanningForDevices() + ble.connectToDevice(device) + } + } + + onConnectedToDevice: { + console.log("Connected") + ble.sendData("s") + } + + onDataReceived: { + console.log("Data received: " + data) + if(data.startsWith("s:")) { + app.remoteState = parseInt(data.slice(2), 16) + } + } + + onStateChanged: { + console.log("state", state) + switch(state) { + case QBluetoothLeUart.ScanFinished: + case QBluetoothLeUart.Idle: + if(!app.foundLamp) ble.startScanningForDevices() + break; + case QBluetoothLeUart.Connecting: + app.foundLamp = false + break; + default: + break; + } + } + } + + Loader { + anchors.fill: parent + sourceComponent: + ble.state === QBluetoothLeUart.Connected && app.remoteState !== -1 ? controllerComponent : + ble.state === QBluetoothLeUart.LocationPermissionDenied ? permissionComponent: + ble.state === QBluetoothLeUart.AdapterTurnedOff ? bluetoothOffComponent: + loadingComponent + } + + Component { + id: loadingComponent + Page { + anchors.fill: parent + BusyIndicator { + anchors.centerIn: parent + } + } + } + + Component { + id: controllerComponent + ControllerPage { + } + } + + Component { + id: permissionComponent + PermissionPage { + } + } + + Component { + id: bluetoothOffComponent + BluetoothOffPage { + + } + } + } +} diff --git a/app/qml.qrc b/app/qml.qrc new file mode 100644 index 0000000..879dd28 --- /dev/null +++ b/app/qml.qrc @@ -0,0 +1,9 @@ + + + main.qml + ControllerPage.qml + ColorPicker.qml + PermissionPage.qml + BluetoothOffPage.qml + + diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/firmware/.vscode/extensions.json b/firmware/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/firmware/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/firmware/include/BluetoothLeUartServer.h b/firmware/include/BluetoothLeUartServer.h new file mode 100644 index 0000000..c94c121 --- /dev/null +++ b/firmware/include/BluetoothLeUartServer.h @@ -0,0 +1,102 @@ +#ifndef BLUETOOTH_LE_UART_SERVER +#define BLUETOOTH_LE_UART_SERVER + +#include +#include +#include +#include +#include + +class BluetoothLeUartServerCallbacks; + +/*! + * \brief The BluetoothLeUartServer class can be used to connect to another Bluetooth LE device (like an Android phone) effordlessly. + * + * Example use + * \code + * #include + * + * BluetoothLeUartServer* server; + * + * class MyCallbacks : public BluetoothLeUartServerCallbacks + * { + * virtual void onDeviceConnectedChanged(bool deviceConnected) + * { + * Serial.println("Device connected changed"); + * }; + * virtual void onDataReceived(String data) + * { + * Serial.println("Got some data: " + data); + * }; + * }; + * + * void setup() + * { + * server = new BluetoothLeUartServer("LedDisplay1", "6e400001-b5a3-f393-e0a9-e50e24dcca9e", "6e400002-b5a3-f393-e0a9-e50e24dcca9e", "6e400003-b5a3-f393-e0a9-e50e24dcca9e"); + * server->setCallbacks(new MyCallbacks()); + * } + * + * void loop() + * { + * if(server->getDeviceConnected()) + * server->sendData("PING"); + * delay(1000); + * } + * \endcode + */ +class BluetoothLeUartServer : protected BLEServerCallbacks, protected BLECharacteristicCallbacks +{ + +public: + explicit BluetoothLeUartServer(String deviceName, const char uartServiceUUID[36], const char rxUUID[36], const char txUUID[36]); + + // befriend for callbacks + friend class BLEServer; + friend class BLECharacteristic; + + // public functions + void setCallbacks(BluetoothLeUartServerCallbacks *callbacks); + void sendData(String data); + + bool getDeviceConnected(); + bool disconnectCurrentDevice(); + + String getDeviceAddress(); + +protected: + // callbacks for BLEServer + void onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) override; + void onDisconnect(BLEServer *pServer) override; + + // callback for BLECharacteristic + void onWrite(BLECharacteristic *rxCharacteristic) override; + +private: + // service and characteristic UUIDs + const char *uartServiceUUID; + const char *rxUUID; + const char *txUUID; + + + esp_gatt_perm_t permissions; + + // BLE Objects + BLEServer *bleServer; + BLEService *bleService; + BLECharacteristic *txCharacteristic; + BLECharacteristic *rxCharacteristic; + + // helpers + bool deviceConnected; + uint16_t deviceConnectionId; + BluetoothLeUartServerCallbacks *callbacks; +}; + +class BluetoothLeUartServerCallbacks +{ +public: + virtual void onDeviceConnectedChanged(bool deviceConnected); + virtual void onDataReceived(String data); +}; + +#endif // BLUETOOTH_LE_UART_SERVER \ No newline at end of file diff --git a/firmware/include/README b/firmware/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/firmware/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/firmware/lib/README b/firmware/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/firmware/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/firmware/platformio.ini b/firmware/platformio.ini new file mode 100644 index 0000000..6a6f2e0 --- /dev/null +++ b/firmware/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_filters = esp32_exception_decoder +monitor_speed = 115200 +lib_deps = + adafruit/Adafruit NeoPixel@^1.10.7 \ No newline at end of file diff --git a/firmware/src/BluetoothLeUartServer.cpp b/firmware/src/BluetoothLeUartServer.cpp new file mode 100644 index 0000000..112efa5 --- /dev/null +++ b/firmware/src/BluetoothLeUartServer.cpp @@ -0,0 +1,112 @@ +#include "BluetoothLeUartServer.h" + +BluetoothLeUartServer::BluetoothLeUartServer(String deviceName, const char uartServiceUUID[36], const char rxUUID[36], const char txUUID[36]) +{ + this->uartServiceUUID = uartServiceUUID; + this->rxUUID = rxUUID; + this->txUUID = txUUID; + + this->callbacks = nullptr; + this->deviceConnected = false; + this->deviceConnectionId = -1; + + // Create the BLE Device + BLEDevice::init(deviceName.c_str()); // Give it a name + + // Create the BLE Server + bleServer = BLEDevice::createServer(); + bleServer->setCallbacks(this); + + // Create the BLE Service + bleService = bleServer->createService(this->uartServiceUUID); + + // Create a BLE Characteristic + txCharacteristic = bleService->createCharacteristic( + this->txUUID, + BLECharacteristic::PROPERTY_NOTIFY); + + txCharacteristic->addDescriptor(new BLE2902()); + + rxCharacteristic = bleService->createCharacteristic( + this->rxUUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_WRITE); + + rxCharacteristic->setCallbacks(this); + + // Start the service + bleService->start(); + + // Start advertising + bleServer->getAdvertising()->addServiceUUID(bleService->getUUID()); + bleServer->getAdvertising()->setScanResponse(true); + bleServer->getAdvertising()->setMinPreferred(0x06); // functions that help with iPhone connections issue + bleServer->getAdvertising()->setMinPreferred(0x12); + bleServer->getAdvertising()->start(); +} + +void BluetoothLeUartServer::setCallbacks(BluetoothLeUartServerCallbacks *callbacks) +{ + this->callbacks = callbacks; +} + +void BluetoothLeUartServer::sendData(String data) +{ + //txCharacteristic->setValue(data.c_str()); + //Serial.printf("String is : '%s' with length of %d\n", data.c_str(), data.length() ); + txCharacteristic->setValue((uint8_t*)data.c_str(),data.length()); + txCharacteristic->notify(); +} + +bool BluetoothLeUartServer::getDeviceConnected() +{ + return this->deviceConnected; +} + +bool BluetoothLeUartServer::disconnectCurrentDevice() { + if(!this->getDeviceConnected()) + return false; + + this->bleServer->disconnect(this->deviceConnectionId); + return true; +} + +String BluetoothLeUartServer::getDeviceAddress() { + + String address = BLEDevice::getAddress().toString().c_str(); + address.toUpperCase(); + + return address; +} + +void BluetoothLeUartServer::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) +{ + // only allow one device + if(this->deviceConnected) + return this->bleServer->disconnect(param->connect.conn_id); + + this->deviceConnected = true; + this->deviceConnectionId = param->connect.conn_id; + + if (this->callbacks != nullptr) + { + this->callbacks->onDeviceConnectedChanged(this->deviceConnected); + } +} + +void BluetoothLeUartServer::onDisconnect(BLEServer *pServer) +{ + this->deviceConnected = false; + this->deviceConnectionId = -1; + bleServer->getAdvertising()->start(); + Serial.println("Restart advertising after disconnect ..."); + + if (this->callbacks != nullptr) + this->callbacks->onDeviceConnectedChanged(this->deviceConnected); +} + +void BluetoothLeUartServer::onWrite(BLECharacteristic *rxCharacteristic) +{ + if (this->callbacks != nullptr) + this->callbacks->onDataReceived(rxCharacteristic->getValue().c_str()); +} \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp new file mode 100644 index 0000000..5373414 --- /dev/null +++ b/firmware/src/main.cpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#include + +#define NUMPIXELS 15 + +Adafruit_NeoPixel pixels(NUMPIXELS, 21, NEO_GRB + NEO_KHZ800); +uint64_t current_state = 0; +uint64_t last_state = 0; + +BluetoothLeUartServer *server; + +class MyCallbacks : public BluetoothLeUartServerCallbacks +{ + virtual void onDeviceConnectedChanged(bool deviceConnected) + { + Serial.println("Device connected changed: " + String(deviceConnected)); + }; + + virtual void onDataReceived(String data) + { + Serial.println("Got some data: " + data); + + if (data.startsWith("s:")) + { + + data.remove(0, 2); + current_state = strtoul(data.c_str(), NULL, 16); + + if (current_state == last_state) + return; + + EEPROM.write(0, current_state >> 24); + EEPROM.write(1, current_state >> 16); + EEPROM.write(2, current_state >> 8); + EEPROM.write(3, current_state); + EEPROM.commit(); + } + else if (data == "s") + { + server->sendData("s:" + String(current_state, HEX)); + } + }; +}; + +void update_color(); +void set_color(uint32_t c); + +void setup() +{ + Serial.begin(115200); + + pixels.begin(); + server = new BluetoothLeUartServer("Hannis birb lamp", "6e400001-b5a3-f393-e0a9-e50e24dcca9e", "6e400002-b5a3-f393-e0a9-e50e24dcca9e", "6e400003-b5a3-f393-e0a9-e50e24dcca9e"); + server->setCallbacks(new MyCallbacks()); + + EEPROM.begin(4); + current_state = EEPROM.read(0) << 24 | EEPROM.read(1) << 16 | EEPROM.read(2) << 8 | EEPROM.read(3); + last_state = current_state; + update_color(); + Serial.println("Setup done"); + Serial.println(current_state); +} + +void loop() +{ + if (current_state != last_state) + { + update_color(); + last_state = current_state; + } + delay(100); +} + +void update_color() +{ + if (current_state >> 24 == 0) + set_color(0); + else + set_color(current_state); +} + +void set_color(uint32_t c) +{ + for (int i = 0; i < NUMPIXELS; i++) + { + pixels.setPixelColor(i, c); + } + pixels.show(); +} \ No newline at end of file diff --git a/firmware/test/README b/firmware/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/firmware/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html