顯示具有 軟體開發 標籤的文章。 顯示所有文章
顯示具有 軟體開發 標籤的文章。 顯示所有文章

Claude Desktop 與 MCP 完整使用指南

文章更新時間: 2025年5月 適用版本: Claude Desktop 最新版本 作者: AI 開發實踐指南

🚀 前言

Claude Desktop 配合 MCP (Model Context Protocol) 為 AI 輔助開發帶來革命性的體驗。本指南將深入介紹如何充分利用 Claude Desktop 的強大功能,包括 CLI 工具、MCP 服務整合、以及各種高效的使用技巧。

📋 目錄


1. Claude Desktop 基礎設置

1.1 安裝 Claude Desktop

重要提醒 Claude Desktop 目前僅支援 macOS 和 Windows,尚不支援 Linux。

1. **下載安裝程式** - 訪問 [Claude 官方網站](https://claude.ai/download) - 選擇適合您作業系統的版本下載 - 按照安裝指示完成安裝
  1. 檢查更新
    # 在 Claude 選單中選擇「檢查更新...」
    # 或使用快捷鍵檢查版本
    

1.2 基本配置

建立基本的工作環境:

# 建立 Claude 設定目錄
mkdir -p <sub>/.claude/commands
mkdir -p </sub>/.claude/config

2. Claude Code 作為 CLI 工具

2.1 命令列參數

Claude Code 提供強大的 CLI 功能,支援多種操作模式:

# 基本啟動
claude

# 無頭模式運行
claude -p

# 傳遞參數啟動
claude --project /path/to/project

# 管道操作
echo "分析這個程式碼" | claude

# 同時運行多個實例
claude &
claude --instance=2 &

2.2 核心功能特性

  • ✅ 多實例支援:可同時運行多個 Claude 實例
  • ✅ 管道整合:與其他命令列工具無縫鏈接
  • ✅ 子代理啟動:Claude 可以啟動自己的實例處理子任務
  • ✅ headless 模式:適合自動化腳本使用

3. 圖像處理功能

3.1 圖像輸入方式

拖曳圖片(macOS)

# 直接將圖片檔案拖曳到終端機視窗中
# 支援格式:PNG, JPG, GIF, WebP

截圖與貼上

# 1. 使用 macOS 截圖快捷鍵
Shift + Command + Control + 4  # 複製螢幕截圖到剪貼板

# 2. 在 Claude Code 中貼上
Control + V  # 注意:是 Control + V,不是 Command + V

3.2 實際應用場景

設計 Mockup 開發

1. 設計師提供 UI mockup
2. 將 mockup 截圖貼入 Claude
3. 要求 Claude 根據設計構建前端介面
4. 實現閉環開發流程

自動化視覺測試

# 使用 Puppeteer MCP server 自動截圖
/mcp puppeteer screenshot --url="http://localhost:3000"
# Claude 會自動分析截圖並提供改進建議

4. MCP 服務深度整合

4.1 什麼是 MCP?

MCP (Model Context Protocol) 是一種開放協議,標準化了應用程式向 LLM 提供上下文的方式。就像 USB-C 為設備提供標準化連接一樣,MCP 為 AI 模型與不同數據源和工具之間提供標準化連接。

4.2 MCP 設定檔配置

在 <sub>/.claude/config.json 中添加 MCP 服務器:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"]
    },
    "postgres": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-postgres", "postgresql://user:pass@localhost/db"]
    },
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
      }
    }
  }
}

4.3 常用 MCP 命令

檔案系統操作

/mcp filesystem list /path/to/directory
/mcp filesystem read /path/to/file.txt
/mcp filesystem write /path/to/new-file.txt "content"
/mcp filesystem search "search_term" /path/to/search

資料庫操作

/mcp postgres query "SELECT * FROM users LIMIT 10"
/mcp postgres schema users
/mcp postgres explain "SELECT * FROM complex_query"

GitHub 整合

/mcp github repos list
/mcp github issues list --repo owner/repo
/mcp github pr create --title "Feature" --body "Description"
/mcp github review --pr 123 --comment "LGTM"

4.4 熱門 MCP 伺服器

伺服器類型 功能描述 安裝命令
Filesystem 檔案系統操作 npm install @modelcontextprotocol/server-filesystem
PostgreSQL 資料庫查詢與管理 npm install @modelcontextprotocol/server-postgres
GitHub Git 操作與程式碼審查 npm install @modelcontextprotocol/server-github
Puppeteer 網頁自動化與截圖 npm install @modelcontextprotocol/server-puppeteer
Slack 團隊通訊整合 npm install @modelcontextprotocol/server-slack
Brave Search 網路搜尋功能 npm install @modelcontextprotocol/server-brave-search

5. claude.md 專案配置

5.1 claude.md 文件概述

claude.md 是 Claude Desktop 的核心配置文件,每次向 Claude 發出請求時都會載入。它充當專案的上下文提示,包含專案特定的指引和設定。

5.2 claude.md 內容結構

基本專案資訊

# 專案名稱:我的 Web 應用程式

## 技術棧
- Frontend: React + TypeScript
- Backend: Node.js + Express
- Database: PostgreSQL
- Styling: Tailwind CSS

## 常用指令

bash
npm run dev        # 啟動開發伺服器
npm run build      # 建構生產版本
npm test          # 執行測試
npm run lint      # 程式碼檢查


## 程式碼風格
- 使用 TypeScript 嚴格模式
- 遵循 ESLint 規則
- 使用 Prettier 格式化
- 採用函數式組件和 Hooks

## 測試指引
- 所有新功能都需要測試覆蓋
- 使用 Jest + React Testing Library
- 最小測試覆蓋率:80%

## Git 工作流程
- 使用 feature 分支開發
- PR 需要通過 CI/CD 檢查
- 需要 code review 才能合併

5.3 claude.md 管理命令

初始化專案配置

/init  # 掃描目錄並自動生成 claude.md

添加專案指引

# 在程式碼中添加指令到 claude.md
# 範例:在檔案中加入註解
# claude.md: 此模組負責用戶認證,請確保遵循安全最佳實踐

5.4 多層級 claude.md 配置

全域設定(/.claude/claude.md)

# 全域 Claude 設定

## 個人偏好
- 偏好使用 TypeScript
- 喜歡函數式程式設計風格
- 重視程式碼可讀性和維護性

## 常用工具
- Git 工作流程偏好
- 編輯器:VS Code
- 終端:iTerm2 + zsh

專案特定設定(./claude.md)

# 當前專案設定
## 專案特殊需求
- 特定的 API 規格
- 專案特有的架構模式
- 團隊協作規範

子目錄設定(./src/components/claude.md)

# 元件開發指引
## React 元件規範
- 使用 TypeScript 介面定義 props
- 實作 error boundary
- 加入適當的 accessibility 屬性

6. 斜線命令系統

6.1 內建斜線命令

Claude Desktop 提供豐富的內建命令,方便使用者在互動式會話中控制 Claude 的行為:

命令 功能描述 使用範例與說明
/help 顯示所有可用命令 /help - 列出所有斜線命令及其簡要說明。
/init 初始化專案設定 /init - 掃描目前目錄結構,並產生一個基礎的 claude.md 檔案,作為專案的初始指引。
/clear 清除對話歷史 /clear - 清除目前的對話記錄,重新開始一個乾淨的會話。
/compact [instructions] 壓縮上下文 /compact - 壓縮目前的對話歷史以節省 token。可選參數 [instructions] 用於指導壓縮過程,例如 /compact "保留最近的程式碼變更"。
/config 檢視/修改設定 /config - 進入設定模式,可以檢視或修改 Claude Code 的設定,例如主題、API 金鑰等。
/cost 顯示 token 使用統計 /cost - 顯示目前會話或專案的 token 使用量統計,幫助使用者追蹤成本。
/doctor 檢查 Claude Code 安裝狀態 /doctor - 執行一系列檢查,確認 Claude Code 的安裝是否完整且運作正常。
/login 切換 Anthropic 帳戶 /login - 允許使用者登入或切換不同的 Anthropic 帳戶。
/logout 登出 Anthropic 帳戶 /logout - 登出目前使用的 Anthropic 帳戶。
/memory 編輯 claude.md 記憶檔案 /memory - 開啟 claude.md 檔案進行編輯,方便快速修改專案指引。
/pr_comments 檢視 Pull Request 評論 /pr_comments - (需整合 GitHub MCP) 檢視指定 Pull Request 的評論。
/review 請求程式碼審查 /review - (需整合 GitHub MCP) 請求 Claude 對指定的程式碼檔案或 Pull Request 進行審查。
/status 檢視帳戶與系統狀態 /status - 顯示目前帳戶資訊、系統狀態以及 MCP 伺服器連線狀態。
/terminal-setup 設定終端機換行快速鍵 /terminal-setup - 自動為 iTerm2 和 VSCode 終端機設定 Shift+Enter 作為換行鍵。
/vim 進入 Vim 模式 /vim - 切換到 Vim 輸入模式,可以使用 Vim 的部分快捷鍵進行編輯。
/bug 回報錯誤 /bug - 將目前的對話內容(包含錯誤訊息)傳送給 Anthropic 團隊以協助改善產品。
/mcp MCP 服務操作 /mcp <server_name> <command> [args] - 與已設定的 MCP 伺服器互動,例如 /mcp filesystem list .。
/commit Git 提交操作 /commit "feat: add new login feature" - (需整合 Git) 執行 Git 提交。也支援 --generate-message 自動產生提交訊息。
/test 執行測試 /test --file src/utils.test.js - (需專案配置) 執行專案中定義的測試。
/lint 程式碼檢查 /lint src/components/ - (需專案配置) 對指定路徑的程式碼執行 Linting。
/refactor 程式碼重構 /refactor --strategy=performance src/heavy_logic.js - (需專案配置) 請求 Claude 協助重構程式碼。

特殊快捷鍵:

  • # 快速記憶: 在輸入的開頭使用 # 可以快速將該行內容儲存到記憶檔案 (claude.md 或其他指定檔案) 中。系統會提示選擇要儲存的記憶檔案。

    # 這是一個重要的專案筆記,需要記住
    
  • 終端機換行:

    • Option + Enter (macOS Terminal.app, iTerm2, VSCode): 預設的換行方式。
    • Shift + Enter (iTerm2, VSCode): 透過 /terminal-setup 命令設定後,可使用此更直觀的快捷鍵。

6.2 自訂斜線命令

在 <sub>/.claude/commands/ 目錄中建立自訂命令:

GitHub Issue 處理命令

<!-- </sub>/.claude/commands/github-issue.md -->
# GitHub Issue 處理

分析並修復以下 GitHub issue:

Issue #{{1}}

請:
1. 理解問題描述
2. 分析相關程式碼
3. 提供修復方案
4. 撰寫測試案例
5. 準備 PR 說明

程式碼審查命令

<!-- <sub>/.claude/commands/code-review.md -->
# 程式碼審查

請對以下程式碼進行全面審查:

檔案:{{1}}

審查重點:
- 程式碼品質
- 效能考量
- 安全性問題
- 最佳實踐遵循
- 測試覆蓋率

請提供具體的改進建議。

6.3 命令使用範例

# 使用自訂命令處理 GitHub issue
/github-issue 123

# 程式碼審查
/code-review src/components/UserProfile.tsx

# 重構優化
/refactor src/utils/dataProcessor.js

7. 使用者介面技巧

7.1 鍵盤快捷鍵

快捷鍵 功能 說明
Tab 自動補全 檔案路徑和命令補全
Escape 中斷操作 立即停止 Claude 的當前任務
Ctrl+C 複製 複製選取內容
Ctrl+V 貼上圖片 貼上剪貼板中的圖片
Cmd+K 快速命令 開啟命令選單
Cmd+, 設定 開啟設定介面

7.2 智慧提示技巧

使用 Tab 補全提升精準度

# 不精確的指令
"分析這個檔案"

# 使用 Tab 補全的精確指令
"分析 src/components/UserProfile.tsx 檔案的效能問題"

善用 Escape 鍵控制對話方向

# 當 Claude 偏離預期時
按 Escape → "請撤銷上一步,改為專注於錯誤處理部分"

8. 版本控制整合

8.1 Git 工作流程最佳實踐

頻繁提交策略

# 完成功能後立即提交
git add .
git commit -m "feat: 新增用戶登入功能"

# 讓 Claude 撰寫提交訊息
/commit --generate-message

智慧提交訊息

# Claude 會分析變更內容並生成語意化提交訊息
/commit --analyze-changes
# 輸出範例:
# "feat(auth): implement JWT-based authentication with refresh tokens
#
# - Add JWT token generation and validation
# - Implement refresh token rotation
# - Add middleware for protected routes
# - Include comprehensive error handling"

8.2 回溯與重新開始策略

戰略性回溯

# 檢查最近的提交
git log --oneline -10

# 回溯到穩定狀態
git reset --hard HEAD</sub>3

# 清除 Claude 對話歷史並重新開始
/clear
"從最後一個穩定的提交重新開始,請實現 XYZ 功能"

9. GitHub 深度整合

9.1 GitHub CLI 設定

安裝與認證

# 安裝 GitHub CLI
brew install gh

# 認證
gh auth login

# 設定預設編輯器
gh config set editor code

9.2 GitHub MCP 伺服器功能

專案管理

/mcp github repos create my-new-project --private
/mcp github repos clone owner/repo
/mcp github repos fork owner/repo

Issue 管理

/mcp github issues create --title "Bug 修復" --body "詳細描述..."
/mcp github issues list --state open --assignee @me
/mcp github issues close 123 --comment "已修復"

Pull Request 工作流程

# 建立 PR
/mcp github pr create --title "新功能:用戶認證" \
  --body "詳細 PR 描述" \
  --draft

# 程式碼審查
/mcp github pr review 456 --approve --comment "LGTM!"

# 合併 PR
/mcp github pr merge 456 --squash

9.3 自動化 GitHub 工作流程

CI/CD 整合

# .github/workflows/claude-review.yml
name: Claude Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  claude-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Claude Code Review
        run: |
          claude /code-review ${{ github.event.pull_request.head.sha }}

10. 上下文管理與成本優化

10.1 Token 使用監控

即時監控

# 檢查當前 token 使用量
/tokens status

# 詳細使用統計
/tokens usage --detailed

# 設定使用限制警告
/tokens limit --warning-at 80% --stop-at 95%

10.2 上下文壓縮策略

智慧壓縮時機

# 任務完成後壓縮
"任務完成,請壓縮上下文準備下一個任務"
/compress --preserve-key-context

# 自動壓縮設定
/settings auto-compress --at 90% --preserve recent-commits

外部記憶替代方案

# 使用 GitHub Issues 作為外部記憶
/mcp github issues create --title "專案計畫" \
  --body "$(echo '目前進度和下一步計畫')"

# 使用 Scratchpads
/scratchpad create project-plan
/scratchpad save current-status "目前實作狀態:..."

10.3 Open Telemetry 整合

設定追蹤

{
  "telemetry": {
    "endpoint": "https://api.datadoghq.com/v1/traces",
    "service": "claude-development",
    "environment": "production",
    "tags": {
      "team": "frontend",
      "project": "user-dashboard"
    }
  }
}

成本分析

# 團隊使用量報告
/telemetry report --team frontend --period month

# 專案成本分析
/telemetry costs --project user-dashboard --breakdown

11. 進階使用技巧

11.1 多專案管理

專案切換

# 切換專案上下文
claude --project <sub>/projects/web-app
claude --project </sub>/projects/mobile-app

# 同時管理多專案
claude --instance web &
claude --instance mobile &

11.2 團隊協作最佳實踐

共享設定

# 建立團隊 claude.md 模板
mkdir -p team-templates/
cp claude.md team-templates/frontend-template.md

# 同步團隊設定
git clone team-claude-configs
ln -s team-claude-configs/frontend.md ./claude.md

11.3 自動化腳本整合

部署腳本

#!/bin/bash
# deploy.sh

echo "開始部署流程..." | claude --context deployment

# 執行測試
npm test

# 建構應用程式
npm run build

# 部署到 staging
echo "部署到 staging 環境" | claude --log-deployment

# Claude 輔助驗證
claude "請驗證 staging 環境的部署狀態"

12. 疑難排解

12.1 常見問題

MCP 連接問題

# 檢查 MCP 服務狀態
/mcp status

# 重啟 MCP 服務
/mcp restart filesystem

# 檢查配置
/mcp config validate

效能優化

