顯示具有 硬體電路學習 標籤的文章。 顯示所有文章
顯示具有 硬體電路學習 標籤的文章。 顯示所有文章

2026年10月3日 星期六

Pi 3 的資源與 GX10 透過 Tailscale 外網溝通

把之前的文章 RaspberryPi Project - MagicMirror Function 用我的gx10做控制

這台 Raspberry Pi 3(1GB RAM、29GB SD 卡)在機架裡只有一項工作:當 Magic Mirror 2 的鏡面時鐘。而控制這台 Pi 3 的「大腦」是遠端的 ASUS Ascent GX10——兩台機器靠 Tailscale 建立一條加密的專有網路,無論在哪個物理位置都能直接對話。

一、Pi 3 的資源盤點

硬體規格

項目 值
CPU BCM2837 quadri-core(4 核 aarch64)
RAM 1GB(現用 527MB / 剩 378MB 可用)
磁碟 SD 卡 29GB,已用 20GB(70%)
溫 64.5°C(mirror 滿載下屬正常)
顯示 HDMI → 類比鏡面(lightdm + X11)
網路 Wi-Fi(區網)+ Tailscale(100.123.106.93)

資源使用現狀

Mem:  1.9G total, 527Mi used, 378Mi available
SD:   20G / 29G (70%)
temp: 64.5°C

1GB RAM 跑 Magic Mirror 2(Node + Electron + X 桌面)已經用到 55%——沒有餘裕再加大模型或瀏覽器多 tab。這台的定位是「只做一件事:時鐘 + 天氣 + 行事曆 + 新聞」,不適合當 LLM 或 Docker 主機。

二、Magic Mirror 2 的資源使用(鏡面程式)

mm2 在 1GB 機子上跑起來不輕——Node 後端 + Electron 前置 + 9 個 module(clock、weather、calendar、newsfeed、compliments、GoogleAssistant、iFrame、alert、updatenotification)吃掉了 527MB。

優化建議(如想省):
1. 把 updatenotification 關掉(每小時拉 git 檢查,吃 CPU)
2. weather 更新間隔改 30 分鐘(預設 15)
3. newsfeed 關掉或改 30 分鐘
4. 關 X 桌面用 start_x11=false(不需要的話)

三、GX10 側的資源(對比)

項目 GX10
CPU 20 核 ARM
RAM 121GB(統一記憶體)
GPU NVIDIA GB10(1 PFLOP AI)
角色 大腦(地端 LLM、監控、AI 決策)

Pi 3 是終端顯示器 + 輕量觸角;GX10 是中央處理 + 決策。兩者分工明確,互不干擾。

四、Tailscale 外網溝通的建置

為什麼選 Tailscale 而不是 VPN/Port Forward

方案 問題
Port Forward 需路由器開埠、動態 IP、攻擊面大
自建 VPN 需一台常開的中繼機
Tailscale 零配置、端到端加密、IP 固定(100.x)、任何網路可連 ✅

雙端 Tailnet 位址

GX10 : 100.69.83.97    (linux,  帳號 atstest666666)
Pi3  : 100.123.106.93  (linux, 帳號 atstest666666)

連線實測(區網 vs Tailnet)

路徑 延遲 用途
GX10 ↔ Pi3 區網(192.168.1.25 ↔ .236) 2-4ms 日常本地控制
GX10 ↔ Pi3 Tailnet(100.x) 88-323ms 出差/外網也能操控

外網路徑走 WireGuard 加密 + DERP 中繼(Pi 3 和 GX10 同時連到 Tailscale 的 DERP relay),雖然比區網慢 30 倍,但免設防火牆、免開埠、免動態 DNS——出差時一樣能 ssh pi@100.123.106.93 操作鏡面。

五、外網溝通的三種典型場景

場景 A:遠端 SSH 改鏡面

# 在出差酒店的筆電 / 手機 Termux — 只要同一 Tailscale 帳號
ssh pi@100.123.106.93
# 改 config.js、清日誌、重啟 mm2
pm2 restart mm2

場景 B:GX10 自動下達(今天的實作)

GX10 上的 AI(gemma4:e4b) → Tailnet HTTP → Pi3 mm2 網頁/模組

mm2 有 http://127.0.0.1:8080,GX10 可用 http://100.123.106.93:8080 遠端觸發 mm2 的 action(要開埠到 0.0.0.0 才有用;建議只開區網,重要操作走 SSH)。

場景 C:檔案交換(備份/圖檔)

# Pi3 → GX10
scp pi@100.123.106.93:/home/pi/20261003.tar.gz ~/backups/
# GX10 → Pi3(推圖給鏡面用)
scp photos.jpg pi@100.123.106.93:/home/pi/MagicMirror/assets/

六、安全模型

層 保護
傳輸 WireGuard 端到端加密(AES-256)
身份 Tailscale 帳號 + 裝置金鑰
來源限制 同一 Tailnet 才能看到 100.x,外部網路完全看不到這台 Pi
SSH 雙重金鑰(本機 key + 遠端 key)免密碼
防火牆 SD 卡上的 ufw 可選開;因為 100.x 已在 WireGuard 保護下,ufw 不必太嚴

重點:Pi 3 的公網 IP 可以是 0(完全 NAT 後面),外部掃埠根本掃不到 100.x——這是 Tailscale 最大的優勢。

七、實戰:今天完成的完整鏈路

[出差手機/家用筆電]
        ↓ (Tailscale, WireGuard)
     Pi 3 (100.123.106.93)
        ↓ SSH + 備份 tar
[出差中的 GX10, 100.69.83.97]
        ↓ (同一 Tailnet 或區網)
     鏡面(時鐘/天氣/行事曆)

今天實際發生過的:
– 17:04 區網 Wi-Fi 斷線 → Tailscale 路徑不受影響
– 18:00 備份 Pi3 鏡面 216MB → Tailscale scp 拉回 GX10
– 22:00 遠端 SSH 改 mm2(更新嘗試、中止、還原、重啟)— 全程 100.123.106.93,不用區網

八、結語

Pi 3 只有 1GB RAM,是最弱的一台;透過 Tailscale 跟 GX10 聯手,卻能從任何地方被控制、被備份、被遠端維護——弱機器也能當強終端。Tailscale 在這裡的核心價值不是「快」,而是「免配置 + 加密 + 固定位址」這三件事組合起來的無腦可達性。

一句話:區網是日常,Tailnet 是出差/急難,同一條 SSH 命令、同一份備份檔案、同一個遠端鏡面——不用記 IP 切換、不用開任何埠。

Pi 5 GPIO 控制踩坑實錄:AI 大模型下達指令的「GPIO busy」真相

上篇文章寫了 GX10 大腦 + Pi 5 現場 I/O 的架構,這篇文章記錄把它真的跑通的完整過程——包含一個花了很久的怪現象:同一支指令,別人能切、我就 busy。

最終成果(先講結論)

一句話 → 地端大模型判定意圖 → HTTP → Pi 5 真實切 GPIO,全程約 1 秒:

$ python3 gx10_ai_controller.py "把燈關了"
✅ Pi5 健康 | 目前: {'devices': {'relay1': 'on'}, ...}
⏱ LLM (gemma4:e4b) 耗時 0.4s
意圖: {"action": "off", "reason": "關燈"}
✏ Pi5 回應: {"dry_run": false, "pin": 22, "state": "off", "status": "success"}

兩端程式(已部署並在跑)

Pi 5 端 — HTTP 控制器(port 5001,systemd 常駐)

import logging, os, threading
from datetime import datetime
from flask import Flask, request, jsonify

DRY_RUN = os.environ.get("DRY_RUN", "0") == "1"
RELAY_PIN = int(os.environ.get("RELAY_PIN", "22"))
ACTIVE_HIGH = os.environ.get("ACTIVE_HIGH", "0") == "1"
logger = ...   # 省略

relay = None
if not DRY_RUN:
    from gpiozero import OutputDevice
    # 低電位觸發繼電器(active_high=False)
    relay = OutputDevice(RELAY_PIN, active_high=ACTIVE_HIGH, initial_value=False)

app = Flask(__name__)

@app.route("/api/device", methods=["POST"])
def control_device():
    data = request.get_json(silent=True) or {}
    action = data.get("action", "").lower()
    if action not in ("on", "off"):
        return jsonify({"status": "error", "message": "action must be on|off"}), 400
    if relay: relay.on() if action == "on" else relay.off()
    return jsonify({"status": "success", "state": action, "dry_run": bool(DRY_RUN),
                    "pin": None if DRY_RUN else RELAY_PIN})

systemd user service(~/.config/systemd/user/pi5-relay.service):

[Service]
Type=simple
Environment=DRY_RUN=0
Environment=RELAY_PIN=22
ExecStart=/home/pi/venv/bin/python3 /home/pi/pi5_relay.py
Restart=always

加上 loginctl enable-linger pi,重開機不需要登入就會自動起服務。

GX10 端 — AI 決策層

SYSTEM_PROMPT = """你是決策樞紐。只回一個 JSON 物件, 格式:
{"action":"on" 或 "off","reason":"簡短說明"}
開/open/turn on→on;關/close/turn off→off;無關控制→{"action":"none"}"""

# 1) 地端 Ollama(gemma4:e4b,temperature=0)解析意圖
# 2) 意圖 → POST http://<pi5>:5001/api/device
# 3) 非 on/off(含 none)一律不下達, 直接回報

踩坑實錄:「GPIO busy」的真相

這是本次最大的坑。現象:Pi 5 上已有一個程式(我稱它 search_bot)能用 gpiozero 正常切繼電器,我寫的新程式卻全部失敗,錯誤一路切換:

嘗試 結果
系統 python + OutputDevice(17) KeyError: PinInfo(number=11, ...)
RPi.GPIO Cannot determine SOC peripheral base address
sysfs GPIO export OSError: Invalid argument
換腳位再試 lgpio.error: 'GPIO busy'

一個一個排除下來,真相其實是最普通的一種:

GPIO pin 是排他的。search_bot 已經 claim 了 GPIO 14/15/17/18/21(4 顆 relay + 1 顆按鈕),任何新程序再 claim 同一支腳就会 busy。

驗證方式:挑一支 search_bot 沒用的腳(GPIO 22)做同一個測試——

✅ OutputDevice(22) 建立成功
GPIO22 -> on
GPIO22 -> off
--- gpiozero 在自由腳位上工作正常 ---

一次就通。所谓「怪現象」,就是「腳被佔用」。

為什麼 RPi.GPIO 會掛?

Pi 5 有多個 GPIO controller(/dev/gpiomem0~4、多個 gpiochip),RPi.GPIO 舊版要猜「SOC peripheral base」就猜錯;gpiozero + gpiozero/lgpio 走 gpiochip 介面,選對 controller 就正常——這就是同一台機子、同樣的 pin,一個能跑一個掛的原因。

