aboutsummaryrefslogtreecommitdiff
path: root/vote/vote.ino
blob: 13b425a233bafba978a5a1dfd4b0e5c2a9a4870b (plain)
  1. // SPDX-FileCopyrightText: 2025 Amal Mazrah <mazrah@ruc.dk>
  2. // SPDX-FileCopyrightText: 2025 Jonas Smedegaard <dr@jones.dk>
  3. // SPDX-FileCopyrightText: 2025 Mennatullah Hatim Kassim <stud-mennatulla@ruc.dk>
  4. // SPDX-FileCopyrightText: 2025 Noor Ahmad <noora@ruc.dk>
  5. // SPDX-FileCopyrightText: 2025 Tanishka Suwalka <tanishkas@ruc.dk>
  6. // SPDX-License-Identifier: GPL-3.0-or-later
  7. /// Mussel vote - an Arduino sketch to monitor mussel biosensors
  8. ///
  9. /// @version 0.0.3
  10. /// @see <https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z2tFBF4gN7ziG9oXtUytVQNYe3VhQ>
  11. /// @see <https://moodle.ruc.dk/course/view.php?id=23504>
  12. // arduino-esp32 Logging system
  13. // activate in Arduino IDE: Tools -> Core Debug Level
  14. // special: set Core Debug Level to Error for plot-friendly output
  15. #define CONFIG_ARDUHAL_ESP_LOG 1
  16. #define LOG_LOCAL_LEVEL CORE_DEBUG_LEVEL
  17. #include <esp32-hal-log.h>
  18. #undef ARDUHAL_LOG_FORMAT
  19. #define ARDUHAL_LOG_FORMAT(letter, format) \
  20. ARDUHAL_LOG_COLOR_##letter "[" #letter "] %s(): " format \
  21. ARDUHAL_LOG_RESET_COLOR "\r\n", __FUNCTION__
  22. // arduino-esp32 Bluetooth Low Energy (BLE) networking stack
  23. #include <BLEDevice.h>
  24. #include <BLEScan.h>
  25. #include <BLEAdvertisedDevice.h>
  26. #include <BLEEddystoneTLM.h>
  27. #include <BLEBeacon.h>
  28. #define SCAN_INTERVAL 100
  29. #define SCAN_WINDOW 99
  30. #define SCAN_TIME_SEC 1
  31. // stack sizes for voters and ballots-per-voter
  32. #define VOTER_MAX 10
  33. #define BALLOT_MAX 5
  34. // Validity timing thresholds
  35. const unsigned long VOTE_TIME_AHEAD = 1 * 60 * 1000; // 1 minute
  36. const unsigned long VOTE_TIME_BEHIND = 2 * 60 * 1000; // 2 minutes
  37. // Classify gape state
  38. enum MusselGapState {
  39. Closed,
  40. Open
  41. };
  42. // Data structures
  43. struct Vote {
  44. unsigned long timestamp;
  45. int measure;
  46. };
  47. struct Voter {
  48. String id; // Mussel ID
  49. Vote votes[BALLOT_MAX]; // Last 5 sensor readings
  50. int voteCount = 0; // Number of readings stored
  51. };
  52. // Global array of mussel voters
  53. Voter voters[VOTER_MAX];
  54. int voterCount = 0;
  55. // pointer to control Bluetooth networking
  56. BLEScan *pBLEScan;
  57. /// Find index of mussel ID in the voters array
  58. int findVoterIndex(const String& id) {
  59. for (int i = 0; i < voterCount; i++) {
  60. if (voters[i].id == id) return i;
  61. }
  62. return -1; // Not found
  63. }
  64. /// Add or update vote for a mussel ID
  65. void storeVoteForMussel(const String& id, unsigned long timestamp, int gape_measure) {
  66. int index = findVoterIndex(id);
  67. // If mussel not found, add new
  68. if (index == -1) {
  69. if (voterCount >= VOTER_MAX) {
  70. Serial.printf("Ignored: Max mussel limit reached (%s)\n", id.c_str());
  71. return;
  72. }
  73. voters[voterCount].id = id;
  74. voters[voterCount].voteCount = 0;
  75. index = voterCount++;
  76. }
  77. Voter &voter = voters[index];
  78. // Maintain a fixed number of stored votes (FIFO logic)
  79. if (voter.voteCount >= BALLOT_MAX) {
  80. for (int i = 1; i < BALLOT_MAX; i++) {
  81. voter.votes[i - 1] = voter.votes[i];
  82. }
  83. voter.voteCount = BALLOT_MAX - 1;
  84. }
  85. // Store the new vote at the end
  86. voter.votes[voter.voteCount++] = {timestamp, gape_measure};
  87. Serial.printf("Vote stored: Time: %lu, Mussel: %s, Gape: %d\n", timestamp, id.c_str(), gape_measure);
  88. }
  89. /// Classify mussel state based on topmost vote
  90. void alignVotes() {
  91. Serial.println("---- Mussel Alignment Status ----");
  92. for (int i = 0; i < voterCount; i++) {
  93. Voter &voter = voters[i];
  94. // Skip mussels with no data
  95. if (voter.voteCount == 0) {
  96. Serial.printf("Mussel ID: %s - No data\n", voter.id.c_str());
  97. continue;
  98. }
  99. // Use latest vote to determine state
  100. Vote latest = voter.votes[voter.voteCount - 1];
  101. String state = (latest.measure >= 0 && latest.measure < 40) ? "Closed" :
  102. (latest.measure >= 40 && latest.measure <= 90) ? "Open" :
  103. "Invalid reading";
  104. Serial.printf("Mussel ID: %s | Latest Gape: %d | State: %s\n",
  105. voter.id.c_str(), latest.measure, state.c_str());
  106. }
  107. Serial.println("---------------------------------");
  108. }
  109. /// Decide whether a vote is valid based on gape and age
  110. const char* qualifyMusselVote(int gape, unsigned long voteTimestamp, unsigned long now) {
  111. // Determine state based on gape
  112. MusselGapState gapState = gape >= 40 && gape <= 90 ? Open : Closed;
  113. const char* gapStateStr = (gapState == Open) ? "Open" : "Closed";
  114. unsigned long age = now - voteTimestamp;
  115. // Log the state
  116. Serial.printf("Qualifying vote | Time since vote: %lu ms | Gape: %d (%s)\n", age, gape, gapStateStr);
  117. // Invalid if mussel is closed
  118. if (gapState == Closed) {
  119. Serial.println("→ INVALID: Mussel is Closed.");
  120. return "invalid";
  121. }
  122. // Invalid if vote is too old
  123. if (age > VOTE_TIME_BEHIND) {
  124. Serial.println("→ INVALID: Vote is too old (>2 minutes).");
  125. return "invalid";
  126. }
  127. // Valid if within 1 minute and mussel is open
  128. if (age <= VOTE_TIME_AHEAD) {
  129. Serial.println("→ VALID: Mussel is Open and vote is recent.");
  130. return "valid";
  131. }
  132. // Catch-all for anything in between
  133. Serial.println("→ INVALID: Vote is in uncertain window time");
  134. return "invalid";
  135. }
  136. /// Output the final vote decision for a mussel
  137. void concludeMusselVote(const String& musselId, const char* validity) {
  138. const char* result = strcmp(validity, "valid") == 0 ? "YES" : "NO";
  139. Serial.printf("Final Vote from Mussel %s %s (Vote was %s)\n", musselId.c_str(), result, validity);
  140. }
  141. // Bluetooth beacon discovery callbacks
  142. class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  143. // decode name and temperature from Eddystone TLM advertisement
  144. void onResult(BLEAdvertisedDevice advertisedDevice) {
  145. if (advertisedDevice.haveName()
  146. && advertisedDevice.getFrameType() == BLE_EDDYSTONE_TLM_FRAME
  147. ) {
  148. BLEEddystoneTLM EddystoneTLM(&advertisedDevice);
  149. // misuse error-only log level for plot-friendly output
  150. #if ARDUHAL_LOG_LEVEL == ARDUHAL_LOG_LEVEL_ERROR
  151. String id_mangled = advertisedDevice.getName();
  152. id_mangled.replace(' ', '_');
  153. id_mangled.replace(':', '=');
  154. Serial.println(id_mangled + ":" + EddystoneTLM.getTemp());
  155. #endif
  156. unsigned long now = millis();
  157. String musselID = advertisedDevice.getName();
  158. int gape = EddystoneTLM.getTemp(); // Referring to gape_measure
  159. // 1. Store vote
  160. storeVoteForMussel(musselID, now, gape);
  161. // 2. Align
  162. alignVotes();
  163. // 3. Qualify
  164. const char* validity = qualifyMusselVote(gape, now, millis());
  165. // 4. Conclude
  166. concludeMusselVote(musselID, validity);
  167. }
  168. }
  169. };
  170. void setup() {
  171. // enable logging to serial
  172. Serial.begin(115200);
  173. esp_log_level_set("*", ESP_LOG_DEBUG);
  174. // setup Bluetooth
  175. BLEDevice::init("");
  176. pBLEScan = BLEDevice::getScan();
  177. pBLEScan->setAdvertisedDeviceCallbacks(
  178. new MyAdvertisedDeviceCallbacks());
  179. pBLEScan->setActiveScan(true);
  180. pBLEScan->setInterval(SCAN_INTERVAL);
  181. pBLEScan->setWindow(SCAN_WINDOW);
  182. }
  183. void loop() {
  184. pBLEScan->start(SCAN_TIME_SEC, false);
  185. pBLEScan->clearResults();
  186. delay(500);
  187. }