---
title: Mussel-driven voting system

date: 2025-04-23

toc-depth: 2

format:
  stylish-report-pdf:
    pdfversion: "2.0"
    pdfstandard: [A-4f, UA-2]
    pdftestphase: latest

metadata-files:
  - _actors.yml
keywords:
  - voting
  - bioindicator
  - Arduino
  - drinking water

breaks: false

---

# Abstract

This project explores the development of a prototype voting system
inspired by the natural behavior of freshwater mussels
detected and examined by biomonitoring.
We have defined the prototype as a "research archetype 3",
meaning that the driving force for this project
is illustration and demostration purposes.
As part of the project requirements,
we implemented three core components:
ESP32 for hardware interaction,
p5.js for visual representation,
and Bluetooth for wireless communication.
The system simulates mussel movements as simple votes,
green for drinkable, red for undrinkable water,
translated into a visual interface.
The outcome highlights
how environmental signals can be simplified
and communicated through interactive design.

# Introduction

In Poland, several waterworks use biomonitoring systems
that rely on freshwater mussels to detect water contamination
by measuring and analysing the mussels' gaping behavior
[@FerreiraRodriguez2023 p. 3732]
Our project aims to simulate a similar system.
We will be using Arduino-based hardware and software to do so,
and have additionally made a p5.js program to simulate night and day,
which influences the pseudomussels' behaviour (reaction).
We have defined a research question to work around:

**How do we simulate and visualise an existing biomonitoring technology
for its core purpose**

Our simulation is inspired by SYMBIO --
a biomonitoring system --
developed by the company PROTE
[@Prote2024].
The SYMBIO system measures the opening angle of the mussel
once per second,
by using magnets attached to their shells.
Superficially, if the mussels gape is closed, it is a bad sign,
and if it is open, they are happy
and there is no sign of water contamination.
There is a wire from each mussel/magnet that connects to a computer.
The computer then processes the data in following phases:
collect, align, qualify (and analyse) the gaping data,
and then --
in our understanding of the process --
treats the now normalized data as votes
[@Prote2024].
The threshold we are inspired by for these votes
will be defined in a a later paragraph.
To be more in depth about the mussels behavior,
the gapes are not as simple
as just opening/closing their gap to signal happy/unhappy,
but is more of a behavioural spectrum.
Therefore, if either half of the mussel closes rapidly,
or if a mussel remains closed for a long time,
it indicates contaminated water.
An alarm will then be triggered
and further laboratory tests are done
[@Nazaruk2016].

In short, the core purpose of this technology is
to gather data, interpret it, analyse it
and then act upon the answer.

To meet this question,
we will be using touch and light sensors
to represent our virtual models on Arduino --
also referred to as pseudomussels --
and they should mimic somewhere close to real mussel responses
such as normal behavior or stressed behavior.

Although our coding logic is inspired
by an existing study and implemenation,
we wish to delineate ourselves from the real-world criterias
that concerns this technology and actual authentic mussel behaviour.

# Project Planning and theoretical framework

## Use of course curriculum

The beginning of our work process
consisted of researching bioindication as a concept
and the associated technology.
Then we tried to define the purpose of our prototype.
Were we interested in the UI/UX design?
or perhaps we wanted to prototype a fitting reaction
that leads to a dramatic output? Should the prototype be useful or
spark reflection? is it merely a state of the art prototype?
Concurrently, we learned about the roles
artefacts can play in a research process,
and used that framework to help define the aim of our project.

The following framework, as introduced in @Wensveen2014,
explains how prototypes can serve different purposes
depending on the type of knowledge a project aims to produce.
A "role" consists of characteristics
that helps guide the design process
and scope of the prototype.
There are four roles to choose from,
and we chose to work from the perspective of role number three,
which views the prototype as a research archetype
and tool for critical reflection,
in both a physical and abstract sense
[@Wensveen2014 p. 8-9].

Next we each had an early prototyping and pseudocoding phase,
were we worked on the sensor behaviour,
on the logic behind the voting mechanism
and the possible communication methods between them.

Our project group has received a brief introduction
to APIs during a lecture session
and through additional notes on the topic,
that are available on the lecture slides for week 5,
but it is explicitly stated as "Not a part of the curicculum".
Hence we have searched for external sources on API knowledge.