三個實戰原則

  1. GPIO 是排他資源 — 多程序共用在 Pi 上一定會撞;分配腳位前,先用 journalctl 或 pids + 程式碼 grep 看誰已經 claim 了
  2. DRY_RUN 是上線安全閘 — 新控制器先跑 DRY_RUN=1,API 鏈路全通、GPIO 不碰,確認無誤再切 0
  3. 選對 pin factory — Pi 5 上 RPi.GPIO(舊)不穩;gpiozero + lgpio backend 是現行正解

可復用清單

項目 值
Pi 5 服務 systemctl --user status pi5-relay
狀態 API GET http://<pi5>:5001/api/status
下達 API POST /api/device {"action":"on\|off"}
換腳位 service RELAY_PIN= 改數字,restart 即可
反相 ACTIVE_HIGH=1
回滾 DRY_RUN DRY_RUN=1,API 鏈路照跑、不碰 pin

結語

「別人能跑、我不能跑」的坑,90% 是資源已被佔用而不是環境壞掉。這次的 GPIO busy 就是如此:把「腳位排他性」這個前提寫進腦子,下次一眼就能看出誰跟誰撞了。

ASUS Ascent GX10 × Raspberry Pi 5:邊緣大腦 + 現場 I/O 的完整控制架構

ASUS Ascent GX10(NVIDIA GB10 Grace Blackwell,1 PFLOP AI 算力)與 Raspberry Pi 5 的結合,是一種「大腦(Brain)+ 現場 I/O(Actuator & Sensor)」的強勢分工:GX10 沒 GPIO,但有頂級 AI 推論;Pi 5 有完整的 GPIO/I2C/SPI/UART,但算力只適合即時控制。兩者過區域網路結在一起,就是完整的邊緣 AI 控制系統。

1. 核心分工:誰做什麼

ASUS Ascent GX10(大腦) Raspberry Pi 5(現場)
硬體角色 GB10 超級晶片、121GB 統一記憶體 GPIO 40 pin、I2C/SPI/UART、Hailo NPU 可擴充
做什麼 地端 LLM(DeepSeek-R1、Llama 3)、視覺辨識、多 Agent 協調 接馬達、感測器、繼電器、LED,產生 PWM/高低電位
輸出入 收環境數據 → AI 推論 → 下高階指令 收高階指令 → 轉實體電信號;感測數據回傳
典型指令 「辨識到異常,關 3 號馬達」 GPIO 17 拉低 → 繼電器動作

關鍵觀念:不要讓大模型直接碰 GPIO(延遲太高、不可重現),也不要讓 Pi 跑大語言模型(算力不够)。中間一層「結構化指令」(JSON)是最乾淨的分界線。

2. 通訊方案比較

兩台都跑 Linux,又都在區域網路內,網路通訊 > USB 序列埠(更快、可擴充、能跨房間):

方案 適用場景 本環境狀態
A. MQTT(推薦) 多設備、事件型、即時開關、可存最後值 ✅ GX10 上 mosquitto 已跑
B. SSH + 指令 除錯、一次性操作、批量指令 ✅ 金鑰免登入已建置
C. REST API 點對點、網頁整合、同步要回傳值 需在 Pi 端跑 Flask/FastAPI

實務分工:狀態與事件走 MQTT、控制與除錯走 SSH、要同步確認的用 HTTP。

3. 實作範例:AI 語音/視覺自動化開關

情境:大模型理解到「開燈」或視覺辨識到「有人進入」→ 自動開關實體繼電器。

步驟一:硬體的連法

  1. GX10 經 10G LAN 連區網(或 Wi-Fi)
  2. Pi 5 連同一區網(本環境實測:192.168.1.236)
  3. 5V 繼電器模組:VCC→Pi 5V、GND→GND、IN→GPIO 17

步驟二:Pi 5 端的 HTTP 控制器(Flask)

from flask import Flask, request, jsonify
import gpiozero

app = Flask(__name__)
relay = gpiozero.OutputDevice(17)   # 接在 GPIO 17 的繼電器/LED

@app.route('/api/device', methods=['POST'])
def control_device():
    action = (request.json or {}).get('action')
    if action == 'on':
        relay.on()
        return jsonify({"status": "success", "message": "Device turned ON"})
    if action == 'off':
        relay.off()
        return jsonify({"status": "success", "message": "Device turned OFF"})
    return jsonify({"status": "error", "message": "Invalid action"}), 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

request.json 加 or {} 避免沒有 body 時崩掉;if __name__ == '__main__' 才是正確的模組保護。

步驟三:GX10 端的 AI 決策

import requests, json

PI5_IP = "192.168.1.236"   # 本環境 Pi 5 的實際位址

def send_to_pi5(action):
    url = f"http://{PI5_IP}:5000/api/device"
    try:
        r = requests.post(url, json={"action": action}, timeout=5)
        print("Pi 5 回應:", r.json())
        return r.json()
    except Exception as e:
        print("連線失敗:", e)
        return None

# GX10 上的地端大模型(Ollama/vLLM)已把語音/視覺判斷成結構化意圖:
ai_decision = {"target": "light", "status": "on"}   # 來自 LLM 的 function-call 結果
send_to_pi5(ai_decision["status"])

requests.post(url, json=...) 會自動帶 Content-Type 並序列化,不用手動 dumps。

方案 A(MQTT)的對照版

# Pi 5:監聽主題,收到就動作
import paho.mqtt.client as mqtt
import gpiozero
relay = gpiozero.OutputDevice(17)

def on_msg(client, userdata, msg):
    if msg.topic == "iot/relay1" and msg.payload == b"on":
        relay.on()
    elif msg.topic == "iot/relay1" and msg.payload == b"off":
        relay.off()

c = mqtt.Client()
c.on_message = on_msg
c.connect("192.168.1.25", 1883, 60)   # GX10 上的 mosquitto
c.subscribe("iot/relay1")
c.loop_forever()
# GX10:AI 決策後 publish
import paho.mqtt.client as mqtt
c = mqtt.Client()
c.connect("192.168.1.25", 1883, 60)
# LLM 判斷 → 開燈
c.publish("iot/relay1", "on", retain=True)

MQTT 的好處:retain=True 讓 Pi 重開機後自動恢復最後狀態;多路繼電器只要多訂閱幾個主題,不用改架構。

4. 安全與穩定(必讀)

  1. Pi 5 的 GPIO 是 3.3V —— 接 5V 模組前確認模組本身有 3.3V 相容的 IN 腳(多數繼電器模組有)
  2. 馬達/繼電器回灌雜訊 —— 大電流迴路與 GPIO 分開供電,地線共接
  3. 網路權限 —— Flask 只綁區網(或用 firewall 限制來源 IP 只允許 192.168.1.25),5000 埠別對外
  4. MQTT 加認證 —— mosquitto 開啟 password_file + TLS,避免區網內誰都能開關燈
  5. 看門狗 —— Pi 端程式用 systemd service + Restart=always,斷線自動回連
  6. 安全聯動 —— 高風險動作(斷氣、斷電源)要「LLM 意圖 + 人工確認」兩段式,純自動只給低風險負載

5. 本機的落地優勢

這套架構的元件這台機房全部現成:
– MQTT broker(mosquitto)已在 GX10 跑
– Pi 5 16GB RAM + NVMe,連 Ollama 小模型都能跑(邊緣備援)
– Hailo NPU 可做視覺預判(偵測到人 → 才叫喚大模型),省算力
– Hermes multi-profile 可直接當「語音/對話層」,對話 → 意圖 → MQTT

下一步建議:①Pi 5 裝 Flask + systemd unit ②GX10 加 intent→MQTT 的 adapter ③用一個 LED 先跑通,再換繼電器。

2023年12月18日 星期一

Bluetooth Speaker

Purpose:
利用現有BT audio receiver board來製作一台藍芽音箱, 從中學習到一些電子模組的焊接組合, Fusion360的3D繪製倒出圖檔到雷射切割機去做箱體的切割. 

INTRODUCTION
1. Bluetooth 5.0 Audio Receiver Board - Controllable Volume
This is a small size and cost-effective Bluetooth 5.0 Audio Receiver module with the auto-reconnect feature. It supports a wide-range power supply of 3.7V~24V and comes with a function button. Dial the button to the left and right to adjust volume, and press it to pause or play. You can use this module to remake your loudspeaker into wireless.



2. Mini Boost DC DC Step Up 1.5A Tegangan Fix Output Pilihan 5V 8V 9V 12V
Basic parameters
The output voltage can be set to 5V/8V/9V/12V, the default is 12V
Input voltage range: 2.5V- 5V
Output performance: Take 3.7V lithium battery input


3. Power Amp MINI SFT-9718 Stereo 60 Watt RMS. DC 12 V. - 15 V.
4. 喇吧與箱體



Circuit:



2023年11月10日 星期五

XBOX joystick modification with ESP32

Purpose:
This topic uses ESP32 to modify the broken XBOX Joystick and analyze the operating actions of two Joysticks and four Buttons (A, B, X, Y)! This topic is also about learning the parts of Joystick and Button!
這個專題是使用ESP32將壞掉的XBOX Joystick改裝, 把兩個Joystick和四個Button(A,B,X,Y)的操作動作解析出來! 這專題是另類的Joystick和Button零件的學習!

Tools: 

圖一: MutilMeter
Use MutilMeter to find the contact location of the Joystick and button, then spot weld it to the header connector and connect it to the designated pin of the ESP32.
利用MutilMeter來找出Joystick和button的接點位置, 然後將其點焊接到header接頭並連接至ESP32的指定腳位.

Fundamental:
Joystick
Dual-axis button rocker module, the rocker can move along the X-axis and Y-axis in two directions (VRx and VRy). Because of its internal variable resistance, it outputs a signal of 0-1023 when moving.
雙軸按鍵搖桿模組,搖桿可沿著 X軸 和 Y軸 兩個方向移動 (VRx 和 VRy)。因其內部的可變電阻,移動時,對外輸出 0-1023 的訊號。



YouTube Demo:

ESP32 Code:

#include <ezButton.h>
//-----Global variable---------------------------------------
#define LED_BUILTIN 2
//--------joystick------------------------------------------
#define VRX_PIN_L  33 // ESP32 pin GPIO33 (ADC)
#define VRY_PIN_L  32 // ESP32 pin GPIO32 (ADC)
#define VRX_PIN_R  35 // ESP32 pin GPIO35 (ADC)
#define VRY_PIN_R  34 // ESP32 pin GPIO34 (ADC)
#define SW_X       18
#define SW_Y       16
#define SW_A       17
#define SW_B       19

#define LEFT_THRESHOLD_L  2200  
#define RIGHT_THRESHOLD_L 900
#define UP_THRESHOLD_L    2200  
#define DOWN_THRESHOLD_L  900  

