// 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; }