You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

17 KiB

展件互動系統 — 開發架構文件(Windows 10 原生版)

三組電腦+螢幕、三台 iPad,使用者透過 iPad 操控螢幕內容,流程結束後上傳圖片至客戶 FTPS,並以 QR Code 顯示對應圖片網址供民眾掃描。

部署環境:Host 為 Windows 10(較舊機器,不使用 Docker),所有服務以原生方式安裝。


1. 系統總覽

1.1 硬體拓撲

角色 數量 跑什麼 網路
Host 主機 1 Caddy + Next.js(pm2)+ Mosquitto(Windows Service)+ 一個 Unity client 有線連 AP/switch(建議)
Unity 主機 2 各一個 Unity client 有線(建議)
iPad 3 PWA(加到主畫面、Single App Mode 鎖機) WiFi
螢幕 3 由各自的 Unity client 輸出

Host 主機(Win10)同時跑 broker、Caddy、Next.js、以及第一組的 Unity client;另外兩台只跑 Unity client。

1.2 服務管理方式(Win10)

服務 啟動方式 開機自啟
Mosquitto Windows Service 服務自動啟動(最先就緒)
Next.js(standalone) pm2 startup 資料夾的 bat
Caddy pm2 startup 資料夾的 bat
  • Mosquitto 走獨立 Windows Service,作為最底層依賴最先起來。
  • Next.js 與 Caddy 由 pm2 管理,開機透過「startup 資料夾的 bat + Windows 自動登入」拉起。
  • 務必設定 Windows 自動登入netplwiz 取消「使用者必須輸入密碼」),否則機器重開停在登入畫面時 bat 不會執行、服務不會啟動。

1.3 資料流(單一 station)

iPad (PWA)  --- MQTT over wss --->  Caddy (TLS 443)  --- ws --->  Mosquitto (localhost:9001)
                                                                       |
                                                                       | MQTT TCP 1883
                                                                       v
                                                            Unity client (subscribe)
                                                                       |
   使用者操作互動內容 ─────────────────────────────────────────────────┘
                                                                       |
   流程結束 ── iPad 觸發 ──> Next.js Server Action ── basic-ftp ──> 客戶 FTPS
                                                       |
                                          上傳成功 + 驗證 URL 可達
                                                       |
                                          publish 最終 URL 至 station topic
                                                       |
                              ┌────────────────────────┴───────────────────┐
                              v                                             v
                       iPad 顯示 QR Code                          Unity 螢幕顯示 QR Code

1.4 技術選型摘要

  • 螢幕內容:Unity(原生 MQTT TCP client)
  • iPad 介面:Next.js + PWA(純 PWA,不走 Apple Developer / IPA),zustand 保存裝置設定
  • 訊息中介:Mosquitto(Windows Service),同時開 1883(TCP,給 Unity)與 9001(WebSocket,經 Caddy 給 iPad)
  • 反向代理 / TLS:Caddy(原生安裝,pm2 管理,手動掛 mkcert 憑證,關閉自動 HTTPS
  • 圖片上傳:Next.js Server Action + basic-ftp(Node runtime)
  • 程序管理:pm2(Next.js + Caddy)

2. 為什麼是純 PWA(決策紀錄)

在「iOS + 永不碰 Apple Developer 簽證 + 展場 Kiosk」三個約束下,PWA 是唯一解:

方案 是否產生 .ipa 簽證到期問題 結論
原生 IPA 每年到期、需重簽 ✗ 排除
Capacitor(WebView 殼) 每年到期 ✗ 排除
Expo / React Native 是(EAS Build) 每年到期 ✗ 排除
純 PWA 無此問題 ✓ 採用

關鍵釐清:

  • Guided Access(引導使用模式)是 iOS 系統層級功能,鎖的是前景 app,不限原生或 Safari。因此 PWA 一樣能被鎖。
  • 展場長展期、無人看管,建議用 Apple Configurator 2 的 Single App Mode / Autonomous Single App Mode 做到「開機自動鎖在這個 PWA、免人工」。此功能不需要 Apple Developer 帳號(Configurator 是免費 Mac 工具,針對自有裝置),與簽證無關。
  • Expo Go 不可用於正式佈署(開發沙盒、功能受限);EAS Build 產出的仍是標準需簽證的 app。

3. 網路與 TLS

3.1 為什麼需要 TLS

PWA 要在 secure context 下運作,且 wss://(MQTT over WebSocket)也要求加密連線。區網內無公網域名,無法用 Let's Encrypt,故採 mkcert 自簽 + iPad 安裝 RootCA

3.2 mkcert 在 Windows

  • 安裝後 mkcert -install 會將 RootCA 裝進 Windows 憑證庫。
  • 產 leaf 給 Caddy 用:mkcert 192.168.1.50 host.localSAN 必須包含連線用的 IP)。
  • CAROOT 路徑在 %LOCALAPPDATA%\mkcert,內含 rootCA.pemrootCA-key.pem
  • rootCA-key.pem 是整個系統的命脈:重簽 leaf 靠它,遺失會導致重簽產生新 Root、iPad 全部要重裝。務必異地備份。