#define LEFT_THRESHOLD_R  2200  
#define RIGHT_THRESHOLD_R 900
#define UP_THRESHOLD_R    2200  
#define DOWN_THRESHOLD_R  900  

#define COMMAND_NO_L     0x00
#define COMMAND_LEFT_L   0x01
#define COMMAND_RIGHT_L  0x02
#define COMMAND_UP_L     0x04
#define COMMAND_DOWN_L   0x08

#define COMMAND_NO_R     0x00
#define COMMAND_LEFT_R   0x01
#define COMMAND_RIGHT_R  0x02
#define COMMAND_UP_R     0x04
#define COMMAND_DOWN_R   0x08

int valueX_L = 0 ; // to store the X-axis value
int valueY_L = 0 ; // to store the Y-axis value
int command_L = COMMAND_NO_R;
int valueX_R = 0 ; // to store the X-axis value
int valueY_R = 0 ; // to store the Y-axis value
int command_R = COMMAND_NO_R;
//----------------------------------------------

int bValue_A = 0; // To store value of the button
int bValue_B = 0; // To store value of the button
int bValue_X = 0; // To store value of the button
int bValue_Y = 0; // To store value of the button
ezButton buttonA(SW_A);
ezButton buttonB(SW_B);
ezButton buttonX(SW_X);
ezButton buttonY(SW_Y);
//------------------------------------------------------------

//--------- Flag structure --------------------------------------
//----------------------------------------------------------
#define LINE_BUFFER_LENGTH 1024
typedef struct _vFlag
{
  uint8_t LEDFlag=0;
  uint8_t BTFlag=0;
}vFlag;
vFlag *flag_Ptr;
vFlag flag;
//--------- uart structure --------------------------------------
//----------uart--------------
typedef struct _vUart
{
  char c;
  int lineIndex = 0;
  int line1Index = 0;
  int BTlineIndex = 0;
  bool lineIsComment;
  bool lineSemiColon;
  char line[128];
  char BTline[20];
  String inputString;
  String BTinputString;
} vUart;
vUart *Uart_Ptr;
vUart Uart;
//-------------------------------------
TaskHandle_t hled;
TaskHandle_t huart;

void vLEDFlashTask(void *pvParameters);
void vUARTTask(void *pvParameters);

void initial()
{
  Serial.println(F("Create Task"));
  //----------------------------------------------------------------------
  // Now set up two tasks to run independently.
  xTaskCreatePinnedToCore(
    vLEDFlashTask, "LEDTask" // A name just for humans
    ,
    1024 // This stack size can be checked & adjusted by reading the Stack Highwater
    ,
    NULL, 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,
    &hled //handle
    ,
    0);

  xTaskCreatePinnedToCore(
    vUARTTask, "UARTTask" // A name just for humans
    ,
    1024 // This stack size can be checked & adjusted by reading the Stack Highwater
    ,
    NULL, 3 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,
    &huart //handle
    ,
    0);

  //----------------------------------------------------------------------
}


void setup() {
  Serial.begin(9600);
  /**240(default 240 160 80 40 20 and 10Mhz)***/
  setCpuFrequencyMhz(160);
  initial();
  //-----------------------------------------------------------------
  buttonA.setDebounceTime(50); // set debounce time to 50 milliseconds
  buttonB.setDebounceTime(50); // set debounce time to 50 milliseconds
  buttonX.setDebounceTime(50); // set debounce time to 50 milliseconds
  buttonY.setDebounceTime(50); // set debounce time to 50 milliseconds
  //------------------------------------------------
  Serial.println(F("System On!"));
  //-------------------------------------------
}

