Hệ Thống Giám Sát Thông Minh Ruộng Lúa

Hệ Thống Giám Sát Thông Minh Ruộng Lúa (còn gọi là "Hệ Thống Quản Lý Nông Nghiệp Thông Minh" hoặc "Hệ Thống Giám Sát Canh Tác Lúa") là giải pháp kết hợp IoT (Internet of Things) + Cloud Platform + Big Data Nông Nghiệp + AI để quản lý ruộng lúa một cách tự động.

Mục Tiêu Chính Của Hệ Thống

Thông qua các cảm biến, mạng truyền thông và thuật toán thông minh, hệ thống thu thập, phân tích và điều khiển các thông số như mực nước, nhiệt độ nước, độ ẩm đất, dữ liệu thời tiết theo thời gian thực, từ đó:

  • Tưới tiêu và thoát nước tự động;
  • Quyết định bón phân và phun thuốc bảo vệ thực vật chính xác;
  • Giám sát video từ xa và cảnh báo tự động;
  • Tiết kiệm nước, năng lượng, tăng năng suất và chất lượng.

Kiến Trúc Hệ Thống (Ba Tầng Điển Hình)

Tầng Cảm Biến (Phần Cứng)

Chịu trách nhiệm thu thập dữ liệu và thực thi điều khiển:

  • Cảm biến:
  • Cảm biến mực nước
  • Cảm biến độ ẩm đất
  • Cảm biến nhiệt độ nước/không khí/ánh sáng/pH/oxy hòa tan
  • Thiết bị điều khiển:
  • Van điện từ, bơm nước, bộ truyền động cống
  • Camera (hồng ngoại/nhìn thấy)
  • Module truyền thông:
  • LoRa, NB-IoT, 4G/5G để truyền dữ liệu

Tầng Mạng và Truyền Thông

  • LoRaWAN, NB-IoT, 4G, WiFi để truyền dữ liệu;
  • Thường kết hợp cổng giao tiếp biên (Raspberry Pi, ESP32, STM32+NB module) để xử lý cục bộ;
  • Các giao thức phổ biến: MQTT, HTTP, Modbus, CoAP.

Tầng Nền Tảng và Ứng Dụng (Phần Mềm)

Chạy trên server hoặc cloud:

  • Hiển thị dữ liệu thời gian thực (dashboard, biểu đồ)
  • Điều khiển từ xa và thực thi chiến lược (logic tưới tiêu tự động)
  • Cảnh báo và thông báo (tin nhắn/notification khi vượt ngưỡng)
  • Lưu trữ và phân tích dữ liệu lịch sử (database + visualization)
  • Dự đoán học máy (quyết định thông minh dựa trên thời tiết)

Công Nghệ và Công Cụ (Dùng Cho Đồ Án)

Module Công Nghệ/Nền Tảng Mô Tả
Thu thập dữ liệu STM32 / ESP32 / Arduino + cảm biến Thu thập và truyền dữ liệu
Truyền thông không dây LoRa / NB-IoT / WiFi / 4G Chọn theo môi trường và khoảng cách
Gateway/Tính toán biên Raspberry Pi / Jetson Nano Xử lý trước, logic điều khiển
Server Flask / Django / Node.js / SpringBoot API và lưu trữ dữ liệu
Database MySQL / InfluxDB / MongoDB Lưu trữ thời gian thực và lịch sử
Frontend Vue / React / ECharts Dashboard giám sát, biểu đồ
Cloud Platform 阿里云 IoT / ThingsBoard / OneNet Nền tảng IoT nhanh
AI TensorFlow / PyTorch / Sklearn Dự đoán tưới tiêu, nhận dạng sâu bệnh

Chức Năng Hệ Thống

Module Chức Năng Mô Tả
Giám sát thời gian thực Hiển thị mực nước, nhiệt độ, độ ẩm, thời tiết từng thửa ruộng
Giám sát video Xem hình ảnh hiện trường từ xa
Điều khiển tự động Tự động bật/tắt van hoặc bơm theo ngưỡng cài đặt
Hệ thống cảnh báo Gửi thông báo khi mực nước/nhiệt độ vượt ngưỡng
Phân tích dữ liệu Biểu đồ xu hướng, so sánh lịch sử, báo cáo tự động
Dự đoán và đề xuất AI phân tích thời điểm tưới tiêu hoặc lượng phân bón tối ưu

