aboutsummaryrefslogtreecommitdiff
path: root/vote/vote.ino
diff options
context:
space:
mode:
authorJonas Smedegaard <dr@jones.dk>2025-04-18 01:24:05 +0200
committerJonas Smedegaard <dr@jones.dk>2025-04-18 01:24:05 +0200
commit6ca4ff06867aa5d7354615c828bd83234d827ff2 (patch)
tree6a42f307f09e05491f8e1eb9bf157e13916dad8d /vote/vote.ino
parentea3988c911b467cbabe38cb9c3e6d0013b53965d (diff)
rop Mussel library; move Arduino sketches below Arduino/
Diffstat (limited to 'vote/vote.ino')
-rw-r--r--vote/vote.ino319
1 files changed, 0 insertions, 319 deletions
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);
-}