// SPDX-FileCopyrightText: 2025 Amal Mazrah // SPDX-FileCopyrightText: 2025 Jonas Smedegaard // SPDX-FileCopyrightText: 2025 Mennatullah Hatim Kassim // SPDX-FileCopyrightText: 2025 Noor Ahmad // SPDX-FileCopyrightText: 2025 Tanishka Suwalka // SPDX-License-Identifier: GPL-3.0-or-later /// Sensor mussel - an Arduino sketch to emulate a mussel biosensor /// /// @version 0.0.3 /// @see /// @see // arduino-esp32 Logging system // activate in Arduino IDE: Tools -> Core Debug Level // special: set Core Debug Level to Error for plot-friendly output #define CONFIG_ARDUHAL_ESP_LOG 1 #define LOG_LOCAL_LEVEL CORE_DEBUG_LEVEL #include #undef ARDUHAL_LOG_FORMAT #define ARDUHAL_LOG_FORMAT(letter, format) \ ARDUHAL_LOG_COLOR_##letter "[" #letter "] %s(): " format \ ARDUHAL_LOG_RESET_COLOR "\r\n", __FUNCTION__ // arduino-esp32 Bluetooth Low Energy (BLE) networking stack #include "BLEDevice.h" #include "BLEBeacon.h" #include "BLEAdvertising.h" #include "BLEEddystoneTLM.h" // Adjust these for production use // // * BEACON_NAME must be unique within deployment // * BEACON_UUID should be unique for each deployment // // @see https://www.uuidgenerator.net/ #define BEACON_NAME "Dummy mussel sensor" #define BEACON_UUID "00000000-0000-0000-0000-000000000000" // maximum accumulated stress #define STRESS_MAX 50 // light sensor #define LIGHT_PIN 34 #define DARKNESS_MAX 1000 // arduino-esp32 Touch sensor #define TOUCH_PIN T0 // T0 is GPIO4 #define TOUCH_THRESHOLD 40 // arduino-esp32 LED PWM Controller (LEDC) as pacemaker for gaping rhythm #define LED_PIN LED_BUILTIN #define LEDC_BITS 7 #define LEDC_FREQ 500 #define LEDC_START_DUTY 0 #define LEDC_TARGET_DUTY 90 #define LEDC_CALM_PACE 3000 #define LEDC_STRESSED_PACE 400 // pacemaker variables int stress = 0; bool touch_detected = false; int pace = LEDC_STRESSED_PACE; bool fade_ended = false; bool fade_in = true; // pointer to control Bluetooth networking BLEAdvertising *pAdvertising; // Touch sensor callback void gotTouch() { // keepPace(); touch_detected = true; pace = LEDC_STRESSED_PACE; } // pacemaker end-of-fade Interrupt Service Routine (ISR) a.k.a. callback void ARDUINO_ISR_ATTR LED_FADE_ISR() { fade_ended = true; keepPace(); } // stress-inducing touch callback void beginTouchDetection() { touchAttachInterrupt(TOUCH_PIN, gotTouch, TOUCH_THRESHOLD); log_d("touch detected"); } // pacemaker initialization void beginPace() { // Setup pacemaker timer ledcAttach(LED_PIN, LEDC_FREQ, LEDC_BITS); // fade in once uncontrolled and then begin fade out with ISR ledcFade(LED_PIN, LEDC_START_DUTY, LEDC_TARGET_DUTY, pace); delay(pace); ledcFadeWithInterrupt(LED_PIN, LEDC_TARGET_DUTY, LEDC_START_DUTY, pace, LED_FADE_ISR); } // pacemaker maintenance void keepPace() { // if (fade_ended || touch_detected) { if (fade_ended) { fade_ended = false; // stress management if (touch_detected) { touch_detected = false; log_i("Stressed by touch!"); if (stress < STRESS_MAX) { stress = stress + 10; } } else if (stress > 0) { stress--; if (stress <= 0) { pace = LEDC_CALM_PACE; log_i("Calmed down..."); } else { log_i("Still stressed..."); } } else { pace = LEDC_CALM_PACE; } // begin fade at decided direction and pace ledcFadeWithInterrupt(LED_PIN, fade_in ? LEDC_START_DUTY : LEDC_TARGET_DUTY, fade_in ? LEDC_TARGET_DUTY : LEDC_START_DUTY, pace, LED_FADE_ISR); // remember next fade direction fade_in = !fade_in; } } // read light intensity and return its non-zero capped value int getLightIntensity() { int value = analogRead(LIGHT_PIN); if (value > DARKNESS_MAX) value = DARKNESS_MAX; log_i("light intensity: %d", value); return DARKNESS_MAX - value; } // fake gape angle as pacemaker position dampened by light intensity int resolveGapeAngle() { int paceAngle = ledcRead(LED_PIN); log_i("pacemaker value: %d", value); int lightIntensity = getLightIntensity(); log_i("light intensity: %d", value); int gapeAngle = paceAngle * lightIntensity / DARKNESS_MAX; // misuse error-only log level for plot-friendly output #if ARDUHAL_LOG_LEVEL == ARDUHAL_LOG_LEVEL_ERROR Serial.printf("pace_angle:%d light/10:%d gape_angle:%d\n", paceAngle, lightIntensity/10, gapeAngle); #endif return gapeAngle; } // Encode static Bluetooth beacon advertisement data void setBeaconAdvertisement() { BLEAdvertisementData oAdvertisementData = BLEAdvertisementData(); oAdvertisementData.setName(BEACON_NAME); pAdvertising->setAdvertisementData(oAdvertisementData); } // Encode variable Bluetooth beacon service data void setBeaconServiceData(int angle) { BLEEddystoneTLM EddystoneTLM; EddystoneTLM.setTemp(angle); log_i("Gape angle: %.2f°", EddystoneTLM.getTemp()); BLEAdvertisementData oScanResponseData = BLEAdvertisementData(); oScanResponseData.setServiceData( BLEUUID((uint16_t)0xFEAA), String( EddystoneTLM.getData().c_str(), EddystoneTLM.getData().length())); pAdvertising->setScanResponseData(oScanResponseData); } void setup() { // enable logging to serial Serial.begin(115200); esp_log_level_set("*", ESP_LOG_DEBUG); if (BEACON_UUID == "00000000-0000-0000-0000-000000000000") Serial.println("Please set a deployment-wide unique BEACON_UUID"); beginPace(); beginTouchDetection(); // setup Bluetooth BLEDevice::init(BEACON_NAME); pAdvertising = BLEDevice::getAdvertising(); setBeaconAdvertisement(); setBeaconServiceData(resolveGapeAngle()); pAdvertising->start(); } void loop() { // update Bluetooth beacon service data setBeaconServiceData(resolveGapeAngle()); delay(500); }