diff --git a/EmotiBit.cpp b/EmotiBit.cpp index 4f8e1c6f..bae94e80 100644 --- a/EmotiBit.cpp +++ b/EmotiBit.cpp @@ -110,8 +110,6 @@ uint8_t EmotiBit::setup(String firmwareVariant) #endif #ifdef ARDUINO_FEATHER_ESP32 - esp_bt_controller_disable(); - // ToDo: assess similarity with btStop(); setCpuFrequencyMhz(CPU_HZ / 1000000); // 80MHz has been tested working to save battery life #endif @@ -455,6 +453,12 @@ uint8_t EmotiBit::setup(String firmwareVariant) while (!Serial.available() && millis() - now < 2000) { +#ifdef ARDUINO_FEATHER_ESP32 + if (digitalRead(buttonPin) && !_bluetoothEnabled ) { + Serial.println("Bluetooth Enabled"); + _bluetoothEnabled = true; + } +#endif // ARDUINO_FEATHER_ESP32 } #ifdef ADAFRUIT_FEATHER_M0 AdcCorrection::AdcCorrectionValues adcCorrectionValues; @@ -952,52 +956,69 @@ uint8_t EmotiBit::setup(String firmwareVariant) Serial.println(factoryTestSerialOutput); sleep(true); } - //WiFi Setup; - Serial.print("\nSetting up WiFi\n"); -#if defined(ADAFRUIT_FEATHER_M0) - WiFi.setPins(8, 7, 4, 2); - WiFi.lowPowerMode(); -#endif - printEmotiBitInfo(); - // turn BLUE on to signify we are trying to connect to WiFi - led.setState(EmotiBitLedController::Led::BLUE, true, true); - uint16_t attemptDelay = 20000; // in mS. ESP32 has been observed to take >10 seconds to resolve an enterprise connection - uint8_t maxAttemptsPerCred = 1; - uint32_t timeout = attemptDelay * maxAttemptsPerCred * _emotiBitWiFi.getNumCredentials() * 2; // Try cycling through all credentials at least 2x before giving up and trying a restart - if (_emotiBitWiFi.isEnterpriseNetworkListed()) - { - // enterprise network is listed in network credential list. - // restart MCU after timeout - _emotiBitWiFi.begin(timeout, maxAttemptsPerCred, attemptDelay); - } - else + + if (_bluetoothEnabled == true) { - // only personal networks listed in credentials list. - // keep trying to connect to networks without any timeout - _emotiBitWiFi.begin(-1, maxAttemptsPerCred, attemptDelay); - } - if (_emotiBitWiFi.status(false) != WL_CONNECTED) - { - // Could not connect to network. software restart and begin setup again. - restartMcu(); + if (!setPowerMode(PowerMode::BLUETOOTH)) + { + _bluetoothEnabled = false; + } } - led.setState(EmotiBitLedController::Led::BLUE, false, true); - if (testingMode == TestingMode::FACTORY_TEST) + + if (_bluetoothEnabled == false) { - // Add Pass or fail - // ToDo: add mechanism to detect fail/pass - EmotiBitFactoryTest::updateOutputString(factoryTestSerialOutput, EmotiBitFactoryTest::TypeTag::WIFI, EmotiBitFactoryTest::TypeTag::TEST_PASS); +#ifdef ARDUINO_FEATHER_ESP32 + // ToDo: assess similarity with btStop(); + _emotiBitBluetooth.disableBluetooth(); +#endif //ARDUINO_FEATHER_ESP32 + //WiFi Setup; + Serial.print("\nSetting up WiFi\n"); +#if defined(ADAFRUIT_FEATHER_M0) + WiFi.setPins(8, 7, 4, 2); + WiFi.lowPowerMode(); +#endif + printEmotiBitInfo(); + // turn BLUE on to signify we are trying to connect to WiFi + led.setState(EmotiBitLedController::Led::BLUE, true, true); + uint16_t attemptDelay = 20000; // in mS. ESP32 has been observed to take >10 seconds to resolve an enterprise connection + uint8_t maxAttemptsPerCred = 1; + uint32_t timeout = attemptDelay * maxAttemptsPerCred * _emotiBitWiFi.getNumCredentials() * 2; // Try cycling through all credentials at least 2x before giving up and trying a restart + if (_emotiBitWiFi.isEnterpriseNetworkListed()) + { + // enterprise network is listed in network credential list. + // restart MCU after timeout + _emotiBitWiFi.begin(timeout, maxAttemptsPerCred, attemptDelay); + } + else + { + // only personal networks listed in credentials list. + // keep trying to connect to networks without any timeout + _emotiBitWiFi.begin(-1, maxAttemptsPerCred, attemptDelay); + } + if (_emotiBitWiFi.status(false) != WL_CONNECTED) + { + // Could not connect to network. software restart and begin setup again. + restartMcu(); + } + led.setState(EmotiBitLedController::Led::BLUE, false, true); + if (testingMode == TestingMode::FACTORY_TEST) + { + // Add Pass or fail + // ToDo: add mechanism to detect fail/pass + EmotiBitFactoryTest::updateOutputString(factoryTestSerialOutput, EmotiBitFactoryTest::TypeTag::WIFI, EmotiBitFactoryTest::TypeTag::TEST_PASS); + } + Serial.println(" WiFi setup Completed"); + #ifdef ARDUINO_FEATHER_ESP32 + Serial.println("Setting up FTP"); + Serial.println("Setting Protocol"); + _fileTransferManager.setProtocol(FileTransferManager::Protocol::FTP); + Serial.println("Setting Auth"); + _fileTransferManager.setFtpAuth("ftp", "ftp"); + #endif + + setPowerMode(PowerMode::NORMAL_POWER); } - Serial.println(" WiFi setup Completed"); - #ifdef ARDUINO_FEATHER_ESP32 - Serial.println("Setting up FTP"); - Serial.println("Setting Protocol"); - _fileTransferManager.setProtocol(FileTransferManager::Protocol::FTP); - Serial.println("Setting Auth"); - _fileTransferManager.setFtpAuth("ftp", "ftp"); - #endif - - setPowerMode(PowerMode::NORMAL_POWER); + typeTags[(uint8_t)EmotiBit::DataType::EDA] = EmotiBitPacket::TypeTag::EDA; typeTags[(uint8_t)EmotiBit::DataType::EDL] = EmotiBitPacket::TypeTag::EDL; typeTags[(uint8_t)EmotiBit::DataType::EDR] = EmotiBitPacket::TypeTag::EDR; @@ -1499,7 +1520,11 @@ void EmotiBit::parseIncomingControlPackets(String &controlPackets, uint16_t &pac static String packet; static EmotiBitPacket::Header header; int16_t dataStartChar = 0; + #ifdef ARDUINO_FEATHER_ESP32 + while (_emotiBitWiFi.readControl(packet) > 0 || _emotiBitBluetooth.readControl(packet) > 0) //Bluetooth and WiFi control packets are read in the same loop + #else while (_emotiBitWiFi.readControl(packet) > 0) + #endif //ARDUINO_FEATHER_ESP32 { Serial.println(packet); dataStartChar = EmotiBitPacket::getHeader(packet, header); @@ -1574,6 +1599,9 @@ void EmotiBit::parseIncomingControlPackets(String &controlPackets, uint16_t &pac else if (header.typeTag.equals(EmotiBitPacket::TypeTag::MODE_WIRELESS_OFF)) { setPowerMode(EmotiBit::PowerMode::WIRELESS_OFF); } + else if (header.typeTag.equals(EmotiBitPacket::TypeTag::MODE_BLUETOOTH)) { + setPowerMode(EmotiBit::PowerMode::BLUETOOTH); + } else if (header.typeTag.equals(EmotiBitPacket::TypeTag::MODE_HIBERNATE)) { setPowerMode(EmotiBit::PowerMode::HIBERNATE); } @@ -1648,9 +1676,9 @@ uint8_t EmotiBit::update() static uint16_t serialPrevAvailable = Serial.available(); if (Serial.available() > serialPrevAvailable) { +#ifdef ARDUINO_FEATHER_ESP32 if(Serial.peek() == 'F') { -#ifdef ARDUINO_FEATHER_ESP32 if(!_sdWrite) // if not writing to SD card { // Stop ISR @@ -1673,15 +1701,24 @@ uint8_t EmotiBit::update() Serial.println("Recording in progess. Cannot start FTP server."); Serial.println("Please end recording and try again"); } -#else - Serial.println("FTP server is only supported on ESP32 boards"); -#endif - } else if (!_debugMode) { printEmotiBitInfo(); } +#else + if (!_debugMode) + { + if (Serial.read() == 'F') + { + Serial.println("FTP server is only supported on ESP32 boards"); + } + else + { + printEmotiBitInfo(); + } + } +#endif } serialPrevAvailable = Serial.available(); @@ -3199,7 +3236,11 @@ void EmotiBit::readSensors() else { // WiFi connected status LED +#ifdef ARDUINO_FEATHER_ESP32 + if (_emotiBitWiFi.isConnected() || _emotiBitBluetooth.deviceConnected) +#else if (_emotiBitWiFi.isConnected()) +#endif // ARDUINO_FEATHER_ESP32 { // Connected to oscilloscope // turn LED on @@ -3207,28 +3248,32 @@ void EmotiBit::readSensors() } else { - if (_emotiBitWiFi.status(false) == WL_CONNECTED) // ToDo: assess if WiFi.status() is thread/interrupt safe + if (_emotiBitWiFi.status(false) == WL_CONNECTED || getPowerMode() == PowerMode::BLUETOOTH) // ToDo: assess if WiFi.status() is thread/interrupt safe { // Not connected to oscilloscope, but connected to wifi // blink LED - static unsigned long onTime = 125; // msec - static unsigned long totalTime = 500; // msec - static bool wifiConnectedBlinkState = false; + unsigned long onTime = 125; // msec + unsigned long totalTime = 500; // msec + if (getPowerMode() == PowerMode::BLUETOOTH) + { + totalTime = 250; // msec + } + static bool ConnectedBlinkState = false; - static unsigned long wifiConnBlinkTimer = millis(); + static unsigned long ConnBlinkTimer = millis(); unsigned long timeNow = millis(); - if (timeNow - wifiConnBlinkTimer < onTime) + if (timeNow - ConnBlinkTimer < onTime) { led.setState(EmotiBitLedController::Led::BLUE, true); } - else if (timeNow - wifiConnBlinkTimer < totalTime) + else if (timeNow - ConnBlinkTimer < totalTime) { led.setState(EmotiBitLedController::Led::BLUE, false); } else { - wifiConnBlinkTimer = timeNow; + ConnBlinkTimer = timeNow; } } else @@ -3446,6 +3491,11 @@ void EmotiBit::sendData() if (getPowerMode() == PowerMode::NORMAL_POWER) { _emotiBitWiFi.sendData(s); } + if (getPowerMode() == PowerMode::BLUETOOTH) { + #ifdef ARDUINO_FEATHER_ESP32 + _emotiBitBluetooth.sendData(s); + #endif //ARDUINO_FEATHER_ESP32 + } writeSdCardMessage(s); firstIndex = lastIndex + 1; } @@ -3476,13 +3526,18 @@ void EmotiBit::sendData() String s = _outDataPackets.substring(firstIndex, lastIndex + 1); - if (getPowerMode() == PowerMode::NORMAL_POWER) { - _emotiBitWiFi.sendData(s); + if (getPowerMode() == PowerMode::NORMAL_POWER) { + _emotiBitWiFi.sendData(s); + } + if (getPowerMode() == PowerMode::BLUETOOTH) { + #ifdef ARDUINO_FEATHER_ESP32 + _emotiBitBluetooth.sendData(s); + #endif //ARDUINO_FEATHER_ESP32 + } + writeSdCardMessage(s); + firstIndex = lastIndex + 1; } - writeSdCardMessage(s); - firstIndex = lastIndex + 1; - } - _outDataPackets = ""; + _outDataPackets = ""; } } @@ -3707,72 +3762,141 @@ EmotiBit::PowerMode EmotiBit::getPowerMode() return _powerMode; } -void EmotiBit::setPowerMode(PowerMode mode) +bool EmotiBit::setPowerMode(PowerMode mode) { - _powerMode = mode; - String modePacket; - sendModePacket(modePacket, _outDataPacketCounter); - if (getPowerMode() == PowerMode::NORMAL_POWER) + bool ret = false; + if (mode == PowerMode::NORMAL_POWER) { - Serial.println("PowerMode::NORMAL_POWER"); - if (_emotiBitWiFi.isOff()) + Serial.println("SetPowerMode(NORMAL_POWER)"); + if (_bluetoothEnabled ) { - unsigned long beginTime = millis(); - _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option - Serial.print("Total WiFi.begin() = "); - Serial.println(millis() - beginTime); + Serial.println("SetPowerMode() FAILED: BLUETOOTH <-> WIFI requires device reset"); } + else + { + if (_emotiBitWiFi.isOff()) + { + unsigned long beginTime = millis(); + _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option + Serial.print("Total WiFi.begin() = "); + Serial.println(millis() - beginTime); + } #ifdef ADAFRUIT_FEATHER_M0 - // For ADAFRUIT_FEATHER_M0, lowPowerMode() is a good balance of performance and battery - WiFi.lowPowerMode(); - // For ESP32 the default WIFI_PS_MIN_MODEM is probably optimal https://www.mischianti.org/2021/03/06/esp32-practical-power-saving-manage-wifi-and-cpu-1/ + // For ADAFRUIT_FEATHER_M0, lowPowerMode() is a good balance of performance and battery + WiFi.lowPowerMode(); + // For ESP32 the default WIFI_PS_MIN_MODEM is probably optimal https://www.mischianti.org/2021/03/06/esp32-practical-power-saving-manage-wifi-and-cpu-1/ #endif - modePacketInterval = NORMAL_POWER_MODE_PACKET_INTERVAL; + modePacketInterval = NORMAL_POWER_MODE_PACKET_INTERVAL; + Serial.println("PowerMode::NORMAL_POWER"); + _powerMode = mode; + ret = true; + } } - else if (getPowerMode() == PowerMode::LOW_POWER) - { - Serial.println("PowerMode::LOW_POWER"); - if (_emotiBitWiFi.isOff()) + else if (mode == PowerMode::LOW_POWER) + { + Serial.println("SetPowerMode(LOW_POWER)"); + if (_bluetoothEnabled ) { - unsigned long beginTime = millis(); - _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option - Serial.print("Total WiFi.begin() = "); - Serial.println(millis() - beginTime); + Serial.println("SetPowerMode() FAILED: BLUETOOTH <-> WIFI requires device reset"); } + else + { + if (_emotiBitWiFi.isOff()) + { + unsigned long beginTime = millis(); + _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option + Serial.print("Total WiFi.begin() = "); + Serial.println(millis() - beginTime); + } #ifdef ADAFRUIT_FEATHER_M0 - WiFi.lowPowerMode(); + WiFi.lowPowerMode(); #endif - modePacketInterval = LOW_POWER_MODE_PACKET_INTERVAL; + modePacketInterval = LOW_POWER_MODE_PACKET_INTERVAL; + Serial.println("PowerMode::LOW_POWER"); + _powerMode = mode; + ret = true; + } } - else if (getPowerMode() == PowerMode::MAX_LOW_POWER) + else if (mode == PowerMode::MAX_LOW_POWER) { - Serial.println("PowerMode::MAX_LOW_POWER"); - if (_emotiBitWiFi.isOff()) + Serial.println("SetPowerMode(MAX_LOW_POWER)"); + if (_bluetoothEnabled ) { - unsigned long beginTime = millis(); - _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option - Serial.print("Total WiFi.begin() = "); - Serial.println(millis() - beginTime); + Serial.println("SetPowerMode() FAILED: BLUETOOTH <-> WIFI requires device reset"); } + else + { + if (_emotiBitWiFi.isOff()) + { + unsigned long beginTime = millis(); + _emotiBitWiFi.begin(100, 1, 100); // This ToDo: create a async begin option + Serial.print("Total WiFi.begin() = "); + Serial.println(millis() - beginTime); + } #ifdef ADAFRUIT_FEATHER_M0 - WiFi.maxLowPowerMode(); + WiFi.maxLowPowerMode(); #endif - // ToDo: for ESP32 There may be some value to explore WIFI_PS_MAX_MODEM https://www.mischianti.org/2021/03/06/esp32-practical-power-saving-manage-wifi-and-cpu-1/ - modePacketInterval = LOW_POWER_MODE_PACKET_INTERVAL; + // ToDo: for ESP32 There may be some value to explore WIFI_PS_MAX_MODEM https://www.mischianti.org/2021/03/06/esp32-practical-power-saving-manage-wifi-and-cpu-1/ + modePacketInterval = LOW_POWER_MODE_PACKET_INTERVAL; + Serial.println("PowerMode::MAX_LOW_POWER"); + _powerMode = mode; + ret = true; + } } - else if (getPowerMode() == PowerMode::WIRELESS_OFF) + else if (mode == PowerMode::WIRELESS_OFF) { + Serial.println("SetPowerMode(WIRELESS_OFF)"); + if (_bluetoothEnabled ) + { +#ifdef ARDUINO_FEATHER_ESP32 // consider moving this ifdef into EmotiBitBluetooth + _emotiBitBluetooth.end(); + // Note: we're NOT setting _bluetoothEnabled = false here because BLUETOOTH <-> WIFI requires device reset +#endif //ARDUINO_FEATHER_ESP32 + } + else + { + _emotiBitWiFi.end(); + } Serial.println("PowerMode::WIRELESS_OFF"); - _emotiBitWiFi.end(); + _powerMode = mode; + ret = true; } - else if (getPowerMode() == PowerMode::HIBERNATE) + else if (mode == PowerMode::BLUETOOTH) + { + Serial.println("SetPowerMode(BLUETOOTH)"); + if (!_bluetoothEnabled ) + { + Serial.println("SetPowerMode() FAILED: BLUETOOTH <-> WIFI requires device reset"); + } + else + { +#ifdef ARDUINO_FEATHER_ESP32 // consider moving this ifdef into EmotiBitBluetooth + if (_emotiBitBluetooth.isOff()) + { + if (_emotiBitBluetooth.begin(emotibitDeviceId) == 0) // ToDo: add EmotiBitBluetooth::SUCCESS for improved readability + { + Serial.println("PowerMode::BLUETOOTH"); + _powerMode = mode; + ret = true; + } + } + // ToDo: consider adding serial output to clarify !isOff() and !begin() +#endif //ARDUINO_FEATHER_ESP32 + } + } + else if (mode == PowerMode::HIBERNATE) { Serial.println("PowerMode::HIBERNATE"); + _powerMode = mode; + ret = true; } else { Serial.println("PowerMode Not Recognized"); } + String modePacket; + sendModePacket(modePacket, _outDataPacketCounter); + return ret; } void EmotiBit::writeSerialData(EmotiBit::DataType t) @@ -3919,6 +4043,10 @@ bool EmotiBit::createModePacket(String &modePacket, uint16_t &packetNumber) { payload += EmotiBitPacket::TypeTag::MODE_HIBERNATE; } + else if (getPowerMode() == PowerMode::BLUETOOTH) + { + payload += EmotiBitPacket::TypeTag::MODE_BLUETOOTH; + } dataCount++; modePacket += EmotiBitPacket::createPacket(EmotiBitPacket::TypeTag::EMOTIBIT_MODE, packetNumber++, payload, dataCount); @@ -4329,7 +4457,8 @@ void EmotiBit::processDebugInputs(String &debugPackets, uint16_t &packetNumber) _emotibitNvmController.eraseEeprom(); } } - else if (c == '>') { + else if (c == '>') + { _sendTestData = true; Serial.println("Entering Sending Test Data Mode"); } @@ -4337,6 +4466,9 @@ void EmotiBit::processDebugInputs(String &debugPackets, uint16_t &packetNumber) { _sendTestData = false; Serial.println("Exiting Sending Test Data Mode"); +#ifdef ADAFRUIT_FEATHER_M0 + startTimer(BASE_SAMPLING_FREQ); +#endif } else if (c == '@' && _sendTestData == true) { @@ -4677,9 +4809,12 @@ void EmotiBit::printEmotiBitInfo() IPAddress ip = WiFi.localIP(); Serial.print("\"ip_address\":\""); Serial.print(ip); - Serial.println("\""); + Serial.println("\","); } - + + Serial.print("\"wireless_mode\":\""); + Serial.print(_bluetoothEnabled ? "bluetooth" : "wifi"); + Serial.print("\""); Serial.println("}}]"); } diff --git a/EmotiBit.h b/EmotiBit.h index 5368853d..a7364264 100644 --- a/EmotiBit.h +++ b/EmotiBit.h @@ -21,7 +21,6 @@ #ifdef ARDUINO_FEATHER_ESP32 #include #include "driver/adc.h" -#include #else #include #include @@ -39,6 +38,9 @@ #include "FileTransferManager.h" #endif #include "EmotiBitConfigManager.h" +#ifdef ARDUINO_FEATHER_ESP32 +#include "EmotiBitBluetooth.h" +#endif class EmotiBit { @@ -266,6 +268,7 @@ class EmotiBit { MAX_LOW_POWER, // data not sent, time-syncing accuracy low LOW_POWER, // data not sent, time-syncing accuracy high NORMAL_POWER, // data sending, time-syncing accuracy high + BLUETOOTH, length }; @@ -280,6 +283,9 @@ class EmotiBit { EmotiBitEda emotibitEda; EmotiBitNvmController _emotibitNvmController; #ifdef ARDUINO_FEATHER_ESP32 + EmotiBitBluetooth _emotiBitBluetooth; + #endif //ARDUINO_FEATHER_ESP32 + #ifdef ARDUINO_FEATHER_ESP32 FileTransferManager _fileTransferManager; #endif EmotiBitConfigManager _emotibitConfigManager; @@ -429,6 +435,7 @@ class EmotiBit { DataType _serialData = DataType::length; volatile bool buttonPressed = false; bool startBufferOverflowTest = false; + bool _bluetoothEnabled = false; void setupFailed(const String failureMode, int buttonPin = -1, bool configFileError = false); bool setupSdCard(bool loadConfig = true); @@ -444,7 +451,8 @@ class EmotiBit { void attachShortButtonPress(void(*shortButtonPressFunction)(void)); void attachLongButtonPress(void(*longButtonPressFunction)(void)); PowerMode getPowerMode(); - void setPowerMode(PowerMode mode); + //void setPowerMode(PowerMode mode); + bool setPowerMode(PowerMode mode); bool writeSdCardMessage(const String &s); int freeMemory(); bool loadConfigFile(const String &filename); @@ -459,7 +467,6 @@ class EmotiBit { bool processThermopileData(); // placeholder until separate EmotiBitThermopile controller is implemented void writeSerialData(EmotiBit::DataType t); void printEmotiBitInfo(); - /** * Copies data buffer of the specified DataType into the passed array diff --git a/EmotiBitBluetooth.cpp b/EmotiBitBluetooth.cpp new file mode 100644 index 00000000..e833bad6 --- /dev/null +++ b/EmotiBitBluetooth.cpp @@ -0,0 +1,234 @@ +#ifdef ARDUINO_FEATHER_ESP32 +#include "EmotiBitBluetooth.h" +#include + +uint8_t EmotiBitBluetooth::begin(const String& emotibitDeviceId) +{ + if (pServer) + { + EmotiBitBluetooth::reconnect(); + Serial.println("Bluetooth already initialized, reconnecting..."); + return 0; // Success + } + + _emotibitDeviceId = emotibitDeviceId; + + Serial.println("Bluetooth tag detected, turning on bluetooth."); + BLEDevice::init(("EmotiBit: " + _emotibitDeviceId).c_str()); + + pServer = BLEDevice::createServer(); + + if (!pServer) + { + Serial.println("ERROR: Failed to create BLE server"); + return 1; + } + + pServer->setCallbacks(new MyServerCallbacks(this)); + BLEService* pService = pServer->createService(EMOTIBIT_SERVICE_UUID); + + pDataTxCharacteristic = pService->createCharacteristic(EMOTIBIT_DATA_TX_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY); + if (!pDataTxCharacteristic) + { + Serial.println("ERROR: Failed to create TX characteristic"); + return 1; + } + pDataTxCharacteristic->addDescriptor(new BLE2902()); + + pDataRxCharacteristic = pService->createCharacteristic(EMOTIBIT_DATA_RX_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_WRITE); + if (!pDataRxCharacteristic) + { + Serial.println("ERROR: Failed to create RX characteristic"); + return 1; + } + pDataRxCharacteristic->setCallbacks(new MyCallbacks()); + + pService->start(); + + EmotiBitBluetooth::startAdvertising(); + _bluetoothReconnect = true; // Allow reconnection after disconnection + + return 0; +} + +void EmotiBitBluetooth::MyServerCallbacks::onConnect(BLEServer* pServer) +{ + server->deviceConnected = true; + server->_connId = pServer->getConnId(); + Serial.println("BLE client connected"); +} + +void EmotiBitBluetooth::MyServerCallbacks::onDisconnect(BLEServer* pServer) +{ + server->deviceConnected = false; + Serial.println("BLE client disconnected"); + //need to restart advertising to allow new connections after disconnection if accidentally disconnected + if (server->_bluetoothReconnect) + { + server->reconnect(); + Serial.println("Restarted BLE advertising"); + } +} + +//ToDO: Current polling aproach can drop back to back writes since the characteristic only holds the last value. +void EmotiBitBluetooth::MyCallbacks::onWrite(BLECharacteristic *pCharacteristic) +{ + std::string rxValue = pCharacteristic->getValue(); + if (rxValue.length() > 0) { + //Serial.print("Received: "); + //Serial.println(rxValue.c_str()); + } +} + +void EmotiBitBluetooth::setDeviceId(const String& emotibitDeviceId) +{ + _emotibitDeviceId = emotibitDeviceId; +} + +void EmotiBitBluetooth::sendData(const String &message) +{ + if (deviceConnected) + { + //TODO: consider truncating via MTU if message is too long + if (pDataTxCharacteristic == nullptr) + { + //Serial.println("ERROR: pDataTxCharacteristic is NULL!"); + return; + } + + //Serial.print("BLE TX: Message length="); + //Serial.print(message.length()); + + // Set the value + pDataTxCharacteristic->setValue(message.c_str()); + + // Call notify - note: doesn't return success/failure status + pDataTxCharacteristic->notify(); + + // Check descriptor status + BLEDescriptor* p2902 = pDataTxCharacteristic->getDescriptorByUUID(BLEUUID((uint16_t)0x2902)); + if (p2902) + { + const uint8_t* val = p2902->getValue(); + if (val) + { + //Serial.print(" | Notifications enabled="); + //Serial.print((val[0] & 0x01) ? "YES" : "NO"); + } + } + + //Serial.print(" | Char ptr=0x"); + //Serial.print((uint32_t)pDataTxCharacteristic, HEX); + //Serial.print(" | Message: "); + //Serial.println(message.c_str()); + } + else + { + //ToDO: consider gating behind a debug flag + Serial.println("unable to send data: deviceConnected=false"); + } +} + +uint8_t EmotiBitBluetooth::readControl(String& packet) +{ + uint8_t numPackets = 0; + packet = ""; + if (deviceConnected) + { + std::string rxValue = pDataRxCharacteristic->getValue(); + if (!rxValue.empty()) + { + //Serial.print("Received: "); + //Serial.println(rxValue.c_str()); + _receivedControlMessage += String(rxValue.c_str()); + + //CLEAR THE CHAR VALUE SO WE DON’T REUSE IT + pDataRxCharacteristic->setValue(""); + } + + String tempPacket = ""; + while (_receivedControlMessage.length() > 0) + { + int c = _receivedControlMessage[0]; + _receivedControlMessage.remove(0, 1); + + if (c == (int)EmotiBitPacket::PACKET_DELIMITER_CSV) + { + numPackets++; + packet = tempPacket; + tempPacket = ""; + _receivedControlMessage = ""; + return numPackets; + } + else + { + if (c == 0) { + // Throw out null term + } + else + { + tempPacket += (char)c; + } + } + } + } + return numPackets; +} + +bool EmotiBitBluetooth::isOff() +{ + return _bluetoothOff; +} + +void EmotiBitBluetooth::end() +{ + if (pServer && deviceConnected) + { + //tear down the old connection + pServer->disconnect(_connId); + Serial.println("BLE client disconnected by end()"); + } + if (pServer) + { + pServer->getAdvertising()->stop(); + Serial.println("BLE advertising stopped"); + } + _bluetoothOff = true; + _bluetoothReconnect = false; +} + +void EmotiBitBluetooth::reconnect() +{ + if (pServer) + { + EmotiBitBluetooth::startAdvertising(); + Serial.println("BLE advertising restarted after reconnect"); + _bluetoothOff = false; + _bluetoothReconnect = true; + } + else + { + Serial.println("ERROR: pServer is NULL, cannot reconnect"); + } +} + +void EmotiBitBluetooth::startAdvertising() +{ + if (pServer) + { + pServer->getAdvertising()->start(); + _bluetoothOff = false; + Serial.println("BLE advertising started"); + } + else + { + Serial.println("ERROR: pServer is NULL, cannot start advertising"); + } +} + +void EmotiBitBluetooth::disableBluetooth() +{ + esp_bt_controller_disable(); +} + +#endif //ARDUINO_FEATHER_ESP32 \ No newline at end of file diff --git a/EmotiBitBluetooth.h b/EmotiBitBluetooth.h new file mode 100644 index 00000000..34f3e9cd --- /dev/null +++ b/EmotiBitBluetooth.h @@ -0,0 +1,125 @@ +/**************************************************************************/ +/*! + @file EmotiBitBluetooth.h + + This file facilitates the use of Bluetooth on the EmotiBit + + EmotiBit invests time and resources providing this open source code, + please support EmotiBit and open-source hardware by purchasing + products from EmotiBit! + + Written by Joseph Jacobson for EmotiBit. + + BSD license, all text here must be included in any redistribution +*/ +/**************************************************************************/ +#ifdef ARDUINO_FEATHER_ESP32 +#pragma once +/*! +* @brief inclusions for BLE Device, Server, Utils, 2902, and Arduino +*/ +#include "Arduino.h" +#include +#include +#include +#include +#include "EmotiBitPacket.h" + +#define EMOTIBIT_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define EMOTIBIT_DATA_RX_CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define EMOTIBIT_DATA_TX_CHARACTERISTIC_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +/*! + * @brief Handles Bluetooth communication for EmotiBit. +*/ +class EmotiBitBluetooth { + public: + BLEServer* pServer = nullptr; ///points to the server + + BLECharacteristic* pDataTxCharacteristic = nullptr; ///points to the data tx characteristic + BLECharacteristic* pDataRxCharacteristic = nullptr; ///points to the data rx characteristic + + bool deviceConnected = false; ///boolean to check if device is connected + String _emotibitDeviceId = ""; ///string to hold device id + String _receivedControlMessage = ""; + bool _bluetoothOff = true; + bool _bluetoothReconnect = false; + uint16_t _connId = 0; + + /*! + * @brief Server callbacks for connections + */ + class MyServerCallbacks: public BLEServerCallbacks { + public: + MyServerCallbacks(EmotiBitBluetooth* server) : server(server) {} + void onConnect(BLEServer* pServer); + void onDisconnect(BLEServer* pServer); + private: + EmotiBitBluetooth* server; + }; + + /*! + * @brief Characteristic callbacks for data transfer + */ + class MyCallbacks : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic); + }; + + /*! + * @brief Initializes the BLE device and starts advertising + * @param emotibitDeviceId ID from setDeviceId + * @return 0 on success, 1 on failure + */ + //TO DO use int for error handling + uint8_t begin(const String& emotibitDeviceId); + + /*! + * @brief Sends data over BLE + * @param message data to be sent + */ + void sendData(const String &message); + + /*! + * @brief Updates the device ID used in BLE advertising + * @param emotibitDeviceId the new device ID to be used in advertising + */ + void setDeviceId(const String& emotibitDeviceId); + + /*! + * @brief Reads control messages from the BLE characteristic + * @param packet the control message packet + */ + uint8_t readControl(String& packet); + + //void update();for when we sync data over BLE + //move to emotibit + //void sdCardFileNaming(); for when we choose bluetooth and there is no rb start time + + /*! + * @brief Ends the BLE connection + */ + void end(); + + /*! + * @brief Checks if the Bluetooth is off + * @return if Bluetooth is off, returns true, otherwise false + */ + bool isOff(); + + /*! + * @brief Reconnects the BLE server if disconnected + */ + void reconnect(); + + /*! + * @brief Starts Advertising + */ + void startAdvertising(); + + /*! + * @brief Disable Bluetooth + */ + void disableBluetooth(); +}; + +#endif //ARDUINO_FEATHER_ESP32 \ No newline at end of file diff --git a/EmotiBit_stock_firmware/EmotiBit_stock_firmware.ino b/EmotiBit_stock_firmware/EmotiBit_stock_firmware.ino index c9fbebe0..482afbdb 100644 --- a/EmotiBit_stock_firmware/EmotiBit_stock_firmware.ino +++ b/EmotiBit_stock_firmware/EmotiBit_stock_firmware.ino @@ -10,16 +10,32 @@ float data[dataSize]; void onShortButtonPress() { - // toggle wifi on/off - if (emotibit.getPowerMode() == EmotiBit::PowerMode::NORMAL_POWER) + if (emotibit._bluetoothEnabled == true) { - emotibit.setPowerMode(EmotiBit::PowerMode::WIRELESS_OFF); - Serial.println("PowerMode::WIRELESS_OFF"); + if (emotibit.getPowerMode() == EmotiBit::PowerMode::BLUETOOTH) + { + emotibit.setPowerMode(EmotiBit::PowerMode::WIRELESS_OFF); + Serial.println("PowerMode::WIRELESS_OFF"); + } + else + { + emotibit.setPowerMode(EmotiBit::PowerMode::BLUETOOTH); + Serial.println("PowerMode::BLUETOOTH"); + } } else { - emotibit.setPowerMode(EmotiBit::PowerMode::NORMAL_POWER); - Serial.println("PowerMode::NORMAL_POWER"); + // toggle wifi on/off + if (emotibit.getPowerMode() == EmotiBit::PowerMode::NORMAL_POWER) + { + emotibit.setPowerMode(EmotiBit::PowerMode::WIRELESS_OFF); + Serial.println("PowerMode::WIRELESS_OFF"); + } + else + { + emotibit.setPowerMode(EmotiBit::PowerMode::NORMAL_POWER); + Serial.println("PowerMode::NORMAL_POWER"); + } } } diff --git a/README.md b/README.md index 8c942661..db4591fc 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,6 @@ See https://github.com/EmotiBit/EmotiBit_Docs/blob/master/Getting_Started.md ### Build - To build on Arduino, Open the EmotiBit_stock_firmware > EmotiBit_stock_firmware.ino. Press the build button. - To build using PlatformIO, follow the instructions in our documentation: [`Building firmware using PlatformIO`]( https://github.com/EmotiBit/EmotiBit_Docs/blob/master/Keep_emotibit_up_to_date.md#building-firmware-using-platformio) + +## Developer Documentation +- [GitHub Workflows and CI/CD](WORKFLOWS_AND_SCRIPTS.md) - Detailed documentation on automated builds, releases, and bash scripts diff --git a/WORKFLOWS_AND_SCRIPTS.md b/WORKFLOWS_AND_SCRIPTS.md new file mode 100644 index 00000000..ea434f4a --- /dev/null +++ b/WORKFLOWS_AND_SCRIPTS.md @@ -0,0 +1,313 @@ +# GitHub Workflows and Bash Scripts Documentation + +This document provides a comprehensive overview of all GitHub workflows and bash scripts in the EmotiBit FeatherWing repository. + +## Table of Contents +- [GitHub Workflows](#github-workflows) + - [Development Workflow: Continuous Build](#development-workflow-continuous-build) + - [Release Workflow: Create Release](#release-workflow-create-release) +- [Bash Scripts](#bash-scripts) + - [CI/CD Scripts](#cicd-scripts) + - [Development Scripts](#development-scripts) + - [Test Scripts](#test-scripts) +- [Development Process](#development-process) +- [Dependency Management Approaches](#dependency-management-approaches) +- [Configuration Files](#configuration-files) +- [Requirements Summary](#requirements-summary) +- [Maintenance Notes](#maintenance-notes) + +--- + +## GitHub Workflows + +The repository uses two GitHub Actions workflows to automate the build and release process. These workflows are defined in `.github/workflows/`. + +### Development Workflow: Continuous Build +**File:** `.github/workflows/build-and-upload-binaries.yml` + +**When it runs:** Automatically on every push to any branch + +**What it does:** Provides continuous integration by building firmware binaries for every commit, ensuring code changes compile successfully. + +**For developers:** +- Push to any branch triggers an automatic build +- Check the Actions tab to see build status +- Build artifacts (`.bin` and `.hex` files) are available for download for 30 days +- Artifacts include both firmware variants plus a dependency report +- Artifact naming: `emotibit-firmware-{version}` (version from `library.properties`) + +**Why it's useful:** +- Validates that changes compile before merging +- Provides downloadable binaries for testing without local builds +- Generates a dependency snapshot for each build +- Ensures all branches maintain buildable state + +**Technical notes:** +- Uses `download_dependencies.sh`, `generate_dependency_report.sh`, and `build.sh` +- Automatically cleans up build environment after completion + +--- + +### Release Workflow: Create Release +**File:** `.github/workflows/create-release.yml` + +**When it runs:** Manually triggered via GitHub Actions UI + +**What it does:** Creates a GitHub release using binaries from the latest successful dev branch build. + +**For developers - How to create a release:** + +1. Ensure the `dev` branch has a successful build +2. Go to Actions → "Create Release from Latest CI Build" → "Run workflow" +3. Optionally provide: + - Custom release name (defaults to `v{version}` from library.properties) + - Pre-release flag (for beta/RC releases) +4. Click "Run workflow" +5. Workflow creates a **draft release** - review it before publishing + +**What gets included:** +- Stock firmware binaries (`.bin` and `.hex` files) from both variants +- Auto-generated release notes with: + - Link to the source build workflow run + - Library dependency versions and git commits + +**Important constraints:** +- Only creates releases from the `dev` branch +- Requires at least one successful build on `dev` +- Releases are created as drafts for manual review +- Version tag format: `v{version}` (e.g., `v1.14.3`) + +**Best practices:** +- Always verify the version number in `library.properties` before creating a release +- Review the draft release before publishing +- Use pre-release flag for beta/testing versions +- Ensure changelog/release notes are updated before publishing + +--- + +## Bash Scripts + +### CI/CD Scripts + +These scripts are used by the GitHub workflows for automated builds and releases. + +#### download_dependencies.sh +**Location:** `./download_dependencies.sh` + +**Purpose:** Downloads all project dependencies based on `library.properties` and `depends_urls.json`. + +**How it works:** +1. Reads dependency list from `library.properties` +2. Gets GitHub repository URLs from `depends_urls.json` +3. Parses dependency names and version specifications +4. Clones each dependency repository at the specified version +5. Downloads to the parent directory (same level as FeatherWing) + +**Features:** +- Supports version pinning with format: `Name (=version)` +- Tries both `v{version}` and `{version}` tag formats +- Falls back to default branch if version tag not found +- Skips already downloaded repositories +- Shallow clone (--depth 1) for efficiency + +**Requirements:** +- `jq` command-line JSON processor +- Git + +**Exit codes:** +- 0: Success +- 1: Missing required files or tools + +--- + +#### generate_dependency_report.sh +**Location:** `./generate_dependency_report.sh` + +**Purpose:** Generates a text report of all dependency versions and git commits. + +**Output:** `dependency_report.txt` in the script directory + +**Report format:** +``` +EmotiBit FeatherWing Dependency Report +Generated: [timestamp] + +LibraryName1: version (git_commit_hash) +LibraryName2: version (git_commit_hash) +... +``` + +**How it works:** +1. Reads dependencies from `library.properties` +2. Gets repository information from `depends_urls.json` +3. For each dependency: + - Locates the downloaded repository + - Retrieves the current git commit hash + - Writes to report file + +**Requirements:** +- `jq` command-line JSON processor +- Dependencies must be already downloaded + +--- + +#### build.sh +**Location:** `./build.sh` + +**Purpose:** Builds all EmotiBit FeatherWing firmware variants using PlatformIO. + +**Builds:** +1. EmotiBit stock firmware (`EmotiBit_stock_firmware/`) +2. EmotiBit stock firmware PPG 100Hz (`EmotiBit_stock_firmware_PPG_100Hz/`) + +**How it works:** +1. Checks if PlatformIO is installed +2. Changes to each firmware directory +3. Runs `pio run` to build the firmware +4. Reports completion + +**Requirements:** +- PlatformIO CLI (`pio` command) +- All dependencies must be downloaded + +**Exit codes:** +- 0: Success (all builds completed) +- 1: PlatformIO not installed or build failed + +--- + +### Development Scripts + +These scripts are for local development and manual dependency management. + +#### CloneEmotiBitFW.sh +**Location:** `./CloneEmotiBitFW.sh` + +**Purpose:** Clones all EmotiBit dependency repositories using SSH. + +**How it works:** +1. Calls `ExtractDepends.sh` to generate dependency list +2. Starts SSH agent and adds SSH key (`~/.ssh/id_ed25519`) +3. Reads repository names from `EmotiBit_FeatherWing_depends.txt` +4. Clones each repository from GitHub using SSH +5. Clones to parent directory (one level up from current) +6. Skips repositories that already exist + +**Requirements:** +- SSH key configured for GitHub (`~/.ssh/id_ed25519`) +- SSH access to EmotiBit GitHub repositories + +**Note:** This is a legacy script. The newer `download_dependencies.sh` is preferred for CI/CD as it uses HTTPS and supports version pinning. + +--- + +#### CheckoutMasterEmotiBitFW.sh +**Location:** `./CheckoutMasterEmotiBitFW.sh` + +**Purpose:** Checks out the master branch for all dependency repositories. + +**How it works:** +1. Calls `ExtractDepends.sh` to generate dependency list +2. Starts SSH agent and adds SSH key +3. Iterates through each repository +4. Changes to repository directory and runs `git checkout master` + +**Requirements:** +- SSH key configured for GitHub +- Repositories must be already cloned + +**Use case:** Useful for ensuring all dependencies are on the master branch for development or testing. + +--- + +#### ExtractDepends.sh +**Location:** `./ExtractDepends.sh` + +**Purpose:** Extracts dependency names from `library.properties` and writes them to a text file. + +**Output:** `EmotiBit_FeatherWing_depends.txt` + +**How it works:** +1. Deletes existing `EmotiBit_FeatherWing_depends.txt` if present +2. Reads `library.properties` line by line +3. Finds the line starting with `depends=` +4. Parses comma-separated dependency list +5. Converts spaces to underscores in repository names +6. Writes each repository name to the output file + +**Note:** This script is called by other development scripts (`CloneEmotiBitFW.sh`, `CheckoutMasterEmotiBitFW.sh`, `UpdateEmotiBitFW.sh`) to generate the repository list. + +--- + +#### UpdateEmotiBitFW.sh +**Location:** `./UpdateEmotiBitFW.sh` + +**Purpose:** Updates all dependency repositories by pulling latest changes. + +**How it works:** +1. Calls `ExtractDepends.sh` to generate dependency list +2. Starts SSH agent and adds SSH key +3. Iterates through each repository +4. Changes to repository directory and runs `git pull` + +**Requirements:** +- SSH key configured for GitHub +- Repositories must be already cloned +- No uncommitted changes in repositories (to avoid conflicts) + +**Use case:** Quick way to update all dependencies to their latest versions. + +## Dependency Management Approaches + +The repository uses two different approaches for dependency management: + +### Modern Approach (CI/CD) +- **Script:** `download_dependencies.sh` +- **Configuration:** `library.properties` + `depends_urls.json` +- **Method:** HTTPS cloning with version pinning +- **Advantages:** + - No SSH key required + - Version control per dependency + - Works in CI/CD environments + - Follows Arduino library conventions + +### Legacy Approach (Local Development) +- **Scripts:** `CloneEmotiBitFW.sh`, `CheckoutMasterEmotiBitFW.sh`, `UpdateEmotiBitFW.sh` +- **Configuration:** `library.properties` + `ExtractDepends.sh` +- **Method:** SSH cloning without version pinning +- **Advantages:** + - Quick updates with git pull + - Works with existing SSH credentials + - Simple for local development + +**Recommendation:** Use the modern approach (`download_dependencies.sh`) for reproducible builds and CI/CD. The legacy scripts remain useful for rapid local development iterations. + +--- + +## Development Process + +### Typical Development Lifecycle + +**Feature Development:** +``` +1. Create feature branch from dev +2. Make code changes +3. Push to GitHub → automatic build runs +4. Download artifacts from Actions tab to test +5. Iterate on changes (each push triggers new build) +6. Create pull request to dev +7. Merge after review and successful build +``` + +**Preparing a Release:** +``` +1. Update version in library.properties and merge featire branch to dev branch +2. Push changes → automatic build runs +3. Verify build succeeds on dev +4. Manually trigger "Create Release" workflow +5. Review draft release: + - Check binaries are correct + - Review auto-generated dependency information + - Add/edit release notes as needed +6. Publish release when ready +``` \ No newline at end of file diff --git a/board_feather_esp32.ini b/board_feather_esp32.ini index 16ba6c5d..6b1999cf 100644 --- a/board_feather_esp32.ini +++ b/board_feather_esp32.ini @@ -8,4 +8,5 @@ build_flags = ; change MCU frequency board_build.f_cpu = 240000000L extra_scripts = pre:../pio_scripts/renameFw.py -firmware_name_board_name = feather_esp32 \ No newline at end of file +firmware_name_board_name = feather_esp32 +board_build.partitions = huge_app.csv \ No newline at end of file diff --git a/library.properties b/library.properties index 904dc66c..82840d15 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=EmotiBit FeatherWing -version=1.14.3 +version=1.14.4 author=Connected Future Labs maintainer=Connected Future Labs sentence=A library written for EmotiBit FeatherWing that supports all sensors included on the wing.