Hướng Mở Rộng (Nâng Cao Dự Án)

  • Tích hợp module nhận dạng hình ảnh (nhận dạng sâu bệnh, thay đổi màu lá);
  • Phát triển App điện thoại hoặc Mini App để điều khiển;
  • Kết hợp với hệ thống bay drone kiểm tra;
  • Liên kết với API cục khí tượng hoặc trạm thời tiết địa phương;
  • Kết nối kho kiến thức nông nghiệp để ra quyết định thông minh.

Sơ Đồ Cấu Trúc Hệ Thống (Logic)

Tầng Cảm Biến/Thiết Bị → Gateway/Biên → Cloud Server (Database+API) → Web/Mobile

Ví dụ:

  • ESP32 thu thập mực nước → Gửi qua MQTT lên阿里云 IoT;
  • Cloud xử lý quy tắc nếu mực nước thấp → Gửi lệnh → ESP32 điều khiển bơm;
  • Đồng thời hiển thị biểu đồ và nhật ký trên trang Web.

Hướng Dự Án Phù Hợp (Theo Độ Khó)

Độ Khó Hướng Dự Án Mô Tả
⭐ Cơ bản Mực nước + Nhiệt độ/Độ ẩm + Điều khiển van 1-2 thửa ruộng, điều khiển ngưỡng đơn giản
⭐⭐ Nâng cao Thêm LoRa/NB-IoT + Cloud Platform Thu thập từ xa nhiều node
⭐⭐⭐ Hoàn chỉnh Dashboard Cloud + AI dự đoán + Điều khiển tự động Hệ thống IoT tổng hợp (phù hợp đồ án tốt nghiệp)
import random
import time
from datetime import datetime

class RiceFieldMonitor:
    def __init__(self):
        # Khởi tạo dữ liệu cảm biến mô phỏng
        self.water_depth = random.uniform(5.0, 10.0)  # Độ sâu nước (cm)
        self.water_temp = random.uniform(20.0, 30.0)  # Nhiệt độ nước (℃)
        self.air_moisture = random.uniform(70.0, 90.0)  # Độ ẩm không khí (%)
        self.valve_status = False  # Trạng thái van
        self.pump_status = False   # Trạng thái bơm

        # Cấu hình ngưỡng
        self.min_water_level = 6.0
        self.max_water_level = 9.0

    def update_sensors(self):
        """Mô phỏng thay đổi dữ liệu cảm biến"""
        # Mô phỏng biến động tự nhiên
        self.water_depth += random.uniform(-0.2, 0.2)
        self.water_temp += random.uniform(-0.1, 0.1)
        self.air_moisture += random.uniform(-0.5, 0.5)
        # Đảm bảo giá trị trong phạm vi hợp lý
        self.water_depth = max(0, min(self.water_depth, 15))
        self.water_temp = max(10, min(self.water_temp, 40))
        self.air_moisture = max(0, min(self.air_moisture, 100))

    def process_control(self):
        """Xử lý logic điều khiển theo mực nước"""
        if self.water_depth < self.min_water_level:
            self.pump_status = True
            self.valve_status = True
            action = "💧 Mực nước thấp → Bật bơm bổ sung nước"
        elif self.water_depth > self.max_water_level:
            self.pump_status = False
            self.valve_status = False
            action = "⚠️ Mực nước cao → Tắt bơm tránh ngập úng"
        else:
            action = "✅ Mực nước bình thường → Duy trì trạng thái"
        return action

    def display_status(self):
        """Hiển thị trạng thái thời gian thực"""
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"\n[{now}]")
        print(f"Mực nước: {self.water_depth:.2f} cm  |  Nhiệt độ: {self.water_temp:.1f}℃  |  Độ ẩm: {self.air_moisture:.1f}%")
        print(f"Trạng thái bơm: {'Bật' if self.pump_status else 'Tắt'} | Van: {'Mở' if self.valve_status else 'Đóng'}")

    def push_to_cloud(self):
        """Mô phỏng đẩy dữ liệu lên đám mây"""
        # Chỉ in ra dữ liệu mô phỏng
        print("☁️  Đang tải dữ liệu lên nền tảng cloud……")

