# ============================================================
# SMART BUS STOP HUB - COMBINED GROUP PROJECT
# Members: 1 (Solar/Lighting) + 2 (Occupancy/Safety) +
# 3 (Weather) + 4 (Air Quality)
#
# ESP32 + OLED + MAX7219 + NeoPixel x2 + PIR + LDR +
# Potentiometer + MQ-2 + DHT22 + BMP180 + Relay +
# Servo + Buzzer
# MQTT + Node-RED Dashboard
# ============================================================
# ============================================================
# IMPORTS
# ============================================================
import network
import time
import ntptime
import dht
import neopixel
import ssd1306
import max7219
import bmp180
from machine import Pin, ADC, I2C, SPI, PWM
from umqtt.simple import MQTTClient
# ============================================================
# WIFI CONFIG
# ============================================================
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASSWORD = ""
# ============================================================
# MQTT CONFIG
# ============================================================
MQTT_BROKER = "broker.hivemq.com"
MQTT_PORT = 1883
MQTT_CLIENT_ID = "busstop_hub_combined"
# Member 1: Solar / Lighting
TOPIC_SOLAR_POWER = b"busstop/solar/power"
TOPIC_LIGHT_RELAY = b"busstop/light/relay"
# Member 2: Occupancy / Safety
TOPIC_OCCUPANCY = b"busstop/occupancy/status"
TOPIC_LDR_VALUE = b"busstop/light/value"
TOPIC_LDR_STATUS = b"busstop/light/status"
TOPIC_SAFETY = b"busstop/safety/status"
# Member 3: Weather
TOPIC_TEMP = b"busstop/weather/temp"
TOPIC_HUMIDITY = b"busstop/weather/humidity"
TOPIC_PRESSURE = b"busstop/weather/pressure"
TOPIC_COMFORT = b"busstop/weather/comfort"
# Member 4: Air Quality
TOPIC_AIR_QUALITY = b"busstop/air/quality"
TOPIC_AIR_CO = b"busstop/air/co"
TOPIC_AIR_ALERT = b"busstop/air/alert"
TOPIC_AIR_RAW = b"busstop/air/raw"
# ============================================================
# GPIO SETUP
# ============================================================
# Member 1: Potentiometer (Solar simulation) -> GPIO 33
pot = ADC(Pin(33))
pot.atten(ADC.ATTN_11DB)
# Member 1: Relay (Street light) -> GPIO 26
relay = Pin(26, Pin.OUT)
# Member 2: PIR Sensor -> GPIO 19
pir = Pin(19, Pin.IN)
# Member 2: LDR Sensor -> GPIO 34
ldr = ADC(Pin(34))
ldr.atten(ADC.ATTN_11DB)
# Member 3: DHT22 -> GPIO 15
dht_sensor = dht.DHT22(Pin(15))
# Member 3: Servo -> GPIO 13
servo_pwm = PWM(Pin(13), freq=50)
# Member 4: MQ-2 Gas Sensor -> GPIO 35
mq2 = ADC(Pin(35))
mq2.atten(ADC.ATTN_11DB)
# Shared: Buzzer -> GPIO 32
buzzer = PWM(Pin(32), freq=1000, duty=0)
# ============================================================
# SHARED I2C - OLED + BMP180
# SCL=GPIO22, SDA=GPIO21
# ============================================================
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
bmp = bmp180.BMP180(i2c)
# ============================================================
# SHARED SPI - MAX7219 DOT MATRIX (4 panels)
# SCK=GPIO18, MOSI=GPIO23, CS=GPIO5
# ============================================================
spi = SPI(1, baudrate=10000000, polarity=0, phase=0,
sck=Pin(18), mosi=Pin(23))
cs_pin = Pin(5, Pin.OUT)
matrix = max7219.Matrix8x8(spi, cs_pin, 4)
matrix.brightness(5)
# ============================================================
# NEOPIXEL
# Safety ring (Member 2) -> GPIO 27, 16 LEDs
# Air quality ring (Member 3/4) -> GPIO 25, 12 LEDs
# ============================================================
NUM_SAFETY = 16
NUM_AIR = 12
neo_safety = neopixel.NeoPixel(Pin(27), NUM_SAFETY)
neo_air = neopixel.NeoPixel(Pin(25), NUM_AIR)
# ============================================================
# STATE VARIABLES
# ============================================================
occupied = False
previous_motion = 0
alert_toggle = False
prev_comfort = ""
screen_index = 0
# ============================================================
# HELPER FUNCTIONS
# ============================================================
def set_neo_safety(color):
for i in range(NUM_SAFETY):
neo_safety[i] = color
neo_safety.write()
def set_neo_air(color):
for i in range(NUM_AIR):
neo_air[i] = color
neo_air.write()
def buzz(pattern):
if pattern == "off":
buzzer.duty(0)
elif pattern == "short":
buzzer.duty(512)
time.sleep_ms(200)
buzzer.duty(0)
elif pattern == "rapid":
for _ in range(3):
buzzer.duty(512)
time.sleep_ms(150)
buzzer.duty(0)
time.sleep_ms(100)
elif pattern == "weather":
for _ in range(2):
buzzer.duty(512)
time.sleep_ms(500)
buzzer.duty(0)
time.sleep_ms(100)
def set_servo_angle(angle):
min_duty = 40
max_duty = 115
duty = int(min_duty + (angle / 180) * (max_duty - min_duty))
servo_pwm.duty(duty)
def matrix_show(text):
matrix.fill(0)
matrix.text(text[:4], 0, 0, 1)
matrix.show()
def matrix_scroll(message, delay_ms=40):
msg_len = len(message) * 6
for x in range(32, -msg_len, -1):
matrix.fill(0)
matrix.text(message, x, 0, 1)
matrix.show()
time.sleep_ms(delay_ms)
def get_time_str():
try:
t = time.localtime(time.time() + 8 * 3600)
hour, minute = t[3], t[4]
period = "PM" if hour >= 12 else "AM"
if hour > 12:
hour -= 12
if hour == 0:
hour = 12
return "{}:{:02d}{}".format(hour, minute, period)
except:
return "??:??"
# ============================================================
# LOGIC FUNCTIONS
# ============================================================
def get_air_level(raw):
if raw < 1000:
return "Good"
elif raw < 2000:
return "Moderate"
elif raw < 3000:
return "Unhealthy"
else:
return "Hazardous"
def get_comfort(temp, hum):
if temp > 32:
return "HOT"
elif hum > 80:
return "UNCOMFORTABLE"
else:
return "COMFORTABLE"
def get_ldr_status(val):
if val < 1200:
return "DARK"
elif val < 2500:
return "DIM"
else:
return "BRIGHT"
def get_safety_status(occupancy, ldr_st):
if occupancy == "OCCUPIED":
if ldr_st == "DARK":
return "LOW VIS"
elif ldr_st == "DIM":
return "CAUTION"
else:
return "SAFE"
else:
if ldr_st == "DARK":
return "DRK+EMPT"
elif ldr_st == "DIM":
return "DIM+EMPT"
else:
return "NORMAL"
# ============================================================
# OLED - 3 rotating screens
# ============================================================
def update_oled(solar_pct, relay_st,
occupancy, ldr_st, safety_st,
temp, hum, pressure, comfort,
air_level, co_ppm):
global screen_index
oled.fill(0)
if screen_index == 0:
oled.text("BUS STOP HUB", 8, 0)
oled.text("Solar:" + str(solar_pct) + "%", 0, 14)
oled.text("Light:" + relay_st, 0, 24)
oled.text("Occ:" + occupancy[:8], 0, 36)
oled.text("Safe:" + safety_st[:8], 0, 48)
elif screen_index == 1:
oled.text(" WEATHER ", 0, 0)
oled.text("T:{:.1f}C".format(temp), 0, 14)
oled.text("H:{:.1f}%".format(hum), 0, 26)
oled.text("P:{:.0f}hPa".format(pressure), 0, 38)
oled.text(comfort[:16], 0, 52)
else:
oled.text("AIR QUALITY ", 0, 0)
oled.text("Lvl:" + air_level[:8], 0, 16)
oled.text("CO :" + str(co_ppm) + "ppm", 0, 30)
oled.text("LDR:" + ldr_st, 0, 44)
screen_index = (screen_index + 1) % 3
oled.show()
# ============================================================
# WIFI + MQTT
# ============================================================
def connect_wifi():
print("Connecting to WiFi", end="")
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect(WIFI_SSID, WIFI_PASSWORD)
while not wifi.isconnected():
print(".", end="")
time.sleep(0.5)
print("\nWiFi Connected! IP:", wifi.ifconfig()[0])
def sync_time():
try:
ntptime.settime()
print("Time synced")
except:
print("Time sync failed")
def connect_mqtt():
client = MQTTClient(MQTT_CLIENT_ID, MQTT_BROKER, port=MQTT_PORT)
client.connect()
print("MQTT Connected to", MQTT_BROKER)
return client
# ============================================================
# STARTUP
# ============================================================
oled.fill(0)
oled.text("SMART BUS STOP", 4, 10)
oled.text("HUB STARTING..", 4, 28)
oled.show()
connect_wifi()
sync_time()
client = connect_mqtt()
set_neo_safety((0, 0, 0))
set_neo_air((0, 0, 0))
set_servo_angle(0)
relay.value(0)
matrix_scroll("BUS STOP HUB", delay_ms=40)
oled.fill(0)
oled.text("ALL SYSTEMS", 12, 18)
oled.text(" READY! ", 4, 34)
oled.show()
time.sleep(2)
# ============================================================
# MAIN LOOP
# ============================================================
while True:
# --------------------------------------------------------
# MEMBER 1 - SOLAR / LIGHTING
# --------------------------------------------------------
solar_raw = pot.read()
solar_pct = int((solar_raw / 4095) * 100)
if solar_raw < 1800:
relay.value(1)
relay_st = "ON"
else:
relay.value(0)
relay_st = "OFF"
# --------------------------------------------------------
# MEMBER 2 - OCCUPANCY + SAFETY
# --------------------------------------------------------
motion = pir.value()
ldr_raw = ldr.read()
if motion == 1 and previous_motion == 0:
occupied = not occupied
print("PIR triggered - occupancy toggled")
previous_motion = motion
occupancy = "OCCUPIED" if occupied else "EMPTY"
ldr_st = get_ldr_status(ldr_raw)
safety_st = get_safety_status(occupancy, ldr_st)
if safety_st == "LOW VIS":
set_neo_safety((255, 255, 0))
elif safety_st == "CAUTION":
set_neo_safety((255, 100, 0))
elif safety_st == "SAFE":
set_neo_safety((0, 100, 0))
else:
set_neo_safety((0, 0, 0))
# --------------------------------------------------------
# MEMBER 3 - WEATHER
# --------------------------------------------------------
try:
dht_sensor.measure()
temp = dht_sensor.temperature()
hum = dht_sensor.humidity()
pressure = bmp.pressure / 100.0
comfort = get_comfort(temp, hum)
except Exception as e:
print("Sensor error:", e)
temp, hum, pressure, comfort = 0.0, 0.0, 0.0, "ERROR"
if comfort == "COMFORTABLE":
set_servo_angle(0)
elif comfort == "UNCOMFORTABLE":
set_servo_angle(90)
elif comfort == "HOT":
set_servo_angle(180)
if comfort != prev_comfort and comfort not in ("ERROR", ""):
if comfort == "HOT":
buzz("weather")
elif comfort == "UNCOMFORTABLE":
buzz("short")
prev_comfort = comfort
# --------------------------------------------------------
# MEMBER 4 - AIR QUALITY
# --------------------------------------------------------
mq2_raw = mq2.read()
co_ppm = round(mq2_raw * 0.05, 2)
air_level = get_air_level(mq2_raw)
if air_level == "Good":
set_neo_air((0, 200, 0))
elif air_level == "Moderate":
set_neo_air((255, 165, 0))
elif air_level == "Unhealthy":
set_neo_air((128, 0, 128))
buzz("short")
else:
set_neo_air((255, 0, 0))
buzz("rapid")
# --------------------------------------------------------
# DOT MATRIX
# --------------------------------------------------------
if air_level in ("Unhealthy", "Hazardous") or safety_st == "LOW VIS":
if alert_toggle:
matrix_show("ALRT")
else:
matrix_show(get_time_str()[:4])
alert_toggle = not alert_toggle
else:
matrix_show(get_time_str()[:4])
# --------------------------------------------------------
# OLED
# --------------------------------------------------------
update_oled(solar_pct, relay_st,
occupancy, ldr_st, safety_st,
temp, hum, pressure, comfort,
air_level, co_ppm)
# --------------------------------------------------------
# MQTT PUBLISH
# --------------------------------------------------------
try:
client.publish(TOPIC_SOLAR_POWER, str(solar_pct))
client.publish(TOPIC_LIGHT_RELAY, relay_st)
client.publish(TOPIC_OCCUPANCY, occupancy)
client.publish(TOPIC_LDR_VALUE, str(ldr_raw))
client.publish(TOPIC_LDR_STATUS, ldr_st)
client.publish(TOPIC_SAFETY, safety_st)
client.publish(TOPIC_TEMP, str(temp))
client.publish(TOPIC_HUMIDITY, str(hum))
client.publish(TOPIC_PRESSURE, str(pressure))
client.publish(TOPIC_COMFORT, comfort)
quality_msg = "raw={},level={}".format(mq2_raw, air_level)
co_msg = "co={}ppm".format(co_ppm)
client.publish(TOPIC_AIR_QUALITY, quality_msg)
client.publish(TOPIC_AIR_CO, co_msg)
client.publish(TOPIC_AIR_RAW, str(mq2_raw))
if air_level in ("Unhealthy", "Hazardous"):
alert_msg = "ALERT:{},AQI:{},CO:{}ppm".format(air_level, mq2_raw, co_ppm)
client.publish(TOPIC_AIR_ALERT, alert_msg)
except Exception as e:
print("MQTT error:", e)
try:
client = connect_mqtt()
except:
pass
# --------------------------------------------------------
# SERIAL MONITOR
# --------------------------------------------------------
print("=" * 45)
print("[SOLAR] Power:{}% Relay:{}".format(solar_pct, relay_st))
print("[OCC] {} LDR:{} Safety:{}".format(occupancy, ldr_st, safety_st))
print("[WEATHER] T:{:.1f}C H:{:.1f}% P:{:.0f}hPa {}".format(temp, hum, pressure, comfort))
print("[AIR] Level:{} CO:{}ppm".format(air_level, co_ppm))
print("[TIME] {}".format(get_time_str()))
print("=" * 45)
time.sleep(3)