void loop()
{
  Serial.print(F("Main at core:"));
  Serial.println(xPortGetCoreID());
  while (1)
  {
    buttonA.loop(); // MUST call the loop() function first
    buttonB.loop(); // MUST call the loop() function first
    buttonX.loop(); // MUST call the loop() function first
    buttonY.loop(); // MUST call the loop() function first
    // Read the button value
    bValue_A = buttonA.getState();
    bValue_B = buttonB.getState();
    bValue_X = buttonX.getState();
    bValue_Y = buttonY.getState();

    if (buttonA.isPressed()) {
      Serial.println("The buttonA is pressed");
      // TODO do something here
    }
    if (buttonA.isReleased()) {
      Serial.println("The buttonA is released");
      // TODO do something here
    }
    if (buttonB.isPressed()) {
      Serial.println("The buttonB is pressed");
      // TODO do something here
    }
    if (buttonB.isReleased()) {
      Serial.println("The buttonB is released");
      // TODO do something here
    }
    if (buttonX.isPressed()) {
      Serial.println("The buttonX is pressed");
      // TODO do something here
    }
    if (buttonX.isReleased()) {
      Serial.println("The buttonX is released");
      // TODO do something here
    }
    if (buttonY.isPressed()) {
      Serial.println("The buttonY is pressed");
      // TODO do something here
    }
    if (buttonY.isReleased()) {
      Serial.println("The buttonY is released");
      // TODO do something here
    }
    //--------------------------------------------------------
    valueX_L = analogRead(VRX_PIN_L);
    valueY_L = analogRead(VRY_PIN_L);
    valueX_R = analogRead(VRX_PIN_R);
    valueY_R = analogRead(VRY_PIN_R);
    // converts the analog value to commands
    // reset commands
    command_L = COMMAND_NO_L;
    command_R = COMMAND_NO_R;
    // check left/right commands
   
    if (valueX_L > LEFT_THRESHOLD_L)
      command_L = command_L | COMMAND_LEFT_L;
    else if (valueX_L < RIGHT_THRESHOLD_L)
      command_L = command_L | COMMAND_RIGHT_L;
   
    // check up/down commands
    if (valueY_L > UP_THRESHOLD_L)
      command_L = command_L | COMMAND_UP_L;
    else if (valueY_L < DOWN_THRESHOLD_L)
      command_L = command_L | COMMAND_DOWN_L;


    // print command to serial and process command
   
    if (command_L & COMMAND_LEFT_L) {
      Serial.println("COMMAND LEFT_L");
      // TODO: add your task here
    }

    if (command_L & COMMAND_RIGHT_L) {
      Serial.println("COMMAND RIGHT_L");
      // TODO: add your task here
    }

    if (command_L & COMMAND_UP_L) {
      Serial.println("COMMAND UP_L");
      // TODO: add your task here
    }

    if (command_L & COMMAND_DOWN_L) {
      Serial.println("COMMAND DOWN_L");
      // TODO: add your task here
    }
   
    //-----------------------------------------------------
    if (valueX_R > LEFT_THRESHOLD_R)
      command_R = command_R | COMMAND_LEFT_R;
    else if (valueX_R < RIGHT_THRESHOLD_R)
      command_R = command_R | COMMAND_RIGHT_R;
   
    // check up/down commands
    if (valueY_R > UP_THRESHOLD_R)
      command_R = command_R | COMMAND_UP_R;
    else if (valueY_R < DOWN_THRESHOLD_R)
      command_R = command_R | COMMAND_DOWN_R;


    // print command to serial and process command
   
    if (command_R & COMMAND_LEFT_R) {
      Serial.println("COMMAND LEFT_R");
      // TODO: add your task here
    }

    if (command_R & COMMAND_RIGHT_R) {
      Serial.println("COMMAND RIGHT_R");
      // TODO: add your task here
    }

    if (command_R & COMMAND_UP_R) {
      Serial.println("COMMAND UP_R");
      // TODO: add your task here
    }

    if (command_R & COMMAND_DOWN_R) {
      Serial.println("COMMAND DOWN_R");
      // TODO: add your task here
    }
    //-----------------------------------------------------
  }//----while(1)-------------------------------------------

}
//--------------------------------------------------------
/*--------------------------------------------------*/
void vLEDFlashTask(void *pvParameters) // This is a task.
{
  (void)pvParameters;
 
  Serial.print(F("LEDTask at core:"));
  Serial.println(xPortGetCoreID());
  pinMode(LED_BUILTIN, OUTPUT);
  for (;;) // A Task shall never return or exit.
  {
    digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
    vTaskDelay(200);
    digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
    vTaskDelay(200);
  }
}

//-------------------------------------------
void vUARTTask(void *pvParameters)
{
  (void)pvParameters;

  Serial.print(F("UARTTask at core:"));
  Serial.println(xPortGetCoreID());
  for (;;)
  {
    while (Serial.available() > 0)
    {
      Uart.c = Serial.read();
 
      if ((Uart.c == '\n') || (Uart.c == '\r'))
      { // End of line reached
        if (Uart.lineIndex > 0)
        { // Line is complete. Then execute!
          Uart.line[Uart.lineIndex] = '\0'; // Terminate string
          //Serial.println( F("Debug") );
          //Serial.println( Uart.inputString );
          processCommand(Uart.line); // do something with the command
          Uart.lineIndex = 0;
          Uart.inputString = "";
        }
        else
        {
          // Empty or comment line. Skip block.
        }
        Uart.lineIsComment = false;
        Uart.lineSemiColon = false;
        Serial.println(F("ok>"));
      }
      else
      {
        //Serial.println( c );
        if ((Uart.lineIsComment) || (Uart.lineSemiColon))
        {
          if (Uart.c == ')')
            Uart.lineIsComment = false; // End of comment. Resume line.
        }
        else
        {
          if (Uart.c == '/')
          { // Block delete not supported. Ignore character.
          }
          else if (Uart.c == '~')
          { // Enable comments flag and ignore all characters until ')' or EOL.
            Uart.lineIsComment = true;
          }
          else if (Uart.c == ';')
          {
            Uart.lineSemiColon = true;
          }
          else if (Uart.lineIndex >= LINE_BUFFER_LENGTH - 1)
          {
            Serial.println("ERROR - lineBuffer overflow");
            Uart.lineIsComment = false;
            Uart.lineSemiColon = false;
          }
          else if (Uart.c >= 'a' && Uart.c <= 'z')
          { // Upcase lowercase
            Uart.line[Uart.lineIndex] = Uart.c - 'a' + 'A';
            Uart.lineIndex = Uart.lineIndex + 1;
            Uart.inputString += (char)(Uart.c - 'a' + 'A');
          }
          else
          {
            Uart.line[Uart.lineIndex] = Uart.c;
            Uart.lineIndex = Uart.lineIndex + 1;
            Uart.inputString += Uart.c;
          }
        }
      }
    } //while (Serial.available() > 0)
    vTaskDelay(10);
  }
}
//------------------------------------------------------------
void processCommand(char *data)
{
  int len, xlen, ylen, zlen, alen;
  char ctemp[20];

  len = Uart.inputString.length();
  if (strstr(data, "VER") != NULL)
  {
    Serial.println(F("W_ATE_Board_20231109"));
  }

}




2023年8月15日 星期二