# ========== Chương Trình Chính ==========
if __name__ == "__main__":
    field = RiceFieldMonitor()
    print("🌾 Hệ thống giám sát ruộng lúa khởi động...\n")

    for i in range(10):  # Mô phỏng 10 chu kỳ thu thập
        field.update_sensors()
        action = field.process_control()
        field.display_status()
        print(action)
        field.push_to_cloud()
        time.sleep(2)  # Chờ 2 giây mô phỏng giám sát thời gian thực


# app.py
import streamlit as st
import pandas as pd
import numpy as np
import time
from datetime import datetime, timedelta
import plotly.express as px
import plotly.graph_objects as go
import pydeck as pdk

st.set_page_config(layout="wide", page_title="Màn Hình Giám Sát Ruộng Lúa", initial_sidebar_state="collapsed")

# --------------------------
# Cấu Hình & Hàm Hỗ Trợ
# --------------------------
@st.cache_data
def generate_fake_data(num_points=120, field_center=(31.0, 120.0)):
"""
Tạo dữ liệu chuỗi thời gian mô phỏng (1 điểm/phút, mặc định 120 điểm=2 giờ)
Cảm biến: Độ ẩm đất(%), Mực nước(cm), EC, Nhiệt độ không khí(°C), Độ ẩm không khí(%)
Mỗi điểm có tọa độ (phân bố quanh thửa ruộng)
"""
now = datetime.utcnow()
timestamps = [now - timedelta(minutes=(num_points - 1 - i)) for i in range(num_points)]
lat0, lon0 = field_center
data = []
sensor_count = 6  # Giả sử 6 node cảm biến
for t in timestamps:
for n in range(sensor_count):
lat = lat0 + np.random.uniform(-0.0008, 0.0008)
lon = lon0 + np.random.uniform(-0.0012, 0.0012)
soil_moist = np.clip(45 + 10*np.sin((t.minute + n)/12.0) + np.random.normal(0,2), 10, 90) # %
water_level = np.clip(12 + 3*np.cos((t.minute + n)/8.0) + np.random.normal(0,0.6), 0, 30) # cm
ec = np.clip(0.6 + 0.2*np.sin((t.minute + n)/6.0) + np.random.normal(0,0.03), 0.2, 2.0) # mS/cm
atmp = np.clip(22 + 6*np.sin((t.hour + n)/3.0) + np.random.normal(0,0.7), -5, 45)
ahum = np.clip(60 + 20*np.cos((t.minute + n)/15.0) + np.random.normal(0,3), 10, 100)
data.append({
"timestamp": t,
"node": f"node_{n+1}",
"lat": lat,
"lon": lon,
"soil_moist": round(soil_moist,2),
"water_level": round(water_level,2),
"ec": round(ec,3),
"air_temp": round(atmp,2),
"air_hum": round(ahum,1),
})
df = pd.DataFrame(data)
return df

def latest_metrics(df):
latest = df.sort_values("timestamp").groupby("node").tail(1)
return {
"avg_soil_moist": latest["soil_moist"].mean(),
"avg_water_level": latest["water_level"].mean(),
"avg_ec": latest["ec"].mean(),
"avg_air_temp": latest["air_temp"].mean(),
"avg_air_hum": latest["air_hum"].mean(),
"last_update": latest["timestamp"].max()
}

def small_kpi_card(label, value, unit=""):
st.metric(label, f"{value:.2f} {unit}" if isinstance(value, (int,float)) else f"{value} {unit}")

# --------------------------
# Chuẩn Bị Dữ Liệu
# --------------------------
# Entry dữ liệu mô phỏng hoặc thực: thay dòng dưới để接入dữ liệu thực
df = generate_fake_data(num_points=240, field_center=(31.2304, 121.4737))  # Điều chỉnh số điểm / tọa độ trung tâm