We decided to implement a loosely coupled network design
as described e.g. in @GeeksforGeeks2024,
to connect the different components
and allow each of them to operate independently,
whilst still being able to exchange data.
We chose to use the BLE Beacon API --
specifically the Eddystone protocol
as described e.g. in @AccentSystems2021 --
and focused on TLM (telementry) frame type.
The original purpose of of TLM
is to transmit temperature and battery level,
but we repurposed it
to make the temperature show the gape angle data instead.
The sensors appear in the debug interface
of one of our android phones,
indicating that any system supporting this API
could interact with our sensors.

## Detecting stressful mussel behavior

We wish to expound some of the ways
that freshwater mussels behave and how the detection works,
in real-life implementation.
The thresholds and logic for our sensors and voting-mechanism
are mostly based upon the following.

The use of a mussel as biodetector requires distinction
between slow-paced valve gaping change (normal),
paused valve gaping at the closed position (resting or starved)
and rapid valve gaping change (stressed)
[@Miller2022 p. 1097; @Robson2006 p. 1200].

Detecting behavioural change to a rapid gaping pace,
measurements are needed at a much higher sampling rate
than that of the normal gaping pace.
E.g. one mussel with normal gaping pace of about 1 minute
required a sample rate of 5 seconds to detect its normal pace,
and another mussel with normal gaping pace of 3-4 minutes
required a sample rate of 0.5 seconds to detect more rapid cycles
[@Robson2009 p. 195].
Another measurement of both normal and stressed behaviour,
assuming that x-axis is in seconds (not hours as indicated),
similarly shows a need for fast sampling rate
to detect a normal pace of about 1.2 minutes
and a stressed pace faster than the visualized resolution
of about 1 second
[@FerreiraRodriguez2023 fig. 2].

One concrete approach used in @Robson2009 and @Robson2010
is to collect data at a sample rate of 0.5 seconds, i.e. 2 Hz,
and convert that into gape angle per second (CHIGA)
to then monitor gape movement instead of gape position.

Additionally, we wish to refer
to the bachelor thesis @GarciaHuertes2016
that covers some of the same themes as our report
but are prototyping with real mussels.
The report covers mussel behaviour and stress,
the SYMBIO system and biomonitoring more in depth,
and has added to our understanding of the subject
[@GarciaHuertes2016 pp. 11-15].

## Copyright and licensing

To encourage collaboration and stimulate a circular gift economy
as introduced by @Mikkelsen2000,
this project is copyleft licensed:
Code parts are licensed
under the GNU Public Licence version 3 or newer,
and non-code parts are licensed
under the Creative Commons crediting share-alike 4.0.

# Analysis and Design (physical components) - Menna

The physical setup of our prototype consists
of a simple Arduino-based circuit.
The following components were used:

ESP 32: Runs the core program that simulates mussel behavior,
reads sensor data and outputs voting signals.
The logical parts of the ESP32 used in our setup
include Bluetooth, a touch sensor input, LEDC (LED control)
and a logging system for tracking behavior and communication.

Breadboard: Used to easily connect all components.

Jumper Wires: Connect the ESP32
to the sensors, LED and power/ground rails on the breadboard.
One wire is repurposed as a touch sensor,
detecting when it is touched
and triggering a behavioral change in the simulated mussel.

LED pins: Light up  to indicate
if the water quality is good or not.

Light Sensor: Detects environmental input
and triggers a behavioral change in the simulated mussel.

P5.js: Used to represent if it is daytime or nightime.

# Coding approach

The coding approach for this project is centered
around simulating behavioral response of mussels
under stress, using sensors
and translate these responses into votes that indicate water quality.
Real-world use of mussels as biosensors relies
on monitoring their behavior to detect environmental stress.
In our project, we simulate this behavior using programmable hardware.

The system is composed of three major components:

* p5.js Interface --
  Simulating environmental changes like day and night.
  Allows the user to control the light conditions using a button.
* Sensor (Arduino) --
  Each sensor simulates a mussel.
  It reads light and touch input,
  changes internal "stress" levels accordingly,
  and outputs a "gape angle" via BLE
  using the Eddystone TLM protocol.