# 清理快取
/cache clear

# 重置設定
/settings reset --keep-auth

# 檢查記憶體使用
/system status

12.2 偵錯模式

啟用詳細日志

claude --debug --log-level verbose

# 查看日志
tail -f <sub>/.claude/logs/debug.log

13. 方案升級建議

13.1 Claude Max 方案優勢

建議升級到 Claude Max 方案的理由:

  • 🚀 更高的 token 限制:適合大型專案開發
  • ⚡ 優先處理速度:減少等待時間
  • 🔧 進階功能存取:包含最新的 MCP 功能
  • 📊 詳細使用分析:更好的成本控制
  • 👥 團隊協作工具:多人開發支援

13.2 成本效益分析

方案 月費 適用場景 建議用戶
免費版 $0 個人學習、小專案 初學者
Pro $20 專業開發、中型專案 獨立開發者
Max $100+ 大型專案、團隊開發 企業用戶

14. 實戰範例

14.1 完整專案開發流程

1. 專案初始化

mkdir my-react-app && cd my-react-app
claude
/init

2. 設定 MCP 服務

/mcp filesystem enable .
/mcp github connect --repo my-username/my-react-app

3. 開發循環

# 開發新功能
"請建立一個用戶登入元件"

# 測試
/test components/Login.test.tsx

# 程式碼審查
/code-review src/components/Login.tsx

# 提交
/commit --generate-message

4. 部署準備

/lint --fix
/test --coverage
/mcp github pr create --title "Add user authentication"

15. 總結

Claude Desktop 配合 MCP 提供了前所未有的 AI 輔助開發體驗。透過:

  • 🔄 完整的 CLI 整合:seamless 命令列操作
  • 🔌 豐富的 MCP 生態系統:連接各種開發工具
  • 📝 智慧的專案配置:claude.md 與斜線命令
  • 🎯 精準的上下文管理:有效控制成本
  • 🚀 自動化工作流程:提升開發效率

掌握這些技巧,您將能充分發揮 AI 輔助開發的威力,大幅提升程式設計效率和程式碼品質。


**最後更新:** 2025年5月29日 **版本:** v2.0

16. Claude Code 進階設定配置

16.1 settings.json 完整配置

Claude Code 使用層次化的 settings.json 文件進行配置,支援全域和專案級別的設定。

配置檔案位置優先順序:

  1. ./claude/settings.json (專案層級)
  2. </sub>/.claude/settings.json (用戶層級)
  3. 系統預設值

完整 settings.json 範例:

{
  "apiKeyHelper": "/bin/generate_temp_api_key.sh",
  "cleanupPeriodDays": 20,
  "env": {
    "NODE_ENV": "development",
    "DEBUG": "true",
    "API_BASE_URL": "https://api.example.com"
  },
  "includeCoAuthoredBy": false,
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-filesystem", "/Users/oliver/projects"]
    },
    "postgres": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-postgres", "postgresql://user:pass@localhost/mydb"]
    },
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
      }
    },
    "brave-search": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-brave-search"],
      "env": {
        "BRAVE_API_KEY": "BSA_xxxxxxxxxxxx"
      }
    }
  },
  "allowedTools": [
    "Bash(npm run test:*)",
    "Edit(src/**)",
    "Read(docs/**)",
    "WebFetch(domain:docs.anthropic.com)",
    "mcp__github__*"
  ]
}

16.2 設定管理命令

基本設定操作:

# 查看當前配置
claude config

# 設定專案配置
claude config set cleanupPeriodDays 30

# 設定全域配置
claude config set -g theme dark

# 檢視特定設定值
claude config get apiKeyHelper

# 移除設定
claude config unset includeCoAuthoredBy

全域配置選項:

# 主題設定
claude config set -g theme dark-daltonized

# 自動更新設定
claude config set -g autoUpdaterStatus disabled

# 通知設定
claude config set -g preferredNotifChannel iterm2_with_bell

# 詳細輸出模式
claude config set -g verbose true

16.3 工具權限管理

使用 /allowed-tools 管理權限:

# 檢視目前權限設定
/allowed-tools

# 添加權限規則到 settings.json
# 然後重新載入設定

常用權限規則範例:

Bash 命令權限:

{
  "allowedTools": [
    "Bash(npm run build)",           // 精確匹配
    "Bash(npm run test:*)",          // 前綴匹配
    "Bash(git add .)",               // Git 操作
    "Bash(docker-compose up -d)",    // Docker 命令
    "Bash(yarn install)"             // 套件管理
  ]
}

檔案操作權限:

{
  "allowedTools": [
    "Read(src/**)",                  // 讀取 src 目錄所有檔案
    "Edit(src/components/**)",       // 編輯元件檔案
    "Edit(<sub>/.zshrc)",               // 編輯家目錄檔案
    "Read(//tmp/build_cache)",      // 絕對路徑
    "Edit(node_modules/**)",        // 排除 node_modules
    "Read(docs/**/*.md)"            // 特定檔案類型
  ]
}

MCP 工具權限:

{
  "allowedTools": [
    "mcp__github__*",                        // 所有 GitHub MCP 工具
    "mcp__postgres__query",                  // 特定資料庫查詢工具
    "mcp__filesystem__read_file",            // 檔案系統讀取
    "mcp__puppeteer__puppeteer_navigate"     // Puppeteer 導航工具
  ]
}

網路存取權限:

{
  "allowedTools": [
    "WebFetch(domain:docs.anthropic.com)",   // 特定網域
    "WebFetch(domain:github.com)",           // GitHub 存取
    "WebFetch(domain:api.openai.com)"        // API 存取
  ]
}

16.4 環境變數完整配置

在 settings.json 中設定環境變數:

{
  "env": {
    "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022",
    "ANTHROPIC_SMALL_FAST_MODEL": "claude-3-haiku-20240307",
    "BASH_DEFAULT_TIMEOUT_MS": "300000",
    "BASH_MAX_TIMEOUT_MS": "600000",
    "BASH_MAX_OUTPUT_LENGTH": "50000",
    "NODE_ENV": "development",
    "DEBUG": "app:*",
    "GITHUB_TOKEN": "ghp_xxxxxxxxxxxx",
    "DATABASE_URL": "postgresql://user:pass@localhost/db"
  }
}

進階環境變數設定:

# API 相關設定
export ANTHROPIC_AUTH_TOKEN="your_custom_token"
export ANTHROPIC_CUSTOM_HEADERS="X-Custom-Header: value"

# Proxy 設定
export HTTP_PROXY="http://proxy.company.com:8080"
export HTTPS_PROXY="http://proxy.company.com:8080"

# 除錯與監控
export CLAUDE_CODE_USE_BEDROCK=1
export CLAUDE_CODE_USE_VERTEX=1
export DISABLE_TELEMETRY=1
export DISABLE_AUTOUPDATER=1

# MCP 相關設定
export MCP_TIMEOUT=30000
export MCP_TOOL_TIMEOUT=10000

16.5 API 金鑰管理

自訂 API 金鑰助手:

#!/bin/bash
# </sub>/.claude/scripts/api_key_helper.sh

# 從環境變數獲取
if [ -n "$CLAUDE_API_KEY" ]; then
    echo "$CLAUDE_API_KEY"
    exit 0
fi

# 從 AWS Secrets Manager 獲取
aws secretsmanager get-secret-value \
    --secret-id "claude-api-key" \
    --query SecretString \
    --output text

# 從 1Password 獲取
op read "op://Private/Claude API Key/credential"

設定 API 金鑰助手:

{
  "apiKeyHelper": "<sub>/.claude/scripts/api_key_helper.sh",
  "env": {
    "CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "3600000"
  }
}

16.6 通知與終端設定

通知設定選項:

# iterm2 通知(推薦)
claude config set -g preferredNotifChannel iterm2

# 帶聲音的 iterm2 通知
claude config set -g preferredNotifChannel iterm2_with_bell

# 終端響鈴
claude config set -g preferredNotifChannel terminal_bell

# 停用所有通知
claude config set -g preferredNotifChannel notifications_disabled

終端優化設定:

# 在 Claude 中執行自動設定
/terminal-setup

# 手動設定 Option+Enter 換行(Terminal.app)
# 在終端偏好設定中:
# 鍵盤 → 將 Option 鍵當作 Meta 鍵使用

# Vim 模式啟用
/vim

# 或透過設定檔
claude config set -g vimMode true

16.7 實際應用範例

開發團隊設定範例:

{
  "cleanupPeriodDays": 7,
  "includeCoAuthoredBy": true,
  "env": {
    "NODE_ENV": "development",
    "ESLINT_CONFIG": "company-standard",
    "PRETTIER_CONFIG": ".prettierrc.company"
  },
  "allowedTools": [
    "Bash(npm run *)",
    "Bash(yarn *)",
    "Bash(git *)",
    "Edit(src/**)",
    "Edit(tests/**)",
    "Read(docs/**)",
    "WebFetch(domain:company-docs.internal)",
    "mcp__github__*",
    "mcp__slack__send_message"
  ],
  "mcpServers": {
    "company-db": {
      "command": "npx",
      "args": ["@company/mcp-database-server"],
      "env": {
        "DB_CONNECTION": "company_dev_db"
      }
    }
  }
}

個人開發者設定範例:

{
  "cleanupPeriodDays": 30,
  "includeCoAuthoredBy": false,
  "apiKeyHelper": "</sub>/.claude/get_api_key.sh",
  "env": {
    "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022",
    "BASH_DEFAULT_TIMEOUT_MS": "180000",
    "DEBUG": "myapp:*"
  },
  "allowedTools": [
    "Bash(*)",
    "Edit(**)",
    "Read(**)",
    "WebFetch(*)",
    "mcp__*"
  ],
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-filesystem", "/Users/username/projects"]
    },
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_personal_token"
      }
    }
  }
}

16.8 配置疑難排解

常見配置問題:

# 檢查配置有效性
claude config validate

# 重置配置到預設值
claude config reset

# 檢視完整配置來源
claude config debug

# 測試 MCP 連接
/mcp status --detailed

# 檢查權限問題
/allowed-tools --validate

配置備份與恢復:

# 備份設定
cp <sub>/.claude/settings.json </sub>/.claude/settings.json.backup

# 恢復設定
cp <sub>/.claude/settings.json.backup </sub>/.claude/settings.json

# 同步團隊設定
git clone https://github.com/company/claude-configs.git
ln -sf claude-configs/team-settings.json <sub>/.claude/settings.json

17. 設定最佳實踐與範例

17.1 不同專案類型的設定模板

React/Next.js 專案設定:

{
  "cleanupPeriodDays": 14,
  "includeCoAuthoredBy": true,
  "env": {
    "NODE_ENV": "development",
    "NEXT_TELEMETRY_DISABLED": "1",
    "FAST_REFRESH": "true"
  },
  "allowedTools": [
    "Bash(npm run dev)",
    "Bash(npm run build)",
    "Bash(npm run test:*)",
    "Bash(npx next:*)",
    "Edit(src/**)",
    "Edit(components/**)",
    "Edit(pages/**)",
    "Edit(app/**)",
    "Read(docs/**)",
    "WebFetch(domain:nextjs.org)",
    "WebFetch(domain:react.dev)"
  ],
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-filesystem", "./src"]
    }
  }
}

Python/Django 專案設定:

{
  "cleanupPeriodDays": 21,
  "env": {
    "PYTHONPATH": ".",
    "DJANGO_SETTINGS_MODULE": "myproject.settings.development",
    "DEBUG": "True"
  },
  "allowedTools": [
    "Bash(python manage.py *)",
    "Bash(pip install *)",
    "Bash(pytest *)",
    "Edit(myapp/**)",
    "Edit(templates/**)",
    "Read(requirements.txt)",
    "mcp__postgres__*"
  ],
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-postgres", "postgresql://user:pass@localhost/mydb"]
    }
  }
}

17.2 安全性設定指南

生產環境安全設定:

{
  "cleanupPeriodDays": 3,
  "includeCoAuthoredBy": true,
  "apiKeyHelper": "/usr/local/bin/get_claude_key_from_vault.sh",
  "env": {
    "NODE_ENV": "production",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
  },
  "allowedTools": [
    "Read(src/**)",
    "Read(docs/**)",
    "Bash(npm run test)",
    "Bash(npm run lint)"
  ]
}

開發環境權限設定:

{
  "allowedTools": [
    "Bash(npm run *)",
    "Bash(git status)",
    "Bash(git add .)",
    "Bash(git commit *)",
    "Edit(src/**)",
    "Edit(tests/**)",
    "Read(**)",
    "WebFetch(domain:docs.*)",
    "mcp__github__issues_*",
    "mcp__github__pulls_*"
  ]
}

17.3 效能優化設定

高效能設定:

{
  "cleanupPeriodDays": 7,
  "env": {
    "BASH_DEFAULT_TIMEOUT_MS": "120000",
    "BASH_MAX_TIMEOUT_MS": "300000",
    "BASH_MAX_OUTPUT_LENGTH": "25000",
    "MCP_TIMEOUT": "20000",
    "MCP_TOOL_TIMEOUT": "8000",
    "DISABLE_COST_WARNINGS": "1"
  }
}

17.4 團隊協作設定

共享團隊設定腳本:

#!/bin/bash
# setup-team-claude.sh

# 建立團隊設定目錄
mkdir -p </sub>/.claude/team-configs

# 下載團隊設定
curl -o <sub>/.claude/team-configs/settings.json \
  https://company.com/claude-configs/team-settings.json

# 建立符號連結
ln -sf </sub>/.claude/team-configs/settings.json <sub>/.claude/settings.json

# 設定團隊斜線命令
git clone https://github.com/company/claude-commands.git </sub>/.claude/commands

echo "Team Claude configuration installed!"

17.5 監控與日誌設定

詳細日誌設定:

{
  "env": {
    "CLAUDE_CODE_LOG_LEVEL": "debug",
    "CLAUDE_CODE_LOG_FILE": "<sub>/.claude/logs/claude.log",
    "BASH_LOG_COMMANDS": "1"
  }
}

監控腳本範例:

#!/bin/bash
# monitor-claude-usage.sh

# 檢查 Claude 使用狀況
claude config get cleanupPeriodDays

# 統計對話數量
find </sub>/.claude/chats -name "*.json" | wc -l

# 檢查磁盤使用量
du -sh <sub>/.claude/

# 清理舊對話
find </sub>/.claude/chats -mtime +30 -delete

設定檔管理建議

  1. 版本控制:將團隊設定檔加入 Git 管理
  2. 環境分離:開發、測試、生產環境使用不同設定
  3. 安全性:敏感資訊使用環境變數或外部金鑰管理
  4. 備份:定期備份個人化設定
  5. 文件化:為團隊設定撰寫清楚的說明文件

本文最初發布於 HackMD @BASHCAT。

當產品遇上全球化:從翻譯地獄到多語系天堂的技術之旅

去年十月,我朋友 Alex 興奮地跟我分享一個好消息:他們公司的 SaaS 產品終於要進軍歐洲市場了!

「很簡單啊,就把英文翻譯成德文、法文、西班牙文,然後上線就行了吧?」Alex 當時這樣跟我說,眼中閃著光芒。

三個月後,Alex 再次出現在我面前,但這次他的表情完全不同了。

「老兄,你知道德國人寫日期是 DD.MM.YYYY 格式嗎?你知道法國人用逗號當小數點嗎?你知道西班牙的增值稅計算方式跟英國完全不同嗎?」

Alex 一口氣問了我三個問題,然後重重地嘆了一口氣:「我以為多語系就是翻譯,結果發現這根本是個技術和文化的大坑。」

不只是翻譯:多語系的真相

Alex 的經歷其實很典型。大多數人聽到「多語系」或「國際化」,第一反應就是「翻譯」。但事實上,真正的多語系產品需要處理的問題遠比翻譯複雜得多。

我記得 Alex 跟我描述他們第一次嘗試「翻譯」產品時的情況:「我們找了個翻譯公司,把所有英文文案都翻譯成德文,然後直接貼上去。」結果呢?德文版本的按鈕文字太長,把整個介面都撐爆了。更糟糕的是,他們的定價頁面顯示的是「$99.99」,但德國用戶看到的應該是「99,99 €」。

這就是為什麼我們需要理解兩個重要概念:**i18n(國際化)**和 l10n(本地化)。

i18n 和 l10n 的關係