ESP32 Temperature and Humidity I2C LCD Display

Purpose:

This Project 利用NTP pool.ntp.org server來抓取現在時間, 並利用DHT22溫溼度感應器, 將現在的時間和溫度, 濕度顯示在 I2C介面的LCD面板上.

This project use the NTP (Network Time Protocol) to capture the current time, and use the DHT22 temperature and humidity sensor to display the current time, temperature, and humidity on the LCD panel of the I2C interface.

Fundamental:

NTP

The Network Time Protocol (NTP) is a networking protocol for clock synchronization between computer systems over packet-switched, variable-latency data networks. 

NTP is a protocol designed to synchronize the clocks of computers over a network to a common timebase (usually UTC).

Coordinated Universal Time (UTC)

NTP意圖將所有參與電腦的協調世界時(UTC)時間同步到幾毫秒的誤差內。

pool.ntp.org來取得時間即可

網路時間協定(英語:Network Time Protocol,縮寫:NTP)

LCD

The LCD1602 comes in 2 possible configurations: I2C configuration and standard configuration. The I2C configuration is usually simpler to use. The default I2C address of the LCD1602 module is 0x27.

DHT22

Digital-output relative humidity & temperature sensor/module

DHT22 output calibrated digital signal. It utilizes exclusive digital-signal-collecting-technique and humidity

sensing technology, assuring its reliability and stability.Its sensing elements is connected with 8-bit single-chip computer.

Every sensor of this model is temperature compensated and calibrated in accurate calibration chamber and the calibration-coefficient is saved in type of programme in OTP memory, when the sensor is detecting, it will cite coefficient from memory.

Circuit:


YouTubeDemo:



Code Introduce:

#include <WiFi.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "DHT.h"
//----------------------------------------------------------------------
LiquidCrystal_I2C LCD = LiquidCrystal_I2C(0x27, 16, 2);

#define NTP_SERVER     "pool.ntp.org"
#define UTC_OFFSET     0
#define UTC_OFFSET_DST 0
//---------------DHT22--------------------
#define DHTPIN 14
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
DHT dht(DHTPIN, DHTTYPE);
float Humidity;
float Temperature;
//--------- Flag structure --------------------------------------
typedef struct _vFlag
{
  uint8_t BTFlag = 0;
  uint8_t DC_Flag = 0;
  uint8_t CANFlag = 0;
  uint8_t I2C_Flag = 0;
  uint8_t RFIDWrite = 0;
  uint8_t RFIDRead = 0;
  uint8_t dht22 = 0;
  uint8_t sensor1_Flag = 0;
  uint8_t initial_Flag = 0;
  uint8_t FunctionFlag = 0;
} vFlag;
vFlag *flag_Ptr;
vFlag flag;
//--------- uart structure --------------------------------------
//----------uart--------------
#define LINE_BUFFER_LENGTH 64
typedef struct _vUart
{
  char c;
  int lineIndex = 0;
  int line1Index = 0;
  int BTlineIndex = 0;
  bool lineIsComment;
  bool lineSemiColon;
  char line[128];
  char BTline[20];
  String inputString;
  String BTinputString;
  String S1inputString;
  int V[16];
  char ctemp[30];
  char I2C_Data[80];
  int DC_Spped = 50;
  float Voltage[16];
  int Buffer[128];
  int StartCnt = 0;
  int ReadCnt = 0;
  int sensorValue = 0;
} vUart;
vUart *Uart_Ptr;
vUart Uart;

//---------------------------------------------------------------------------------
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
//----------------------------------------------------------------
TaskHandle_t hled;
TaskHandle_t huart;
//------------------------------------------------------------------------------
void initial()
{
  Serial.println(F("Create Task"));
  //----------------------------------------------------------------------
  xTaskCreatePinnedToCore(
    vUARTTask, "UARTTask" // A name just for humans
    ,
    1024 // This stack size can be checked & adjusted by reading the Stack Highwater
    ,
    NULL, 3 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,
    &huart //handle
    ,
    0);

  //--------------- create task----------------------------------
  xTaskCreatePinnedToCore(
    vLEDTask, "LEDTask" // A name just for humans
    ,
    1024 // This stack size can be checked & adjusted by reading the Stack Highwater
    ,
    NULL, 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    ,
    &hled //handle
    ,
    0);
  //----------------------------------------------------------------------
}

void setup()
{
  Serial.begin(9600);
  Serial.println(F("init"));
  initial();
  pinMode(LED_BUILTIN, OUTPUT);
  LCD.init();
  LCD.backlight();
  LCD.setCursor(0, 0);
  LCD.print("Connecting to ");
  LCD.setCursor(0, 1);
  LCD.print("WiFi ");

  WiFi.begin("Wokwi-GUEST", "", 6);
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    spinner();
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  LCD.clear();
  LCD.setCursor(0, 0);
  LCD.println("Online");
  LCD.setCursor(0, 1);
  LCD.println("Updating time...");
  configTime(8*3600, 0, "pool.ntp.org","time.nist.gov"); // enable NTP for Taipei time
  //configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);

  dht.begin();
}