3.3 憑證效期

  • leaf 憑證(cert.pem / key.pem:mkcert v1.4.4 起效期約 2 年多。到期只需在 host 重簽,iPad 完全不用動(iPad 信任的是 RootCA,非 leaf)。
  • RootCA:效期約 10 年。到期才需重新在每台 iPad 安裝(屆時設備早已汰換)。
  • 重簽時 SAN 必須與原本一致(同組 IP / 主機名),否則 PWA 連線會因 SAN 不符報錯。

3.4 iPad 安裝 RootCA 流程(易漏)

  1. rootCA.pem 傳到 iPad(AirDrop / 郵件 / 網頁下載)並安裝描述檔。
  2. 設定 → 一般 → VPN 與裝置管理 安裝描述檔。
  3. 設定 → 一般 → 關於本機 → 憑證信任設定 手動「啟用完整信任」。只裝描述檔不夠,這步必做。

3.5 固定 IP

Host 必須使用固定 IP(DHCP 保留或靜態)。iPad 端以 zustand 保存 host IP,且憑證 SAN 綁定 IP,host 換 IP 會導致設定與憑證同時失效。

3.6 Windows 防火牆

Windows Defender 防火牆需手動放行對外 port:

  • 443(Caddy,對 iPad WiFi 開放)
  • 1883(Mosquitto TCP,對另外兩台 Unity 主機的區網 IP 開放)
  • 9001 僅 localhost 內部使用(Caddy 反代),不需對外開放

Caddy 與 Mosquitto 首次啟動時 Windows 可能跳防火牆詢問,選允許。

3.7 WiFi 即時性建議

  • 專用 AP、專用 SSID,不與公眾 WiFi 混用。
  • 控制訊號用 QoS 0、payload 最小化,高頻操作(連續拖曳)在 iPad 端 throttle/debounce 後再 publish。
  • Host 與 Unity 主機盡量走有線,只讓 iPad 走 WiFi。

4. MQTT 設計

4.1 連線方式

來源 協定 目標 TLS
iPad(PWA / MQTT.js) MQTT over WebSocket wss://<host>/mqtt → Caddy → Mosquitto localhost:9001 是(Caddy 收 TLS)
Unity client MQTT over TCP <host>:1883 否(區網內 TCP)

Caddy 與 Mosquitto 同在一台 host,Caddy 直接反代 localhost:9001,無 Docker 跨網路問題。TLS 集中在 Caddy;Mosquitto 走明文 ws / tcp(封閉區網內)。

4.2 Topic 命名規範

station/{stationId}/... 命名空間,stationId1 / 2 / 3

Topic 方向 QoS Retained 說明
station/{id}/control iPad → Unity 0 即時控制訊號(操作、移動、選取)
station/{id}/state Host → all 1 流程狀態(single source of truth),重連後可立即取得當前狀態
station/{id}/command iPad → Host 1 流程指令(如「開始上傳」)
station/{id}/result Host → all 1 最終圖片 URL,iPad 與 Unity 皆訂閱以顯示 QR
station/{id}/error Host → all 1 明確的錯誤狀態(帶錯誤碼/原因)
station/{id}/status/ipad iPad LWT 1 iPad 上下線(Last Will)
station/{id}/status/unity Unity LWT 1 Unity 上下線(Last Will)

4.3 即時性與穩定性原則

  • QoS 分級:即時控制 QoS 0(降延遲);狀態轉換 / 結果 / 錯誤 QoS 1(確保送達)。
  • Retained + LWT:用 LWT 偵測斷線;retained message 讓裝置重連後立即取得最新狀態與結果。
  • 狀態 single source of truth:流程狀態以 Host 為準(station/{id}/state),不可只存在各 client 記憶體。任一 client 重開後須能由 retained state 接回原進度。

4.4 iPad 與螢幕的配對

  • iPad 以 zustand persist 保存 { hostIp, stationId }
  • iPad 須提供「重新設定」入口(因 iOS 可能在系統清理時清除 PWA storage,設定遺失需可重設)。
  • 配對機制建議:首次開啟顯示設定頁手動輸入 / 選擇 stationId 與 host IP;之後自動沿用。

5. 圖片上傳與 QR 顯示

5.1 流程

  1. 流程結束,iPad 透過 station/{id}/command 或直接呼叫 Next.js Server Action 觸發上傳。
  2. Server Action 用 basic-ftp 上傳圖片至客戶 FTPS(已驗證可用)。
  3. 上傳成功後,對「對外 HTTP(S) URL」做一次 HEAD 驗證可達,再產生 QR。
  4. Host 將最終 URL publish 到 station/{id}/result(retained, QoS 1)。
  5. iPad 與 Unity 各自訂閱 result任一端皆可渲染 QR。URL 的產生與驗證集中在 Host 一處,新增螢幕顯示為零成本。

5.2 注意事項

  • 「上傳成功」≠「URL 可被掃描存取」:FTPS 是上傳協定,掃描走 HTTP(S)。需與客戶確認上傳路徑對應的對外 URL 規則與 web server。
  • 上傳到可讀之間可能有延遲:故步驟 3 先 HEAD 驗證,避免掃到 404;必要時加重試。
  • FTPS passive mode 與防火牆:確認 passive port range 未被擋。
  • Server Action runtimebasic-ftp 是 Node API,須跑在 Node runtime(standalone 預設即是,勿標為 edge)。
  • 並發控制:三台可能同時送,上傳建議以 queue 串接而非無限制並發,避免打爆 FTPS 連線數。
  • Windows 路徑:暫存圖片路徑等請用 path.join 或 Windows 路徑,勿寫死 POSIX 斜線。

5.3 失敗處理

  • 失敗時 Host 透過 station/{id}/error 廣播明確錯誤狀態(錯誤碼/原因),iPad 與 Unity 兩端皆顯示失敗,維持 single source of truth。
  • 提供自動重試(數次)+ 手動重試按鈕,讓現場工作人員可一鍵重送。
  • 考慮失敗時本地暫存圖片、事後補傳,避免使用者該張圖片遺失。

6. 部署(Win10 原生)

6.1 安裝清單

元件 安裝方式
Node.js 20 LTS 官方 Windows installer(Win10 可正常運作)
pm2 npm i -g pm2
Caddy 下載 Windows 版 caddy.exe,放固定路徑(如 C:\caddy\
Mosquitto 官方 Windows installer,安裝時註冊為 Windows Service
mkcert 下載 Windows 版執行檔,mkcert -install

6.2 目錄建議

C:\exhibit\
  web\                       # Next.js 專案
    .next\standalone\        # build 產物,pm2 在此跑 server.js
  caddy\
    caddy.exe
    Caddyfile
    certs\
      cert.pem
      key.pem
  scripts\
    start-services.bat       # 放進 startup 資料夾

6.3 啟動順序與依賴

  • Mosquitto(Windows Service)開機自動啟動,最先就緒。
  • Next.js 與 Caddy 由 bat 啟動;兩者彼此無依賴,皆連 Mosquitto。
  • bat 開頭可加短暫等待,確保 Mosquitto service 已就緒。

7. 範本檔案

7.1 Caddyfile

{
	# 關閉自動 HTTPS:區網自簽憑證,不走 ACME
	auto_https off
}

# 以 host IP 作為 site address
https://192.168.1.50 {
	# 手動指定 mkcert 產出的憑證(Windows 路徑)
	tls C:\caddy\certs\cert.pem C:\caddy\certs\key.pem

	# MQTT over WebSocket → 同機 Mosquitto 9001
	@mqtt path /mqtt*
	handle @mqtt {
		reverse_proxy localhost:9001
	}

	# 其餘流量 → Next.js
	handle {
		reverse_proxy localhost:3000
	}
}

Caddy 會偵測憑證檔變動並嘗試熱載;重簽後通常只需覆寫 cert.pem / key.pem。實際行為請以你使用的 Caddy 版本實測確認,必要時 caddy reload

7.2 mosquitto.conf(Windows Service)

# TCP listener — 給區網內的 Unity client
listener 1883 0.0.0.0
protocol mqtt

# WebSocket listener — 由同機 Caddy 反代給 iPad(僅需 localhost)
listener 9001 localhost
protocol websockets

# 封閉區網環境,允許匿名連線
allow_anonymous true

# 持久化(保留 retained message 與 session)
persistence true
persistence_location C:\Program Files\mosquitto\data\

# 日誌
log_dest file C:\Program Files\mosquitto\mosquitto.log
log_type error
log_type warning
log_type notice

9001 綁 localhost 即可(只給同機 Caddy 反代用,不需對外)。1883 綁 0.0.0.0 讓區網內 Unity 主機連得到。修改 conf 後需重啟 Mosquitto service 生效。

7.3 Next.js standalone 設定

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};

module.exports = nextConfig;

Build 後處理(standalone 不含靜態資源,須手動複製):

:: 在專案根目錄 build 後執行
xcopy /E /I /Y public .next\standalone\public
xcopy /E /I /Y .next\static .next\standalone\.next\static

standalone 產物跑的是 node server.js不是 next startpublic\.next\static 漏複製會導致靜態資源 404。

7.4 啟動 bat:C:\exhibit\scripts\start-services.bat

放進 startup 資料夾(Win + Rshell:startup),搭配 Windows 自動登入。

@echo off
:: 等待 Mosquitto service 就緒
timeout /t 5 /nobreak

:: 清掉前次殘留的 pm2 程序,確保乾淨啟動
pm2 delete all

:: 啟動 Next.js standalone
cd /d C:\exhibit\web\.next\standalone
pm2 start server.js --name nextjs

:: 啟動 Caddy(用 caddy run 讓 pm2 接管,勿用 caddy start)
pm2 start "C:\caddy\caddy.exe" --name caddy -- run --config C:\caddy\Caddyfile

:: 儲存目前程序清單
pm2 save

環境變數(FTPS 帳密、對外 URL)建議用 pm2 ecosystem 檔注入,避免寫死於程式碼:

C:\exhibit\web\.next\standalone\ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'nextjs',
      script: 'server.js',
      env: {
        NODE_ENV: 'production',
        PORT: '3000',
        FTPS_HOST: '<ftps-host>',
        FTPS_USER: '<ftps-user>',
        FTPS_PASSWORD: '<ftps-password>',
        FTPS_SECURE: 'true',
        PUBLIC_IMAGE_BASE_URL: '<向客戶確認後填入>',
      },
    },
  ],
};

