diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 13fa4a9..7d5ad02 --- a/README.md +++ b/README.md @@ -1,37 +1,40 @@ # Difference Equation Analyzer -差分方程式與數位濾波器分析工具。後端使用 FastAPI,前端使用 Vue 3 + Vite 建置後由 FastAPI 以靜態檔案服務。 +Difference Equation Analyzer 是一個用來設計、檢視與驗證離散時間濾波器的工具。後端使用 FastAPI、NumPy、SciPy 與 Pandas;前端使用 Vue 3、Vite、Tailwind CSS 與 Plotly。 + +主要功能: + +- 設計 Lowpass、Highpass、Bandpass、Notch、1P1Z、2P1Z、2P2Z、PID、SOGI 濾波器。 +- 比較浮點係數與 fixed-point 係數的頻率響應。 +- 手動微調 b/a 係數、a1/a2 組合、Q 格式位元數與 fixed-point 整數係數。 +- 上傳 CSV 時域資料,套用目前差分方程並繪製輸入/輸出波形。 +- 匯出包含濾波輸出欄位的 CSV。 +- 透過 USB serial 將 fixed-point 係數命令送到 MCU。 ## 專案結構 -- `dea_api.py`:FastAPI 後端、API 端點、LAN 存取限制、安全標頭。 -- `src/`:Vue 前端原始碼。 -- `static/index.html`:正式 UI 入口,載入建置後的前端資源。 -- `static/build/app.js`、`static/build/app.css`:前端建置產物,正式服務會載入這兩個檔案。 -- `static/style.css`:既有自訂 CSS,由 `src/style.css` 引入。 -- `tests/`:後端回歸測試。 -- `dea.service`:systemd 服務設定範本。 -- `dea_legacy.py`:舊 Streamlit 版本,僅供參考。 - -## 使用網址 - -本機: - -```bash -http://localhost:8000/ui/ +```text +dea_api.py FastAPI 入口與 HTTP route +dea/ 後端核心模組 + bode.py 頻率響應計算 + config.py 限制值與 MCU serial 設定 + csv_processing.py CSV 讀取、驗證、濾波與匯出 + filter_design.py 濾波器設計 + mcu.py MCU serial port 列表與命令寫入 + schemas.py Pydantic request schema + security.py LAN 存取限制與安全標頭 + validation.py 係數、頻率與有限值驗證 +src/ Vue 3 前端原始碼 +static/ FastAPI 對外提供的靜態 UI +static/build/ Vite build 輸出 +tests/ unittest 測試 +dea.service systemd service 範例 +chirp_signal.csv CSV 上傳測試範例 ``` -區網其他裝置: +## 安裝 -```bash -http://192.168.2.58:8000/ui/ -``` - -根路徑 `/` 會自動導向 `/ui/`。 - -## 後端環境 - -建立或重建 Python 虛擬環境: +在目標主機上建立 Python venv 並安裝依賴: ```bash cd /home/wisetop/diff-eq-analyzer @@ -39,38 +42,113 @@ python3 -m venv .venv .venv/bin/pip install -r requirements.txt ``` -本機開發啟動: - -```bash -.venv/bin/uvicorn dea_api:app --host 127.0.0.1 --port 8000 -``` - -## 前端建置 - -前端依賴由 `package.json` / `package-lock.json` 管理。首次安裝: +安裝前端依賴: ```bash npm install ``` -重新建置正式前端: +建置前端: ```bash npm run build ``` -建置後會更新: +Vite 會輸出到: ```text static/build/app.js static/build/app.css ``` -注意:正式頁面不再載入 Vue CDN 或 Tailwind CDN;Vue 會被打包到 `static/build/app.js`。目前仍使用 Plotly CDN。 +## 執行 -## systemd 服務 +本機開發可直接啟動 FastAPI: -查看狀態: +```bash +.venv/bin/uvicorn dea_api:app --host 127.0.0.1 --port 8000 +``` + +瀏覽器開啟: + +```text +http://localhost:8000/ui/ +``` + +部署在區網主機時可用: + +```text +http://:8000/ui/ +``` + +根路徑 `/` 會自動導向 `/ui/`。 + +## API + +目前主要 API: + +- `POST /api/design`:依濾波器參數產生 b/a 係數。 +- `POST /api/bode`:計算單組 b/a 係數的頻率響應。 +- `POST /api/bode/compare`:一次計算浮點與 fixed-point 兩組頻率響應。 +- `POST /api/filter`:上傳 CSV 並回傳圖表用的濾波結果。 +- `POST /api/filter/download`:上傳 CSV 並下載包含 `<欄位>_filtered` 的結果 CSV。 +- `GET /api/mcu/ports`:列出可用 serial ports。 +- `POST /api/mcu/write`:送出 MCU 命令。 + +MCU 寫入命令格式固定為: + +```text +bodeplot=b0,b1,b2,a1,a2 +``` + +前端會使用目前 fixed-point 係數自動組出此命令。 + +## CSV 上傳 + +限制與行為: + +- CSV 檔案大小上限為 32 MB。 +- CSV 必須有標題列與至少一筆資料。 +- 可選擇要濾波的訊號欄位;若第一欄像 `time`、`t`、`sec`、`x`、`index`,前端會預設選第二欄。 +- 圖表回應最多回傳 5000 點,避免大型 CSV 讓前端過重。 +- 下載 CSV 會保留完整資料,不套用圖表降採樣。 + +專案根目錄的 `chirp_signal.csv` 是上傳測試範例,約 19 MB、500001 行,欄位為: + +```text +time,value +``` + +## MCU Serial 設定 + +MCU serial 預設值在 `dea/config.py`,也可用環境變數覆蓋: + +```bash +MCU_SERIAL_PORT=/dev/ttyUSB2 +MCU_BAUD_RATE=115200 +MCU_WRITE_TIMEOUT=1.0 +``` + +若使用 systemd,可在 `dea.service` 的 `[Service]` 區塊加入: + +```ini +Environment="MCU_SERIAL_PORT=/dev/ttyUSB2" +Environment="MCU_BAUD_RATE=115200" +Environment="MCU_WRITE_TIMEOUT=1.0" +``` + +更新 service 後重新載入: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart dea.service +``` + +注意:MCU 功能需要 `pyserial>=3.5`,已列在 `requirements.txt`。 + +## systemd + +查看服務狀態: ```bash sudo systemctl status dea.service @@ -84,13 +162,13 @@ sudo systemctl stop dea.service sudo systemctl restart dea.service ``` -查看即時日誌: +查看 log: ```bash sudo journalctl -u dea.service -f ``` -修改 `dea.service` 或 `/etc/systemd/system/dea.service` 後: +若修改 `/etc/systemd/system/dea.service`: ```bash sudo systemctl daemon-reload @@ -105,7 +183,7 @@ Python 語法檢查: .venv/bin/python -m py_compile dea_api.py tests/test_dea_api.py ``` -後端測試: +後端測試使用 Python 內建 `unittest`,不需要安裝或執行 `pytest`: ```bash .venv/bin/python -m unittest discover -s tests @@ -117,52 +195,50 @@ Python 語法檢查: npm run build ``` -## 安全與效能設定 +建議完整驗證順序: -後端目前包含: +```bash +.venv/bin/pip install -r requirements.txt +.venv/bin/python -m unittest discover -s tests +npm run build +sudo systemctl restart dea.service +``` -- 僅允許 private / loopback IP 存取。 -- CSP、安全回應標頭、`X-Frame-Options: DENY`、`nosniff`。 -- CSV 上傳大小限制:32 MB。 -- 圖表用 CSV 回傳最多 5000 點,下載 CSV 仍保留完整資料。 -- 係數數量上限:64。 -- 濾波器頻率、係數有限值、`a[0]` 等輸入驗證。 +## 安全與限制 -目前 CSP 的 `script-src` 僅允許: +- API middleware 只允許 private / loopback IP 存取。 +- 回應會加上 CSP、`Referrer-Policy`、`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`。 +- CSP 的 script 來源允許本機與 Plotly CDN: ```text 'self' https://cdn.plot.ly ``` -因此不再需要 `'unsafe-eval'`。 +- 係數數量上限為 64。 +- CSV 上限為 32 MB。 +- 頻率、係數、`a[0]` 與 CSV 訊號欄位都有後端驗證。 ## 常見問題 -### 網頁空白 +### 頁面沒有更新 -1. 強制刷新瀏覽器:`Ctrl+F5`。 -2. 確認服務狀態: - -```bash -sudo systemctl status dea.service -``` - -3. 確認靜態檔案存在: +1. 瀏覽器按 `Ctrl+F5` 強制重新整理。 +2. 確認前端 build 檔存在: ```bash ls static/build/app.js static/build/app.css ``` -4. 若剛改前端,請重新建置並重啟: +3. 重新 build 並重啟服務: ```bash npm run build sudo systemctl restart dea.service ``` -### 服務無法啟動,出現 status=203/EXEC +### systemd 顯示 `status=203/EXEC` -通常是 `.venv` 路徑或 `uvicorn` 腳本路徑失效。重建虛擬環境後再重啟: +通常是 `.venv` 或 `uvicorn` 路徑不正確。可重建 venv: ```bash rm -rf .venv @@ -170,3 +246,23 @@ python3 -m venv .venv .venv/bin/pip install -r requirements.txt sudo systemctl restart dea.service ``` + +### 找不到 pytest + +本專案不使用 pytest。請改用: + +```bash +.venv/bin/python -m unittest discover -s tests +``` + +### MCU 串口清單是空的 + +1. 確認 USB 裝置已插入。 +2. 確認執行服務的使用者有 serial port 權限。 +3. 檢查預設 port: + +```bash +echo $MCU_SERIAL_PORT +``` + +4. 可在頁面按「刷新」重新讀取 serial ports。 diff --git a/dea/config.py b/dea/config.py index 1983672..86ec0ae 100755 --- a/dea/config.py +++ b/dea/config.py @@ -1,7 +1,11 @@ +import os + MAX_COEFFICIENTS = 64 MAX_CSV_BYTES = 32 * 1024 * 1024 MAX_PLOT_POINTS = 5000 BODE_POINTS = 500 BODE_MAX_MULTIPLIER = 3.162 NYQUIST_MARGIN = 0.999999 - +MCU_SERIAL_PORT = os.getenv("MCU_SERIAL_PORT", "/dev/ttyUSB2") +MCU_BAUD_RATE = int(os.getenv("MCU_BAUD_RATE", "115200")) +MCU_WRITE_TIMEOUT = float(os.getenv("MCU_WRITE_TIMEOUT", "1.0")) diff --git a/dea/mcu.py b/dea/mcu.py new file mode 100755 index 0000000..9549476 --- /dev/null +++ b/dea/mcu.py @@ -0,0 +1,73 @@ +import re + +from fastapi import HTTPException + +from .config import MCU_BAUD_RATE, MCU_SERIAL_PORT, MCU_WRITE_TIMEOUT +from .schemas import MCUWriteParams + + +MCU_CMD_PATTERN = re.compile(r"^bodeplot=-?\d+,-?\d+,-?\d+,-?\d+,-?\d+$") + + +def _serial_module(): + try: + import serial + except ImportError as exc: + raise HTTPException(status_code=500, detail="MCU 串口功能需要安裝 pyserial") from exc + return serial + + +def write_mcu_command_response(params: MCUWriteParams): + cmd = params.command.strip() + if not MCU_CMD_PATTERN.fullmatch(cmd): + raise HTTPException(status_code=400, detail="MCU 指令格式錯誤") + + target_port = (params.port or MCU_SERIAL_PORT).strip() + if not target_port: + raise HTTPException(status_code=400, detail="MCU 串口不可為空") + + serial = _serial_module() + payload = (cmd + "\r\n").encode("ascii", errors="strict") + + try: + with serial.Serial( + port=target_port, + baudrate=MCU_BAUD_RATE, + timeout=MCU_WRITE_TIMEOUT, + write_timeout=MCU_WRITE_TIMEOUT, + ) as ser: + written = ser.write(payload) + ser.flush() + except serial.SerialException as exc: + raise HTTPException(status_code=500, detail=f"MCU 串口傳送失敗: {str(exc)}") from exc + + return { + "status": "ok", + "command": cmd, + "port": target_port, + "baudrate": MCU_BAUD_RATE, + "bytes_written": int(written), + } + + +def list_mcu_ports_response(): + _serial_module() + try: + from serial.tools import list_ports + except ImportError as exc: + raise HTTPException(status_code=500, detail="MCU 串口清單功能需要安裝 pyserial") from exc + + ports = [ + { + "device": port.device, + "description": port.description or "", + "hwid": port.hwid or "", + } + for port in list_ports.comports() + ] + + return { + "default_port": MCU_SERIAL_PORT, + "baudrate": MCU_BAUD_RATE, + "ports": ports, + } diff --git a/dea/schemas.py b/dea/schemas.py index 61e2ba2..0982e91 100755 --- a/dea/schemas.py +++ b/dea/schemas.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from pydantic import BaseModel, Field @@ -42,3 +42,8 @@ class BodeParams(BaseModel): class BodeCompareParams(BaseModel): ideal: BodeParams fixed: BodeParams + + +class MCUWriteParams(BaseModel): + command: str = Field(min_length=1, max_length=128) + port: Optional[str] = Field(default=None, max_length=256) diff --git a/dea_api.py b/dea_api.py index 0d5fabd..ecee096 100755 --- a/dea_api.py +++ b/dea_api.py @@ -19,7 +19,8 @@ from dea.csv_processing import ( read_csv_upload, ) from dea.filter_design import design_response -from dea.schemas import BodeCompareParams, BodeParams, DesignParams +from dea.mcu import list_mcu_ports_response, write_mcu_command_response +from dea.schemas import BodeCompareParams, BodeParams, DesignParams, MCUWriteParams from dea.security import ( SECURITY_HEADERS, add_security_headers, @@ -103,6 +104,26 @@ async def filter_csv( raise HTTPException(status_code=400, detail=f"CSV處理失敗: {str(e)}") +@app.get("/api/mcu/ports") +def list_mcu_ports(): + try: + return list_mcu_ports_response() + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"MCU 串口清單讀取失敗: {str(e)}") + + +@app.post("/api/mcu/write") +def write_mcu_command(params: MCUWriteParams): + try: + return write_mcu_command_response(params) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"MCU 指令傳送失敗: {str(e)}") + + @app.post("/api/filter/download") async def filter_csv_download( file: UploadFile = File(...), diff --git a/requirements.txt b/requirements.txt index c50f263..da69852 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pydantic>=2.0.0 numpy>=1.24.0 pandas>=2.0.0 scipy>=1.10.0 +pyserial>=3.5 diff --git a/src/App.vue b/src/App.vue index c56c167..56494ee 100644 --- a/src/App.vue +++ b/src/App.vue @@ -72,9 +72,35 @@ 濾波器設計 - +
+ + +
+
+
+ + +
+

{{ mcuStatus }}

+
-
+
@@ -113,10 +139,10 @@ -
+
+ :class="params.bp_order === o ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-600 dark:text-gray-400 border-slate-300 dark:border-gray-700'" + class="selector-button flex-1 rounded py-1.5 text-sm border transition-colors">{{ o }}
@@ -212,11 +238,6 @@ class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-2 text-sm text-slate-900 dark:text-gray-100"> - -
- 調整上方參數會自動覆寫 a, b 係數。你也可以隨時手動修改下方係數進行微調。 -
@@ -256,7 +277,7 @@
-
+

@@ -292,11 +313,11 @@ class="text-sm text-slate-500 dark:text-gray-400 px-3 py-3 cursor-pointer hover:text-slate-600 dark:hover:text-gray-300"> b 係數倍率微調
-
+
靈敏度: