From 6ca4ff06867aa5d7354615c828bd83234d827ff2 Mon Sep 17 00:00:00 2001 From: Jonas Smedegaard Date: Fri, 18 Apr 2025 01:24:05 +0200 Subject: rop Mussel library; move Arduino sketches below Arduino/ --- Arduino/sensor/Mussel_Sensor_Beacon.md | 35 +++ Arduino/sensor/sensor.ino | 215 ++++++++++++++++ Arduino/sensor/sensor.puml | 26 ++ Arduino/vote/Mussel_Beacon_Voting.md | 20 ++ Arduino/vote/vote.ino | 319 ++++++++++++++++++++++++ Arduino/vote/vote.puml | 27 ++ Makefile | 3 - Mussel.zip | Bin 19857 -> 0 bytes Mussel/Mussel.cpp | 290 --------------------- Mussel/Mussel.h | 78 ------ Mussel/Mussel_begin.puml | 10 - Mussel/Mussel_read.puml | 24 -- Mussel/README.md | 38 --- Mussel/examples/button/button.ino | 20 -- Mussel/examples/button_states/button_states.ino | 20 -- Mussel/examples/command/command.ino | 19 -- Mussel/examples/minute/minute.ino | 20 -- Mussel/examples/seconds/seconds.ino | 19 -- Mussel/examples/seconds/seconds.puml | 15 -- Mussel/examples/sensor/Mussel_Sensor_Beacon.md | 7 - Mussel/examples/sensor/sensor.ino | 54 ---- Mussel/examples/sensor/sensor.puml | 26 -- Mussel/examples/temperature/temperature.ino | 21 -- Mussel/examples/vote/Mussel_Beacon_Voting.md | 20 -- Mussel/examples/vote/vote.ino | 56 ----- Mussel/examples/vote/vote.puml | 27 -- Mussel/examples/voting/voting.ino | 23 -- Mussel/examples/voting/voting.puml | 15 -- Mussel/keywords.txt | 17 -- Mussel/library.properties | 10 - _make/_zip.mk | 22 -- report.qmd | 24 +- sensor/Mussel_Sensor_Beacon.md | 35 --- sensor/sensor.ino | 215 ---------------- sensor/sensor.puml | 26 -- vote/Mussel_Beacon_Voting.md | 20 -- vote/vote.ino | 319 ------------------------ vote/vote.puml | 27 -- 38 files changed, 651 insertions(+), 1511 deletions(-) create mode 100644 Arduino/sensor/Mussel_Sensor_Beacon.md create mode 100644 Arduino/sensor/sensor.ino create mode 100644 Arduino/sensor/sensor.puml create mode 100644 Arduino/vote/Mussel_Beacon_Voting.md create mode 100644 Arduino/vote/vote.ino create mode 100644 Arduino/vote/vote.puml delete mode 100644 Makefile delete mode 100644 Mussel.zip delete mode 100644 Mussel/Mussel.cpp delete mode 100644 Mussel/Mussel.h delete mode 100644 Mussel/Mussel_begin.puml delete mode 100644 Mussel/Mussel_read.puml delete mode 100644 Mussel/README.md delete mode 100644 Mussel/examples/button/button.ino delete mode 100644 Mussel/examples/button_states/button_states.ino delete mode 100644 Mussel/examples/command/command.ino delete mode 100644 Mussel/examples/minute/minute.ino delete mode 100644 Mussel/examples/seconds/seconds.ino delete mode 100644 Mussel/examples/seconds/seconds.puml delete mode 100644 Mussel/examples/sensor/Mussel_Sensor_Beacon.md delete mode 100644 Mussel/examples/sensor/sensor.ino delete mode 100644 Mussel/examples/sensor/sensor.puml delete mode 100644 Mussel/examples/temperature/temperature.ino delete mode 100644 Mussel/examples/vote/Mussel_Beacon_Voting.md delete mode 100644 Mussel/examples/vote/vote.ino delete mode 100644 Mussel/examples/vote/vote.puml delete mode 100644 Mussel/examples/voting/voting.ino delete mode 100644 Mussel/examples/voting/voting.puml delete mode 100644 Mussel/keywords.txt delete mode 100644 Mussel/library.properties delete mode 100644 _make/_zip.mk delete mode 100644 sensor/Mussel_Sensor_Beacon.md delete mode 100644 sensor/sensor.ino delete mode 100644 sensor/sensor.puml delete mode 100644 vote/Mussel_Beacon_Voting.md delete mode 100644 vote/vote.ino delete mode 100644 vote/vote.puml diff --git a/Arduino/sensor/Mussel_Sensor_Beacon.md b/Arduino/sensor/Mussel_Sensor_Beacon.md new file mode 100644 index 0000000..89beeec --- /dev/null +++ b/Arduino/sensor/Mussel_Sensor_Beacon.md @@ -0,0 +1,35 @@ +# Mussel Sensor Beacon + +1. Reads sensors. + +2. Normalises sensor data as a gape angle in the range of 0°-90°. + +3. Broadcasts normalised sensor data as a beacon on a bluetooth network. + +## Scanner apps + +These Android apps have been found usable +for monitoring this type of sensor: + + * FeasyBeacon on Play store + * NanoBeacon BLE Scanner on Play store + * decodes Eddystone TLM temperature as Fahrenheit + * does not decode Eddystone URL + * SemBeacon on Play store + * detects URL-only but not TLM-only Eddystone Beacon + * too aggressive caching misses changing data + * UI optimized for SemBeacon + * nRF Connect for Mobile on Play store + * misses changing data + * UI not specific to beacons + +These Android apps are potentially interesting as well: + + * Beacon Locator on F-droid + * unreliable detection + * active development + * BLE Radar on F-droid + * detects URL-only but not TLM-only Eddystone Beacon + * UI not specific to beacons + * AltBeacon Loc on Play store + * detects URL-only but not TLM-only Eddystone Beacon diff --git a/Arduino/sensor/sensor.ino b/Arduino/sensor/sensor.ino new file mode 100644 index 0000000..8d43b22 --- /dev/null +++ b/Arduino/sensor/sensor.ino @@ -0,0 +1,215 @@ +// 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); +} diff --git a/Arduino/sensor/sensor.puml b/Arduino/sensor/sensor.puml new file mode 100644 index 0000000..3f033d1 --- /dev/null +++ b/Arduino/sensor/sensor.puml @@ -0,0 +1,26 @@ +@startuml +'start +:instantiate mussel object; +:instantiate bluetooth object; +group init +:setup mussel sensors; +:setup bluetooth beacon; +end group +split +while (each 500ms) +group loop { +:read sensors; +:normalize sensor data +as a gape angle; +:add gape angle to beacon; +end group +endwhile +-[hidden]-> +kill +split again +while (each 100ms) +:broadcast beacon; +endwhile +-[hidden]-> +kill +@enduml diff --git a/Arduino/vote/Mussel_Beacon_Voting.md b/Arduino/vote/Mussel_Beacon_Voting.md new file mode 100644 index 0000000..33a0893 --- /dev/null +++ b/Arduino/vote/Mussel_Beacon_Voting.md @@ -0,0 +1,20 @@ +## Mussel Beacon Voting + +1. Scans bluetooth network for beacons. + +2. Collects mussel name and gape angle +as decoded from each detected beacon, +together with the time of detection in milliseconds since boot. + +3. Aligns the collected data +to the format of ballots for a water quality vote. + +4. Qualifies the ballots for criteria of the water quality vote +(e.g. timeliness and sanity of gape angles). + +5. Concludes a vote based on collected, aligned and qualified ballots. + +6. Acts on the voting result, +e.g. turns on a steady light for "code green" +or a blinking light for "code yellow", +or turns on a blinking light and shuts off a valve for "code red". diff --git a/Arduino/vote/vote.ino b/Arduino/vote/vote.ino new file mode 100644 index 0000000..3d4b141 --- /dev/null +++ b/Arduino/vote/vote.ino @@ -0,0 +1,319 @@ +// 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 + +/// Mussel vote - an Arduino sketch to monitor mussel biosensors +/// +/// @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 +#include +#include +#include +#include +#define SCAN_INTERVAL 100 +#define SCAN_WINDOW 99 +#define SCAN_TIME_SEC 1 + +// stack sizes for voters and ballots-per-voter +#define VOTER_MAX 10 +#define BALLOT_MAX 5 + +// Validity timing thresholds +const unsigned long VOTE_TIME_TOLERANCE = 1 * 60 * 1000; // 1 minute + +// track vote outcome +#define LED_PIN LED_BUILTIN +#define LED1_PIN 2 // Blue +#define LED2_PIN 33 // Green +bool waterIsDrinkable = true; // true means LED off, false means LED on + +// Classify gape state +enum MusselGapState { + Closed, + Open +}; + +// Data structures +struct Vote { + unsigned long timestamp; + int measure; +}; + +struct Voter { + String id; // Mussel ID + Vote votes[BALLOT_MAX]; // Last 5 sensor readings + int voteCount = 0; // Number of readings stored +}; + +// Global array of mussel voters +Voter voters[VOTER_MAX]; +int voterCount = 0; + +// pointer to control Bluetooth networking +BLEScan *pBLEScan; + +/// Find index of mussel ID in the voters array +int findVoterIndex(const String& id) { + for (int i = 0; i < voterCount; i++) { + if (voters[i].id == id) return i; + } + return -1; // Not found +} + +/// Add or update data for a mussel voting ballot +void collectBallotData( + const String& id, unsigned long timestamp, int gape_measure +) { + int index = findVoterIndex(id); + + // If mussel not found, add new + if (index == -1) { + if (voterCount >= VOTER_MAX) { + log_i("Ignored: Max mussel limit reached (%s)", + id.c_str()); + return; + } + voters[voterCount].id = id; + voters[voterCount].voteCount = 0; + index = voterCount++; + } + + Voter &voter = voters[index]; + + // Maintain a fixed number of stored ballots (FIFO logic) + if (voter.voteCount >= BALLOT_MAX) { + for (int i = 1; i < BALLOT_MAX; i++) { + voter.votes[i - 1] = voter.votes[i]; + } + voter.voteCount = BALLOT_MAX - 1; + } + + // Store the new draft ballot at the end + voter.votes[voter.voteCount++] = {timestamp, gape_measure}; + log_i("Vote stored: Time: %lu, Mussel: %s, Gape: %d", + timestamp, id.c_str(), gape_measure); +} + +/// Clean outdated voting ballots and mussels with no valid ballots +void cleanOldBallotData() { + unsigned long now = millis(); + + for (int i = 0; i < voterCount; ) { + Voter &voter = voters[i]; + int newCount = 0; + + // Shift valid ballots to the front + for (int j = 0; j < voter.voteCount; j++) { + unsigned long age = now - voter.votes[j].timestamp; + + if (age < VOTE_TIME_TOLERANCE) { + voter.votes[newCount++] = voter.votes[j]; + } else { + log_i("Dropped old ballot for Mussel %s (age: %lums)", + voter.id.c_str(), age); + } + } + + voter.voteCount = newCount; + + // If all ballots are dropped, remove the mussel + if (voter.voteCount == 0) { + log_i("Removing Mussel %s - No valid ballots left", + voter.id.c_str()); + + for (int k = i; k < voterCount - 1; k++) { + voters[k] = voters[k + 1]; + } + voterCount--; + continue; // Do not increment i since the list shifted + } + + i++; // Only increment if we didn't remove current voter + } +} + +/// Classify mussel state based on topmost vote +void alignVotes() { + for (int i = 0; i < voterCount; i++) { + Voter &voter = voters[i]; + + // Skip mussels with no data + if (voter.voteCount == 0) { + log_i("Mussel ID: %s - No data", + voter.id.c_str()); + continue; + } + + // Use latest vote to determine state + Vote latest = voter.votes[voter.voteCount - 1]; + String state = (latest.measure >= 0 && latest.measure < 40) + ? "Closed" + : (latest.measure >= 40 && latest.measure <= 90) + ? "Open" + : "Invalid reading"; + + log_i("Mussel ID: %s | Latest Gape: %d | State: %s", + voter.id.c_str(), latest.measure, state.c_str()); + } +} + +/// Decide whether a voting ballot is valid based on age +const char* qualifyBallot( + unsigned long voteTimestamp, unsigned long now +) { + unsigned long age = now - voteTimestamp; + + if (age <= VOTE_TIME_TOLERANCE) { + log_i("VALID: Ballot is within 1 minute (age: %lums)", + age); + return "valid"; + } else { + log_i("INVALID: Ballot is older than 1 minute (age: %lums)", + age); + return "invalid"; + } +} + +/// Resolve the outcome of the voting procedure +void concludeVote() { + int openVotes = 0; + int totalValidVotes = 0; + + // First, align votes and qualify them + for (int i = 0; i < voterCount; i++) { + Voter &voter = voters[i]; + + // Skip mussels with no data + if (voter.voteCount == 0) continue; + + // Use latest vote to determine state + Vote latest = voter.votes[voter.voteCount - 1]; + unsigned long now = millis(); + + // Check if the vote is valid using the qualify function + const char* validity = qualifyBallot(latest.timestamp, now); + + if (strcmp(validity, "valid") == 0) { + totalValidVotes++; // Count valid votes + + // Align and classify mussel state + String state = (latest.measure >= 0 && latest.measure < 40) + ? "Closed" + : (latest.measure >= 40 && latest.measure <= 90) + ? "Open" + : "Invalid reading"; + + // Count valid "Open" votes + if (state == "Open") { + openVotes++; + } + } + } + + // Determine the threshold (half of total valid votes, rounded down) + int threshold = totalValidVotes / 2; + waterIsDrinkable = (openVotes >= threshold); + + // Same or more "Open" votes than the threshold means water is ok + if (openVotes >= threshold) { + log_i("YES, water is drinkable (open: %d, valid: %d)", + openVotes, totalValidVotes); + } else { + log_i("NO, water is not drinkable (open: %d, valid: %d)", + openVotes, totalValidVotes); + } +} + +// Bluetooth beacon discovery callbacks +class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { + + // decode name and temperature from Eddystone TLM advertisement + void onResult(BLEAdvertisedDevice advertisedDevice) { + if (advertisedDevice.haveName() + && advertisedDevice.getFrameType() == BLE_EDDYSTONE_TLM_FRAME + ) { + BLEEddystoneTLM EddystoneTLM(&advertisedDevice); + + // misuse error-only log level for plot-friendly output +#if ARDUHAL_LOG_LEVEL == ARDUHAL_LOG_LEVEL_ERROR + String id_mangled = advertisedDevice.getName(); + id_mangled.replace(' ', '_'); + id_mangled.replace(':', '='); + Serial.println(id_mangled + ":" + EddystoneTLM.getTemp()); +#endif + + unsigned long now = millis(); + String musselID = advertisedDevice.getName(); + int gape = EddystoneTLM.getTemp(); + + collectBallotData(musselID, now, gape); + } + } +}; + +void setup() { + // enable logging to serial + Serial.begin(115200); + esp_log_level_set("*", ESP_LOG_DEBUG); + + // start with LED off (HIGH = off on active-low boards) + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, HIGH); + + pinMode(LED1_PIN, OUTPUT); + pinMode(LED2_PIN, OUTPUT); + digitalWrite(LED1_PIN, LOW); + digitalWrite(LED2_PIN, LOW); + + // setup Bluetooth + BLEDevice::init(""); + pBLEScan = BLEDevice::getScan(); + pBLEScan->setAdvertisedDeviceCallbacks( + new MyAdvertisedDeviceCallbacks()); + pBLEScan->setActiveScan(true); + pBLEScan->setInterval(SCAN_INTERVAL); + pBLEScan->setWindow(SCAN_WINDOW); +} + +void loop() { + + pBLEScan->start(SCAN_TIME_SEC, false); + pBLEScan->clearResults(); + + alignVotes(); + concludeVote(); + cleanOldBallotData(); // Tidy the voter stack + + digitalWrite(LED_PIN, waterIsDrinkable ? LOW : HIGH); + + // LED Logic: GREEN if drinkable, RED if not + if (waterIsDrinkable) { + digitalWrite(LED2_PIN, HIGH); // GREEN ON + digitalWrite(LED1_PIN, LOW); // RED OFF + Serial.println("Water is DRINKABLE - GREEN LED ON"); + } else { + digitalWrite(LED2_PIN, LOW); // GREEN OFF + digitalWrite(LED1_PIN, HIGH); // RED ON + Serial.println("Water is NOT DRINKABLE - RED LED ON"); + } + + delay(500); +} diff --git a/Arduino/vote/vote.puml b/Arduino/vote/vote.puml new file mode 100644 index 0000000..a4958f7 --- /dev/null +++ b/Arduino/vote/vote.puml @@ -0,0 +1,27 @@ +@startuml +:instantiate mussel object; +:instantiate bluetooth object; +group init +:setup mussel voting; +:setup bluetooth scanner; +end group +split +while (each beacon detected) +group "bluetooth callback" { +:collect beacon data; +end group +endwhile +-[hidden]-> +kill +split again +while (each 500ms) +group loop { +:allign beacon data as ballots; +:qualify ballots for a vote; +:conclude vote result; +:act on vote result; +end group +endwhile +-[hidden]-> +kill +@enduml diff --git a/Makefile b/Makefile deleted file mode 100644 index 95eb718..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -ZIP_DIRS = Mussel - -include _make/*.mk diff --git a/Mussel.zip b/Mussel.zip deleted file mode 100644 index 6493596..0000000 Binary files a/Mussel.zip and /dev/null differ diff --git a/Mussel/Mussel.cpp b/Mussel/Mussel.cpp deleted file mode 100644 index a05f329..0000000 --- a/Mussel/Mussel.cpp +++ /dev/null @@ -1,290 +0,0 @@ -// 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 - -/// Mussel - a small library for Arduino to emulate a mussel biosensor -/// -/// * v0.0.2 -/// * rewrite attitude #2 to also handle button press -/// * add voting functions and example -/// -/// * v0.0.1 -/// * initial release to radicle -/// -/// @version 0.0.2 -/// @see -/// @see - -#include "Mussel.h" -#include "Arduino.h" - -/// Main constructor -/// -/// @param attitude behavioral profile as integer -Mussel::Mussel(int attitude) { - _attitude = attitude; -} - -/// Constructor for attitudes using an input pin -/// -/// @param attitude behavioral profile as integer -/// @param pin Used pin as uint8_t -Mussel::Mussel(int attitude, uint8_t pin) { - _attitude = attitude; - _pin = pin; -} - -/// Constructor for attitudes using an input pin and a sensor type -/// -/// @param attitude behavioral profile as integer -/// @param pin Used pin as uint8_t -/// @param type type of sensor as uint8_t -Mussel::Mussel(int attitude, uint8_t pin, uint8_t type) -#ifdef DHT_H - : mussel_dht(pin, type) -#endif -{ - _attitude = attitude; - _pin = pin; -} - -/// Setup function -void Mussel::begin() { - switch(_attitude) { - case 1: - _boolState = HIGH; - - // reset timer - _time = millis(); - - // use INPUT_PULLDOWN to signal when the button is held down - // (not PULLUP which signals when it is released) - pinMode(_pin, INPUT_PULLDOWN); - break; -#ifdef DHT_H - case 3: - mussel_dht.begin(); - break; -#endif - case 4: - // use INPUT_PULLDOWN to signal when the button is held down - // (not PULLUP which signals when it is released) - pinMode(_pin, INPUT_PULLDOWN); - break; - case 5: - _boolState = HIGH; - _count = 0; - _time = 0; - - // Enable internal pull-up resistor for button - pinMode(_pin, INPUT_PULLUP); - break; - case 6: - _boolState = HIGH; - break; - } -} - -/// Description of mussel -/// -/// @return name and attitude of mussel as String -String Mussel::desc() { - String _str; - - switch(_attitude) { - case 1: - _str = "closed 10 seconds every 50 seconds and on button push"; - break; - case 2: - _str = "closed 4 seconds every 8 seconds"; - break; - case 3: - _str = "closed when cold"; - break; - case 4: - _str = "closed when button is pushed"; - break; - case 5: - _str = "changes state when button is pushed"; - break; - case 6: - _str = "changes state on ON/OFF command"; - break; - case 10: - _str = "handles voting"; - break; - default: - _str = "undefined [" + static_cast(_attitude) + "]"; - break; - } - - return _str; -} - -/// Sensor reading -/// -/// * Values 0-99 is a measured relative mussel gape size -/// * Values 100-254 are reserved for future use -/// * Value 255 is an internal error -/// -/// @return relative gape size as value 0-255 encoded as int -int Mussel::read() { - int _read; - - switch(_attitude) { - case 1: - if (digitalRead(_pin) - // TODO: account for rollover - or (millis() - _time) > MUSSEL_NORMAL_PACE * 1000 - ) { - _read = 2; - _boolState = HIGH; - - // reset timer - _time = millis(); - } else if (_boolState == HIGH - and (millis() - _time) > MUSSEL_STRESS_PACE * 1000 - ) { - _read = 42; - _boolState = LOW; - - // reset timer - _time = millis(); - } - break; - case 2: - // 42 if current second modulo 12 is below 9, else 2 - _read = (static_cast(millis() / 1000) % 12) < 9 - ? 42 - : 2; - break; - case 3: -#ifdef DHT_H - // temperature in Celsius - _read = mussel_dht.readTemperature(); -#else - _read = 255; -#endif - break; - case 4: - // 2 if button is pressed, else 42 - _read = digitalRead(_pin) == HIGH - ? 2 - : 42; - break; - case 5: { - bool _reading = digitalRead(_pin); // Read button state - - // Debounce logic: - // Ensures a single press isn't detected multiple times - if (_reading != _boolState) { - _time = millis(); // Reset debounce timer - } - - if ((millis() - _time) > MUSSEL_DEBOUNCE_DELAY) { - // Check for button press (transition from HIGH to LOW) - if (_reading == LOW && _boolState == HIGH) { - _count++; // Increment click count - if (_count > 3) { - _count = 0; // Reset cycle after 3 clicks - } - switch (_count) { - case 1: _read = 2; break; // State: Angry - case 2: _read = 42; break; // State: Happy - case 3: _read = 15; break; // State: Unsure - case 0: _read = 99; break; // State: Off - } - } - } - - _boolState = _reading; // Update button state - break; - } - case 6: - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - - if (command.equalsIgnoreCase("ON")) { - _boolState = HIGH; - } - else if (command.equalsIgnoreCase("OFF")) { - _boolState = LOW; - } - } - _read = _boolState == HIGH - ? 42 - : 2; - break; - default: - _read = 255; - break; - } - - return _read; -} - -/// Function to push data onto the stack -bool Mussel::push(String id, unsigned long timestamp, int measure) { - - // Check if stack is full - if (top >= STACK_SIZE - 1) { - Serial.println("Stack Full"); - - // Return false if stack is full - return false; - } - top++; - idStack[top] = id; - timeStack[top] = timestamp; - measureStack[top] = measure; - - // Return true on successful push - return true; -} - -/// Function to print stack contents -void Mussel::printStack() { - for (int i = top; i >= 0; i--) { - Serial.print("ID: "); Serial.print(idStack[i]); - Serial.print(", Time: "); Serial.print(timeStack[i]); - Serial.print(", measure: "); Serial.println(measureStack[i]); - } -} - -bool qualifyVote(Vote vote, unsigned long currentTime) { - - // If the measure is 42 (YES), check timestamp validity - if (vote.measure == 42) { - // If the vote's timestamp is within 1 minute, count it as YES - if (currentTime - vote.timestamp <= MUSSEL_VOTE_TIME_AHEAD) { - return true; - } - // If the vote's timestamp is older than 2 minutes, count it as NO - else if (currentTime - vote.timestamp > MUSSEL_VOTE_TIME_BEHIND) { - return false; - } - } - - // If the measure is 2, always count the vote as NO - if (vote.measure == 2) { - return false; - } - - // Default case: vote is invalid if no conditions are met - return false; -} - -/// Dump internal variables, formatted for use with Serial Plotter -/// -/// @return internal variables as String -String Mussel::debug() { - return static_cast( - "pin:") + _pin - + "\tboolState:" + _boolState - + "\ttime:" + _time - + "\tcount:" + _count; -} diff --git a/Mussel/Mussel.h b/Mussel/Mussel.h deleted file mode 100644 index ba063c2..0000000 --- a/Mussel/Mussel.h +++ /dev/null @@ -1,78 +0,0 @@ -// 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 - -/// Mussel - a small library for Arduino to emulate a mussel biosensor -/// -/// @version 0.0.2 -/// @see -/// @see - -#ifndef Mussel_h -#define Mussel_h -#include "Arduino.h" - -// seconds -#define MUSSEL_NORMAL_PACE 50U -#define MUSSEL_STRESS_PACE 10U -#define MUSSEL_VOTE_TIME_AHEAD 60000U // 1 minute -#define MUSSEL_VOTE_TIME_BEHIND 120000U // 2 minutes - -// milliseconds -#define MUSSEL_DEBOUNCE_DELAY 50U - -// Limited size NOW!! can be transformed into infinite array -#define STACK_SIZE 1000 - -struct Vote { - String id; - unsigned long timestamp; - int measure; -}; - -class Mussel { - public: - - // Default constructor - Mussel(const int attitude); - - // Constructor for attitudes using an input pin - Mussel(const int attitude, const uint8_t pin); - - // Constructor for attitudes using an input pin and a sensor type - Mussel( - const int attitude, - const uint8_t pin, - const uint8_t type); - - void begin(); - String desc(); - int read(); - String debug(); - bool push(String id, unsigned long timestamp, int measure); - void printStack(); - bool qualifyVote(Vote vote, unsigned long currentTime); - - private: - int _attitude; - int _pin; - bool _boolState; - byte _count; - unsigned long _time; -#ifdef DHT_H - DHT mussel_dht; -#endif - - // Array to store ID strings - String idStack[STACK_SIZE]; - unsigned long timeStack[STACK_SIZE]; - int measureStack[STACK_SIZE]; - - // Index of the top element in the stack, -1 means stack is empty - int top = -1; -}; - -#endif diff --git a/Mussel/Mussel_begin.puml b/Mussel/Mussel_begin.puml deleted file mode 100644 index 17fb9fa..0000000 --- a/Mussel/Mussel_begin.puml +++ /dev/null @@ -1,10 +0,0 @@ -@startuml -start -if (type ?) equals (defined) then -:init sensor driver; -endif -if (pin ?) equals (defined) then -:configure input pin; -endif -stop -@enduml diff --git a/Mussel/Mussel_read.puml b/Mussel/Mussel_read.puml deleted file mode 100644 index e9190bb..0000000 --- a/Mussel/Mussel_read.puml +++ /dev/null @@ -1,24 +0,0 @@ -@startuml -start -switch (attitude ?) -case (1) -://TODO//; -case (2) -:set time to current second; -if (time mod 12 ?) equals (< 9) then -:set gape to "42"; -else (>= 9) -:set gape to "2"; -endif -case (3) -:read temperature sensor; -case (4) -:read temperature sensor; -if (button ?) equals (not pressed) then -:set gape to "42"; -else (pressed) -:set gape to "2"; -endif -endswitch -stop -@enduml diff --git a/Mussel/README.md b/Mussel/README.md deleted file mode 100644 index 994f3a7..0000000 --- a/Mussel/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Mussel - -Mussel is a tiny library to emulate a mussel biosensor. - -## Installation - -Download the ZIP archive from the `Raw` link at -, -then open the Arduino IDE -and choose Sketch > Include Library > Add .ZIP Library... -and select your downloaded file. - -You should now see in File > Examples > Mussel -an entry for the `basic_usage` example. - -## Motivation - -For exploration of various biosensor behaviours, -hooking into various networking setups with various looping constructs, -maintaining the behaviours as a library seemed convenient. - -## Features - -* Super simple API -* Offers multiple behaviours. - -## Requirements - -* An Arduino — http://arduino.cc/ - -## Copyright and licensing - -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 diff --git a/Mussel/examples/button/button.ino b/Mussel/examples/button/button.ino deleted file mode 100644 index 5efae46..0000000 --- a/Mussel/examples/button/button.ino +++ /dev/null @@ -1,20 +0,0 @@ -#include - -// instantiate with attitude #4, -// and pin 4 connected to a button -Mussel mussel(4, 4); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/button_states/button_states.ino b/Mussel/examples/button_states/button_states.ino deleted file mode 100644 index 1d91b41..0000000 --- a/Mussel/examples/button_states/button_states.ino +++ /dev/null @@ -1,20 +0,0 @@ -#include - -// instantiate with attitude #5, -// and pin 9 connected to a button -Mussel mussel(5, 9); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/command/command.ino b/Mussel/examples/command/command.ino deleted file mode 100644 index f359e00..0000000 --- a/Mussel/examples/command/command.ino +++ /dev/null @@ -1,19 +0,0 @@ -#include - -// instantiate with attitude #6 -Mussel mussel(6); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/minute/minute.ino b/Mussel/examples/minute/minute.ino deleted file mode 100644 index 4bf59ce..0000000 --- a/Mussel/examples/minute/minute.ino +++ /dev/null @@ -1,20 +0,0 @@ -#include - -// instantiate with attitude #2, -// and pin 4 connected to a button -Mussel mussel(2, 4); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/seconds/seconds.ino b/Mussel/examples/seconds/seconds.ino deleted file mode 100644 index 928537c..0000000 --- a/Mussel/examples/seconds/seconds.ino +++ /dev/null @@ -1,19 +0,0 @@ -#include - -// instantiate with attitude #2 -Mussel mussel(2); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/seconds/seconds.puml b/Mussel/examples/seconds/seconds.puml deleted file mode 100644 index 55746ff..0000000 --- a/Mussel/examples/seconds/seconds.puml +++ /dev/null @@ -1,15 +0,0 @@ -@startuml -'start -:instantiate object; -group init -:activate object; -:fetch and print object description; -end group -while (each 1s) -group loop { -:fetch and print gape angle; -end group -endwhile --[hidden]-> -kill -@enduml diff --git a/Mussel/examples/sensor/Mussel_Sensor_Beacon.md b/Mussel/examples/sensor/Mussel_Sensor_Beacon.md deleted file mode 100644 index e252104..0000000 --- a/Mussel/examples/sensor/Mussel_Sensor_Beacon.md +++ /dev/null @@ -1,7 +0,0 @@ -## Mussel Sensor Beacon - -1. Reads sensors. - -2. Normalises sensor data as a gape angle in the range of 0°-90°. - -3. Broadcasts normalised sensor data as a beacon on a bluetooth network. diff --git a/Mussel/examples/sensor/sensor.ino b/Mussel/examples/sensor/sensor.ino deleted file mode 100644 index 77b443f..0000000 --- a/Mussel/examples/sensor/sensor.ino +++ /dev/null @@ -1,54 +0,0 @@ -/* - EddystoneTLM beacon by BeeGee based on https://github.com/pcbreflux/espressif/blob/master/esp32/arduino/sketchbook/ESP32_Eddystone_TLM_deepsleep/ESP32_Eddystone_TLM_deepsleep.ino - EddystoneTLM frame specification https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md -*/ - -#include "BLEDevice.h" -#include "BLEBeacon.h" -#include "BLEAdvertising.h" -#include "BLEEddystoneTLM.h" - -#include - -#define BEACON_POWER ESP_PWR_LVL_N12 - -// See the following for generating UUIDs: -// https://www.uuidgenerator.net/ -BLEAdvertising *pAdvertising; - -#define BEACON_UUID "8ec76ea3-6668-48da-9866-75be8bc86f4d" // UUID 1 128-Bit (may use linux tool uuidgen or random numbers via https://www.uuidgenerator.net/) - -Mussel mussel(2); - -// Check -// https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md -// and http://www.hugi.scene.org/online/coding/hugi%2015%20-%20cmtadfix.htm -// for the temperature value. It is a 8.8 fixed-point notation -void setBeacon() { - BLEEddystoneTLM EddystoneTLM; - EddystoneTLM.setTemp(mussel.read()); // 3000 = 30.00 ˚C - Serial.printf("Temperature is %.2f°C\n", EddystoneTLM.getTemp()); - - BLEAdvertisementData oAdvertisementData = BLEAdvertisementData(); - BLEAdvertisementData oScanResponseData = BLEAdvertisementData(); - oScanResponseData.setServiceData(BLEUUID((uint16_t)0xFEAA), String(EddystoneTLM.getData().c_str(), EddystoneTLM.getData().length())); - - oAdvertisementData.setName("ESP32 TLM Beacon"); - pAdvertising->setAdvertisementData(oAdvertisementData); - pAdvertising->setScanResponseData(oScanResponseData); -} - -void setup() { - Serial.begin(115200); - mussel.begin(); - BLEDevice::init("TLMBeacon"); - BLEDevice::setPower(BEACON_POWER); - pAdvertising = BLEDevice::getAdvertising(); - setBeacon(); - pAdvertising->start(); -} - -void loop() { - setBeacon(); - delay(500); -} diff --git a/Mussel/examples/sensor/sensor.puml b/Mussel/examples/sensor/sensor.puml deleted file mode 100644 index 3f033d1..0000000 --- a/Mussel/examples/sensor/sensor.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -'start -:instantiate mussel object; -:instantiate bluetooth object; -group init -:setup mussel sensors; -:setup bluetooth beacon; -end group -split -while (each 500ms) -group loop { -:read sensors; -:normalize sensor data -as a gape angle; -:add gape angle to beacon; -end group -endwhile --[hidden]-> -kill -split again -while (each 100ms) -:broadcast beacon; -endwhile --[hidden]-> -kill -@enduml diff --git a/Mussel/examples/temperature/temperature.ino b/Mussel/examples/temperature/temperature.ino deleted file mode 100644 index 6b4ca08..0000000 --- a/Mussel/examples/temperature/temperature.ino +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include - -// instantiate with attitude #3, -// and PIN 15 connected to an external sensor of type DHT11 -Mussel mussel(3, 15, DHT11); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - Serial.printf("gap:%d\n", - mussel.read()); - - delay(1000); -} diff --git a/Mussel/examples/vote/Mussel_Beacon_Voting.md b/Mussel/examples/vote/Mussel_Beacon_Voting.md deleted file mode 100644 index 33a0893..0000000 --- a/Mussel/examples/vote/Mussel_Beacon_Voting.md +++ /dev/null @@ -1,20 +0,0 @@ -## Mussel Beacon Voting - -1. Scans bluetooth network for beacons. - -2. Collects mussel name and gape angle -as decoded from each detected beacon, -together with the time of detection in milliseconds since boot. - -3. Aligns the collected data -to the format of ballots for a water quality vote. - -4. Qualifies the ballots for criteria of the water quality vote -(e.g. timeliness and sanity of gape angles). - -5. Concludes a vote based on collected, aligned and qualified ballots. - -6. Acts on the voting result, -e.g. turns on a steady light for "code green" -or a blinking light for "code yellow", -or turns on a blinking light and shuts off a valve for "code red". diff --git a/Mussel/examples/vote/vote.ino b/Mussel/examples/vote/vote.ino deleted file mode 100644 index ec9d185..0000000 --- a/Mussel/examples/vote/vote.ino +++ /dev/null @@ -1,56 +0,0 @@ -/* - Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp - Ported to Arduino ESP32 by Evandro Copercini - Changed to a beacon scanner to report iBeacon, EddystoneURL and EddystoneTLM beacons by beegee-tokyo - Upgraded Eddystone part by Tomas Pilny on Feb 20, 2023 -*/ - -#include - -#include -#include -#include -#include -#include - -#include - -int scanTime = 1; //In seconds -BLEScan *pBLEScan; - -Mussel mussel(10); - -class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { - void onResult(BLEAdvertisedDevice advertisedDevice) { - if (advertisedDevice.haveName() - && advertisedDevice.getFrameType() == BLE_EDDYSTONE_TLM_FRAME - ) { - BLEEddystoneTLM EddystoneTLM(&advertisedDevice); - mussel.push( - advertisedDevice.getName(), - millis(), - EddystoneTLM.getTemp() - ); - } - } -}; - -void setup() { - Serial.begin(115200); - mussel.begin(); - - BLEDevice::init(""); - pBLEScan = BLEDevice::getScan(); //create new scan - pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); - pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster - pBLEScan->setInterval(100); - pBLEScan->setWindow(99); // less or equal setInterval value -} - -void loop() { - // put your main code here, to run repeatedly: - BLEScanResults *foundDevices = pBLEScan->start(scanTime, false); - pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory - mussel.printStack(); - delay(500); -} diff --git a/Mussel/examples/vote/vote.puml b/Mussel/examples/vote/vote.puml deleted file mode 100644 index a4958f7..0000000 --- a/Mussel/examples/vote/vote.puml +++ /dev/null @@ -1,27 +0,0 @@ -@startuml -:instantiate mussel object; -:instantiate bluetooth object; -group init -:setup mussel voting; -:setup bluetooth scanner; -end group -split -while (each beacon detected) -group "bluetooth callback" { -:collect beacon data; -end group -endwhile --[hidden]-> -kill -split again -while (each 500ms) -group loop { -:allign beacon data as ballots; -:qualify ballots for a vote; -:conclude vote result; -:act on vote result; -end group -endwhile --[hidden]-> -kill -@enduml diff --git a/Mussel/examples/voting/voting.ino b/Mussel/examples/voting/voting.ino deleted file mode 100644 index db4364d..0000000 --- a/Mussel/examples/voting/voting.ino +++ /dev/null @@ -1,23 +0,0 @@ -#include - -// instantiate for vote handling -Mussel mussel(10); - -void setup() { - Serial.begin(115200); - mussel.begin(); - - Serial.printf("\n\nDevice ready: %s\n", - mussel.desc().c_str()); -} - -void loop() { - - // Inject sample data - mussel.push("A1", millis(), random(90)); - mussel.push("B2", millis(), random(90)); - mussel.push("C3", millis(), random(90)); - mussel.printStack(); - - delay(1000); -} diff --git a/Mussel/examples/voting/voting.puml b/Mussel/examples/voting/voting.puml deleted file mode 100644 index 8460d90..0000000 --- a/Mussel/examples/voting/voting.puml +++ /dev/null @@ -1,15 +0,0 @@ -@startuml -'start -:instantiate object; -group init -:activate object; -end group -while (each ½s) -group loop { -:register 3 fake votes; -:fetch and print voting tally; -end group -endwhile --[hidden]-> -kill -@enduml diff --git a/Mussel/keywords.txt b/Mussel/keywords.txt deleted file mode 100644 index 4f3eaf1..0000000 --- a/Mussel/keywords.txt +++ /dev/null @@ -1,17 +0,0 @@ -####################################### -# Syntax Coloring Map for Mussel library -####################################### - -####################################### -# Datatypes (KEYWORD1) -####################################### - -Mussel KEYWORD1 - -####################################### -# Methods and Functions (KEYWORD2) -####################################### - -read KEYWORD2 -begin KEYWORD2 -desc KEYWORD2 diff --git a/Mussel/library.properties b/Mussel/library.properties deleted file mode 100644 index 604ef3b..0000000 --- a/Mussel/library.properties +++ /dev/null @@ -1,10 +0,0 @@ -name=Mussel -version=0.0.2 -author= Amal Mazrah , Jonas Smedegaard , Mennatullah Hatim Kassim , Noor Ahmad and Tanishka Suwalka -maintainer=Jonas Smedegaard -sentence=Arduino library to emulate a mussel biosensor. -paragraph=Arduino library to to emulate a mussel biosensor. -category=Sensors -url=https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z2tFBF4gN7ziG9oXtUytVQNYe3VhQ -architectures=* -includes=Mussel.h diff --git a/_make/_zip.mk b/_make/_zip.mk deleted file mode 100644 index b610802..0000000 --- a/_make/_zip.mk +++ /dev/null @@ -1,22 +0,0 @@ -# Make snippet for generating/updating zip archive of a directory -# -# Copyright 2025, Jonas Smedegaard -# SPDX-License-Identifier: GPL-3+ -# -# Setup: -# In main Makefile... -# * set variable ZIP_DIRS -# * include this make snippet -# -# Dependencies: -# * zip - -# list of relative paths to PDF documents -#ZIP_DIRS = \ -# subdir \ -# deep/subdir - -$(ZIP_DIRS:%=zip-of-%): zip-of-%: - zip -r $*.zip $* - -.PHONY: $(ZIP_DIRS:%=zip-of-%) diff --git a/report.qmd b/report.qmd index 461a87c..efebaff 100644 --- a/report.qmd +++ b/report.qmd @@ -286,7 +286,7 @@ but it is unclear with current phrasing what that is. ## Sensor system ```{.plantuml} -!include sensor/sensor.puml +!include Arduino/sensor/sensor.puml ``` ## Voting system @@ -298,7 +298,7 @@ It sends signals out to say "im here, i exist". Then Boom! a bluetooth connection is made. ```{.plantuml} -!include vote/vote.puml +!include Arduino/vote/vote.puml ``` *TODO* @@ -357,25 +357,19 @@ about consuming a lot of space and resources* \appendix -# Mussel library {.appendix} +# Code -## Headers +## P5.js sketch `light.js` {.appendix} -```{.c include="Mussel/Mussel.h" code-line-numbers="true"} -``` - -## Source code - -```{.c include="Mussel/Mussel.cpp" code-line-numbers="true"} +```{.js include="P5/light.js" code-line-numbers="true"} ``` -## Example sketch +## Arduino sketch `sensor.js` {.appendix} -```{.c include="Mussel/examples/seconds/seconds.ino" code-line-numbers="true"} +```{.cpp include="Arduino/sensor/sensor.ino" code-line-numbers="true"} ``` +## Arduino sketch `vote.js` {.appendix} -# P5.js sketch `light.js` {.appendix} - -```{.js include="P5/light.js" code-line-numbers="true"} +```{.cpp include="Arduino/vote/vote.ino" code-line-numbers="true"} ``` diff --git a/sensor/Mussel_Sensor_Beacon.md b/sensor/Mussel_Sensor_Beacon.md deleted file mode 100644 index 89beeec..0000000 --- a/sensor/Mussel_Sensor_Beacon.md +++ /dev/null @@ -1,35 +0,0 @@ -# Mussel Sensor Beacon - -1. Reads sensors. - -2. Normalises sensor data as a gape angle in the range of 0°-90°. - -3. Broadcasts normalised sensor data as a beacon on a bluetooth network. - -## Scanner apps - -These Android apps have been found usable -for monitoring this type of sensor: - - * FeasyBeacon on Play store - * NanoBeacon BLE Scanner on Play store - * decodes Eddystone TLM temperature as Fahrenheit - * does not decode Eddystone URL - * SemBeacon on Play store - * detects URL-only but not TLM-only Eddystone Beacon - * too aggressive caching misses changing data - * UI optimized for SemBeacon - * nRF Connect for Mobile on Play store - * misses changing data - * UI not specific to beacons - -These Android apps are potentially interesting as well: - - * Beacon Locator on F-droid - * unreliable detection - * active development - * BLE Radar on F-droid - * detects URL-only but not TLM-only Eddystone Beacon - * UI not specific to beacons - * AltBeacon Loc on Play store - * detects URL-only but not TLM-only Eddystone Beacon diff --git a/sensor/sensor.ino b/sensor/sensor.ino deleted file mode 100644 index 8d43b22..0000000 --- a/sensor/sensor.ino +++ /dev/null @@ -1,215 +0,0 @@ -// 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); -} diff --git a/sensor/sensor.puml b/sensor/sensor.puml deleted file mode 100644 index 3f033d1..0000000 --- a/sensor/sensor.puml +++ /dev/null @@ -1,26 +0,0 @@ -@startuml -'start -:instantiate mussel object; -:instantiate bluetooth object; -group init -:setup mussel sensors; -:setup bluetooth beacon; -end group -split -while (each 500ms) -group loop { -:read sensors; -:normalize sensor data -as a gape angle; -:add gape angle to beacon; -end group -endwhile --[hidden]-> -kill -split again -while (each 100ms) -:broadcast beacon; -endwhile --[hidden]-> -kill -@enduml diff --git a/vote/Mussel_Beacon_Voting.md b/vote/Mussel_Beacon_Voting.md deleted file mode 100644 index 33a0893..0000000 --- a/vote/Mussel_Beacon_Voting.md +++ /dev/null @@ -1,20 +0,0 @@ -## Mussel Beacon Voting - -1. Scans bluetooth network for beacons. - -2. Collects mussel name and gape angle -as decoded from each detected beacon, -together with the time of detection in milliseconds since boot. - -3. Aligns the collected data -to the format of ballots for a water quality vote. - -4. Qualifies the ballots for criteria of the water quality vote -(e.g. timeliness and sanity of gape angles). - -5. Concludes a vote based on collected, aligned and qualified ballots. - -6. Acts on the voting result, -e.g. turns on a steady light for "code green" -or a blinking light for "code yellow", -or turns on a blinking light and shuts off a valve for "code red". diff --git a/vote/vote.ino b/vote/vote.ino deleted file mode 100644 index 3d4b141..0000000 --- a/vote/vote.ino +++ /dev/null @@ -1,319 +0,0 @@ -// 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 - -/// Mussel vote - an Arduino sketch to monitor mussel biosensors -/// -/// @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 -#include -#include -#include -#include -#define SCAN_INTERVAL 100 -#define SCAN_WINDOW 99 -#define SCAN_TIME_SEC 1 - -// stack sizes for voters and ballots-per-voter -#define VOTER_MAX 10 -#define BALLOT_MAX 5 - -// Validity timing thresholds -const unsigned long VOTE_TIME_TOLERANCE = 1 * 60 * 1000; // 1 minute - -// track vote outcome -#define LED_PIN LED_BUILTIN -#define LED1_PIN 2 // Blue -#define LED2_PIN 33 // Green -bool waterIsDrinkable = true; // true means LED off, false means LED on - -// Classify gape state -enum MusselGapState { - Closed, - Open -}; - -// Data structures -struct Vote { - unsigned long timestamp; - int measure; -}; - -struct Voter { - String id; // Mussel ID - Vote votes[BALLOT_MAX]; // Last 5 sensor readings - int voteCount = 0; // Number of readings stored -}; - -// Global array of mussel voters -Voter voters[VOTER_MAX]; -int voterCount = 0; - -// pointer to control Bluetooth networking -BLEScan *pBLEScan; - -/// Find index of mussel ID in the voters array -int findVoterIndex(const String& id) { - for (int i = 0; i < voterCount; i++) { - if (voters[i].id == id) return i; - } - return -1; // Not found -} - -/// Add or update data for a mussel voting ballot -void collectBallotData( - const String& id, unsigned long timestamp, int gape_measure -) { - int index = findVoterIndex(id); - - // If mussel not found, add new - if (index == -1) { - if (voterCount >= VOTER_MAX) { - log_i("Ignored: Max mussel limit reached (%s)", - id.c_str()); - return; - } - voters[voterCount].id = id; - voters[voterCount].voteCount = 0; - index = voterCount++; - } - - Voter &voter = voters[index]; - - // Maintain a fixed number of stored ballots (FIFO logic) - if (voter.voteCount >= BALLOT_MAX) { - for (int i = 1; i < BALLOT_MAX; i++) { - voter.votes[i - 1] = voter.votes[i]; - } - voter.voteCount = BALLOT_MAX - 1; - } - - // Store the new draft ballot at the end - voter.votes[voter.voteCount++] = {timestamp, gape_measure}; - log_i("Vote stored: Time: %lu, Mussel: %s, Gape: %d", - timestamp, id.c_str(), gape_measure); -} - -/// Clean outdated voting ballots and mussels with no valid ballots -void cleanOldBallotData() { - unsigned long now = millis(); - - for (int i = 0; i < voterCount; ) { - Voter &voter = voters[i]; - int newCount = 0; - - // Shift valid ballots to the front - for (int j = 0; j < voter.voteCount; j++) { - unsigned long age = now - voter.votes[j].timestamp; - - if (age < VOTE_TIME_TOLERANCE) { - voter.votes[newCount++] = voter.votes[j]; - } else { - log_i("Dropped old ballot for Mussel %s (age: %lums)", - voter.id.c_str(), age); - } - } - - voter.voteCount = newCount; - - // If all ballots are dropped, remove the mussel - if (voter.voteCount == 0) { - log_i("Removing Mussel %s - No valid ballots left", - voter.id.c_str()); - - for (int k = i; k < voterCount - 1; k++) { - voters[k] = voters[k + 1]; - } - voterCount--; - continue; // Do not increment i since the list shifted - } - - i++; // Only increment if we didn't remove current voter - } -} - -/// Classify mussel state based on topmost vote -void alignVotes() { - for (int i = 0; i < voterCount; i++) { - Voter &voter = voters[i]; - - // Skip mussels with no data - if (voter.voteCount == 0) { - log_i("Mussel ID: %s - No data", - voter.id.c_str()); - continue; - } - - // Use latest vote to determine state - Vote latest = voter.votes[voter.voteCount - 1]; - String state = (latest.measure >= 0 && latest.measure < 40) - ? "Closed" - : (latest.measure >= 40 && latest.measure <= 90) - ? "Open" - : "Invalid reading"; - - log_i("Mussel ID: %s | Latest Gape: %d | State: %s", - voter.id.c_str(), latest.measure, state.c_str()); - } -} - -/// Decide whether a voting ballot is valid based on age -const char* qualifyBallot( - unsigned long voteTimestamp, unsigned long now -) { - unsigned long age = now - voteTimestamp; - - if (age <= VOTE_TIME_TOLERANCE) { - log_i("VALID: Ballot is within 1 minute (age: %lums)", - age); - return "valid"; - } else { - log_i("INVALID: Ballot is older than 1 minute (age: %lums)", - age); - return "invalid"; - } -} - -/// Resolve the outcome of the voting procedure -void concludeVote() { - int openVotes = 0; - int totalValidVotes = 0; - - // First, align votes and qualify them - for (int i = 0; i < voterCount; i++) { - Voter &voter = voters[i]; - - // Skip mussels with no data - if (voter.voteCount == 0) continue; - - // Use latest vote to determine state - Vote latest = voter.votes[voter.voteCount - 1]; - unsigned long now = millis(); - - // Check if the vote is valid using the qualify function - const char* validity = qualifyBallot(latest.timestamp, now); - - if (strcmp(validity, "valid") == 0) { - totalValidVotes++; // Count valid votes - - // Align and classify mussel state - String state = (latest.measure >= 0 && latest.measure < 40) - ? "Closed" - : (latest.measure >= 40 && latest.measure <= 90) - ? "Open" - : "Invalid reading"; - - // Count valid "Open" votes - if (state == "Open") { - openVotes++; - } - } - } - - // Determine the threshold (half of total valid votes, rounded down) - int threshold = totalValidVotes / 2; - waterIsDrinkable = (openVotes >= threshold); - - // Same or more "Open" votes than the threshold means water is ok - if (openVotes >= threshold) { - log_i("YES, water is drinkable (open: %d, valid: %d)", - openVotes, totalValidVotes); - } else { - log_i("NO, water is not drinkable (open: %d, valid: %d)", - openVotes, totalValidVotes); - } -} - -// Bluetooth beacon discovery callbacks -class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { - - // decode name and temperature from Eddystone TLM advertisement - void onResult(BLEAdvertisedDevice advertisedDevice) { - if (advertisedDevice.haveName() - && advertisedDevice.getFrameType() == BLE_EDDYSTONE_TLM_FRAME - ) { - BLEEddystoneTLM EddystoneTLM(&advertisedDevice); - - // misuse error-only log level for plot-friendly output -#if ARDUHAL_LOG_LEVEL == ARDUHAL_LOG_LEVEL_ERROR - String id_mangled = advertisedDevice.getName(); - id_mangled.replace(' ', '_'); - id_mangled.replace(':', '='); - Serial.println(id_mangled + ":" + EddystoneTLM.getTemp()); -#endif - - unsigned long now = millis(); - String musselID = advertisedDevice.getName(); - int gape = EddystoneTLM.getTemp(); - - collectBallotData(musselID, now, gape); - } - } -}; - -void setup() { - // enable logging to serial - Serial.begin(115200); - esp_log_level_set("*", ESP_LOG_DEBUG); - - // start with LED off (HIGH = off on active-low boards) - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, HIGH); - - pinMode(LED1_PIN, OUTPUT); - pinMode(LED2_PIN, OUTPUT); - digitalWrite(LED1_PIN, LOW); - digitalWrite(LED2_PIN, LOW); - - // setup Bluetooth - BLEDevice::init(""); - pBLEScan = BLEDevice::getScan(); - pBLEScan->setAdvertisedDeviceCallbacks( - new MyAdvertisedDeviceCallbacks()); - pBLEScan->setActiveScan(true); - pBLEScan->setInterval(SCAN_INTERVAL); - pBLEScan->setWindow(SCAN_WINDOW); -} - -void loop() { - - pBLEScan->start(SCAN_TIME_SEC, false); - pBLEScan->clearResults(); - - alignVotes(); - concludeVote(); - cleanOldBallotData(); // Tidy the voter stack - - digitalWrite(LED_PIN, waterIsDrinkable ? LOW : HIGH); - - // LED Logic: GREEN if drinkable, RED if not - if (waterIsDrinkable) { - digitalWrite(LED2_PIN, HIGH); // GREEN ON - digitalWrite(LED1_PIN, LOW); // RED OFF - Serial.println("Water is DRINKABLE - GREEN LED ON"); - } else { - digitalWrite(LED2_PIN, LOW); // GREEN OFF - digitalWrite(LED1_PIN, HIGH); // RED ON - Serial.println("Water is NOT DRINKABLE - RED LED ON"); - } - - delay(500); -} diff --git a/vote/vote.puml b/vote/vote.puml deleted file mode 100644 index a4958f7..0000000 --- a/vote/vote.puml +++ /dev/null @@ -1,27 +0,0 @@ -@startuml -:instantiate mussel object; -:instantiate bluetooth object; -group init -:setup mussel voting; -:setup bluetooth scanner; -end group -split -while (each beacon detected) -group "bluetooth callback" { -:collect beacon data; -end group -endwhile --[hidden]-> -kill -split again -while (each 500ms) -group loop { -:allign beacon data as ballots; -:qualify ballots for a vote; -:conclude vote result; -:act on vote result; -end group -endwhile --[hidden]-> -kill -@enduml -- cgit v1.2.3