若改用 ecosystem 檔,bat 中 Next.js 那行改為:

cd /d C:\exhibit\web\.next\standalone
pm2 start ecosystem.config.js

8. 開工檢查清單

部署現場最容易出問題的順序:WiFi 延遲 > iPad 憑證信任那步 > 開機服務未自動拉起

  • Host 設為固定 IP,憑證 SAN 包含此 IP
  • mkcert CAROOT(%LOCALAPPDATA%\mkcert,含 rootCA-key.pem)已備份至異地
  • 三台 iPad 已安裝 RootCA 且啟用完整信任
  • 三台 iPad 設定 Single App Mode(Apple Configurator)並測試開機自動鎖機
  • iPad 自動鎖定設為「永不」+ PWA 端 Wake Lock 實測
  • PWA manifest display: standalone、防呆 CSS(overscroll-behavioruser-selecttouch-action
  • PWA service worker 更新策略:能偵測新版並刷新,避免跑到舊 code
  • zustand persist 設定(hostIp / stationId)+「重新設定」入口
  • Mosquitto 已註冊為 Windows Service 且設為自動啟動
  • Windows 自動登入已設定netplwiz),bat 才會在開機後執行
  • start-services.bat 已放入 startup 資料夾
  • pm2 跑 Caddy 用 caddy run(非 caddy start
  • Next.js standalone 已複製 public\.next\static
  • Windows 防火牆放行 443、1883(對 Unity 主機)
  • 與客戶確認 FTPS 上傳路徑對應的對外 HTTP(S) URL 規則
  • 與客戶確認 FTPS passive port range 未被防火牆擋
  • 上傳後 HEAD 驗證 URL 可達再產生 QR
  • 上傳失敗的自動重試 + 手動重試 + 本地暫存補傳
  • 實際重新開機測試:確認 Mosquitto / Next.js / Caddy 三者皆自動起來、iPad 連得上

9. 待釐清事項

  1. FTPS 上傳路徑 → 對外 URL 對應規則(最高優先,影響 QR 內容是否可掃)
  2. iPad 與 station 的配對機制細節(手動輸入 / QR 綁定 / URL 參數)
  3. QR 最終顯示位置(iPad 先做,預期螢幕也需顯示 — 架構已支援零成本擴充)
  4. Caddy 版本對憑證檔變動的熱載行為(決定重簽後是否需手動 reload)
  5. Host 機器的 Win10 build 版本(確認 Node 20 相容性;過舊則退 Node 18)