你可以把 i18n 想像成蓋房子的鋼筋結構,l10n 則是外牆的裝修。i18n 是在產品開發階段就設計好的技術架構,讓產品能夠支援多種語言和文化;l10n 則是實際把產品適配到特定地區的過程。

i18n(Internationalization):

  • 代碼架構的國際化設計
  • 支援多種字符編碼(UTF-8、UTF-16)
  • 靈活的 UI 佈局(支援從右到左的文字方向)
  • 可變的數據格式(日期、時間、數字、貨幣)
  • 動態語言資源載入
  • 文化中性的圖標和顏色設計

l10n(Localization):

  • 實際的文字翻譯
  • 地區特定的格式調整
  • 文化適應性修改
  • 法規合規性調整
  • 當地支付方式整合
  • 本地化的客戶服務

就像 Alex 遇到的問題,如果一開始沒有做好 i18n 的準備,後面的 l10n 就會變成一場災難。他們花了整整三個月重新架構系統,才能正確支援多語系。

技術架構:i18n 的核心要素

讓我用 Alex 的實際經歷來解釋 i18n 的技術實現。

當 Alex 的團隊開始重構產品時,他們發現最大的問題是所有的文字都直接寫在代碼裡:

// 原來的代碼
const welcomeMessage = "Welcome to our platform!";
const priceLabel = "$" + price.toFixed(2);
const dateDisplay = new Date().toLocaleDateString();

後來他們才知道,正確的做法應該是把所有文字提取到資源文件中:

// 重構後的代碼
const welcomeMessage = t("welcome.message");
const priceLabel = formatCurrency(price, locale);
const dateDisplay = formatDate(new Date(), locale);

這個過程叫做「資源文件分離」。Alex 說:「說實話,這個重構過程真的很痛苦,我們花了兩個月才把所有硬編碼的文字找出來。但做完之後感覺整個世界都亮了。」

資源文件結構設計

他們採用了 JSON 格式的多層級資源文件結構:

// locales/en.json
{
  "welcome": {
    "message": "Welcome to our platform!",
    "subtitle": "Start your journey with us"
  },
  "product": {
    "pricing": {
      "monthly": "Monthly Plan",
      "yearly": "Annual Plan",
      "save": "Save {{percentage}}%"
    }
  },
  "errors": {
    "validation": {
      "required": "This field is required",
      "email": "Please enter a valid email address"
    }
  }
}
// locales/de.json
{
  "welcome": {
    "message": "Willkommen auf unserer Plattform!",
    "subtitle": "Beginnen Sie Ihre Reise mit uns"
  },
  "product": {
    "pricing": {
      "monthly": "Monatsplan",
      "yearly": "Jahresplan",
      "save": "Sparen Sie {{percentage}}%"
    }
  }
}

核心技術挑戰深度解析

1. 字符編碼和 RTL 支援

你知道阿拉伯文是從右到左讀的嗎?這不只是文字方向的問題,整個 UI 佈局都要翻轉。Alex 告訴我,他們第一次支援阿拉伯文時,整個導航列都亂了。

/* RTL 支援的 CSS 解決方案 */
.container {
  direction: ltr; /* 預設左到右 */
}

.container[dir="rtl"] {
  direction: rtl; /* 阿拉伯文、希伯來文等 */
}

/* 使用邏輯屬性 */
.sidebar {
  margin-inline-start: 20px; /* 自動適配文字方向 */
  padding-inline-end: 15px;
}

2. 複雜的數據格式適配

不同地區的日期、時間、數字、貨幣格式差異巨大。Alex 分享了一個讓他印象深刻的例子:

// 多地區格式處理
const formatters = {
  "en-US": {
    date: new Intl.DateTimeFormat("en-US"),
    currency: new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }),
    number: new Intl.NumberFormat("en-US"),
  },
  "de-DE": {
    date: new Intl.DateTimeFormat("de-DE"),
    currency: new Intl.NumberFormat("de-DE", {
      style: "currency",
      currency: "EUR",
    }),
    number: new Intl.NumberFormat("de-DE"),
  },
};

// 使用範例
const price = 1234.56;
console.log(formatters["en-US"].currency.format(price)); // $1,234.56
console.log(formatters["de-DE"].currency.format(price)); // 1.234,56 €

3. 動態語言載入和性能優化

為了提升性能,你不能一次載入所有語言的資源文件。Alex 他們實現了按需載入:

// 動態載入語言資源
const loadLanguage = async (locale) => {
  try {
    const response = await fetch(`/locales/${locale}.json`);
    const translations = await response.json();
    i18n.addResourceBundle(locale, "translation", translations);
    await i18n.changeLanguage(locale);
  } catch (error) {
    console.error("Failed to load language:", error);
    // 回退到預設語言
    await i18n.changeLanguage("en");
  }
};

4. 文字長度變化和 UI 適配

德文通常比英文長 30%,這意味著你的 UI 設計要考慮彈性佈局。Alex 說:「我們的按鈕文字從 'Save' 變成 'Speichern',結果把整個工具列都擠壞了。」

/* 彈性佈局設計 */
.button {
  min-width: 120px; /* 確保最小寬度 */
  padding: 8px 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 響應式文字大小 */
.button-text {
  font-size: clamp(0.875rem, 2vw, 1rem);
}

5. 複數形式處理

不同語言的複數規則差異很大。英文只有單數和複數,但俄文有複雜的複數形式:

// 複數形式處理
const pluralRules = {
  en: {
    0: "no items",
    1: "one item",
    other: "{{count}} items",
  },
  ru: {
    0: "нет элементов",
    1: "один элемент",
    few: "{{count}} элемента",
    many: "{{count}} элементов",
    other: "{{count}} элементов",
  },
};

自動化革命:AI 如何改變翻譯遊戲

在 Alex 經歷痛苦的重構過程時,AI 翻譯技術也在快速發展。根據 Lokalise 的最新報告,AI 翻譯的採用率在 2024 年暴增了 533%!

AI 自動化翻譯工作流程

現在的 AI 翻譯已經遠超我們的想像。GPT-4、Claude 這些大型語言模型不只能翻譯文字,還能理解上下文和語調。

Alex 後來跟我分享了他們的自動化翻譯工作流:

  1. 內容提取:系統自動識別需要翻譯的文字
  2. AI 初翻:使用 GPT-4 進行初步翻譯
  3. 人工校對:專業譯者進行品質檢查
  4. 批量更新:翻譯完成後自動更新到產品中

「這套流程讓我們的翻譯效率提升了 300%,」Alex 說,「原本需要兩週的翻譯工作,現在三天就能完成。」

但是 AI 翻譯也不是萬能的。它在處理技術術語、法律文件、醫療內容時還是有限制。而且不同語言的訓練數據量差異很大,所以翻譯品質也會有差異。

實戰工具:我的多語系技術棧推薦

經過 Alex 的實戰驗證,我整理了一套實用的技術棧。Alex 跟我說:「選對工具真的很重要,可以省下一半的開發時間。」

前端框架整合解決方案

React + i18next:最成熟的 React 國際化解決方案

// React + i18next 範例
import { useTranslation } from "react-i18next";

function ProductCard({ product }) {
  const { t, i18n } = useTranslation();

  const changeLanguage = (lng) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>{t("product.price", { price: product.price })}</p>
      <button onClick={() => changeLanguage("de")}>
        {t("buttons.switchToGerman")}
      </button>
    </div>
  );
}

Vue + Vue i18n:Vue 生態系統的標準選擇

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ $t("product.price", { price: product.price }) }}</p>
    <button @click="changeLanguage('de')">
      {{ $t("buttons.switchToGerman") }}
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    changeLanguage(locale) {
      this.$i18n.locale = locale;
    },
  },
};
</script>

Angular i18n:Angular 內建的國際化支援

// Angular i18n 範例
import { Component } from "@angular/core";

@Component({
  selector: "app-product",
  template: `
    <div class="product-card">
      <h3>{{ product.name }}</h3>
      <p i18n="@@product.price">Price: {{ product.price | currency }}</p>
      <button (click)="changeLanguage('de')" i18n="@@buttons.switchToGerman">
        Switch to German
      </button>
    </div>
  `,
})
export class ProductComponent {
  // 組件邏輯
}

翻譯管理平台比較

Lokalise:功能最全面,有強大的 API 和工作流

  • 優點:完整的翻譯工作流、強大的 API、支援多種檔案格式
  • 缺點:價格較高,小團隊可能負擔不起
  • 適合:中大型企業、需要複雜工作流的專案
  • 月費:$120+ 起(專業版)

Alex 說:「Lokalise 的自動 QA 功能救了我們很多次,它會自動檢查翻譯中的變數、標籤是否正確。」

Crowdin:適合開源項目和社群翻譯

  • 優點:社群翻譯功能強大、對開源項目免費
  • 缺點:企業級功能相對較少
  • 適合:開源項目、預算有限的團隊
  • 月費:$40+ 起(專業版)

Phrase:企業級解決方案,整合度高

  • 優點:企業級安全、強大的整合能力
  • 缺點:學習曲線較陡峭
  • 適合:大型企業、需要高度客製化的專案
  • 月費:$90+ 起(專業版)

AI 翻譯服務深度評測

DeepL API:翻譯品質最好,特別是歐洲語言

// DeepL API 整合範例
const translateWithDeepL = async (text, targetLang) => {
  const response = await fetch("https://api-free.deepl.com/v2/translate", {
    method: "POST",
    headers: {
      Authorization: `DeepL-Auth-Key ${process.env.DEEPL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text: [text],
      target_lang: targetLang,
    }),
  });

  const data = await response.json();
  return data.translations[0].text;
};
  • 優點:翻譯品質極高,特別是歐洲語言
  • 缺點:支援的語言相對較少(31 種)
  • 定價:$6.99/月(Pro 500K 字符)

Google Translate API:語言支援最廣泛

// Google Translate API 範例
const { Translate } = require("@google-cloud/translate").v2;
const translate = new Translate({ key: process.env.GOOGLE_API_KEY });

const translateWithGoogle = async (text, targetLang) => {
  const [translation] = await translate.translate(text, targetLang);
  return translation;
};
  • 優點:支援 100+ 語言,API 穩定
  • 缺點:品質不如 DeepL,特別是複雜句子
  • 定價:$20/百萬字符

ChatGPT API:理解上下文最準確

// ChatGPT API 翻譯範例
const translateWithChatGPT = async (text, targetLang, context) => {
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-4",
      messages: [
        {
          role: "system",
          content: `You are a professional translator. Translate the following text to ${targetLang}. Context: ${context}`,
        },
        {
          role: "user",
          content: text,
        },
      ],
    }),
  });

  const data = await response.json();
  return data.choices[0].message.content;
};
  • 優點:理解上下文,處理創意內容能力強
  • 缺點:成本較高,API 限制較多
  • 定價:$0.03/1K tokens(GPT-4)

其他實用工具

babel-plugin-i18next-extract:自動提取翻譯字串

// 自動提取翻譯配置
module.exports = {
  plugins: [
    [
      "i18next-extract",
      {
        locales: ["en", "de", "fr"],
        outputPath: "locales/{{locale}}.json",
        keyAsDefaultValue: true,
      },
    ],
  ],
};

i18n-ally:VSCode 插件,提供翻譯預覽和管理功能

react-i18next-gitbook:自動生成翻譯文件

Alex 總結他的經驗:「工具選擇要根據團隊規模和專案需求。小專案用 Crowdin + DeepL 就夠了,大企業還是選 Lokalise + 多個翻譯服務的組合比較安全。」

語音合成:下一個戰場

讓我們聊聊語音合成。現在不只是文字需要多語系,語音內容也需要。

Alex 的產品有個語音導覽功能,原本只有英文版本。但當他們要進入歐洲市場時,發現用戶更希望聽到母語解說。「說實話,我們一開始想找配音員,但報價讓我們嚇了一跳。」Alex 說,「10 分鐘的內容,5 種語言,光配音費就要 $15,000。」

AI 語音合成的技術突破

現在的 AI 語音合成技術真的很驚人。ElevenLabs、OpenAI 的 TTS 都能生成非常自然的多語言語音。你甚至可以保持同一個「聲音個性」,但說不同的語言。

ElevenLabs 語音克隆:

// ElevenLabs API 整合
const generateSpeech = async (text, voiceId, language) => {
  const response = await fetch(
    `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
    {
      method: "POST",
      headers: {
        "xi-api-key": process.env.ELEVENLABS_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        text: text,
        model_id: "eleven_multilingual_v2",
        voice_settings: {
          stability: 0.5,
          similarity_boost: 0.5,
        },
      }),
    }
  );

  return response.blob();
};

OpenAI TTS 整合:

// OpenAI TTS API
const createSpeech = async (text, language) => {
  const response = await fetch("https://api.openai.com/v1/audio/speech", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "tts-1-hd",
      input: text,
      voice: "alloy",
      speed: 1.0,
    }),
  });

  return response.blob();
};

語音本地化的挑戰

語音風格適配:不同文化對語音的偏好差異很大。德國人偏好較為正式的語調,美國人喜歡友善親切的風格。

語音速度調整:研究顯示,不同語言的理想語速不同。英文約 150-160 字/分鐘,德文約 130-140 字/分鐘。

情感表達:AI 語音現在能夠表達複雜情感,但需要針對不同文化調整情感的表達方式。

實際應用案例

Alex 跟我分享了他們的語音本地化流程:

  1. 內容預處理:調整語音腳本適應不同語言的節奏
  2. 語音生成:使用 ElevenLabs 生成多語言版本
  3. 品質檢查:由母語人士檢查語音自然度
  4. A/B 測試:測試不同語音風格的用戶接受度

「用戶聽到德文版的語音導覽時,完全不敢相信這是 AI 生成的。」Alex 說,「而且成本只有傳統配音的 1/10。」

語音合成的成本效益

傳統配音 vs AI 語音合成:

  • 傳統配音:$100-300/分鐘,修改困難
  • AI 語音合成:$0.1-1/分鐘,可隨時調整

多語言語音專案的投資回報:

  • 初期投資:$5,000-15,000(系統建置)
  • 維護成本:$500-2,000/月(API 費用)
  • 預期效益:用戶參與度提升 40-60%

未來趨勢:即時語音翻譯

最令人興奮的是即時語音翻譯技術。想像一下,用戶可以用英文提問,系統自動翻譯並用德文回答,聲音還保持一致性。

// 即時語音翻譯概念
const realtimeTranslation = async (audioStream, targetLanguage) => {
  // 1. 語音轉文字
  const transcript = await speechToText(audioStream);

  // 2. 文字翻譯
  const translated = await translateText(transcript, targetLanguage);

  // 3. 文字轉語音
  const audio = await textToSpeech(translated, targetLanguage);

  return audio;
};

這技術現在還在發展階段,但已經看到很多有趣的應用場景。Alex 說:「我們正在測試這個功能,如果成功的話,客戶服務的效率可能提升 10 倍。」

成本與效益:值得投資嗎?

我知道很多人會問:多語系化到底值不值得投資?

讓我用數據說話。根據我們收集的資料,多語系網站的平均轉換率比單語系網站高 40%。而且搜尋引擎對多語系內容的排名也更友好。

真實案例:Alex 的投資回報分析

Alex 的產品在完成歐洲本地化後,三個月內的收入增長了 85%。當然,這不全是多語系的功勞,但它確實是重要因素。

投資成本明細:

  • 系統重構(i18n):$45,000(3 個月開發時間)
  • 翻譯服務(l10n):$12,000(5 種語言)
  • 工具和平台:$8,000/年(Lokalise + DeepL)
  • 本地化測試:$15,000(UX 測試和調整)

總投資:$80,000

回報效益:

  • 第一年新增收入:$340,000
  • 投資回報率:325%
  • 回本時間:8 個月

不同規模公司的投資策略

小型團隊(<10 人):

  • 推薦策略:從主要市場開始,使用 AI 翻譯 + 人工校對
  • 預算範圍:$5,000-15,000
  • 工具組合:Crowdin + DeepL API
  • 預期回報:6-12 個月回本

中型公司(10-100 人):

  • 推薦策略:建立完整的多語系工作流,投資專業工具
  • 預算範圍:$25,000-75,000
  • 工具組合:Lokalise + 多個翻譯服務
  • 預期回報:4-8 個月回本

大型企業(>100 人):

  • 推薦策略:建立內部多語系團隊,投資企業級解決方案
  • 預算範圍:$100,000-500,000
  • 工具組合:Phrase + 混合翻譯策略
  • 預期回報:3-6 個月回本

市場數據支持