* Voting System (Arduino) --
  This unit scans BLE beacons sent by sensor mussels.
  Each beacon includes a simulated gape angle.
  The latest measurements per mussel are stored and evaluated,
  if the data is recent and valid.
  It determines whether the water is drinkable using a voting rule.

```{.plantuml}
!include components.puml
```

This UML diagram
shows how the different parts of the system talk to each other:

1. Light (p5.js)
  * There's a button that lets you switch between daylight and moonlight.
  * When you click it, it changes the "light" condition
    and sends that to the sensor mussel.
2. Sensor (Arduino)
   Uses an Arduino to act like a mussel biosensor.
   It's responsible for detecting light (using a light sensor)
   and touch,
   simulating the way a real mussel might react to environmental changes
  * READ: Reads the light level
    using a light sensor attached to the mussel
    and detects the touch input.
  * NORMALIZE: It calculates the raw data
    based on touch and light data
    and converts that into a gape measure --
    how much the mussel opens and closes.

    Then the data (Mussel ID, time and the gape measure) is sent to Vote via Bluetooth Low energy (BLE) protocol.

3. Vote (Arduino)

    This part takes the sensor data from the mussels and turns it into a voting system to decide if the water is drinkable or not.

    * COLLECT: Recives the mussel's data (ID, time and gape measure) and stores it into a stack.Each mussel can store up to 5 recent readings. If a mussel isn’t already in the system, it gets added.
    * ALIGN: Looks at the most recent reading (vote) from each mussel and checks if the gape angle means the mussel is Open (gape between 40–90) or Closed (gape between 0–39). This helps classify each mussel's current state.
    * QUALIFY: Filters out old votes. A vote is valid only if it's less than 1 minute old. This ensures the decision is based on real-time conditions.
    * CONCLUDE: Counts how many valid votes are “Open.” If half or more of the valid votes are Open, it decides that the water is drinkable (GREEN light). If fewer than half are Open, the system decides the water is not drinkable (RED light).

    The LEDs on the Arduino are used to show this decision:

    Green = Drinkable, red = Undrinkable.

## Sensor system

```{.plantuml}
!include Arduino/sensor/sensor.puml
```

This UML Diagram explains how a sensor system for a mussel behaviour simulator works.

1. *Instantiate* *mussel* *object* -  This refers to initializing mussel's gaping rhymthm control and sensors. We have used functions such as
**keepPace**(), **beginPace**() and **resolveGapeAngle**() for simulating the mussel's behaviour.

2. *Instantiate* *bluetooth* *object* - Setting up the Bluetooth system for communication.

```{.cpp}
BLEDevice::init(BEACON_NAME);
pAdvertising = BLEDevice::getAdvertising(;
```

**Init** **Block**

3. *Setup* *mussel* *sensors* - Initializes the sensors attached to the mussel.

```{.cpp}
beginPace();
beginTouchDetection();
```
These set up the LED-based pacemaker and touch sensor.

4. *Setup* *bluetooth* *beacon* - Preparing the bluetooth device to send data.

```{.cpp}
setBeaconAdvertisement();
setBeaconServiceData(resolveGapeAngle());
pAdvertising->start();
```

**Loop (each 500ms)**

5. *Read sensors* - Get data from the mussel.

```{.cpp}
resolveGapeAngle()
```
This function reads the light sensor and LED Output to estimate the gape angle.

6. *Normalize sensor data as a gape angle* - Processes sensor values into a meaningful format.

```{.python}
int gapeAngle = paceAngle * lightIntensity
```

7. *Add gape angle to beacon* - Attaches the processed gape angle to the bluetooth beacon.

```{.cpp}
setBeaconServiceData(resolveGapeAngle())
```

**Parallel 100ms Loop**

8. *Broadcast beacon* - continuously sends bluetooth beacon data

```{.cpp}
pAdvertising->start()
```
BLE stack itself handles frequent broadcasting.

## Voting system

```{.plantuml}
!include Arduino/vote/vote.puml
```
This UML Diagram outlines a bluetooth-based mussel biomonitoring voting system.