metrics = latest_metrics(df)

# --------------------------
# Bố Cục Trang (Phong Cách Dashboard)
# --------------------------
st.title("🌾 Màn Hình Trực Quan Ruộng Lúa Thông Minh")
st.subheader(f"Thời điểm cập nhật dữ liệu cuối (UTC): {metrics['last_update'].strftime('%Y-%m-%d %H:%M:%S')}")

# Hàng KPI phía trên
kpi_col1, kpi_col2, kpi_col3, kpi_col4, kpi_col5 = st.columns([1.4,1.2,1,1,1])
with kpi_col1:
small_kpi_card("Độ ẩm đất trung bình", metrics["avg_soil_moist"], "%")
with kpi_col2:
small_kpi_card("Mực nước trung bình", metrics["avg_water_level"], "cm")
with kpi_col3:
small_kpi_card("EC trung bình", metrics["avg_ec"], "mS/cm")
with kpi_col4:
small_kpi_card("Nhiệt độ không khí TB", metrics["avg_air_temp"], "°C")
with kpi_col5:
small_kpi_card("Độ ẩm không khí TB", metrics["avg_air_hum"], "%")

st.markdown("---")

# Trái: Biểu đồ xu hướng, Phải: Bản đồ & chi tiết node
left, right = st.columns([2.2,1])
with left:
st.markdown("### Xu Hướng Thời Gian Cảm Biến")
# Người dùng chọn khoảng thời gian và chỉ tiêu
nodes = sorted(df["node"].unique().tolist())
selected_node = st.selectbox("Chọn node (All=Toàn bộ)", ["All"] + nodes, index=0)
metric_options = ["soil_moist", "water_level", "ec", "air_temp", "air_hum"]
metric_map = {
"soil_moist": "Độ ẩm đất (%)",
"water_level": "Mực nước (cm)",
"ec": "EC (mS/cm)",
"air_temp": "Nhiệt độ không khí (°C)",
"air_hum": "Độ ẩm không khí (%)"
}
selected_metrics = st.multiselect("Chọn chỉ tiêu hiển thị (chọn nhiều)", metric_options, default=["soil_moist","water_level"])
time_window = st.slider("Khoảng thời gian (phút)", min_value=30, max_value=1440, value=240, step=30)

cutoff = df["timestamp"].max() - timedelta(minutes=time_window)
df_window = df[df["timestamp"] >= cutoff]
if selected_node != "All":
df_window = df_window[df_window["node"] == selected_node]

# Plotly Line Chart — Nhiều chỉ tiêu cùng biểu đồ (dùng trục phụ)
fig = go.Figure()
colors = px.colors.qualitative.T10
for i, m in enumerate(selected_metrics):
# Tổng hợp trung bình theo thời gian (across nodes)
agg = df_window.groupby("timestamp")[m].mean().reset_index()
fig.add_trace(go.Scatter(
x=agg["timestamp"],
y=agg[m],
name=metric_map[m],
yaxis="y1" if i < 2 else "y2"  # 2 chỉ tiêu đầu đặt trục chính, còn lại đặt trục phụ
))
fig.update_layout(
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
margin={"l":30,"r":10,"t":30,"b":30},
xaxis_title="Thời gian (UTC)",
yaxis=dict(title="Chỉ tiêu chính"),
yaxis2=dict(title="Chỉ tiêu phụ", overlaying="y", side="right")
)
st.plotly_chart(fig, use_container_width=True)

# So sánh node (Box/Violin)
st.markdown("#### So Sánh Phân Bố Node (Gần nhất)")
compare_metric = st.selectbox("Chọn chỉ tiêu so sánh", metric_options, index=0)
box_fig = px.box(df_window, x="node", y=compare_metric, points="all", title=f"{metric_map[compare_metric]} theo từng node")
st.plotly_chart(box_fig, use_container_width=True)