Common Sense Advisory 研究數據:

  • 72% 的用戶更願意在母語網站上購買
  • 55% 的用戶只在母語網站上購買
  • 多語系網站的 SEO 排名平均提升 47%

CSA Research 全球調查:

  • 多語系支援讓客戶滿意度提升 60%
  • 多語系客戶的生命週期價值高 35%
  • 多語系內容的社交分享率高 2.3 倍

隱性成本和風險

需要考慮的隱性成本:

  • 持續的翻譯更新(產品迭代時)
  • 多語系客戶服務成本
  • 法規合規成本(GDPR、數據保護法)
  • 多語系 QA 和測試成本

風險評估:

  • 翻譯品質風險:可能影響品牌形象
  • 技術維護風險:多語系系統複雜度較高
  • 市場進入風險:不是所有市場都適合

投資決策框架

我建議用這個框架來評估是否值得投資:

市場機會評估:

  1. 目標市場的用戶規模和付費意願
  2. 競爭對手的多語系成熟度
  3. 法規和文化進入門檻

技術準備度評估:

  1. 現有系統的國際化準備度
  2. 團隊的多語系開發經驗
  3. 技術架構的擴展能力

資源投入評估:

  1. 可用預算和時間資源
  2. 長期維護能力
  3. 人力資源配置

Alex 跟我說:「多語系化不是成本,而是投資。關鍵是要有長期視野,不要期望立即回報。」

投資成本方面,初期的 i18n 重構確實需要投入,但有了 AI 翻譯的加持,後續的 l10n 成本大幅降低。現在一個完整的多語系專案,成本比三年前降低了 60%。

常見陷阱和解決方案

讓我分享一些 Alex 踩過的坑,以及他們如何解決的:

文化敏感度陷阱

顏色和圖片的文化含義:

顏色、圖片、手勢在不同文化中有不同含義。Alex 告訴我一個讓他印象深刻的例子:"我們的產品用綠色代表'成功',結果發現在某些中東國家,綠色有宗教含義,不適合商業場景。"

/* 文化適應的顏色設計 */
:root {
  --success-color: #28a745; /* 預設綠色 */
  --warning-color: #ffc107; /* 預設黃色 */
  --error-color: #dc3545; /* 預設紅色 */
}

/* 中東地區的顏色調整 */
[data-region="middle-east"] {
  --success-color: #007bff; /* 藍色替代綠色 */
}

/* 中國地區的顏色調整 */
[data-region="china"] {
  --success-color: #dc3545; /* 紅色代表好運 */
  --error-color: #6c757d; /* 灰色代表錯誤 */
}

圖片和圖標的文化適應:

// 圖片本地化配置
const imageLocalization = {
  default: {
    handshake: "/images/handshake-western.jpg",
    success: "/images/thumbs-up.jpg",
  },
  "middle-east": {
    handshake: "/images/handshake-middle-east.jpg",
    success: "/images/ok-sign-alternative.jpg", // 避免可能冒犯的手勢
  },
  japan: {
    handshake: "/images/bow.jpg", // 日本更習慣鞠躬
    success: "/images/success-japan.jpg",
  },
};

法規合規的複雜性

GDPR 合規實作:

Alex 說他們在德國上線時,光是處理 GDPR 就花了一個月。「我們以為只是加個 Cookie 同意框,結果發現涉及整個數據處理流程。」

// GDPR 合規的數據處理
class GDPRCompliance {
  constructor(region) {
    this.region = region;
    this.consentTypes = ["necessary", "analytics", "marketing"];
  }

  async requestConsent() {
    if (this.region === "eu") {
      return await this.showGDPRConsent();
    }
    return { necessary: true }; // 非歐盟地區預設同意
  }

  async showGDPRConsent() {
    // 顯示詳細的 GDPR 同意界面
    const consent = await this.displayConsentModal();
    await this.recordConsent(consent);
    return consent;
  }

  async handleDataSubjectRights(request) {
    // 處理用戶的數據主體權利請求
    switch (request.type) {
      case "access":
        return await this.exportUserData(request.userId);
      case "deletion":
        return await this.deleteUserData(request.userId);
      case "portability":
        return await this.exportPortableData(request.userId);
    }
  }
}

時區和日期處理的複雜性

這比想像中複雜得多。你需要考慮夏令時、不同的日曆系統(比如佛曆、伊斯蘭曆)。

// 複雜的時區和日期處理
class InternationalDateHandler {
  constructor() {
    this.calendars = {
      gregorian: "iso8601",
      buddhist: "buddhist",
      islamic: "islamic",
      hebrew: "hebrew",
    };
  }

  formatDate(date, locale, calendar = "gregorian") {
    const options = {
      calendar: this.calendars[calendar],
      timeZone: this.getTimezone(locale),
      year: "numeric",
      month: "long",
      day: "numeric",
    };

    return new Intl.DateTimeFormat(locale, options).format(date);
  }

  getTimezone(locale) {
    const timezoneMap = {
      "en-US": "America/New_York",
      "de-DE": "Europe/Berlin",
      "ja-JP": "Asia/Tokyo",
      "th-TH": "Asia/Bangkok",
    };
    return timezoneMap[locale] || "UTC";
  }

  handleBusinessHours(locale) {
    const businessHours = {
      "en-US": { start: "09:00", end: "17:00" },
      "de-DE": { start: "08:00", end: "16:00" },
      "es-ES": { start: "09:00", end: "14:00", siesta: true },
      "ae-AE": { start: "08:00", end: "13:00", fridayOff: true },
    };
    return businessHours[locale] || businessHours["en-US"];
  }
}

支付和貨幣的地區差異

不同地區的支付習慣差異很大。德國人喜歡銀行轉帳,美國人習慣信用卡,中國人偏愛移動支付。

// 支付方式本地化
class PaymentLocalization {
  constructor() {
    this.paymentMethods = {
      US: ["credit_card", "paypal", "apple_pay"],
      DE: ["sepa_debit", "giropay", "sofort"],
      CN: ["alipay", "wechat_pay", "unionpay"],
      JP: ["konbini", "bank_transfer", "credit_card"],
      IN: ["upi", "paytm", "razorpay"],
    };
  }

  getAvailablePaymentMethods(countryCode) {
    return this.paymentMethods[countryCode] || ["credit_card"];
  }

  formatCurrency(amount, currency, locale) {
    const formatter = new Intl.NumberFormat(locale, {
      style: "currency",
      currency: currency,
      currencyDisplay: "symbol",
    });

    return formatter.format(amount);
  }

  handleTaxCalculation(amount, countryCode) {
    const taxRates = {
      DE: 0.19, // 德國 VAT
      FR: 0.2, // 法國 VAT
      US: 0.0825, // 美國各州不同,這裡是平均值
      GB: 0.2, // 英國 VAT
      JP: 0.1, // 日本消費稅
    };

    const taxRate = taxRates[countryCode] || 0;
    return {
      subtotal: amount,
      tax: amount * taxRate,
      total: amount * (1 + taxRate),
    };
  }
}

字體和排版陷阱

多語言字體支援:

/* 多語言字體堆疊 */
.multilingual-text {
  font-family: 
    /* 拉丁字母 */ "Inter", "Roboto", "Segoe UI", /* 中文 */ "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", /* 日文 */ "Hiragino Kaku Gothic ProN", "Meiryo",
    /* 韓文 */ "Malgun Gothic", "Dotum", /* 阿拉伯文 */ "Tahoma",
    "Arabic Typesetting", /* 備用 */ sans-serif;
}

/* RTL 文字特殊處理 */
[dir="rtl"] .multilingual-text {
  text-align: right;
  direction: rtl;
}

內容長度變化的 UI 適應

響應式文字容器:

/* 彈性容器設計 */
.text-container {
  min-height: 2.5rem;
  display: flex;
  align-items: center;
  padding: 0.5rem 1rem;

  /* 德文等長語言的特殊處理 */
  word-break: break-word;
  hyphens: auto;
}

/* 按鈕文字的自適應 */
.button {
  min-width: 120px;
  padding: 0.5rem 1rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

  /* 長文字時的處理 */
  @media (max-width: 768px) {
    min-width: 100px;
    font-size: 0.875rem;
  }
}

Alex 的經驗總結:「多語系化最大的挑戰不是技術,而是文化理解。我們現在每進入一個新市場,都會請當地的文化顧問幫我們檢查所有細節。」

未來趨勢:多語系的下一個階段

AI 技術還在快速發展,我預測接下來幾年會有這些趨勢:

實時翻譯:用戶可以即時切換語言,無需重新載入頁面。

上下文感知翻譯:AI 會根據用戶的行為和偏好調整翻譯風格。

多模態本地化:不只是文字和語音,還包括圖片、視頻的自動本地化。

個性化語言體驗:根據用戶的語言能力水平調整內容複雜度。

成功案例分析:向大型企業學習

讓我們看看一些大型企業是如何成功實現多語系化的:

Netflix:內容本地化的典範

Netflix 不只是翻譯字幕,更是投資本地化內容製作。他們在各地設立制作工作室,製作符合當地文化的原創內容。

核心策略:

  • 本地化內容製作:不只翻譯,更要製作當地原創內容
  • 智能字幕翻譯:使用 AI 輔助翻譯,人工校對品質
  • 個性化推薦:根據地區文化調整內容推薦算法
  • 多語言介面:支援 30+ 語言,介面完全本地化

成果:90% 的觀看時間來自非英語內容,全球用戶超過 2.3 億。

Spotify:音樂文化的精確本地化

Spotify 在多語系化上做得非常細致,不只是翻譯介面,更重視各地的音樂文化。

特色功能:

  • 本地音樂推薦:根據地區音樂偏好調整算法
  • 文化節慶播列:針對不同地區的節慶定製播列
  • 多語言搜尋:支援歌曲名稱的多語言搜尋
  • 本地藝人支持:為各地藝人提供專屬平台
// Spotify 的本地化推薦算法概念
class LocalizedRecommendation {
  constructor(userLocation, culturalContext) {
    this.location = userLocation;
    this.cultural = culturalContext;
  }

  getRecommendations(userPreferences) {
    const globalTrends = this.getGlobalTrends();
    const localTrends = this.getLocalTrends(this.location);
    const culturalPreferences = this.getCulturalPreferences(this.cultural);

    return this.blend(
      userPreferences,
      globalTrends,
      localTrends,
      culturalPreferences
    );
  }
}

Airbnb:使用者體驗的深度本地化

Airbnb 的多語系化不只是語言,更包括了整個使用者體驗的文化適應。

特色實作:

  • 本地支付方式:每個市場都支援當地主流支付方式
  • 文化適應設計:色彩、圖片都根據當地文化調整
  • 本地法規合規:不同地區的房屋租賃法規完全不同
  • 社区建設:為各地建立在地社群,提供本地化服務

成功關鍵:

  • 深度市場研究:每進入一個新市場都會做深度研究
  • 當地團隊:在主要市場設立本地化團隊
  • 持續優化:根據用戶反饋持續調整本地化策略

中國企業的全球化經驗

TikTok 的全球本地化:

  • 內容算法本地化:根據各地文化調整內容推薦
  • 創作者支持:為各地創作者提供本地化工具和支持
  • 合規策略:主動適應各地的法規要求

小米的全球化策略:

  • 硬體本地化:根據不同市場調整產品规格
  • 軟體本地化:MIUI 支援 70+ 語言
  • 生態本地化:在各地建立本地化的生態系統

未來趨勢:多語系的下一個階段

AI 技術還在快速發展,我預測接下來幾年會有這些趨勢:

1. 即時多語言交互

用戶可以即時切換語言,無需重新載入頁面。甚至可以在同一個對話中使用多種語言。

// 即時語言切換概念
class RealtimeLanguageSwitching {
  constructor() {
    this.activeTranslations = new Map();
    this.translationCache = new Map();
  }

  async switchLanguage(targetLang, preserveContext = true) {
    // 保持用戶上下文,即時切換語言
    const currentContext = this.getCurrentContext();
    const translations = await this.getTranslations(targetLang);

    if (preserveContext) {
      this.mergeContextWithTranslations(currentContext, translations);
    }

    this.applyTranslations(translations);
  }
}

2. 上下文感知翻譯

AI 會根據用戶的行為、偏好和使用情境調整翻譯風格。不同的用戶看到的翻譯可能會不一樣。

// 個性化翻譯引擎
class PersonalizedTranslation {
  constructor(userId) {
    this.userId = userId;
    this.userProfile = this.loadUserProfile(userId);
  }

  async translate(text, targetLang) {
    const context = {
      userLevel: this.userProfile.languageLevel,
      preferences: this.userProfile.stylePreferences,
      domain: this.getCurrentDomain(),
      timeOfDay: new Date().getHours(),
    };

    return await this.contextAwareTranslate(text, targetLang, context);
  }
}

3. 多模態全方位本地化

不只是文字和語音,還包括圖片、視頻、AR/VR 內容的自動本地化。

圖片內容本地化:

  • AI 能識別圖片中的文字並自動翻譯
  • 根據文化背景調整圖片中的人物、場景
  • 自動生成符合當地美學的設計

視頻內容本地化:

  • 即時語音翻譯和字幕生成
  • 視頻中的文字識別和替換
  • 根據文化背景調整視覺元素

4. 個性化語言體驗

根據用戶的語言能力水平、學習目標和使用場景調整內容複雜度。

適應性語言學習:

  • 系統能識別用戶的語言水平
  • 自動調整詞彙難度和語法複雜度
  • 提供互動式的語言學習建議

5. 區塊鏈驅動的翻譯經濟

未來可能會有去中心化的翻譯平台,讓全球的翻譯者共同協作,提供更高品質的翻譯服務。

6. 神經機器翻譯技術

新一代的 AI 模型將能夠:

  • 理解更深層次的文化內涵
  • 處理更複雜的語言現象(雙關語、歌詞、詩歌)
  • 提供接近人類翻譯員的品質

Alex 跟我說:「未來的多語系產品不只是支援多種語言,而是能夠真正理解不同文化的使用者需求,提供真正個性化的體驗。」

常用資源和工具清單

免費資源:

付費工具:

學習資源:

記住,多語系化不是一次性的項目,而是一個持續的過程。市場在變化,技術在進步,用戶的需求也在演化。

Alex 現在已經是我們圈子裡的多語系專家了。他常說:「當初以為是翻譯問題,現在發現是個技術和商業的大機會。最重要的是,不要把多語系看成負擔,而要看成進入全球市場的機會。」

而且最重要的是,隨著 AI 技術的發展,多語系化的門檻正在快速降低。現在開始行動,正是最好的時機。

你準備好讓你的產品走向世界了嗎?


本文涉及的工具和技術都在快速發展中,建議讀者關注最新的技術動態。如果你在多語系化過程中遇到具體問題,歡迎交流討論。


本文最初發布於 HackMD @BASHCAT。

Arduino 遇上 Protocol Buffers:當微控制器也要說「高效語言」

Arduino 電路實戰

那是去年夏天,我們要建置一個環境監測系統,用 20 個 ESP8266 節點收集工廠各角落的溫濕度資料。看起來很簡單對吧?用 JSON 格式,HTTP POST 到雲端,搞定!

結果事情沒那麼順利。每個感測器每分鐘上傳一次資料,WiFi 網路很快就被塞爆了。更糟糕的是,有些 ESP8266 因為 JSON 序列化耗用太多記憶體,經常出現重啟的狀況。

當時的我第一次深深體會到:在微控制器的世界裡,每個位元組都很珍貴。

後來一位資深同事建議我試試 Protocol Buffers,我當時的反應是:"這玩意不是給大型系統用的嗎?Arduino 這種小板子能跑得動?"

事實證明,不但跑得動,而且效果驚人。同樣的資料,傳輸量減少了 60%,記憶體使用量降低 40%,系統再也沒有當機過。

今天我想分享這個改變我對嵌入式開發認知的技術:如何在 Arduino 和單晶片上使用 Protocol Buffers。

為什麼微控制器需要「瘦身」的數據格式?

在聊技術實作之前,我們先理解一下為什麼要在資源受限的環境中使用 Protocol Buffers。

Arduino 的現實限制

拿最常見的 Arduino Uno 來說:

  • SRAM:只有 2KB
  • Flash Memory:32KB(還要扣除 bootloader)
  • 處理器:16MHz 的 8位元 AVR

這意味著什麼?一個 JSON 字串可能就佔用了你 10% 的記憶體!