void loop()
{
  //

  Serial.print(F("Main at core:"));
  Serial.println(xPortGetCoreID());
  while(1)
  {
    if(flag.dht22==0)
    {
      for(int i=0;i<40;i++)
      {
        printLocalTime();
        delay(200);
      }
      LCD.clear();
      flag.dht22=1;
    }
   
   
    if(flag.dht22==1)
    {
      float h = dht.readHumidity();

      float t = dht.readTemperature();

      float f = dht.readTemperature(true);
     
      if (isnan(h) || isnan(t) || isnan(f))
      {
        Serial.println("Failed to read from DHT sensor!");
        return;
      }

      float hif = dht.computeHeatIndex(f, h);

      float hic = dht.computeHeatIndex(t, h, false);

      Serial.print("Humidity: ");
      Serial.print(h);
      Serial.print(" % ");
      Serial.print("Temperature: ");
      Serial.print(t);
      Serial.print(" *C ");
      Serial.print(f);
      Serial.print(" *F ");
      Serial.print("Heat index: ");
      Serial.print(hic);
      Serial.print(" *C ");
      Serial.print(hif);
      Serial.println(" *F");

      String Line="Temp: "+String(t);
      String Line1="Humi: "+String(h);
      for(int i=16;i>=0;i--)
      {
        LCD.setCursor(i,0);
        LCD.print(Line);
        LCD.print(" ");
        LCD.setCursor(i,1);
        LCD.print(Line1);
        LCD.print(" ");
        delay(200);
      }
      delay(200);
      LCD.clear();
      flag.dht22=0;
    }
    vTaskDelay(5);
  }
}
//-------------------------------------------
void vUARTTask(void *pvParameters)
{
  (void)pvParameters;

  Serial.print(F("UARTTask at core:"));
  Serial.println(xPortGetCoreID());
  vTaskDelay(100);
  for (;;)
  {
    while (Serial.available() > 0)
    {
      Uart.c = Serial.read();
 
      if ((Uart.c == '\n') || (Uart.c == '\r'))
      { // End of line reached
        if (Uart.lineIndex > 0)
        { // Line is complete. Then execute!
          Uart.line[Uart.lineIndex] = '\0'; // Terminate string
          //Serial.println( F("Debug") );
          //Serial.println( Uart.inputString );
          processCommand(Uart.line); // do something with the command
          Uart.lineIndex = 0;
          Uart.inputString = "";
        }
        else
        {
          // Empty or comment line. Skip block.
        }
        Uart.lineIsComment = false;
        Uart.lineSemiColon = false;
        Serial.println(F("ok>"));
      }
      else
      {
        //Serial.println( c );
        if ((Uart.lineIsComment) || (Uart.lineSemiColon))
        {
          if (Uart.c == ')')
            Uart.lineIsComment = false; // End of comment. Resume line.
        }
        else
        {
          if (Uart.c == '/')
          { // Block delete not supported. Ignore character.
          }
          else if (Uart.c == '~')
          { // Enable comments flag and ignore all characters until ')' or EOL.
            Uart.lineIsComment = true;
          }
          else if (Uart.c == ';')
          {
            Uart.lineSemiColon = true;
          }
          else if (Uart.lineIndex >= LINE_BUFFER_LENGTH - 1)
          {
            Serial.println("ERROR - lineBuffer overflow");
            Uart.lineIsComment = false;
            Uart.lineSemiColon = false;
          }
          else if (Uart.c >= 'a' && Uart.c <= 'z')
          { // Upcase lowercase
            Uart.line[Uart.lineIndex] = Uart.c - 'a' + 'A';
            Uart.lineIndex = Uart.lineIndex + 1;
            Uart.inputString += (char)(Uart.c - 'a' + 'A');
          }
          else
          {
            Uart.line[Uart.lineIndex] = Uart.c;
            Uart.lineIndex = Uart.lineIndex + 1;
            Uart.inputString += Uart.c;
          }
        }
      }
    } //while (Serial.available() > 0)
    vTaskDelay(5);
  }
}
//-------------------------------------------------------------------------
static void vLEDTask(void *pvParameters)
{
  (void)pvParameters;

  Serial.println(F("LEDTask at core:"));
  Serial.println(xPortGetCoreID());
  pinMode(LED_BUILTIN, OUTPUT);
  for (;;) // A Task shall never return or exit.
  {
    digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
    vTaskDelay(200);
    digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
    vTaskDelay(200);
  }
}
//----------------------------------------
void processCommand(char *data)
{
  int len, xlen, ylen, zlen, alen;
  int tempDIO;
  String stemp;

  len = Uart.inputString.length();
  //---------------------------------------
  if (strstr(data, "VER") != NULL)
  {
    Serial.println(F("ESP32_20230811"));
  }
  //-------------- RFID --------------------
  if (strstr(data, "DHT22_ON") != NULL)
  {
    flag.dht22 = 1;
    Serial.println(F("DHT22_ON"));
  }
  if (strstr(data, "DHT22_OFF") != NULL)
  {
    flag.dht22 = 0;
    Serial.println(F("DHT22_OFF"));
  }
 
}
//-----------------------------------------
void printLocalTime()
{
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    LCD.setCursor(0, 1);
    LCD.println("Connection Err");
    return;
  }
  //LCD.clear();
  LCD.setCursor(0, 0);
  LCD.println("Online");

  LCD.setCursor(8, 0);
  LCD.println(&timeinfo, "%H:%M:%S");

  LCD.setCursor(0, 1);
  LCD.println(&timeinfo, "%d/%m/%Y   %Z");
}

//----------------------------------------------
void spinner()
{
  static int8_t counter = 0;
  const char* glyphs = "\xa1\xa5\xdb";
  LCD.setCursor(15, 1);
  LCD.print(glyphs[counter++]);
  if (counter == strlen(glyphs)) {
    counter = 0;
  }
}
//-----------------------------------------