Concept
EcoStation is a modular, low-cost environmental monitoring system designed to investigate the changing conditions of a place through temperature, humidity, air-quality signals, sound and soil moisture. By translating environmental signals into accessible readings, the station provides a starting point for observing differences between locations, identifying patterns and asking questions about the relationships between environmental conditions and ecological processes. Designed as an adaptable sensing platform, EcoStation can evolve from a single prototype into a distributed network of monitoring points that supports site investigation, environmental awareness and evidence-informed ecological interventions.
Schematic and Bill of Materials

Components Used –
- Grove Beginner Kit for Arduino – 25.67€
- Soil Moisture Sensor FC-28/YL-69 – 5.60€
- Air Quality Sensor MQ 135 – 6.49€
- Bread Board – 1.9€
Code
Main Code
#include <Wire.h> //loads arduino’s i2c library. DHT20 talks to I2C.
#include “DHT.h” //loads seed DHT library
#include <U8g2lib.h> //loads display library
// defines pins
#define SOUND_PIN A0
#define SALINITY_PIN A1
#define AIR_PIN A2
#define POT_PIN A6
#define LED_PIN 13
#define WINDOW_SIZE 20 //each average uses last 20 readings
#define SAMPLES_PER_LOOP 3 //Each pass of loop() takes 3 readings, which gives about 20 readings per second
#define SOUND_READS 180
const int AIR_CLEAN_RAW = 25;
const int AIR_DIRTY_RAW = 300;
const int SALINITY_FRESH_RAW = 700;
const int SALINITY_SEA_RAW = 250;
const int SALINITY_DRY_RAW = 980;
const int SOUND_QUIET_RAW = 19;
const int SOUND_QUIET_DB = 40;
const int SOUND_LOUD_RAW = 35;
const int SOUND_LOUD_DB = 80;
int rawSoundSamples[WINDOW_SIZE] = { 0 };
int rawSalinitySamples[WINDOW_SIZE] = { 0 };
int rawAirSamples[WINDOW_SIZE] = { 0 };
long nSamples = 0;
int soundReading = 0;
float soundRaw = 0;
float salinityRaw = 0;
float airRaw = 0;
float soundDB = 0;
float salinityPSU = 0;
float airPPM = 0;
float temperature = 0;
float humidity = 0;
bool inWater = false;
bool airConnected = false;
bool dhtConnected = false;
int salinityScore = 0;
int temperatureScore = 0;
int airScore = 0;
int soundScore = 0;
int humidityScore = 0;
int ecoScore = 0;
void setup() {
Serial.begin(115200);
pinMode(SOUND_PIN, INPUT);
pinMode(SALINITY_PIN, INPUT);
pinMode(AIR_PIN, INPUT);
pinMode(POT_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
startMovingAverage();
startTemperatureHumidity();
startDisplay();
}
void loop() {
takeSamples();
calculateAverages();
updateSound();
updateSalinity();
updateAir();
readTemperatureHumidity();
calculateScores();
if (airPPM == 1000) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
printValues();
int screen = map(analogRead(POT_PIN), 0, 1024, 0, 7);
drawDisplay(screen);
}
void printValues() {
Serial.print(“Eco:”);
Serial.print(ecoScore);
Serial.print(” Salinity:”);
Serial.print(salinityPSU);
Serial.print(” Air/10:”);
Serial.print(airPPM / 10);
Serial.print(” Sound:”);
Serial.print(soundDB);
Serial.print(” Temperature:”);
Serial.print(temperature);
Serial.print(” Humidity:”);
Serial.println(humidity);
}
Sensors
void updateSalinity() {
inWater = salinityRaw > 5 && salinityRaw < SALINITY_DRY_RAW;
float conductance = (1023.0 – salinityRaw) / salinityRaw;
float freshConductance = (1023.0 – SALINITY_FRESH_RAW) / SALINITY_FRESH_RAW;
float seaConductance = (1023.0 – SALINITY_SEA_RAW) / SALINITY_SEA_RAW;
salinityPSU = 0.5 + (conductance – freshConductance) * 34.5 / (seaConductance – freshConductance);
salinityPSU = constrain(salinityPSU, 0, 40);
}
void updateAir() {
airConnected = airRaw > 10;
airPPM = map(airRaw, AIR_CLEAN_RAW, AIR_DIRTY_RAW, 400, 1000);
airPPM = constrain(airPPM, 350, 1000);
}
void readSound() {
int lowest = 1023;
int highest = 0;
for (int i = 0; i < SOUND_READS; i++) {
int reading = analogRead(SOUND_PIN);
if (reading < lowest) {
lowest = reading;
}
if (reading > highest) {
highest = reading;
}
}
soundReading = highest – lowest;
}
void updateSound() {
soundDB = map(soundRaw, SOUND_QUIET_RAW, SOUND_LOUD_RAW, SOUND_QUIET_DB, SOUND_LOUD_DB);
soundDB = constrain(soundDB, 0, 1000);
}
DHT dht(DHT20);
int dhtCounter = 0;
void startTemperatureHumidity() {
Wire.begin();
dht.begin();
}
void readTemperatureHumidity() {
dhtCounter++;
if (dhtCounter == 15) {
dhtCounter = 0;float dhtValues[2]; if (dht.readTempAndHumidity(dhtValues) == 0) { humidity = dhtValues[0]; temperature = dhtValues[1]; dhtConnected = true; } else { dhtConnected = false; }
}
}
Scoring
void calculateScores() {
salinityScore = 0;
if (inWater) {
salinityScore = map(salinityPSU, 0, 40, 100, 1);
salinityScore = constrain(salinityScore, 1, 100);
}
airScore = 0;
if (airConnected) {
airScore = map(airPPM, 350, 1000, 100, 1);
airScore = constrain(airScore, 1, 100);
}
soundScore = map(soundDB, 0, 1000, 100, 1);
soundScore = constrain(soundScore, 1, 100);
temperatureScore = 0;
humidityScore = 0;
if (dhtConnected) {
if (temperature < 12) {
temperatureScore = map(temperature, 0, 12, 1, 100);
} else if (temperature > 22) {
temperatureScore = map(temperature, 22, 60, 100, 1);
} else {
temperatureScore = 100;
}
temperatureScore = constrain(temperatureScore, 1, 100);if (humidity < 40) { humidityScore = map(humidity, 0, 40, 1, 100); } else if (humidity > 70) { humidityScore = map(humidity, 70, 100, 100, 1); } else { humidityScore = 100; } humidityScore = constrain(humidityScore, 1, 100);
}
long total = 0;
int weights = 0;
if (salinityScore > 0) {
total += salinityScore * 30;
weights += 30;
}
if (temperatureScore > 0) {
total += temperatureScore * 25;
weights += 25;
}
if (airScore > 0) {
total += airScore * 25;
weights += 25;
}
if (soundScore > 0) {
total += soundScore * 12;
weights += 12;
}
if (humidityScore > 0) {
total += humidityScore * 8;
weights += 8;
}
if (weights > 0) {
ecoScore = total / weights;
} else {
ecoScore = 0;
}
}
Scoring System
The Eco Station scoring system converts the raw readings from five environmental sensors into a single Eco Score ranging from 1 to 100. Each sensor reading is first calibrated into a standard unit and then converted into a score based on predefined environmental reference values.
Each sensor is assigned a different weight according to its importance in the overall assessment: salinity (30%), temperature (25%), air quality (25%), sound (12%), and humidity (8%). The individual scores are multiplied by their respective weights to calculate their contribution to the final Eco Score.
The final score is calculated by adding the weighted contributions of all available sensors and dividing by the sum of their weights. This ensures that the Eco Score remains valid even if some sensors are unavailable.
The resulting Eco Score provides a simplified overview of the environmental conditions at a given location, with higher scores indicating conditions closer to the defined reference ranges.

Prototype

The main screen displays the overall Eco Score, alongside individual scores for air quality, temperature, humidity, and noise. Additional screens provide detailed readings for each parameter, with numerical values, environmental condition labels such as “Good,” “Comfort,” and “Ideal,” and trend graphs.
A potentiometer allows users to switch between screens, each showing real-time sensor data, condition labels, and graphs.

Next Steps
Our next development moves EcoStation from a single device to a network of identical stations placed across contrasting sites in the city. Each station will keep scoring its own signals locally, then send its readings over an infrared link to a WiFi-enabled receiver that collects data from the whole network. From there, the scores will feed a web dashboard where we can monitor every station in real time, compare sites side by side and follow how each eco score changes over days and seasons. The aim is to turn isolated readings into a live map of ecosystem health, where one number per place shows where the city is thriving and where it needs attention.