我實際測試過一個簡單的溫濕度讀取:

{
  "device_id": "sensor_001",
  "timestamp": 1640995200,
  "temperature": 25.6,
  "humidity": 60.3,
  "battery": 87
}

這個 JSON 就要 98 bytes。如果你的設備每分鐘上傳一次資料,光是緩衝幾筆資料就可能讓記憶體吃緊。

網路頻寬的珍貴

在 IoT 場景中,很多設備使用的是:

  • WiFi:訊號可能不穩定,傳輸失敗需要重送
  • 3G/4G:按流量計費,每 MB 都是錢
  • LoRa/NB-IoT:傳輸速度慢,資料包大小有嚴格限制

每節省一個位元組,都直接影響系統的穩定性和運營成本。

電池壽命的考量

許多 IoT 設備需要電池供電數月甚至數年。更小的資料包意味著:

  • 更短的傳輸時間
  • 更少的 CPU 運算
  • 更長的電池壽命

認識 nanopb:微控制器的專屬 Protocol Buffers

IoT 系統架構

Google 官方的 Protocol Buffers 庫對 Arduino 來說太重了,這時候 nanopb 就是我們的救星。

nanopb 的特色

nanopb 是專門為嵌入式系統設計的 Protocol Buffers 實作,它有以下特點:

  • 極小的程式碼體積:通常只需要幾 KB 的 Flash 空間
  • 純 C 語言:沒有動態記憶體分配,執行效率高
  • 靜態緩衝區:編譯時就確定記憶體使用量
  • 跨平台相容:與標準 Protocol Buffers 完全相容

實際大小比較

讓我用實際數據告訴你差異有多大:

相同的感測器資料:

格式 大小 記憶體使用 序列化時間
JSON 98 bytes 200 bytes 12ms
nanopb 24 bytes 80 bytes 3ms

你看,同樣的資料,nanopb 只用了 JSON 四分之一的大小!

實戰項目:打造你的第一個 protobuf 感測器

現在讓我們動手實作一個實際的專案:使用 ESP8266 + DHT22 感測器,透過 nanopb 上傳溫濕度資料。

硬體準備

你需要:

  • ESP8266 開發板(如 NodeMCU)
  • DHT22 溫濕度感測器
  • 4.7kΩ 電阻
  • 麵包板和跳線

接線圖:

  • DHT22 VCC → ESP8266 3.3V
  • DHT22 GND → ESP8266 GND
  • DHT22 DATA → ESP8266 D4(GPIO2)
  • 4.7kΩ 電阻連接 VCC 和 DATA

步驟一:定義 Protocol Buffers Schema

首先建立 sensor.proto 檔案:

syntax = "proto3";

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    float temperature = 3;
    float humidity = 4;
    int32 battery_level = 5;
    bool status_ok = 6;
}

步驟二:生成 nanopb C 代碼

下載 nanopb 工具後,執行:

python nanopb_generator.py sensor.proto

這會生成兩個檔案:

  • sensor.pb.h:標頭檔
  • sensor.pb.c:實作檔

生成的結構看起來像這樣:

typedef struct _SensorReading {
    char device_id[32];
    int64_t timestamp;
    float temperature;
    float humidity;
    int32_t battery_level;
    bool status_ok;
} SensorReading;

步驟三:Arduino 程式實作

#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "sensor.pb.h"

// WiFi 設定
const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";
const char* serverURL = "http://your-server.com/api/sensor";

// DHT 感測器設定
#define DHT_PIN 2
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

// protobuf 緩衝區
uint8_t buffer[128];
size_t message_length;

void setup() {
    Serial.begin(115200);
    dht.begin();
    
    // 連接 WiFi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("WiFi connected!");
}

void loop() {
    // 讀取感測器資料
    float temp = dht.readTemperature();
    float hum = dht.readHumidity();
    
    if (isnan(temp) || isnan(hum)) {
        Serial.println("Failed to read from DHT sensor!");
        delay(5000);
        return;
    }
    
    // 建立 protobuf 訊息
    SensorReading reading = SensorReading_init_zero;
    strcpy(reading.device_id, "esp8266_001");
    reading.timestamp = WiFi.getTime();  // 需要設定 NTP
    reading.temperature = temp;
    reading.humidity = hum;
    reading.battery_level = analogRead(A0);  // 假設連接電池檢測電路
    reading.status_ok = true;
    
    // 序列化為 protobuf 格式
    pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    bool status = pb_encode(&stream, SensorReading_fields, &reading);
    message_length = stream.bytes_written;
    
    if (!status) {
        Serial.println("Failed to encode protobuf message");
        return;
    }
    
    // 發送到伺服器
    sendToServer(buffer, message_length);
    
    // 深度睡眠 5 分鐘(節省電力)
    Serial.println("Going to deep sleep...");
    ESP.deepSleep(5 * 60 * 1000000);  // 5 分鐘,單位是微秒
}

void sendToServer(uint8_t* data, size_t length) {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        http.begin(serverURL);
        http.addHeader("Content-Type", "application/x-protobuf");
        
        int httpResponseCode = http.POST(data, length);
        
        if (httpResponseCode > 0) {
            String response = http.getString();
            Serial.printf("HTTP Response: %d\n", httpResponseCode);
            Serial.println("Data sent successfully!");
        } else {
            Serial.printf("Error sending data: %d\n", httpResponseCode);
        }
        
        http.end();
    } else {
        Serial.println("WiFi not connected");
    }
}

步驟四:伺服器端接收

簡單的 Python 伺服器範例:

from flask import Flask, request
import sensor_pb2  # 由 protoc 生成

app = Flask(__name__)

@app.route('/api/sensor', methods=['POST'])
def receive_sensor_data():
    try:
        # 解析 protobuf 資料
        reading = sensor_pb2.SensorReading()
        reading.ParseFromString(request.data)
        
        print(f"Device: {reading.device_id}")
        print(f"Temperature: {reading.temperature}°C")
        print(f"Humidity: {reading.humidity}%")
        print(f"Battery: {reading.battery_level}%")
        
        # 這裡可以存入資料庫或進行其他處理
        
        return "OK", 200
    except Exception as e:
        print(f"Error: {e}")
        return "Error", 400

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

性能測試與實際數據

性能對比圖

我做了詳細的性能測試,比較 JSON 和 nanopb 在 ESP8266 上的表現:

資料大小比較

單筆感測器讀取資料:

欄位 JSON nanopb 節省比例
device_id "esp8266_001" (12 bytes) field_tag + string (13 bytes) -8%
timestamp 1640995200 (10 bytes) varint (5 bytes) +50%
temperature 25.6 (4 bytes) fixed32 (5 bytes) -25%
humidity 60.3 (4 bytes) fixed32 (5 bytes) -25%
battery_level 87 (2 bytes) varint (2 bytes) 0%
status_ok true (4 bytes) bool (2 bytes) +50%
總計 98 bytes 37 bytes +62%

記憶體使用量測試

使用 ESP8266 的記憶體監控功能,我測量了實際的記憶體使用:

void printMemoryUsage() {
    uint32_t free_heap = ESP.getFreeHeap();
    uint32_t max_free_block = ESP.getMaxFreeBlockSize();
    
    Serial.printf("Free heap: %d bytes\n", free_heap);
    Serial.printf("Max free block: %d bytes\n", max_free_block);
}

測試結果:

操作 JSON 記憶體使用 nanopb 記憶體使用 節省
序列化 250 bytes 100 bytes 60%
網路傳輸緩衝 150 bytes 60 bytes 60%
總記憶體需求 400 bytes 160 bytes 60%

處理時間測試

unsigned long start_time, end_time;

// JSON 序列化測試
start_time = micros();
String json = createJSONString(temp, hum, battery);
end_time = micros();
Serial.printf("JSON serialization: %lu μs\n", end_time - start_time);

// nanopb 序列化測試
start_time = micros();
pb_encode(&stream, SensorReading_fields, &reading);
end_time = micros();
Serial.printf("nanopb serialization: %lu μs\n", end_time - start_time);

測試結果:

  • JSON 序列化:平均 8,500 μs
  • nanopb 序列化:平均 2,100 μs
  • nanopb 快了 75%!

進階應用:多感測器智能監測網路

讓我們把視野放得更大一點,設計一個更複雜的系統。

場景設計

假設你要監測一個溫室,需要收集:

  • 多個位置的溫濕度
  • 土壤濕度
  • 光照強度
  • 二氧化碳濃度

彈性的 Schema 設計

syntax = "proto3";

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    string location = 3;
    
    // 使用 oneof 讓一個訊息可以包含不同類型的資料
    oneof sensor_data {
        TemperatureHumidity temp_hum = 10;
        SoilMoisture soil = 11;
        LightLevel light = 12;
        CO2Level co2 = 13;
    }
}

message TemperatureHumidity {
    float temperature = 1;
    float humidity = 2;
}

message SoilMoisture {
    float moisture_percent = 1;
    float ph_level = 2;
}

message LightLevel {
    int32 lux = 1;
    string spectrum = 2;  // "full", "uv", "ir"
}

message CO2Level {
    int32 ppm = 1;
    bool alarm_triggered = 2;
}

MQTT 整合

在大型 IoT 系統中,MQTT 是更好的選擇:

#include <PubSubClient.h>

WiFiClient espClient;
PubSubClient client(espClient);

const char* mqtt_server = "your-mqtt-broker.com";
const char* mqtt_topic = "greenhouse/sensors";

void setup() {
    // ... 其他初始化代碼 ...
    
    client.setServer(mqtt_server, 1883);
    connectToMQTT();
}

void connectToMQTT() {
    while (!client.connected()) {
        Serial.print("Attempting MQTT connection...");
        String clientId = "ESP8266Client-";
        clientId += String(random(0xffff), HEX);
        
        if (client.connect(clientId.c_str())) {
            Serial.println("connected");
        } else {
            Serial.print("failed, rc=");
            Serial.print(client.state());
            Serial.println(" try again in 5 seconds");
            delay(5000);
        }
    }
}

void publishSensorData(uint8_t* data, size_t length) {
    if (!client.connected()) {
        connectToMQTT();
    }
    
    bool result = client.publish(mqtt_topic, data, length);
    if (result) {
        Serial.println("Data published successfully");
    } else {
        Serial.println("Failed to publish data");
    }
}

踩坑經驗與優化技巧

在實際使用過程中,我遇到了不少坑,這裡分享一些寶貴經驗。

坑一:字串長度限制

問題:nanopb 預設的字串長度限制可能不夠用。

解決方案:在 .options 檔案中指定最大長度:

# sensor.options
SensorReading.device_id max_size:32
SensorReading.location max_size:64

然後重新生成程式碼:

python nanopb_generator.py sensor.proto

坑二:浮點數精度問題

問題:有些感測器讀取的值有很多小數點,但實際上不需要這麼高精度。

解決方案:在傳輸前量化數值:

// 將溫度量化到 0.1 度精度
int32_t quantized_temp = (int32_t)(temperature * 10);
reading.temperature = quantized_temp / 10.0f;  // 或者直接使用整數欄位

更好的方案:直接使用整數:

message SensorReading {
    string device_id = 1;
    int64 timestamp = 2;
    int32 temperature_x10 = 3;  // 實際溫度乘以 10
    int32 humidity_x10 = 4;     // 實際濕度乘以 10
}

這樣可以進一步減小資料大小,因為整數的 varint 編碼更有效率。

坑三:記憶體碎片化

問題:頻繁的序列化/反序列化可能導致記憶體碎片化。

解決方案:使用靜態緩衝區池:

// 預分配多個緩衝區
#define BUFFER_COUNT 3
#define BUFFER_SIZE 128

static uint8_t buffer_pool[BUFFER_COUNT][BUFFER_SIZE];
static int current_buffer = 0;

uint8_t* getNextBuffer() {
    uint8_t* buffer = buffer_pool[current_buffer];
    current_buffer = (current_buffer + 1) % BUFFER_COUNT;
    return buffer;
}

坑四:WiFi 連線不穩定

問題:在網路不穩定的環境下,資料傳輸容易失敗。

解決方案:實作本地緩存和重試機制:

#include <SPIFFS.h>

void saveDataToLocal(uint8_t* data, size_t length) {
    String filename = "/data_" + String(millis()) + ".pb";
    File file = SPIFFS.open(filename, "w");
    if (file) {
        file.write(data, length);
        file.close();
        Serial.println("Data saved locally: " + filename);
    }
}

void uploadPendingData() {
    Dir dir = SPIFFS.openDir("/");
    while (dir.next()) {
        if (dir.fileName().startsWith("data_")) {
            File file = dir.openFile("r");
            if (file) {
                size_t length = file.size();
                uint8_t* buffer = new uint8_t[length];
                file.readBytes((char*)buffer, length);
                file.close();
                
                if (sendToServer(buffer, length)) {
                    // 傳送成功,刪除本地檔案
                    SPIFFS.remove(dir.fileName());
                    Serial.println("Uploaded and deleted: " + dir.fileName());
                }
                
                delete[] buffer;
            }
        }
    }
}

效能調優秘訣

1. 選擇合適的數值類型

// 不好:使用 int64 存儲小數值
int64 sensor_id = 1;  // 浪費空間

// 好:使用合適的類型
int32 sensor_id = 1;  // 對大多數應用已經足夠

2. 批次傳輸

與其每次讀取就傳輸一次,不如累積幾筆資料一起傳:

message SensorBatch {
    string device_id = 1;
    repeated SensorReading readings = 2;
}

3. 壓縮大型資料

對於某些場景(如音訊資料、影像資料),可以在 protobuf 層面再加壓縮:

#include <ArduinoLZ77.h>  // 輕量級壓縮庫

void compressAndSend(uint8_t* data, size_t length) {
    uint8_t compressed[256];
    size_t compressed_size = lz77_compress(data, length, compressed, sizeof(compressed));
    
    if (compressed_size < length) {
        // 壓縮有效,使用壓縮資料
        sendToServer(compressed, compressed_size);
    } else {
        // 壓縮無效,使用原始資料
        sendToServer(data, length);
    }
}

除錯與測試技巧

開發過程中,除錯是不可避免的,這裡分享一些實用技巧。

序列化資料檢查

void printProtobufData(uint8_t* data, size_t length) {
    Serial.print("Protobuf data (");
    Serial.print(length);
    Serial.print(" bytes): ");
    
    for (size_t i = 0; i < length; i++) {
        if (data[i] < 16) Serial.print("0");
        Serial.print(data[i], HEX);
        Serial.print(" ");
    }
    Serial.println();
}

反序列化測試

bool testSerialization() {
    // 建立測試資料
    SensorReading original = SensorReading_init_zero;
    strcpy(original.device_id, "test_device");
    original.temperature = 25.5f;
    original.humidity = 60.0f;
    
    // 序列化
    uint8_t buffer[128];
    pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    bool encode_status = pb_encode(&ostream, SensorReading_fields, &original);
    
    if (!encode_status) {
        Serial.println("Encoding failed");
        return false;
    }
    
    // 反序列化
    SensorReading decoded = SensorReading_init_zero;
    pb_istream_t istream = pb_istream_from_buffer(buffer, ostream.bytes_written);
    bool decode_status = pb_decode(&istream, SensorReading_fields, &decoded);
    
    if (!decode_status) {
        Serial.println("Decoding failed");
        return false;
    }
    
    // 驗證資料
    bool success = (strcmp(original.device_id, decoded.device_id) == 0) &&
                   (fabs(original.temperature - decoded.temperature) < 0.01f) &&
                   (fabs(original.humidity - decoded.humidity) < 0.01f);
    
    if (success) {
        Serial.println("Serialization test passed");
    } else {
        Serial.println("Serialization test failed");
    }
    
    return success;
}

記憶體洩漏檢查

void memoryLeakTest() {
    uint32_t initial_free = ESP.getFreeHeap();
    Serial.printf("Initial free heap: %d bytes\n", initial_free);
    
    // 執行 1000 次序列化操作
    for (int i = 0; i < 1000; i++) {
        SensorReading reading = SensorReading_init_zero;
        strcpy(reading.device_id, "test");
        reading.temperature = i % 100;
        
        uint8_t buffer[128];
        pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
        pb_encode(&stream, SensorReading_fields, &reading);
        
        if (i % 100 == 0) {
            uint32_t current_free = ESP.getFreeHeap();
            Serial.printf("Iteration %d, free heap: %d bytes\n", i, current_free);
        }
    }
    
    uint32_t final_free = ESP.getFreeHeap();
    Serial.printf("Final free heap: %d bytes\n", final_free);
    Serial.printf("Memory change: %d bytes\n", (int32_t)final_free - (int32_t)initial_free);
}