with right:
st.markdown("### Bản Đồ Ruộng & Chi Tiết Node")
latest = df.sort_values("timestamp").groupby("node").tail(1)
# Dùng streamlit.pydeck hiển thị heatmap/node
midpoint = (latest["lat"].mean(), latest["lon"].mean())
st.markdown(f"Vị trí trung tâm:{midpoint[0]:.6f}, {midpoint[1]:.6f}")
# Lớp pydeck
layer = pdk.Layer(
"ScatterplotLayer",
data=latest,
get_position='[lon, lat]',
get_fill_color='[255 * (1 - (soil_moist-10)/80), 80 + ((soil_moist-10)/80)*175, 60]',
get_radius=50,
pickable=True
)
view_state = pdk.ViewState(latitude=midpoint[0], longitude=midpoint[1], zoom=16, pitch=30)
deck = pdk.Deck(layers=[layer], initial_view_state=view_state, tooltip={"text":"Node: {node}\nĐộ ẩm đất: {soil_moist}%\nMực nước: {water_level} cm\nEC: {ec}"})
st.pydeck_chart(deck)

st.markdown("#### Giá Trị Mới Nhất Node")
st.dataframe(latest[["node","lat","lon","soil_moist","water_level","ec","air_temp","air_hum","timestamp"]].reset_index(drop=True))

# --------------------------
# Quy Tắc Cảnh Báo & Tự Động Hóa (Ví Dụ)
# --------------------------
st.markdown("---")
st.markdown("## Quy Tắc Cảnh Báo & Liên Kết Tự Động (Ví Dụ)")
col_a, col_b, col_c = st.columns(3)
with col_a:
soil_thresh = st.number_input("Ngưỡng dưới độ ẩm đất(%)", value=20.0)
with col_b:
water_thresh = st.number_input("Ngưỡng dưới mực nước(cm)", value=3.0)
with col_c:
ec_thresh = st.number_input("Ngưỡng trên EC (mS/cm)", value=1.5)

# Tính toán cảnh báo
latest_all = df.sort_values("timestamp").groupby("node").tail(1)
alerts = []
for _, row in latest_all.iterrows():
if row["soil_moist"] < soil_thresh:
alerts.append((row["node"], "Độ ẩm đất thấp", row["soil_moist"]))
if row["water_level"] < water_thresh:
alerts.append((row["node"], "Mực nước thấp", row["water_level"]))
if row["ec"] > ec_thresh:
alerts.append((row["node"], "EC cao", row["ec"]))

if alerts:
st.error(f"Phát hiện {len(alerts)} cảnh báo bất thường:")
for a in alerts:
st.write(f"- Node {a[0]}:{a[1]} = {a[2]}")
else:
st.success("Hiện tại không có cảnh báo")

# --------------------------
# Xuất Dữ Liệu Lịch Sử & Truy Vấn Tùy Chỉnh
# --------------------------
st.markdown("---")
st.markdown("### Xuất Dữ Liệu Lịch Sử")
export_period = st.selectbox("Khoảng thời gian xuất", ["1 giờ gần nhất","6 giờ gần nhất","24 giờ gần nhất","Toàn bộ"], index=2)
if export_period == "1 giờ gần nhất":
dr = df["timestamp"].max() - timedelta(hours=1)
elif export_period == "6 giờ gần nhất":
dr = df["timestamp"].max() - timedelta(hours=6)
elif export_period == "24 giờ gần nhất":
dr = df["timestamp"].max() - timedelta(hours=24)
else:
dr = df["timestamp"].min() - timedelta(seconds=1)
export_df = df[df["timestamp"] >= dr]
st.write(f"Bản ghi khớp: {len(export_df)} 条")
@st.cache_data
def to_excel_bytes(df):
import io
with io.BytesIO() as output:
writer = pd.ExcelWriter(output, engine="openpyxl")
df.to_excel(writer, index=False, sheet_name="history")
writer.save()
return output.getvalue()

if st.button("Tải Excel"):
b = to_excel_bytes(export_df)
st.download_button("Nhấn để tải lịch sử.xlsx", data=b, file_name="rice_field_history.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

Đăng vào ngày 30 tháng 5 lúc 07:09