本專案是一個「智慧盆栽監測 + 自動/手動澆水」系統,整合 感測器資料擷取、Linux Driver(Kernel Module)、跨網路 TCP 通訊、Web 介面遠端控制,並提供完整的歷史紀錄(含自動/手動澆水事件)。
✅ 兩大功能
自動模式:定期讀取感測器;土壤太乾時自動澆水,適中則不動作。
手動模式:確認與 Server 連線狀態、遠端查看目前狀態/歷史紀錄、遠端觸發澆水(觸發前先回報當下土壤濕度並二次確認)。
本系統主要由 兩台 Raspberry Pi + 使用者裝置 組成,並透過 虛擬網路(如 ZeroTier) 讓使用者可以跨網段直接以瀏覽器存取 Web UI。
- Web Server RPi(控制中心 / Web UI + Server)
web_server.c:提供簡易 HTTP 網頁按鈕介面(查詢/澆水/歷史紀錄…)client.c:接收網頁端的需求,轉成 TCP 指令送給server.cserver.c(本 repo 為server1.c):核心 server,同時接收- Sensor RPi 上傳的感測資料(記錄到
sensor_log.csv) - 使用者查詢/控制指令(回覆目前狀態、歷史紀錄、最後澆水時間、手動澆水…)
- Sensor RPi 上傳的感測資料(記錄到
- Sensor RPi(感測與執行端)
- Kernel drivers:
timer_air_temp_driver.c:DHT22 溫溼度讀取(timer + workqueue)timer_soil_sensor.c:ADS1115 I2C ADC 讀取土壤感測電壓(timer polling)pump_driver.c:GPIO 控制水泵/繼電器(char device)
- User-space tasks:
timer_sensor_task.c(簡報:sensor_task.c):定時讀取/dev/*並更新 Shared Memorydecision_task.c:自動澆水決策 + 接收手動澆水訊號(SIGUSR1)remote_task.c:與 Web Server RPi 通訊,上傳資料、接收澆水命令
- 使用者裝置(手機/平板/筆電)
- 透過瀏覽器連線到 Web Server RPi 的
http://<virtual_ip>:8181/ - 以按鈕觸發查詢與澆水(澆水前會先顯示土壤濕度並二次確認)
📌 虛擬網路重點:ZeroTier/Remote.It 類工具會替每台設備分配「Virtual IP」,讓不同網段的裝置像在同一個 LAN 中互連(簡報中以 ZeroTier 示意)。
- ✅ 自動澆水決策(條件可調整)
- ✅ 手動遠端澆水(二次確認防誤觸)
- ✅ 即時顯示:溫度/濕度/土壤濕度、Pump 狀態
- ✅ 歷史紀錄:最近 N 筆資料、上次澆水時間(含 Auto/Manual)
- ✅ OS 技巧實作
- Linux Kernel Module:char device、GPIO、I2C、timer/workqueue、mutex
- User-space:POSIX shared memory、mmap、signals、pthread、select、多 socket 端口
- Web:手刻 HTTP server、fork/exec、IPC pipe
以下為「簡報版」的架構補充:包含 連線拓樸(使用者裝置 ↔ Web Server RPi ↔ Sensor RPi)與 程式/task 架構(Shared Memory + Signal + Driver)。
- 使用者(手機/平板/筆電)透過瀏覽器進入 Web UI
- Web UI 由 Web Server RPi 提供(
web_server.c) - Web Server RPi 與 Sensor RPi 透過虛擬網路互連(ZeroTier 示意),Sensor RPi 週期上傳感測資料並接收澆水命令
| 角色 | Port | 用途 |
|---|---|---|
| Web Server RPi(server) | 55666 |
接收 client.c 的查詢/控制命令 |
| Web Server RPi(server) | 8888 |
接收 Sensor RPi 上傳資料(struct/binary) |
| Sensor RPi(remote_task) | 8889 |
接收「澆水觸發」命令 |
| Web Server RPi(web_server) | 8181 |
提供瀏覽器操作介面 |
⚠️ IP/Port 目前多為硬編碼,跨網路部署時請務必調整(見 設定與客製化)。
- Shared Memory (
/plant_shm):sensor_task定時更新,decision_task與remote_task直接讀取 - Signals
SIGUSR1:Web 端手動澆水 → Sensor RPi 觸發decision_task執行澆水SIGUSR2:自動澆水事件通知(decision_task→remote_task),讓 server 記錄 Auto Watering log
- Drivers
air_temp_driver/soil_sensor_driver提供最新感測值給 user-spacepump_driver提供最小化 GPIO 控制界面(配合繼電器驅動水泵)
下圖是對簡報內容的「文字化」對照,方便在 GitHub 直接閱讀。
flowchart LR
U["User Devices\nBrowser"] -->|HTTP:8181| WEB["Web Server RPi\nweb_server.c"]
WEB -->|exec ./client| CLI["client.c"]
CLI -->|TCP commands| S[(server.c / server1.c)]
S -->|reply status/history| CLI
subgraph Sensor["Sensor RPi"]
DHT["/dev/dht22\nair_temp_driver"]
SOIL["/dev/soil_sensor\nADS1115 driver"]
PUMP["/dev/etx_device\npump_driver"]
ST["sensor_task\n(timer_sensor_task.c)"]
DEC["decision_task.c"]
REM["remote_task.c"]
SHM[(Shared Memory\n/plant_shm)]
DHT --> ST
SOIL --> ST
ST --> SHM
SHM --> DEC
SHM --> REM
DEC -->|write 1/0| PUMP
REM -->|SIGUSR1 manual| DEC
DEC -->|SIGUSR2 auto event| REM
end
REM -->|sensor data| S
S -->|manual watering cmd| REM
S -->|log to CSV| LOG[(sensor_log.csv)]
以下接線依照簡報中的實作圖(並對齊程式碼中的 GPIO/I2C 設定)。
- VCC → Raspberry Pi 3.3V(或依模組需求 5V)
- GND → Raspberry Pi GND
- DATA →
GPIO4(程式:timer_air_temp_driver.c的#define DHT_GPIO 4) - 建議 DATA 加 4.7k~10k 上拉至 VCC(符合 DHT22 典型接法)
- 土壤感測器(Analog out)→ ADS1115
A0 - ADS1115 ↔ Raspberry Pi(I2C-1)
- SDA →
GPIO2 (SDA1) - SCL →
GPIO3 (SCL1) - VDD → 3.3V
- GND → GND
- ADDR → GND(對應位址
0x48,程式:I2C_BOARD_INFO("ads1115", 0x48))
- SDA →
簡報圖中 ADDR 有接線(通常接地),這與程式碼使用
0x48完整對上。
- Raspberry Pi 透過 GPIO 控制繼電器,繼電器再去切換水泵電源(避免 GPIO 電壓/電流不足)
- Relay VCC → 5V(依繼電器模組規格)
- Relay GND → GND
- Relay IN →
GPIO21(程式:pump_driver.c的#define GPIO_21 21) - 水泵電源與繼電器 COM/NO 端子依你的泵/電源規格接線(注意共地與反灌電流保護)
| 模組 | Pi 端腳位 | 程式對應 |
|---|---|---|
| DHT22 DATA | GPIO4 | timer_air_temp_driver.c |
| ADS1115 SDA/SCL | GPIO2 / GPIO3 (I2C-1) | timer_soil_sensor.c |
| ADS1115 ADDR | GND(0x48) | timer_soil_sensor.c |
| Relay IN(Pump) | GPIO21 | pump_driver.c |
(以下以本次上傳的檔案命名為主)
.
├── server1.c # 中央 server:收感測 + 收 client cmd + 記錄 csv
├── client.c # client:發送文字 cmd 到 server1
├── web_server.c # 簡易 HTTP server:網頁按鈕 -> 呼叫 client -> 回傳結果
│
├── docs/
│ ├── architecture_network.png # (from slides) 使用者裝置 ↔ Web/Sensor RPi 拓樸
│ ├── architecture_tasks.png # (from slides) task/driver/IPC 架構
│ └── wiring.png # (from slides) 硬體接線圖
├── timer_air_temp_driver.c # Kernel module:DHT22 driver(timer + workqueue)
├── timer_soil_sensor.c # Kernel module:ADS1115 soil driver(timer polling)
├── pump_driver.c # Kernel module:GPIO21 pump char driver
│
├── timer_sensor_task.c # user daemon:讀 /dev/* -> 寫入 shared memory
├── decision_task.c # user daemon:自動澆水決策 + manual(SIGUSR1) 澆水
├── remote_task.c # user daemon:上傳資料到 server + 接收澆水命令(8889)
│
└── sensor_rpi_run_timer.sh # 一鍵載入模組 + tmux 同時跑三支 user 程式```
---
## 快速開始
> 以下指令示意以 Linux / Raspberry Pi OS 為主。
> Kernel module 載入/卸載需要 root 權限(`sudo`)。
### A) Web Server RPi / VM 端(server1/server + client + web_server)
> 簡報中此三支程式部署在 **Web Server RPi**(同一台負責 Web UI 與核心 server)。若你改放在 VM/PC 端,流程不變,只需調整 IP/Port。
#### 1) 編譯
```bash
gcc -O2 -Wall -o server1 server1.c
gcc -O2 -Wall -o client client.c
gcc -O2 -Wall -o web_server web_server.c
./server1
# Server listening on ports 55666 (client) and 8888 (sensor)..../web_server
# 預設監聽 8181用瀏覽器開啟:
http://<web_server_ip>:8181/
在每個 driver 檔案同目錄準備 Makefile(可共用):
obj-m += timer_air_temp_driver.o
obj-m += timer_soil_sensor.o
obj-m += pump_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean編譯:
makegcc -O2 -Wall -pthread -o timer_sensor_task timer_sensor_task.c
gcc -O2 -Wall -pthread -o remote_task remote_task.c
gcc -O2 -Wall -o decision_task decision_task.cchmod +x sensor_rpi_run_timer.sh
./sensor_rpi_run_timer.sh此 script 會:
rmmod/insmod重新載入三個.ko- 清除 shared memory 與 pid 檔
- 用 tmux 同時跑:
timer_sensor_task(sudo)remote_taskdecision_task(sudo)
角色
- 同時監聽兩個 port(
select()多工)CLIENT_PORT=55666:接收文字命令(connect/current/history/lastwater/watering)SENSOR_PORT=8888:接收 Sensor RPi 上傳的SensorData結構(binary)
核心機制
select():同一個 thread 監聽兩個 listening socketlatest_data:快取最新一筆感測資料供current查詢sensor_log.csv:所有資料落地(含 Auto/Manual Watering 標記)
支援命令(與 client.c 對應)
checking......\n→ 回覆Server connection OK.current state of plant...\n→ 回覆最新溫/濕/土壤濕度watering......!\n→ 記錄 Manual Watering,並轉發到 Sensor RPi:172.26.213.194:8889historical record...\n→ 回覆最近 20 筆紀錄(HISTORY_LINES=20)last watered time......\n→ 回覆上一次 Auto/Manual Watering 的 timestampwatering check\n→ 回覆「目前土壤濕度 + 是否仍要澆水」
⚠️ 注意:程式啟動時會用"w"重新建立sensor_log.csv(會覆蓋舊檔)。若希望保留舊資料,請改為"a"或加上日期檔名。
角色
- 依
argv[1]決定要送的命令字串,連線到固定SERVER_IP/SERVER_PORT
可用指令
./client connect
./client current
./client watering_check
./client watering_force
./client history
./client lastwater
⚠️ SERVER_IP與SERVER_PORT是硬編碼(預設172.26.107.26:55666),跨網路部署時務必修改。
角色
- 在
WEB_SERVER_PORT=8181提供網頁按鈕介面 - 收到
GET /client?cmd=...後:fork()- 子行程
execl("./client", "./client", arg, NULL) - 透過
pipe()把 client 的 stdout 抓回來 - 回傳給瀏覽器(text/plain 或 HTML)
二次確認澆水
- 首次按「Watering」→
cmd=watering_check:- 先顯示
The Soil Moisture status is XX %. Still watering? - 提供
Yes -> cmd=watering_force/No -> 回首頁
- 先顯示
cmd=watering_force會真正觸發client watering_force→ 送出watering......!\n
OS 技巧
fork + exec + pipe + dup2:把子行程輸出當成 Web API 的 response- 無需額外 framework,單檔就能跑的 demo 型 Web 控制台
做了什麼
- 以 GPIO bit-banging 讀 DHT22 的 40-bit 資料(含 checksum)
- 使用 Timer + Workqueue 避免在 Timer callback 中做長時間 busy-wait
- 每
INTERVAL_SEC=2秒更新一次快取值
提供的介面
- 建立 char device:
/dev/dht22 - user-space
read()會得到字串:Temp:[25.3], Hum:[60.4]
OS 技巧
timer_list:週期性排程work_struct:把耗時工作丟到 process contextmutex:保護快取溫溼度資料,避免 read 與更新互搶
做了什麼
- 透過 Linux I2C API 與 ADS1115 溝通:
- 送 config 到 register
0x01 - 讀 conversion register
0x00
- 送 config 到 register
- 每 1 秒更新一次
latest_voltage_mv read()直接回傳快取電壓(字串,單位 Volt)
提供的介面
- 建立 char device:
/dev/soil_sensor(device 名稱依DEVICE_NAME) - 回傳格式:
2.345\n(表示 2.345V)
⚠️ 你目前的timer_sensor_task.c使用/dev/timer_soil_sensor,若實際節點是/dev/soil_sensor,請把路徑改一致。
做了什麼
- 讀取:
/dev/dht22/dev/soil_sensor(或/dev/timer_soil_sensor,視你的 device node)
- 土壤電壓轉濕度百分比(線性換算):
1.333V = 100%、3.300V = 0%
- 以 POSIX SHM 建立共享記憶體:
shm_open("/plant_shm")mmap()讓其他程序(decision/remote)直接讀取最新狀態
OS 技巧
- POSIX Shared Memory:跨 process 的高效共享
mmap:避免使用檔案/pipe 反覆序列化
做了什麼
- 建立 char device:
/dev/etx_device write()接收'1'/'0'控制 GPIO21 高/低read()回傳 GPIO21 目前狀態(0/1)
OS 技巧
- 完整 char device life-cycle:
alloc_chrdev_region/cdev_addclass_create/device_createcopy_from_user/copy_to_user
- GPIO API:
gpio_request/gpio_set_value/gpio_get_value - 也示範
gpio_export()讓 GPIO 出現在 sysfs(便於 debug)
資料來源
- 從
/plant_shm讀取最新感測資料(mmap 共享記憶體)
自動澆水條件(可改)
soil_moisture < 60
temperature > 23.0
humidity < 60.0執行澆水
- 呼叫
write_gpio(1)開啟 Pump →sleep(5)→write_gpio(0)關閉 - 自動澆水時:
shared_data->water_flag = 1kill(remote_pid, SIGUSR2)通知remote_task(讓下一次上傳帶上 Auto flag)
手動澆水
- 收到
SIGUSR1→ 澆水 5 秒
OS 技巧
- Signal:
SIGUSR1(manual)、SIGUSR2(同步 auto flag) - IPC:pid 檔(
decision_pid.txt/remote_pid.txt)讓 process 互相找到對方 pid - 透過 driver node
/dev/etx_device控制硬體
兩個執行緒
-
sensor_sender_thread- 每 2 秒連線到 Server
DATA_PORT=8888 - 傳送
SensorDatastruct(binary) - 送完把
water_flag清成 0
- 每 2 秒連線到 Server
-
watering_receiver_thread- 在本機
CMD_PORT=8889listen - 收到
watering......!\n→kill(decision_pid, SIGUSR1)觸發手動澆水
- 在本機
OS 技巧
pthread:同時做「週期上傳」與「命令接收」- TCP server/client 雙角色
- Signal handler:
SIGUSR2收到 auto watering 通知時把water_flag設 1
做的事情:
- 重新載入三個 kernel module(避免舊狀態殘留)
- 清除
/dev/shm/plant_shm與 pid 檔 - 使用 tmux 同時啟動三支 daemon,方便同時看 log:
timer_sensor_taskremote_taskdecision_task
Server 端 server1.c 的 SensorData:
typedef struct {
float temperature;
float humidity;
int soil_moisture;
time_t timestep;
int water_flag; // 1 表示 Auto Watering 事件
} SensorData;
⚠️ 你的timer_sensor_task.c目前 struct 少了water_flag欄位。
目前做法「能動」是因為 remote/decision 會自行維護water_flag,但建議統一 struct 版本(見 能精進的部分)。
- 正常資料:
timestamp, ip, T, H, M - 自動澆水:同一行末尾加上
"<-- Auto Watering" - 手動澆水:寫入
"<-- Manual Watering"(T/H/M 會填 -1)
client.cSERVER_IPSERVER_PORT
server1.cCLIENT_PORT=55666SENSOR_PORT=8888- 轉發澆水到 Sensor RPi 的 IP:
172.26.213.194:8889
remote_task.cSERVER_IPDATA_PORT=8888CMD_PORT=8889
web_server.cWEB_SERVER_PORT=8181
decision_task.c→should_water()
你可以改成更貼近植物需求的策略,例如:- 加入「澆水冷卻時間」(避免短時間連續澆水)
- 僅以土壤濕度判斷,或加入光照/季節補償
-
/dev 節點不存在
- 確認
.ko是否成功insmod - 看
dmesg | tail -n 50 lsmod | grep -E "dht22|soil|pump|timer"
- 確認
-
permission denied
- 讀寫
/dev/*或載入 module 需要sudo - 建議把程式改成 systemd service + udev rule(見加分項)
- 讀寫
-
土壤 device node 名稱不一致
timer_soil_sensor.c建立的是/dev/soil_sensortimer_sensor_task.c卻讀/dev/timer_soil_sensor- 請統一其中一邊
-
歷史紀錄一直被清掉
server1.c每次啟動會用"w"重建sensor_log.csv- 改成
"a"或改成每天一個 log 檔
-
網路連不上
- 確認 Server IP/Port 是否正確(跨網路需 NAT/Port Forwarding/ZeroTier/Remote.It 等)
- 防火牆:
ufw status/iptables -L - 先用
telnet <ip> <port>或nc -vz <ip> <port>測試
-
統一 SensorData struct 定義
- 建議建立
include/sensordata.h由所有程式共用 - 避免 shared memory / network struct 不一致造成 padding/對齊問題
- 建議建立
-
通訊協定改成可擴充格式
- 目前 sensor 上傳是直接傳 binary struct(受 endian、對齊影響)
- 可改成 JSON/CBOR/Protobuf 並加上版本欄位
-
Web Server 增加基本安全性
- 加上 token / 簡易登入,避免同網段任何人都能觸發澆水
- 澆水加入 Rate limit / Cooldown
-
改用 systemd 管理 daemon
- 取代 tmux script,使開機自動啟動、可自動重啟、log 可由 journal 管理
-
更精準的控制策略
- Pump 改用 PWM/定量(例如 flow sensor 或計時+校正)
- 加入「澆水後等待土壤回應時間」再二次判斷
-
Driver 更完整
- soil driver 可加 ioctl 設定 ADS1115 通道/增益/取樣率
- dht driver 可加 error counter、並把最後一次成功時間公開
pump_driver.c參考了 EmbeTronicX 的 GPIO char driver 範例(檔頭標示作者)。- 其他程式與 driver 為本專案整合與實作。