1. *Instantiate mussel object* - Mussels are abstracted using a Voter strct. A global array is initialized to hold mussels and their latest votes.

```{.cpp}
struct Voter {
  String id;               // Mussel ID
  Vote votes[BALLOT_MAX];  // Last 5 sensor readings
  int voteCount = 0;       // Number of readings stored
};
Voter voters[VOTER_MAX];
```

2. *Instantiate bluetooth object* - BLE stack is initialized, responsible for detecting nearby beacons.

```{.cpp}
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
```

**Init**

3. *Setup mussel voting* - **setup()** initializes LEDs, Serial logging, and sets the BLE callbacks.

4. *Setup bluetooth scanner* - **MyAdvertisedDeviceCallbacks()** starts the bluetooth scanning to detect the nearby devices.

**Event-Driven Component**

5. *Each beacon detected (Trigger)* - The conditional event **MyAdvertisedDeviceCallbacks** method is triggered on beacon detection.

6. *Collect beacon data* - Reads and stores the information (Mussel ID, timestamp and Gape measure) from each detected beacon.

```{.cpp}
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);
    }
  }
```
**collectBallotData()** adds or updates the voter’s record with the new vote (FIFO for 5 ballots max).

**Continuous Loop (500 ms)**

7. *Align beacon data as ballots* - **alignVotes()** checks each mussel’s latest gape value and classifies it as Open, Closed, or Invalid

```{.cpp}
String state = (latest.measure >= 0 && latest.measure < 40)
      ? "Closed"
      : (latest.measure >= 40 && latest.measure <= 90)
        ? "Open"
        : "Invalid reading";
```

8. *Qualify Ballot for a vote* - **qualifyBallot()** checks if the vote is within the 1-minute validity window.

```{.cpp}
if (age <= VOTE_TIME_TOLERANCE) {
    log_i("VALID: Ballot is within 1 minute (age: %lums)",
      age);
    return "valid";
```

9. *Conclude vote result* - **concludeVote()** counts valid ballots and checks how many are "Open". If majority are OPEN the water is drinkable.

```{.cpp}
waterIsDrinkable = (openVotes >= threshold);
```
10. *act on vote result* - Turns LEDs on/off and prints status.

```{.cpp}
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");
  }
```
**Background Cleanup**

**cleanOldBallotData()** drops ballots older than VOTE_TIME_TOLERANCE and removes voters with no valid data.

```{.cpp}
if (age < VOTE_TIME_TOLERANCE) {
        voter.votes[newCount++] = voter.votes[j];
      }
```

# Testing

Testing was carried out using a breadboard setup with an ESP32 board simulating a mussel, equipped with both a light sensor and a wire touch sensor.

**Component 1: p5.js Day/Night Simulation**

* Goal : Verify that the virtual environment affects the mussel’s state.
* Result: The light sensor reliably registered a change in light level, leading to a detectable difference in gape angle, confirming successful integration between p5.js and sensor logic.

**Component 2: Sensor Mussels**

* Goal: Validate the mussel’s ability to interpret light and touch as stress indicators and output a normalized gape angle.
* Result: The sensor system correctly responded to light changes and touch input, adjusting the internal pace and generating gape angles in the expected range (0–90). All data was successfully encoded in the TLM frame of the BLE beacon.

**Component 3: Voting System**

* Goal: Ensure the system correctly collects, filters, and evaluates gape data to output the correct decision via LED.
* Result:
    * Valid gape values within the 1-minute window were correctly added to the vote stack.

    * Majority logic (≥50% OPEN) correctly activated the green LED for "drinkable" status.

    * RED LED activated when fewer than half were open.

    * Outdated values (older than 1 minute) were dropped out of the stack.

**Stress Testing and Timing**

We also tested time-sensitive behavior by introducing delays between beacon broadcasts. The system was able to correctly validate and invalidate (remove from the stack) votes based on the 1-minute age threshold, ensuring decisions are made in real-time.

Overall, the system passed all critical functional tests.

# Program Use Case

This project simulates how mussels can be used to monitor water quality. Each mussel is represented by a small setup made with an ESP32 board, sensors (light and touch), and LED lights.

