diff options
author | Jonas Smedegaard <dr@jones.dk> | 2025-04-18 01:24:05 +0200 |
---|---|---|
committer | Jonas Smedegaard <dr@jones.dk> | 2025-04-18 01:24:05 +0200 |
commit | 6ca4ff06867aa5d7354615c828bd83234d827ff2 (patch) | |
tree | 6a42f307f09e05491f8e1eb9bf157e13916dad8d /vote | |
parent | ea3988c911b467cbabe38cb9c3e6d0013b53965d (diff) |
rop Mussel library; move Arduino sketches below Arduino/
Diffstat (limited to 'vote')
-rw-r--r-- | vote/Mussel_Beacon_Voting.md | 20 | ||||
-rw-r--r-- | vote/vote.ino | 319 | ||||
-rw-r--r-- | vote/vote.puml | 27 |
3 files changed, 0 insertions, 366 deletions
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 <mazrah@ruc.dk> -// SPDX-FileCopyrightText: 2025 Jonas Smedegaard <dr@jones.dk> -// SPDX-FileCopyrightText: 2025 Mennatullah Hatim Kassim <stud-mennatulla@ruc.dk> -// SPDX-FileCopyrightText: 2025 Noor Ahmad <noora@ruc.dk> -// SPDX-FileCopyrightText: 2025 Tanishka Suwalka <tanishkas@ruc.dk> -// SPDX-License-Identifier: GPL-3.0-or-later - -/// Mussel vote - an Arduino sketch to monitor mussel biosensors -/// -/// @version 0.0.3 -/// @see <https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z2tFBF4gN7ziG9oXtUytVQNYe3VhQ> -/// @see <https://moodle.ruc.dk/course/view.php?id=23504> - -// 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 <esp32-hal-log.h> -#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 <BLEScan.h> -#include <BLEAdvertisedDevice.h> -#include <BLEEddystoneTLM.h> -#include <BLEBeacon.h> -#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 |