# BASE STATION (Board 2, LoRa address 2) - Phase 1
# ESP32 + RYLR998 + LCD 16x2 (I2C) + 4 status LEDs + active buzzer
# MicroPython. Matches the "Phase 1 Block Diagram".
#
# Wiring (ESP32 GPIO):
# RYLR998 : VDD-3V3 (never 5 V), GND, TXD-16, RXD-17
# LCD I2C : VCC-5V (VIN), GND, SDA-21, SCL-22 (backpack address 0x27)
# Buzzer : GPIO25 (+), GND (-)
# LEDs : each GPIO -> 220 ohm -> LED -> GND
# Green GPIO33 slow blink = base running
# Blue GPIO32 quick flash = message received
# Yellow GPIO26 ON = no signal 30 min, or field battery low
# Red GPIO27 ON + buzzer = EARTHQUAKE
#
# Receives: HB,1,battV,noise_g,tempC,hum% EVT,1,peak_g,ratio
# Answers: ACK,n (n = number of messages received)
from machine import Pin, I2C, UART, PWM
import time
# ---------- settings ----------
MY_ADDR = 2
FIELD_ADDR = 1
NO_SIGNAL_S = 60 # real station: 1800 (30 min). 60 s for testing.
LOW_BATT_V = 3.40 # field battery warning level
ALARM_S = 10 # how long red LED + buzzer stay on after a quake
# ---------- LEDs and buzzer ----------
green = Pin(33, Pin.OUT, value=0)
blue = Pin(32, Pin.OUT, value=0)
yellow = Pin(26, Pin.OUT, value=0)
red = Pin(27, Pin.OUT, value=0)
buzzer = PWM(Pin(25), freq=2000, duty_u16=0) # PWM works for active and passive buzzers
# ---------- LCD 16x2 with PCF8574 I2C backpack ----------
class LCD:
def __init__(self, i2c, addr=0x27):
self.i2c, self.addr, self.bl = i2c, addr, 0x08 # 0x08 = backlight on
time.sleep_ms(50)
for v in (0x30, 0x30, 0x30, 0x20): # wake up, 4-bit mode
self._write4(v)
time.sleep_ms(5)
for c in (0x28, 0x0C, 0x06, 0x01): # 2 lines, display on, clear
self._send(c, 0)
time.sleep_ms(2)
def _write4(self, v):
b = v | self.bl
self.i2c.writeto(self.addr, bytes([b | 0x04, b])) # pulse the E pin
def _send(self, v, rs):
self._write4((v & 0xF0) | rs)
self._write4(((v << 4) & 0xF0) | rs)
def line(self, row, text):
self._send(0x80 | (0x40 if row else 0x00), 0) # move to start of row
for ch in (text + " " * 16)[:16]:
self._send(ord(ch), 1)
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
lcd = LCD(i2c)
lcd.line(0, "Base station")
lcd.line(1, "Starting...")
# ---------- LoRa radio ----------
lora = UART(2, baudrate=115200, tx=17, rx=16, timeout=50)
def send_at(cmd, wait_ms=300):
lora.write(cmd + "\r\n")
time.sleep_ms(wait_ms)
r = lora.read()
text = r.decode().strip() if r else ""
print(cmd, "->", text or "(no reply)")
return text
def parse_rcv(line):
"""'+RCV=sender,length,data,RSSI,SNR' - data may contain commas."""
sender, length, rest = line[5:].split(",", 2)
n = int(length)
rssi, snr = rest[n + 1:].split(",")
return int(sender), rest[:n], int(rssi), int(snr)
def stamp():
"""Time since the base started, as hh:mm:ss (GPS/clock time comes later)."""
s = time.ticks_ms() // 1000
return "%02d:%02d:%02d" % (s // 3600, s // 60 % 60, s % 60)
send_at("AT")
send_at("AT+ADDRESS=%d" % MY_ADDR)
send_at("AT+NETWORKID=18")
send_at("AT+BAND=915000000")
send_at("AT+PARAMETER=9,7,1,12")
# ---------- state ----------
msg_count = 0
last_heard = time.ticks_ms()
field_batt = None
temp = hum = None
rssi = 0
blue_off = 0
alarm_until = time.ticks_ms()
next_lcd = 0
print(stamp(), "Base station ready, listening...")
while True:
now = time.ticks_ms()
# 1) Messages from the field station
while lora.any():
raw = lora.readline()
try:
line = raw.decode().strip()
except Exception:
continue
if not line.startswith("+RCV="):
continue # +OK etc.
sender, data, rssi, snr = parse_rcv(line)
msg_count += 1
last_heard = now
blue_off = time.ticks_add(now, 150) # quick blue flash
lora.write("AT+SEND=%d,%d,%s\r\n" % (sender, len("ACK,%d" % msg_count), "ACK,%d" % msg_count))
print("%s From %d: %s (RSSI %d dBm, SNR %d dB)" % (stamp(), sender, data, rssi, snr))
parts = data.split(",")
if parts[0] == "HB" and len(parts) >= 6:
field_batt = float(parts[2])
temp, hum = float(parts[4]), float(parts[5])
elif parts[0] == "EVT" and len(parts) >= 4:
alarm_until = time.ticks_add(now, ALARM_S * 1000)
print("%s *** EARTHQUAKE at station %s: peak %s g, STA/LTA %s ***"
% (stamp(), parts[1], parts[2], parts[3]))
next_lcd = now # refresh LCD right away
# 2) Work out the status
alarm = time.ticks_diff(alarm_until, now) > 0
no_signal = time.ticks_diff(now, last_heard) > NO_SIGNAL_S * 1000
low_batt = field_batt is not None and field_batt < LOW_BATT_V
# 3) LEDs and buzzer
green.value((now // 1000) % 2) # slow blink: 1 s on, 1 s off
blue.value(time.ticks_diff(blue_off, now) > 0)
yellow.value(no_signal or low_batt)
red.value(alarm)
buzzer.duty_u16(32768 if alarm and (now // 250) % 2 else 0) # beep-beep
# 4) LCD, once a second
if time.ticks_diff(now, next_lcd) >= 0:
next_lcd = time.ticks_add(now, 1000)
status = "QUAKE!" if alarm else "NO SIG" if no_signal else "LOWBAT" if low_batt else "OK"
if temp is None:
lcd.line(0, "Waiting... " + status)
lcd.line(1, "No data yet")
else:
lcd.line(0, "T%.1fC H%d%% %s" % (temp, hum, status))
lcd.line(1, "Bat%.2fV %ddBm" % (field_batt, rssi))
time.sleep_ms(10)
Loading
esp32-devkit-c-v4
esp32-devkit-c-v4