**Purpose**

The system acts like a group of mussels reacting to their environment. When mussels sense good water conditions, they open more often. This prototype mimics that behavior using sensors and lights, helping users visualize how environmental stress can lead to a “vote” to determine the water quality.

**Users**

Researchers, students, or testers interacting with the system

**User Guide**

1. Set Up the Hardware
    * Each mussel is made with an ESP32, a light sensor, a touch wire, and LEDs.
    * ESP32’s built-in LED also shows how often the mussel is “opening” and “closing.”
2. Start the System
    * Upload and run the sensor sketch on each ESP32. This sketch makes them behave like mussels that respond to light and touch.
    * Upload and run the voting sketch on another Arduino that collects input from the mussel ESP32s.
3. Triggering a Mussel
    * You can touch the wire or change the light on the sensor to stress the mussel. The frequency of opening and closing reflects on the inbulit LED.
4. Collecting data
    * Each mussel sends its state (open/closed) through Bluetooth to the central voting system.
5. Voting on Water Quality
    * If the majority of mussels are “open,” the system shows a green LED, meaning the water is drinkable.

The system uses simple sensor input (light and touch) to simulate natural mussel behavior and combines multiple inputs to make a collective decision, just like mussels do in real environments.

# Discussion and reflections

This project does not involve generative artificial intelligence (AI)
due to its scope of data collecting and processing
from a source of simple simulations of living organisms.
Related but different projects might sensibly involve AI,
e.g. training an AI with sensor data from real living mussels,
either unsupervised to aid in discovering behavioural patterns
like CHIGA,
Or in a future
where a (non-AI) domain model for mussel behaviours is established,
an AI could be trained supervised,
i.e. generate an AI model supervised with the use of such domain model,
e.g. to help calibrate sensors.

This project succesfully demonstrates
that a loosely coupled set of systems can mimic the systems in Poland
where the behaviour of 8 mussels "vote" about the quality of water.
The concrete output, however,
do not reasonably reflect the logic of the Polish systems,
because the mussel simulation is too simplistic in that
a) mussel gape rhythm is simulated as simple linear movements
whereas real mussel gape rhythm is closer to a sigmoid curve,
and consequently b) voting is based on "is gape closed"
rather than the more telling "what is the CHIGA pattern"
requiring a more realistic rhythm,
leading to our setup concluding "bad water quality" fairly randomly.
That said, the code is structured so that it should only require
minimal changes to change the evaluation logic
given more realistic sensor data,
mainly by rewriting the function `alignVotes()`
(see code listing lines 154-176 in @sec-vote of Appendix).

# Conclusion

This project demonstrates the research question
"How do we simulate and visualise an existing biomonitoring technology
for its core purpose".
We defined the core purpose
of both the technology and our aim with the prototype
through desk research.
The core purpose of this technology is
to gather data, interpret it, analyse it
and then act upon the answer --
and our prototype serves as aresearch tool for critical reflection.

To answer the now defined research question, we did the following steps:
Through sensor-based pseudomussels (ESP32s),
bluetooth communication and code,
we simulated mussel reaction and behavior to enviromental stimuli --
this being light and touch, and not poisoned water, as in reality.
The reactions (data) were collected, aligned into binary answers
and analyzed as yes/no "votes".
Depending on the outcome, the observer (ourselves) will be alerted
through LED lights attached to a breadboard,
signifying the water quality, based on which color light turns on:
either green light (drinkable water)
or red light (contamination).
We believe through these steps in our prototype,
that we succeed in simulating and visualizing
the core purpose of the SYMBIO system's way
of biomonitoring freshwatermussels.

# Bibliography {.appendix}

\begingroup
\raggedright
::: {#refs}
:::
\endgroup

\appendix

# P5.js sketch `light.js` {.appendix}

```{.js include="p5.js/light.js" code-line-numbers="true"}
```

# Arduino sketch `sensor.js` {.appendix}

```{.cpp include="Arduino/sensor/sensor.ino" code-line-numbers="true"}
```

# Arduino sketch `vote.js` {#sec-vote .appendix}

```{.cpp include="Arduino/vote/vote.ino" code-line-numbers="true"}
```