實際專案案例:智能植栽監控系統

讓我分享一個完整的實際專案,展示如何在真實環境中應用這些技術。

專案背景

我幫朋友設計了一個智能植栽監控系統,用於管理他的小型有機農場。系統需要:

  • 24/7 監控土壤濕度、光照、溫濕度
  • 自動灌溉控制
  • 手機 App 即時查看
  • 低功耗運行(太陽能供電)
  • 資料歷史記錄和分析

系統架構

感測器節點 → WiFi → MQTT Broker → 雲端伺服器 → 手機 App
     ↓
  SD 卡備份

完整的 Proto Schema

syntax = "proto3";

// 感測器讀取資料
message SensorReading {
    string node_id = 1;
    int64 timestamp = 2;
    string location = 3;
    
    // 環境資料
    float temperature = 10;
    float humidity = 11;
    int32 light_lux = 12;
    
    // 土壤資料
    float soil_moisture = 20;
    float soil_temperature = 21;
    float ph_level = 22;
    
    // 系統狀態
    float battery_voltage = 30;
    int32 signal_strength = 31;
    bool pump_active = 32;
    
    // 錯誤和警告
    repeated string warnings = 40;
}

// 控制命令
message ControlCommand {
    string target_node = 1;
    int64 timestamp = 2;
    
    oneof command {
        PumpControl pump = 10;
        ConfigUpdate config = 11;
        SystemCommand system = 12;
    }
}

message PumpControl {
    bool enable = 1;
    int32 duration_seconds = 2;
}

message ConfigUpdate {
    int32 reading_interval = 1;
    float moisture_threshold = 2;
    bool auto_irrigation = 3;
}

message SystemCommand {
    enum Command {
        RESTART = 0;
        DEEP_SLEEP = 1;
        FACTORY_RESET = 2;
        UPDATE_FIRMWARE = 3;
    }
    Command command = 1;
}

Arduino 節點程式(簡化版)

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "plant_monitor.pb.h"

// 硬體定義
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define SOIL_MOISTURE_PIN A0
#define PUMP_RELAY_PIN 5
#define BATTERY_PIN A1

// 感測器物件
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient espClient;
PubSubClient mqtt(espClient);

// 設定參數
const char* wifi_ssid = "FarmWiFi";
const char* wifi_password = "your_password";
const char* mqtt_server = "farm-mqtt.example.com";
const char* node_id = "plant_node_001";

// 運行參數
int reading_interval = 300;  // 5分鐘
float moisture_threshold = 30.0;  // 30%
bool auto_irrigation = true;

void setup() {
    Serial.begin(115200);
    
    // 初始化硬體
    dht.begin();
    pinMode(PUMP_RELAY_PIN, OUTPUT);
    digitalWrite(PUMP_RELAY_PIN, LOW);
    
    // 連接網路
    connectWiFi();
    mqtt.setServer(mqtt_server, 1883);
    mqtt.setCallback(onMqttMessage);
    connectMQTT();
    
    Serial.println("Plant monitoring node started");
}

void loop() {
    if (!mqtt.connected()) {
        connectMQTT();
    }
    mqtt.loop();
    
    // 讀取感測器資料
    SensorReading reading = readAllSensors();
    
    // 檢查是否需要灌溉
    if (auto_irrigation && reading.soil_moisture < moisture_threshold) {
        activateIrrigation();
        reading.pump_active = true;
    }
    
    // 發送資料
    sendSensorData(reading);
    
    // 深度睡眠節省電力
    Serial.printf("Sleeping for %d seconds\n", reading_interval);
    ESP.deepSleep(reading_interval * 1000000);
}

SensorReading readAllSensors() {
    SensorReading reading = SensorReading_init_zero;
    
    // 基本資訊
    strcpy(reading.node_id, node_id);
    reading.timestamp = getUnixTime();
    strcpy(reading.location, "Greenhouse_A");
    
    // 環境感測器
    reading.temperature = dht.readTemperature();
    reading.humidity = dht.readHumidity();
    reading.light_lux = readLightSensor();
    
    // 土壤感測器
    reading.soil_moisture = readSoilMoisture();
    reading.soil_temperature = readSoilTemperature();
    reading.ph_level = readPHLevel();
    
    // 系統狀態
    reading.battery_voltage = readBatteryVoltage();
    reading.signal_strength = WiFi.RSSI();
    reading.pump_active = false;
    
    // 檢查警告
    checkWarnings(reading);
    
    return reading;
}

void sendSensorData(const SensorReading& reading) {
    uint8_t buffer[256];
    pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
    
    bool status = pb_encode(&stream, SensorReading_fields, &reading);
    if (!status) {
        Serial.println("Failed to encode sensor data");
        return;
    }
    
    // 發送到 MQTT
    bool sent = mqtt.publish("farm/sensors/data", buffer, stream.bytes_written);
    if (sent) {
        Serial.printf("Sent %d bytes of sensor data\n", stream.bytes_written);
    } else {
        Serial.println("Failed to send sensor data");
        // 保存到 SD 卡作為備份
        saveToSDCard(buffer, stream.bytes_written);
    }
}

void onMqttMessage(char* topic, byte* payload, unsigned int length) {
    if (strcmp(topic, "farm/control/commands") == 0) {
        // 解析控制命令
        ControlCommand command = ControlCommand_init_zero;
        pb_istream_t stream = pb_istream_from_buffer(payload, length);
        
        if (pb_decode(&stream, ControlCommand_fields, &command)) {
            processControlCommand(command);
        }
    }
}

void processControlCommand(const ControlCommand& command) {
    if (strcmp(command.target_node, node_id) != 0) {
        return;  // 不是給這個節點的命令
    }
    
    switch (command.which_command) {
        case ControlCommand_pump_tag:
            if (command.command.pump.enable) {
                activateIrrigation(command.command.pump.duration_seconds);
            } else {
                digitalWrite(PUMP_RELAY_PIN, LOW);
            }
            break;
            
        case ControlCommand_config_tag:
            // 更新配置
            reading_interval = command.command.config.reading_interval;
            moisture_threshold = command.command.config.moisture_threshold;
            auto_irrigation = command.command.config.auto_irrigation;
            Serial.println("Configuration updated");
            break;
            
        case ControlCommand_system_tag:
            switch (command.command.system.command) {
                case SystemCommand_Command_RESTART:
                    ESP.restart();
                    break;
                case SystemCommand_Command_DEEP_SLEEP:
                    ESP.deepSleep(0);
                    break;
                // ... 其他系統命令
            }
            break;
    }
}

成果展示

這個系統運行了一年多,效果非常好:

資料傳輸效率:

  • 平均每筆資料:45 bytes(protobuf)vs 180 bytes(JSON)
  • 每天傳輸資料:約 288 筆 × 45 bytes = 12.96 KB
  • 如果用 JSON:約 288 筆 × 180 bytes = 51.84 KB
  • 節省了 75% 的頻寬

電池續航力:

  • 使用 18650 鋰電池 + 太陽能板
  • 持續運行時間:夏季無限,冬季可達 2 週(無陽光)
  • 資料傳輸量減少直接延長了電池壽命

系統穩定性:

  • 運行 365 天,只有 3 次因為網路問題丟失資料
  • 本地 SD 卡備份機制確保資料不遺失
  • 記憶體使用量穩定,無內存洩漏

開發工具與環境建置

讓我分享一套完整的開發工具鏈,讓你快速上手。

工具安裝

1. 安裝 nanopb

# 從 GitHub 下載
git clone https://github.com/nanopb/nanopb.git
cd nanopb
git submodule update --init

# 建置生成器
cd generator/proto
make

# 設定環境變數
export PATH=$PATH:/path/to/nanopb/generator

2. Arduino IDE 設定

在 Arduino IDE 中安裝所需的庫:

  • PubSubClient(MQTT 客戶端)
  • DHT sensor library
  • ArduinoJson(如果需要 JSON 比較)

3. 建立專案結構

my_iot_project/
├── proto/
│   ├── sensor.proto
│   ├── sensor.options
│   └── generate.sh
├── arduino/
│   ├── main/
│   │   ├── main.ino
│   │   ├── sensor.pb.h
│   │   └── sensor.pb.c
│   └── libraries/
│       └── nanopb/
├── server/
│   ├── sensor_pb2.py
│   └── server.py
└── docs/
    └── README.md

自動化腳本

建立 generate.sh 簡化開發流程:

#!/bin/bash
# proto/generate.sh

echo "Generating nanopb files..."
python /path/to/nanopb/generator/nanopb_generator.py sensor.proto

echo "Copying to Arduino project..."
cp sensor.pb.h sensor.pb.c ../arduino/main/

echo "Generating Python files..."
protoc --python_out=../server sensor.proto

echo "Done!"

VS Code 擴展配置

建立 .vscode/tasks.json:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Generate Protobuf",
            "type": "shell",
            "command": "./proto/generate.sh",
            "group": "build",
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared"
            }
        }
    ]
}

這樣你就可以用 Ctrl+Shift+P → "Tasks: Run Task" → "Generate Protobuf" 快速生成程式碼。

常見問題與解決方案

Q1: nanopb 編譯錯誤

問題:Arduino IDE 報告 nanopb 相關的編譯錯誤。

解決:

  1. 確認 nanopb 版本相容性(建議使用 0.4.x 版本)
  2. 檢查 .options 檔案設定
  3. 確認生成的 .pb.h 和 .pb.c 檔案在正確位置
// 在 .ino 檔案開頭加入
#define PB_ENABLE_MALLOC 0  // 禁用動態記憶體分配
#define PB_FIELD_32BIT 1    // 支援大型訊息

Q2: 記憶體不足

問題:ESP8266 出現記憶體不足,設備重啟。

解決:

  1. 減少緩衝區大小
  2. 使用更精簡的 protobuf schema
  3. 實作記憶體池管理
// 使用更小的緩衝區
#define PROTOBUF_BUFFER_SIZE 64  // 而不是 256
uint8_t buffer[PROTOBUF_BUFFER_SIZE];

Q3: 資料解析失敗

問題:伺服器端無法解析 Arduino 發送的 protobuf 資料。

解決:

  1. 確認兩端使用相同的 .proto 檔案
  2. 檢查資料傳輸的 Content-Type
  3. 除錯序列化資料
// 除錯輸出
void debugProtobufData(uint8_t* data, size_t length) {
    Serial.printf("Protobuf hex dump (%d bytes):\n", length);
    for (int i = 0; i < length; i++) {
        Serial.printf("%02X ", data[i]);
        if ((i + 1) % 16 == 0) Serial.println();
    }
    Serial.println();
}

Q4: WiFi 連線問題

問題:在某些環境下 WiFi 連線不穩定。

解決:實作更強健的連線管理

void ensureWiFiConnection() {
    int retry_count = 0;
    const int max_retries = 10;
    
    while (WiFi.status() != WL_CONNECTED && retry_count < max_retries) {
        Serial.printf("WiFi connecting... (%d/%d)\n", retry_count + 1, max_retries);
        WiFi.begin(ssid, password);
        delay(5000);
        retry_count++;
    }
    
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi connection failed, entering deep sleep");
        ESP.deepSleep(60 * 1000000);  // 休眠 1 分鐘後重試
    }
}

未來發展與技術趨勢

邊緣計算整合

隨著 ESP32-S3 等更強大的微控制器出現,我們可以在設備端進行更多資料處理:

message ProcessedData {
    string device_id = 1;
    int64 timestamp = 2;
    
    // 原始資料的統計摘要
    StatisticalSummary temperature_stats = 10;
    StatisticalSummary humidity_stats = 11;
    
    // 異常檢測結果
    repeated AnomalyAlert anomalies = 20;
}

message StatisticalSummary {
    float mean = 1;
    float min = 2;
    float max = 3;
    float std_dev = 4;
    int32 sample_count = 5;
}

機器學習推論

未來的 IoT 設備可能會整合 TinyML,在本地進行簡單的機器學習推論:

message MLPrediction {
    string model_id = 1;
    int64 timestamp = 2;
    float confidence = 3;
    
    oneof prediction {
        PlantHealthPrediction plant_health = 10;
        WeatherForecast weather = 11;
        EquipmentFailure failure_risk = 12;
    }
}

5G 和低軌衛星

隨著 5G 和低軌衛星網路的普及,更多偏遠地區的 IoT 設備可以接入網路。Protocol Buffers 的高效率將變得更加重要,特別是在衛星通信的高延遲環境下。

最後的話:小設備,大智慧

回想起那個讓我頭痛一個月的專案,如果當時就知道 nanopb 這個神器,該省多少時間啊!

Protocol Buffers 在 Arduino 和單晶片上的應用,讓我深刻體會到:限制往往是創新的催化劑。正是因為有了記憶體、頻寬、電力的限制,我們才被迫去尋找更高效的解決方案。

在 IoT 的世界裡,每個位元組都有它的價值。當你的設備需要運行數月甚至數年,當你的網路頻寬只有幾 KB/s,當你的記憶體只有幾 KB 時,選擇正確的技術就變得至關重要。

nanopb 不只是一個工具,它代表了一種思維方式:如何在有限的資源下做出無限的可能。

給新手的建議

如果你是第一次嘗試在 Arduino 上使用 Protocol Buffers,我的建議是:

  1. 從小開始:先用最簡單的 schema,確保整個流程跑得通
  2. 測量一切:記憶體使用量、傳輸時間、資料大小都要實際測量
  3. 保持耐心:除錯嵌入式系統比除錯伺服器程式困難得多
  4. 記錄經驗:把遇到的問題和解決方案記錄下來,下次會用到

展望未來

物聯網正在快速發展,邊緣計算、人工智慧、5G 通信等技術都在重塑這個領域。但無論技術如何進步,資源效率始終是嵌入式系統的核心議題。

Protocol Buffers 為我們提供了一個優雅的解決方案,讓小小的 Arduino 也能說一口流利的「高效語言」。

希望這篇文章能夠幫助你在自己的 IoT 專案中更好地應用 Protocol Buffers。如果你有任何問題或想要分享你的經驗,歡迎留言討論!

記住:在 IoT 的世界裡,小設備也能有大智慧。


相關資源

官方文檔

教學資源

開發工具

硬體建議

  • 入門級:Arduino Uno + ESP8266 WiFi 模組
  • 推薦級:ESP32 開發板(內建 WiFi/藍牙)
  • 專業級:ESP32-S3 或 STM32 系列

標籤: #Arduino #ProtocolBuffers #nanopb #IoT #嵌入式系統 #ESP32 #感測器


本文最初發布於 HackMD @BASHCAT。

KiCad Python API (kipy) 快速入門指南

最後更新日期: 2025年4月25日 適用版本: KiCad 9.0+

這份快速入門指南旨在幫助您快速開始使用 KiCad Python API (kipy) 進行開發。通過幾個簡單的示例,您將學習如何連接到 KiCad、操作電路板並執行常見任務。

目錄

KiCad Python API 簡介

KiCad 9.0 引入了全新的 IPC API(進程間通信應用程式介面),這是一個穩定的介面,旨在替代舊版的 SWIG Python 綁定。這個新 API 的主要特點有:

  • 穩定性:設計為穩定的介面,不會因為 KiCad 內部重構而變化
  • 語言無關:支持與 Python 以外的其他語言編寫的軟體互操作
  • 進程獨立:使用 Protocol Buffers 和 NNG 透過 UNIX 套接字在進程間傳輸消息
  • 易於使用:提供了 kipy 這個官方的 Python 綁定庫,使得與 IPC API 的交互更加容易 截圖 2025-04-25 晚上10.25.43

注意:SWIG 綁定在 KiCad 9.0 中已被標記為棄用,計劃在 KiCad 10.0(預計2026年2月發布)中被完全移除。建議新的開發工作使用新的 IPC API。

基礎設置

在開始之前,請確保:

  1. 您已安裝 KiCad 9.0 或更高版本
  2. 在 KiCad 中啟用了 API 服務器(首選項 > 插件中啟用)
  3. 已安裝 kipy(使用 pip install kicad-python)

啟用 API 服務器

要使用 kipy,您需要在 KiCad 中啟用 API 服務器:

  1. 打開 KiCad
  2. 進入 首選項 > 插件
  3. 勾選 啟用 API 服務器
  4. 重啟 KiCad 使設置生效

安裝 kipy 包

在命令行中運行以下命令安裝 kipy 包:

pip install kicad-python

如果您需要特定版本或開發版本,可以使用以下命令:

pip install kicad-python==0.1.0  # 安裝特定版本
# 或者
pip install git+https://gitlab.com/kicad/code/kicad-python.git  # 安裝開發版本

核心概念與架構

kipy 的核心架構基於以下幾個關鍵概念:

  1. KiCad 類 - 主要的入口點,用於連接到運行中的 KiCad 實例
  2. Board 類 - 代表一個 KiCad PCB 電路板,允許您查詢和修改電路板上的物件
  3. Project 類 - 代表一個 KiCad 項目,提供對項目級設置的訪問
  4. Wrapper 類 - 大多數 kipy 物件都繼承自 Wrapper,提供對底層 protobuf 消息的訪問
  5. 幾何類 - 提供 Vector2、Angle、Box2 等用於操作座標和幾何形狀的類

示例 1: 連接到 KiCad 並獲取版本信息

截圖 2025-04-25 晚上10.35.58

from kipy.kicad import KiCad

# 創建 KiCad 實例並連接到運行中的 KiCad
kicad = KiCad()

# 獲取 KiCad 版本
version = kicad.get_version()
print(f"連接到 KiCad 版本: {version}")

# 獲取 API 版本
api_version = kicad.get_api_version()
print(f"API 版本: {api_version}")

# 檢查版本兼容性
try:
    kicad.check_version()
    print("版本兼容!")
except Exception as e:
    print(f"版本不兼容: {e}")

示例 2: 獲取當前電路板信息

from kipy.kicad import KiCad

kicad = KiCad()

# 獲取當前活動文檔
active_doc = kicad.get_active_document()
if active_doc and active_doc.type == 2:  # 2 = DOCTYPE_BOARD
    # 獲取電路板
    board = kicad.get_board(active_doc)
    
    # 獲取基本信息
    print(f"電路板名稱: {board.name}")
    
    # 獲取軌道數量
    tracks = board.get_tracks()
    print(f"軌道數量: {len(tracks)}")
    
    # 獲取過孔數量
    vias = board.get_vias()
    print(f"過孔數量: {len(vias)}")
    
    # 獲取封裝數量
    footprints = board.get_footprints()
    print(f"封裝數量: {len(footprints)}")
    
    # 獲取網絡數量
    nets = board.get_nets()
    print(f"網絡數量: {len(nets)}")
else:
    print("當前沒有打開的電路板")

示例 3: 分析封裝和焊盤

from kipy.kicad import KiCad
from kipy.util.units import to_mm

kicad = KiCad()

# 獲取當前電路板
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 獲取所有封裝
footprints = board.get_footprints()

for fp in footprints:
    ref = fp.reference_field.text.value
    val = fp.value_field.text.value
    x_mm = to_mm(fp.position.x)
    y_mm = to_mm(fp.position.y)
    
    print(f"{ref} ({val}) 位於 ({x_mm:.2f}mm, {y_mm:.2f}mm)")
    
    # 獲取焊盤
    pads = fp.definition.pads
    print(f"  焊盤數量: {len(pads)}")
    
    # 列出焊盤信息
    for pad in pads:
        pad_num = pad.number
        net_name = pad.net.name if pad.net.name else "無網絡"
        print(f"  焊盤 {pad_num}: 連接到網絡 '{net_name}'")
    
    print()  # 空行

示例 4: 修改電路板 - 添加文本

from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.proto.common.types.enums_pb2 import HorizontalAlignment, VerticalAlignment
from datetime import datetime

kicad = KiCad()
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 開始一個提交事務
commit = board.begin_commit()

# 創建文本
text = BoardText()
text.value = "添加於 " + datetime.now().strftime("%Y-%m-%d")
text.position = Vector2.from_xy(from_mm(100), from_mm(100))
text.layer = BoardLayer.BL_F_SilkS

# 設置文本屬性
text.attributes.size = Vector2.from_xy(from_mm(1.5), from_mm(1.5))
text.attributes.horizontal_alignment = HorizontalAlignment.HA_CENTER
text.attributes.vertical_alignment = VerticalAlignment.VA_CENTER
text.attributes.bold = True

# 添加到電路板
created_items = board.create_items(text)
print(f"添加了 {len(created_items)} 個新項目")

# 提交更改
board.push_commit(commit, "添加文本標籤")
print("更改已提交")

示例 5: 創建導線和過孔

from kipy.kicad import KiCad
from kipy.board_types import Track, Via
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer, ViaType

kicad = KiCad()
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 開始一個提交事務
commit = board.begin_commit()

# 創建網絡(這裡使用現有網絡)
nets = board.get_nets()
if len(nets) > 0:
    net = nets[0]  # 使用第一個網絡
    
    # 創建一個軌道
    track = Track()
    track.start = Vector2.from_xy(from_mm(100), from_mm(100))
    track.end = Vector2.from_xy(from_mm(120), from_mm(100))
    track.width = from_mm(0.25)  # 0.25mm 寬
    track.layer = BoardLayer.BL_F_Cu  # 頂層銅箔
    track.net = net
    
    # 添加軌道到電路板
    board.create_items(track)
    
    # 創建一個過孔
    via = Via()
    via.position = Vector2.from_xy(from_mm(120), from_mm(100))
    via.type = ViaType.VT_THROUGH
    via.diameter = from_mm(0.8)
    via.drill_diameter = from_mm(0.4)
    via.net = net
    
    # 添加過孔到電路板
    board.create_items(via)
    
    # 提交更改
    board.push_commit(commit, "添加軌道和過孔")
    print("添加了軌道和過孔")
else:
    board.drop_commit(commit)
    print("沒有可用的網絡")

示例 6: 創建一個簡單的 KiCad 插件

import os
import pcbnew
from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
import datetime

class DateStampPlugin(pcbnew.ActionPlugin):
    def __init__(self):
        super().__init__()
        self.name = "添加日期戳記"
        self.category = "修改 PCB"
        self.description = "在電路板上添加當前日期"
        self.show_toolbar_button = True
        # 插件圖標路徑
        self.icon_file_name = os.path.join(os.path.dirname(__file__), "date_icon.png")

    def Run(self):
        # 連接到 KiCad
        kicad = KiCad()
        
        # 獲取當前電路板
        active_doc = kicad.get_active_document()
        board = kicad.get_board(active_doc)
        
        # 開始提交事務
        commit = board.begin_commit()
        
        # 創建日期文本
        date_text = BoardText()
        date_text.value = "生成日期: " + datetime.datetime.now().strftime("%Y-%m-%d")
        date_text.position = Vector2.from_xy(from_mm(150), from_mm(150))
        date_text.layer = BoardLayer.BL_F_SilkS
        
        # 設置文本屬性
        date_text.attributes.size = Vector2.from_xy(from_mm(1.5), from_mm(1.5))
        
        # 添加到電路板
        board.create_items(date_text)
        
        # 提交更改
        board.push_commit(commit, "添加日期戳記")
        
        # 通知用戶
        print("已添加日期戳記到電路板")

# 注冊插件
DateStampPlugin().register()

保存上面的代碼為 date_stamp.py,然後將其放置在 KiCad 插件目錄中:

  • Windows: %APPDATA%\kicad\9.0\scripting\plugins\
  • Linux: <sub>/.local/share/kicad/9.0/scripting/plugins/
  • macOS: </sub>/Library/Application Support/kicad/9.0/scripting/plugins/

示例 7: 透過pytho繪製舉矩形

from kipy import KiCad
from kipy.board_types import BoardRectangle, BoardLayer
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.common_types import GraphicAttributes, Color

# 連線到 KiCad
kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 定義要繪製的層
layers = [
    BoardLayer.BL_F_Cu,        # 頂層銅箔
    BoardLayer.BL_B_Cu,        # 底層銅箔
    BoardLayer.BL_F_SilkS,     # 頂層絲印
    BoardLayer.BL_B_SilkS,     # 底層絲印
    BoardLayer.BL_F_Mask,      # 頂層阻焊
    BoardLayer.BL_B_Mask,      # 底層阻焊
    BoardLayer.BL_Edge_Cuts,   # 邊緣切割
    BoardLayer.BL_Dwgs_User,   # 用戶圖形層
]

# 獲取圖形元素預設設置
graphics_defaults = board.get_graphics_defaults()

# 設置方框的基本參數
center_x = from_mm(100)  # 中心點 X 座標
center_y = from_mm(100)  # 中心點 Y 座標
max_size = from_mm(50)   # 最大方框的寬度/高度
min_size = from_mm(10)   # 最小方框的寬度/高度
num_rectangles = 5       # 每一層繪製的方框數量

# 在每一層繪製同心方形
for layer_index, layer in enumerate(layers):
    # 方框間的大小差異
    size_step = (max_size - min_size) // (num_rectangles - 1)
    
    # 在當前層繪製多個同心方形
    for i in range(num_rectangles):
        # 計算當前方框的大小
        current_size = max_size - i * size_step
        
        # 計算方框的左上角和右下角座標
        half_size = current_size // 2
        top_left = Vector2.from_xy(center_x - half_size, center_y - half_size)
        bottom_right = Vector2.from_xy(center_x + half_size, center_y + half_size)
        
        # 創建方框
        rect = BoardRectangle()
        rect.top_left = top_left
        rect.bottom_right = bottom_right
        rect.layer = layer
        
        # 設置線寬
        rect.attributes.stroke.width = from_mm(0.2)
        
        # 根據層和方框大小設置不同的填充
        # 偶數層和偶數方框使用實心填充,其他使用無填充
        if (layer_index % 2 == 0) and (i % 2 == 0):
            rect.attributes.fill.mode = 1  # 實心填充
        else:
            rect.attributes.fill.mode = 0  # 無填充
        
        # 添加到電路板
        board.create_items(rect)

# 提交變更
board.push_commit(commit, message="在不同層繪製同心方形")
print("在多個層上繪製了同心方形")

截圖 2025-04-25 晚上10.28.10

截圖 2025-04-25 晚上10.28.31

常見任務快速參考

單位轉換

from kipy.util.units import from_mm, to_mm

# 毫米轉換為 KiCad 內部單位(納米)
position_nm = from_mm(10)  # 10mm 轉換為納米

# KiCad 內部單位轉換為毫米
size_mm = to_mm(1000000)  # 1,000,000 納米轉換為毫米

獲取層名稱

from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.board_layer import canonical_name

# 獲取層的標準名稱
layer = BoardLayer.BL_F_Cu
layer_name = canonical_name(layer)  # 返回 "F.Cu"

獲取選定的項目

# 獲取選定的項目
selected_items = board.get_selection()
print(f"選定了 {len(selected_items)} 個項目")

# 添加項目到選擇
board.add_to_selection(some_item)

# 清除選擇
board.clear_selection()

保存電路板

# 保存當前電路板
board.save()

# 另存為新文件
board.save_as("/path/to/new/board.kicad_pcb")

進階 API 使用技巧

處理事務提交

在 KiCad Python API 中,所有對電路板的修改都應該通過事務提交進行。這不僅可以確保修改被正確應用,還允許用戶撤銷操作。

from kipy.kicad import KiCad

kicad = KiCad()
board = kicad.get_board()

# 開始一個提交事務
commit = board.begin_commit()

# 進行修改...
# 例如創建、更新或刪除項目

try:
    # 提交更改並提供說明信息(顯示在撤銷/重做菜單中)
    board.push_commit(commit, "我的腳本修改")
    print("修改已成功提交")
except Exception as e:
    # 如果出現錯誤,放棄提交
    board.drop_commit(commit)
    print(f"修改失敗: {e}")

批量處理項目

當需要處理大量項目時,將它們批量處理可以提高效率:

from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer

kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 創建多個文本項目
texts = []
for i in range(10):
    text = BoardText()
    text.value = f"項目 {i}"
    text.position = Vector2.from_xy(from_mm(100 + i*10), from_mm(100))
    text.layer = BoardLayer.BL_F_SilkS
    texts.append(text)

# 批量創建項目
created_items = board.create_items(texts)
print(f"批量創建了 {len(created_items)} 個項目")

# 提交更改
board.push_commit(commit, "批量添加文本項目")

自定義插件目錄結構

對於複雜的插件,建議使用以下目錄結構:

my_plugin/
├── plugin.json           # 插件配置文件
├── icon.png              # 插件圖標
├── __init__.py           # 初始化文件
├── main.py               # 主要入口點
├── dialog.py             # 對話框 UI
└── utils/                # 工具函數
    ├── __init__.py
    └── helpers.py

plugin.json 文件示例:

{
    "$schema": "https://go.kicad.org/api/schemas/v1",
    "identifier": "com.example.my-plugin",
    "name": "我的 KiCad 插件",
    "description": "一個實用的 KiCad PCB 編輯器插件",
    "version": "1.0.0",
    "author": {
        "name": "您的名字",
        "contact": {
            "web": "https://example.com"
        }
    },
    "runtime": {
        "type": "python",
        "min_version": "3.9"
    },
    "actions": [
        {
            "identifier": "my-action",
            "name": "執行我的功能",
            "description": "示範插件功能",
            "show-button": true,
            "scopes": ["pcb"],
            "entrypoint": "main.py"
        }
    ]
}

與圖形用戶界面集成

KiCad Python API 可以與 wxPython 結合,創建與 KiCad 風格一致的用戶界面:

import wx
from kipy.kicad import KiCad

class MyDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title="我的插件對話框")
        
        # 創建控件
        self.text_ctrl = wx.TextCtrl(self)
        self.button = wx.Button(self, label="確定")
        
        # 設置佈局
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(wx.StaticText(self, label="請輸入文本:"), 0, wx.ALL, 5)
        sizer.Add(self.text_ctrl, 0, wx.EXPAND|wx.ALL, 5)
        sizer.Add(self.button, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
        
        self.SetSizerAndFit(sizer)
        
        # 綁定事件
        self.button.Bind(wx.EVT_BUTTON, self.on_button_click)
    
    def on_button_click(self, event):
        text = self.text_ctrl.GetValue()
        print(f"用戶輸入: {text}")
        self.EndModal(wx.ID_OK)
        
def run_plugin():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 創建對話框
    app = wx.App.Get()
    with MyDialog(wx.GetApp().GetTopWindow()) as dlg:
        if dlg.ShowModal() == wx.ID_OK:
            print("對話框確認")
            # 在這裡執行操作
        else:
            print("對話框取消")

常見問題解答 (FAQ)

Q: 新的 IPC API 與舊的 SWIG 綁定有什麼區別?

A: 新的 IPC API 與舊版 SWIG 綁定相比有以下主要區別:

  1. 穩定性:IPC API 設計為穩定接口,不會隨著 KiCad 內部代碼重構而改變
  2. 進程分離:IPC API 在單獨的進程中運行,通過 IPC 機制與 KiCad 通信,而 SWIG 綁定直接在 KiCad 進程中運行
  3. 語言無關:IPC API 可以從多種編程語言訪問,而不僅僅是 Python
  4. 更現代的 API 設計:提供了更一致、更易於使用的接口
  5. 穩定的 ABI:插件不需要針對每個 KiCad 版本重新編譯

Q: 如何調試 kipy 腳本?

A: 您可以使用標準的 Python 調試技術:

  1. 使用 print 語句輸出調試信息
  2. 使用 Python 的 logging 模塊記錄信息
  3. 使用 VSCode 或 PyCharm 等 IDE 的調試器進行交互式調試
  4. 使用 try-except 塊捕獲並打印詳細的錯誤信息
import logging

# 設置日誌記錄
logging.basicConfig(level=logging.DEBUG,
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                   filename='kipy_debug.log')

try:
    # 您的代碼
    kicad = KiCad()
    board = kicad.get_board()
    logging.info(f"成功獲取電路板: {board.name}")
except Exception as e:
    logging.error(f"發生錯誤: {e}", exc_info=True)

Q: 我的 kipy 腳本無法連接到 KiCad,可能的原因是什麼?

A: 常見的連接問題包括:

  1. KiCad 中沒有啟用 API 服務器(首選項 > 插件中啟用)
  2. KiCad 版本與 kipy 版本不兼容
  3. 未運行 KiCad 或運行了多個 KiCad 實例
  4. 系統防火牆或安全軟件阻止了進程間通信

首先確保 KiCad 正在運行,並在設置中啟用了 API 服務器。然後嘗試重新安裝與您 KiCad 版本兼容的 kipy 版本。

Q: 如何創建使用 kipy 的獨立工具(非插件)?

A: 您可以創建直接使用 kipy 與運行中的 KiCad 通信的獨立 Python 腳本:

#!/usr/bin/env python3
from kipy.kicad import KiCad

def main():
    try:
        # 連接到運行中的 KiCad 實例
        kicad = KiCad()
        print(f"已連接到 KiCad {kicad.get_version()}")
        
        # 獲取當前電路板
        board = kicad.get_board()
        if not board:
            print("未找到打開的電路板")
            return
            
        # 執行您的操作
        # ...
        
    except Exception as e:
        print(f"錯誤: {e}")

if __name__ == "__main__":
    main()

運行這樣的腳本時,確保 KiCad 已經在運行並且已經打開了電路板。

Q: 我可以使用 kipy 創建完整的電路板嗎?

A: 是的,您可以從頭開始創建電路板,添加所有的元件和軌道。然而,通常更實用的做法是從現有的電路板開始,然後修改它。

Q: 我如何處理錯誤和例外?

A: kipy 會抛出 ApiError 和 ConnectionError 等異常。您應該處理這些異常以確保腳本在出現問題時能够正常處理:

from kipy.errors import ApiError, ConnectionError

try:
    # kipy 代碼
    kicad = KiCad()
    # ...
except ConnectionError as e:
    print(f"無法連接到 KiCad: {e}")
except ApiError as e:
    print(f"API 錯誤: {e}")

性能優化技巧

使用 KiCad Python API 處理大型電路板時,以下是一些提高性能的技巧:

  1. 批量處理:一次性提交多個更改,而不是逐個提交
  2. 限制重繪:在批量操作期間禁用重繪,完成後再恢復
  3. 使用適當的數據結構:例如使用 dict 建立網絡名稱到網絡對象的映射
  4. 避免不必要的查詢:緩存經常訪問的數據,而不是反复查詢

以下是優化批量更新操作的示例:

from kipy.kicad import KiCad

kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 預先獲取所有需要的數據
tracks = board.get_tracks()
nets = {net.name: net for net in board.get_nets()}

# 批量處理項目
to_update = []
for track in tracks:
    if track.width < from_mm(0.2):  # 找出寬度小於 0.2mm 的軌道
        track.width = from_mm(0.2)  # 設置為 0.2mm
        to_update.append(track)

# 一次性更新所有修改的軌道
if to_update:
    board.update_items(to_update)
    board.push_commit(commit, f"將 {len(to_update)} 條軌道寬度更新為 0.2mm")
    print(f"已更新 {len(to_update)} 條軌道")
else:
    board.drop_commit(commit)
    print("沒有需要更新的軌道")

實用範例與實際應用案例

自動化設計規則檢查

以下範例展示如何使用 kipy 進行設計規則檢查(DRC)並生成報告:

from kipy.kicad import KiCad
from kipy.util.units import to_mm
import csv
import datetime

def check_track_clearance(board, min_clearance_mm=0.2):
    """檢查軌道之間的最小間距"""
    tracks = board.get_tracks()
    violations = []
    
    # 這裡只是一個簡化的示例
    # 實際的間距檢查需要更複雜的算法
    for i, track1 in enumerate(tracks):
        for track2 in tracks[i+1:]:
            if track1.layer == track2.layer and track1.net != track2.net:
                # 這裡應有實際計算兩條軌道之間最小距離的代碼
                distance_mm = 0.1  # 假設值,實際需要計算
                if distance_mm < min_clearance_mm:
                    violations.append({
                        'track1': f"({to_mm(track1.start.x):.2f}, {to_mm(track1.start.y):.2f}) - ({to_mm(track1.end.x):.2f}, {to_mm(track1.end.y):.2f})",
                        'track2': f"({to_mm(track2.start.x):.2f}, {to_mm(track2.start.y):.2f}) - ({to_mm(track2.end.x):.2f}, {to_mm(track2.end.y):.2f})",
                        'distance': f"{distance_mm:.2f}mm",
                        'required': f"{min_clearance_mm:.2f}mm"
                    })
    return violations

def export_drc_report(violations, filename):
    """將違規導出為 CSV 報告"""
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=['track1', 'track2', 'distance', 'required'])
        writer.writeheader()
        for v in violations:
            writer.writerow(v)
    print(f"報告已保存至 {filename}")

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    print(f"正在檢查電路板: {board.name}")
    violations = check_track_clearance(board)
    
    if violations:
        print(f"發現 {len(violations)} 個間距違規")
        filename = f"drc_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        export_drc_report(violations, filename)
    else:
        print("未發現間距違規")

if __name__ == "__main__":
    main()

自動生成裝配圖層

這個範例展示如何使用 kipy 在電路板上生成裝配圖層標記,用於製造和裝配:

from kipy.kicad import KiCad
from kipy.board_types import BoardText, BoardCircle
from kipy.geometry import Vector2
from kipy.util.units import from_mm, to_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
import math

def create_fiducial_marks(board, layer=BoardLayer.BL_F_SilkS):
    """在電路板角落創建基準標記"""
    commit = board.begin_commit()
    
    # 獲取電路板邊界
    board_outline = board.get_board_outline()
    bb = board_outline.bounding_box
    
    # 計算角落位置(留出 5mm 邊距)
    margin = from_mm(5)
    corners = [
        Vector2.from_xy(bb.min_x + margin, bb.min_y + margin),  # 左下
        Vector2.from_xy(bb.max_x - margin, bb.min_y + margin),  # 右下
        Vector2.from_xy(bb.max_x - margin, bb.max_y - margin),  # 右上
        Vector2.from_xy(bb.min_x + margin, bb.max_y - margin)   # 左上
    ]
    
    # 創建基準標記(十字加圓)
    marks = []
    for i, pos in enumerate(corners):
        # 創建圓
        circle = BoardCircle()
        circle.center = pos
        circle.radius = from_mm(1)
        circle.layer = layer
        marks.append(circle)
        
        # 創建標籤
        text = BoardText()
        text.value = f"FID{i+1}"
        text.position = Vector2.from_xy(pos.x, pos.y + from_mm(2))
        text.layer = layer
        marks.append(text)
    
    # 創建元素並提交
    board.create_items(marks)
    board.push_commit(commit, "添加裝配基準標記")
    return len(marks)

def generate_assembly_labels(board):
    """為每個封裝生成裝配標籤"""
    commit = board.begin_commit()
    
    footprints = board.get_footprints()
    labels = []
    
    for fp in footprints:
        ref = fp.reference_field.text.value
        val = fp.value_field.text.value
        
        # 在元件上方2mm處創建標籤
        text = BoardText()
        text.value = f"{ref}:{val}"
        # 計算位置,考慮元件旋轉
        angle_rad = math.radians(fp.orientation.degrees)
        offset_x = -from_mm(2) * math.sin(angle_rad)
        offset_y = from_mm(2) * math.cos(angle_rad)
        text.position = Vector2.from_xy(fp.position.x + offset_x, fp.position.y + offset_y)
        text.layer = BoardLayer.BL_F_Fab  # 放在製造層
        labels.append(text)
    
    board.create_items(labels)
    board.push_commit(commit, "添加裝配標籤")
    return len(labels)

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    num_fiducials = create_fiducial_marks(board)
    print(f"已添加 {num_fiducials} 個基準標記")
    
    num_labels = generate_assembly_labels(board)
    print(f"已添加 {num_labels} 個裝配標籤")

if __name__ == "__main__":
    main()

PCB 自動佈局輔助工具

下面是一個簡單的工具,用於自動排列電路板上的某些元件:

from kipy.kicad import KiCad
from kipy.geometry import Vector2
from kipy.util.units import from_mm, to_mm
import re

def arrange_components_in_grid(board, ref_pattern, rows, cols, spacing_mm):
    """將匹配模式的元件按網格排列"""
    commit = board.begin_commit()
    
    # 查找匹配的元件
    footprints = board.get_footprints()
    matching_fps = []
    
    pattern = re.compile(ref_pattern)
    for fp in footprints:
        ref = fp.reference_field.text.value
        if pattern.match(ref):
            matching_fps.append(fp)
    
    if not matching_fps:
        print(f"找不到匹配 '{ref_pattern}' 的元件")
        return 0
    
    # 最多處理 rows*cols 個元件
    count = min(len(matching_fps), rows * cols)
    
    # 設置起始位置為第一個匹配元件的位置
    start_x = matching_fps[0].position.x
    start_y = matching_fps[0].position.y
    
    # 計算間距
    spacing_x = from_mm(spacing_mm)
    spacing_y = from_mm(spacing_mm)
    
    # 按網格排列元件
    for i in range(count):
        row = i // cols
        col = i % cols
        
        fp = matching_fps[i]
        fp.position = Vector2.from_xy(
            start_x + col * spacing_x,
            start_y + row * spacing_y
        )
    
    # 更新元件位置
    board.update_items(matching_fps[:count])
    board.push_commit(commit, f"按網格排列 {count} 個元件")
    
    return count

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 例如,將所有 LED 元件排成 3x4 網格,間距 10mm
    count = arrange_components_in_grid(board, r"LED\d+", 3, 4, 10)
    print(f"已排列 {count} 個元件")

if __name__ == "__main__":
    main()

自動創建PCB測試點

以下範例展示如何為特定網絡自動添加測試點:

from kipy.kicad import KiCad
from kipy.board_types import Via
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import ViaType

def add_test_points(board, net_names, diameter_mm=1.0, drill_mm=0.5):
    """為指定網絡添加測試點"""
    commit = board.begin_commit()
    
    # 獲取所有網絡
    nets = board.get_nets()
    net_dict = {net.name: net for net in nets if net.name}
    
    # 查找匹配的網絡
    test_points = []
    for net_name in net_names:
        if net_name in net_dict:
            net = net_dict[net_name]
            
            # 獲取這個網絡的軌道,找到一個合適的位置
            tracks = [t for t in board.get_tracks() if t.net.code == net.code]
            if tracks:
                # 使用第一條軌道的中點作為測試點位置
                track = tracks[0]
                pos_x = (track.start.x + track.end.x) / 2
                pos_y = (track.start.y + track.end.y) / 2
                
                # 創建測試點(使用過孔)
                via = Via()
                via.position = Vector2.from_xy(pos_x, pos_y)
                via.type = ViaType.VT_THROUGH
                via.diameter = from_mm(diameter_mm)
                via.drill_diameter = from_mm(drill_mm)
                via.net = net
                
                test_points.append(via)
                print(f"為網絡 '{net_name}' 添加測試點")
            else:
                print(f"網絡 '{net_name}' 沒有軌道,無法添加測試點")
        else:
            print(f"找不到網絡 '{net_name}'")
    
    if test_points:
        board.create_items(test_points)
        board.push_commit(commit, f"添加 {len(test_points)} 個測試點")
        return len(test_points)
    else:
        board.drop_commit(commit)
        return 0

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 指定要添加測試點的網絡名稱
    net_names = ["GND", "VCC", "RST", "SCL", "SDA"]
    count = add_test_points(board, net_names)
    
    print(f"共添加了 {count} 個測試點")

if __name__ == "__main__":
    main()

與其他工具集成

與 Git 版本控制集成

這個範例展示如何使用 kipy 實現 KiCad 與 Git 版本控制的集成:

import os
import subprocess
from kipy.kicad import KiCad
import datetime

def git_commit_pcb_changes(board, commit_message=None):
    """保存電路板並將更改提交到 Git"""
    # 獲取電路板文件路徑
    board_path = board.filename
    
    if not os.path.isfile(board_path):
        print("電路板尚未保存,無法提交到 Git")
        return False
    
    # 保存電路板
    board.save()
    print(f"已保存電路板到: {board_path}")
    
    # 獲取 Git 倉庫根目錄
    try:
        repo_root = subprocess.check_output(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=os.path.dirname(board_path),
            text=True
        ).strip()
    except subprocess.CalledProcessError:
        print("當前目錄不是 Git 倉庫")
        return False
    
    # 獲取相對路徑
    rel_path = os.path.relpath(board_path, repo_root)
    
    # 添加到暫存區
    try:
        subprocess.run(
            ["git", "add", rel_path],
            cwd=repo_root,
            check=True
        )
        
        # 創建提交信息
        if not commit_message:
            commit_message = f"更新電路板設計 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"
        
        # 提交更改
        subprocess.run(
            ["git", "commit", "-m", commit_message],
            cwd=repo_root,
            check=True
        )
        
        print(f"已將電路板更改提交到 Git: {commit_message}")
        return True
    except subprocess.CalledProcessError as e:
        print(f"Git 操作失敗: {e}")
        return False

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 進行一些修改...
    
    # 保存並提交修改
    git_commit_pcb_changes(board, "添加新的電源元件和軌道")

if __name__ == "__main__":
    main()

與 BOM 管理系統集成

以下範例展示如何生成物料清單(BOM)並與外部系統集成:

from kipy.kicad import KiCad
import csv
import json
import requests

def generate_bom(board):
    """生成電路板的物料清單"""
    footprints = board.get_footprints()
    
    # 按值和封裝分組元件
    components = {}
    for fp in footprints:
        ref = fp.reference_field.text.value
        val = fp.value_field.text.value
        pkg = fp.definition.identifier.lib_id.name if hasattr(fp.definition, 'identifier') else "Unknown"
        
        key = f"{val}|{pkg}"
        if key not in components:
            components[key] = {
                'value': val,
                'package': pkg,
                'references': [],
                'quantity': 0
            }
        
        components[key]['references'].append(ref)
        components[key]['quantity'] += 1
    
    # 轉換為列表
    bom_list = list(components.values())
    
    # 為每個項目添加引用字符串
    for item in bom_list:
        item['references_str'] = ", ".join(sorted(item['references']))
    
    return bom_list

def export_bom_csv(bom_list, filename):
    """將 BOM 導出為 CSV 文件"""
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.DictWriter(
            csvfile,
            fieldnames=['value', 'package', 'references_str', 'quantity'],
            extrasaction='ignore'
        )
        writer.writeheader()
        writer.writerows(bom_list)
    print(f"BOM 已導出至 {filename}")
    return filename

def upload_bom_to_system(bom_list, api_url, api_key):
    """將 BOM 上傳到外部系統"""
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {api_key}'
    }
    
    payload = {
        'project_name': 'My KiCad Project',
        'components': bom_list
    }
    
    try:
        response = requests.post(api_url, headers=headers, json=payload)
        response.raise_for_status()
        print(f"BOM 已成功上傳: {response.json().get('message', '')}")
        return True
    except requests.exceptions.RequestException as e:
        print(f"上傳失敗: {e}")
        return False

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 生成 BOM
    bom_list = generate_bom(board)
    
    # 導出為 CSV
    export_bom_csv(bom_list, "bom_export.csv")
    
    # 上傳到外部系統(需要實際的 API 端點和密鑰)
    # upload_bom_to_system(bom_list, "https://api.example.com/bom", "your_api_key")

if __name__ == "__main__":
    main()

API 參考

主要類和模塊

  • kipy.kicad:包含 KiCad 類,用於連接到 KiCad 實例
  • kipy.board_types:包含 Board、Track、Via、Footprint 等類
  • kipy.geometry:包含 Vector2、Angle、Box2 等幾何類
  • kipy.util:包含單位轉換和其他實用功能
  • kipy.proto:包含底層 protobuf 類型定義
  • kipy.errors:包含異常類型

常用常數

  • BoardLayer:定義電路板層(如 BL_F_Cu、BL_B_Cu、BL_F_SilkS)
  • ViaType:定義過孔類型(如 VT_THROUGH、VT_BLIND_BURIED)
  • DocumentType:定義文檔類型(如 DOCTYPE_BOARD、DOCTYPE_SCHEMATIC)

幾何操作

  • Vector2:表示二維向量或點
  • Angle:表示角度,提供度和弧度之間的轉換
  • Box2:表示二維軸對齊框

下一步


本文最初發布於 HackMD @BASHCAT。

8GB 的 RK3588 能跑多聰明的 LLM?ROCK 5C 用 NPU 實測 5 個模型

先講結論,省得你滑到最後: 在一片 8GB 的 RK3588 板子上,用 NPU 跑得最聰明的是 Qwen3-4B-Instruct-2507,我出的 7 題全對,但每秒只吐 3.7 個 token。 想要順一點的對話體驗,Qwen3.5-2B 的 8.4 tok/s 是比...