☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
iot 進階

Raspberry Pi 進階維運

遠端管理多台 Pi(Ansible)

Ansible 安裝與設定

# 在管理機上安裝 Ansible // 從管理機統一控制所有 Pi
sudo apt install -y ansible  // 安裝 Ansible

# 建立主機清單檔案 // 列出所有要管理的 Pi
cat > /etc/ansible/hosts << 'EOF'  // 寫入主機清單
[pos_machines]                     // POS 機器群組
pos-store-a ansible_host=192.168.1.101 ansible_user=pi  // 門市 A 的 POS
pos-store-b ansible_host=192.168.1.102 ansible_user=pi  // 門市 B 的 POS
pos-store-c ansible_host=192.168.1.103 ansible_user=pi  // 門市 C 的 POS

[kiosk_machines]                   // Kiosk 機器群組
kiosk-lobby ansible_host=192.168.1.201 ansible_user=pi  // 大廳 Kiosk
kiosk-entrance ansible_host=192.168.1.202 ansible_user=pi  // 入口 Kiosk

[all:vars]                         // 所有機器的共用變數
ansible_python_interpreter=/usr/bin/python3  // 指定 Python 路徑
EOF

Ansible Playbook 範例

# deploy_pos.yml // POS 部署 Playbook
---
- name: 部署 POS 應用程式    # Playbook 名稱 // 描述用途
  hosts: pos_machines          # 目標主機群組 // 所有 POS 機器
  become: yes                  # 使用 sudo // 需要管理員權限

  tasks:                       # 任務列表 // 依序執行
    - name: 更新系統套件       # 任務名稱 // 確保系統最新
      apt:                     # 使用 apt 模組 // 套件管理
        update_cache: yes      # 更新套件庫 // apt update
        upgrade: yes           # 升級套件 // apt upgrade

    - name: 安裝 .NET Runtime  # 任務名稱 // 安裝執行環境
      shell: |                 # 使用 shell 模組 // 執行指令
        curl -sSL https://dot.net/v1/dotnet-install.sh | bash  # 安裝腳本 // 官方安裝

    - name: 部署 POS 應用      # 任務名稱 // 複製應用程式
      copy:                    # 使用 copy 模組 // 檔案複製
        src: ./publish/         # 來源路徑 // 本地編譯結果
        dest: /opt/pos-app/    # 目標路徑 // Pi 上的安裝位置

    - name: 重啟 POS 服務      # 任務名稱 // 套用新版本
      systemd:                 # 使用 systemd 模組 // 服務管理
        name: pos-app          # 服務名稱 // POS 應用服務
        state: restarted       # 狀態 // 重啟服務

批量執行指令

# 對所有 POS 機器執行指令 // 一次管理多台
ansible pos_machines -m shell -a "uptime"  // 查看所有機器運行時間

# 批量重啟 Kiosk 服務 // 一次重啟所有 Kiosk
ansible kiosk_machines -m systemd -a "name=kiosk state=restarted"  // 重啟服務

# 執行 Playbook // 部署更新到所有 POS
ansible-playbook deploy_pos.yml  // 執行部署腳本

SSH Tunnel 穿越 NAT

💡 比喻:挖隧道回家 你的 Pi 在門市的區域網路裡,就像住在一個封閉社區裡的人。 SSH Tunnel 就是從社區裡「挖一條隧道」通到外面的伺服器, 這樣你在任何地方都能透過這條隧道連回門市的 Pi。

反向 SSH Tunnel

# 在 Pi 上建立反向隧道 // 從門市 Pi 連到雲端伺服器
ssh -fN \                              // 背景執行(不開 shell)
  -R 2222:localhost:22 \               // 反向轉發:雲端的 2222 → Pi 的 22
  -o ServerAliveInterval=60 \          // 每 60 秒發送心跳
  -o ServerAliveCountMax=3 \           // 心跳失敗 3 次就斷線
  -o ExitOnForwardFailure=yes \        // 轉發失敗就退出
  tunnel-user@cloud-server.com         // 雲端伺服器帳號

# 從雲端伺服器連回 Pi // 透過隧道遠端管理
ssh -p 2222 pi@localhost               // 連到本地 2222 就是連到 Pi

自動重連腳本

#!/bin/bash                              // 指定直譯器
# autossh_tunnel.sh // 自動重連 SSH 隧道

REMOTE_USER="tunnel"                   // 雲端伺服器帳號
REMOTE_HOST="cloud-server.com"         // 雲端伺服器位址
REMOTE_PORT=2222                         // 雲端轉發埠號
LOCAL_PORT=22                            // 本地 SSH 埠號
LOG_FILE="/var/log/ssh-tunnel.log"     // 隧道日誌檔

while true; do                           // 無限迴圈
    echo "$(date): 建立 SSH 隧道..." >> $LOG_FILE  // 記錄嘗試連線

    # 建立隧道 // 斷線會自動跳到下一輪迴圈
    ssh -N \                             // 不開 shell
      -R ${REMOTE_PORT}:localhost:${LOCAL_PORT} \  // 反向轉發
      -o ServerAliveInterval=60 \        // 心跳間隔
      -o ServerAliveCountMax=3 \         // 心跳重試
      -o ExitOnForwardFailure=yes \      // 失敗就退出
      -o StrictHostKeyChecking=no \      // 不檢查主機金鑰
      ${REMOTE_USER}@${REMOTE_HOST}      // 遠端伺服器

    echo "$(date): SSH 隧道斷開,10秒後重連..." >> $LOG_FILE  // 記錄斷線
    sleep 10                             // 等 10 秒再重連
done

systemd service 設定

# SSH Tunnel 自動啟動服務 // 開機自動建立隧道
sudo cat > /etc/systemd/system/ssh-tunnel.service << 'EOF'  // 寫入 service 檔
[Unit]                                   // 單元設定
Description=SSH Reverse Tunnel           // 服務描述
After=network-online.target              // 網路就緒後啟動
Wants=network-online.target              // 需要網路

[Service]                                // 服務設定
ExecStart=/home/pi/autossh_tunnel.sh     // 啟動腳本
Restart=always                           // 總是重啟
RestartSec=10                            // 重啟間隔
User=pi                                  // 執行用戶

[Install]                                // 安裝設定
WantedBy=multi-user.target               // 多用戶模式啟動
EOF

sudo systemctl enable ssh-tunnel.service  // 啟用自動啟動
sudo systemctl start ssh-tunnel.service   // 立即啟動

自動更新機制

Webhook 觸發更新

// 自動更新控制器 // 接收 Webhook 觸發部署
[ApiController] // 標記為 API 控制器
[Route("api/[controller]")] // 路由設定
public class DeployController : ControllerBase // 部署控制器
{
    private readonly ILogger<DeployController> _logger; // 日誌記錄器

    // 建構函式 // 注入日誌
    public DeployController( // 建構部署控制器
        ILogger<DeployController> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 接收部署 Webhook // GitHub Actions 完成後呼叫
    [HttpPost("webhook")] // POST api/deploy/webhook
    public IActionResult TriggerDeploy( // 觸發部署方法
        [FromHeader(Name = "X-Deploy-Token")] string? token) // 部署令牌
    {
        // 驗證令牌 // 防止未授權的部署
        if (token != Environment.GetEnvironmentVariable("DEPLOY_TOKEN")) // 比對令牌
        {
            _logger.LogWarning("收到未授權的部署請求"); // 記錄可疑行為
            return Unauthorized(); // 拒絕未授權請求
        }

        // 執行部署腳本 // 背景更新應用程式
        _logger.LogInformation("開始執行部署..."); // 記錄開始部署

        Task.Run(() => ExecuteDeployScript()); // 背景執行部署

        return Ok(new { message = "部署已觸發" }); // 回傳成功
    }

    // 執行部署腳本 // 實際的更新步驟
    private void ExecuteDeployScript() // 部署腳本方法
    {
        try // 嘗試部署
        {
            var process = new System.Diagnostics.Process(); // 建立程序
            process.StartInfo.FileName = "/home/pi/deploy.sh"; // 部署腳本路徑
            process.StartInfo.UseShellExecute = false; // 不使用 shell
            process.Start(); // 啟動部署
            process.WaitForExit(); // 等待完成

            _logger.LogInformation("部署完成,退出碼:{Code}", // 記錄結果
                process.ExitCode); // 傳入退出碼
        }
        catch (Exception ex) // 捕捉例外
        {
            _logger.LogError(ex, "部署失敗"); // 記錄錯誤
        }
    }
}

系統監控

C# 系統監控服務

// 系統監控服務 // 監控 Pi 的 CPU、記憶體、溫度
public class SystemMonitorService // 系統監控服務類別
{
    private readonly ILogger<SystemMonitorService> _logger; // 日誌記錄器

    // 系統狀態模型 // 記錄各項系統指標
    public class SystemStatus // 系統狀態類別
    {
        public double CpuUsagePercent { get; set; } // CPU 使用率
        public double MemoryUsagePercent { get; set; } // 記憶體使用率
        public double TemperatureCelsius { get; set; } // CPU 溫度
        public double DiskUsagePercent { get; set; } // 磁碟使用率
        public TimeSpan Uptime { get; set; } // 系統運行時間
        public DateTime Timestamp { get; set; } = DateTime.UtcNow; // 記錄時間
    }

    // 建構函式 // 注入日誌
    public SystemMonitorService( // 建構監控服務
        ILogger<SystemMonitorService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 取得系統狀態 // 讀取各項指標
    public SystemStatus GetStatus() // 取得狀態方法
    {
        var status = new SystemStatus // 建立狀態物件
        {
            CpuUsagePercent = GetCpuUsage(), // 讀取 CPU 使用率
            MemoryUsagePercent = GetMemoryUsage(), // 讀取記憶體使用率
            TemperatureCelsius = GetCpuTemperature(), // 讀取 CPU 溫度
            DiskUsagePercent = GetDiskUsage(), // 讀取磁碟使用率
            Uptime = GetUptime() // 讀取運行時間
        };

        // 溫度過高警告 // 超過 70°C 就記錄警告
        if (status.TemperatureCelsius > 70) // 溫度門檻
        {
            _logger.LogWarning("CPU 溫度過高:{Temp}°C", // 記錄警告
                status.TemperatureCelsius); // 傳入溫度
        }

        return status; // 回傳狀態
    }

    // 讀取 CPU 溫度 // 從系統檔案取得
    private double GetCpuTemperature() // CPU 溫度方法
    {
        try // 嘗試讀取
        {
            var temp = System.IO.File.ReadAllText( // 讀取溫度檔案
                "/sys/class/thermal/thermal_zone0/temp"); // Linux 溫度檔路徑
            return double.Parse(temp.Trim()) / 1000.0; // 轉換為攝氏度
        }
        catch // 讀取失敗
        {
            return -1; // 回傳 -1 表示無法讀取
        }
    }

    // 讀取記憶體使用率 // 從 /proc/meminfo 取得
    private double GetMemoryUsage() // 記憶體使用率方法
    {
        try // 嘗試讀取
        {
            var info = System.IO.File.ReadAllLines("/proc/meminfo"); // 讀取記憶體資訊
            var total = ParseMemInfo(info, "MemTotal"); // 取得總記憶體
            var available = ParseMemInfo(info, "MemAvailable"); // 取得可用記憶體
            return total > 0 ? (1.0 - (double)available / total) * 100 : 0; // 計算使用率
        }
        catch // 讀取失敗
        {
            return -1; // 回傳 -1
        }
    }

    private long ParseMemInfo(string[] lines, string key) => // 解析記憶體資訊
        long.TryParse(lines // 從行列中尋找
            .FirstOrDefault(l => l.StartsWith(key))? // 找到指定鍵
            .Split(':').LastOrDefault()? // 取值部分
            .Trim().Split(' ').FirstOrDefault(), out var val) // 取數字
            ? val : 0; // 回傳解析結果

    private double GetCpuUsage() => 0; // CPU 使用率(待實作)
    private double GetDiskUsage() => 0; // 磁碟使用率(待實作)
    private TimeSpan GetUptime() => TimeSpan.Zero; // 運行時間(待實作)
}

Log 集中管理

Serilog 設定

// Serilog 集中日誌設定 // 將 Pi 的日誌傳到中央 Server
// Program.cs 中的設定 // 應用程式啟動時設定

// 安裝 NuGet 套件 // Serilog 相關套件
// dotnet add package Serilog.AspNetCore // ASP.NET Core 整合
// dotnet add package Serilog.Sinks.Http // HTTP 傳送
// dotnet add package Serilog.Sinks.File // 本地檔案備份

// 設定 Serilog // 同時寫入本地和遠端
public static class SerilogConfig // Serilog 設定類別
{
    public static void Configure( // 設定方法
        string machineName, // 機器名稱
        string centralLogUrl) // 中央日誌伺服器網址
    {
        Log.Logger = new LoggerConfiguration() // 建立日誌設定
            .MinimumLevel.Information() // 最低記錄等級
            .Enrich.WithProperty("MachineName", machineName) // 加入機器名稱
            .Enrich.WithProperty("AppName", "POS") // 加入應用名稱
            .WriteTo.Console() // 輸出到主控台
            .WriteTo.File( // 寫入本地檔案
                "/var/log/pos-app/log-.txt", // 本地日誌路徑
                rollingInterval: RollingInterval.Day, // 每日輪替
                retainedFileCountLimit: 7) // 保留 7 天
            .WriteTo.Http(centralLogUrl) // 傳送到中央伺服器
            .CreateLogger(); // 建立 Logger
    }
}

SD 卡防損壞(Read-Only 檔案系統)

# 設定 Read-Only 根檔案系統 // 防止突然斷電導致 SD 卡損壞

# 安裝 overlayroot // 覆蓋式唯讀檔案系統
sudo apt install -y overlayroot        // 安裝 overlayroot

# 編輯 overlayroot 設定 // 啟用唯讀模式
sudo cat > /etc/overlayroot.local.conf << 'EOF'  // 寫入設定
overlayroot="tmpfs"                  // 使用 tmpfs 作為覆蓋層
EOF

# 掛載 tmpfs 給需要寫入的目錄 // 日誌和暫存檔案
echo "tmpfs /var/log tmpfs defaults,noatime,size=50M 0 0" | \  // 日誌目錄
  sudo tee -a /etc/fstab              // 加入開機掛載

echo "tmpfs /tmp tmpfs defaults,noatime,size=100M 0 0" | \  // 暫存目錄
  sudo tee -a /etc/fstab              // 加入開機掛載

# 需要永久寫入時暫時解除唯讀 // 更新程式時使用
sudo overlayroot-chroot               // 進入可寫入模式
# 在這裡做需要永久保存的修改 // 例如更新應用程式
exit                                   // 退出可寫入模式

備份與還原

SD 卡映像檔備份

# 備份整張 SD 卡 // 建立完整映像檔
sudo dd if=/dev/mmcblk0 \              // 來源:SD 卡裝置
  of=/backup/pos-image-$(date +%Y%m%d).img \  // 目標:映像檔(含日期)
  bs=4M \                              // 區塊大小 4MB
  status=progress                      // 顯示進度

# 壓縮映像檔 // 節省儲存空間
sudo gzip /backup/pos-image-$(date +%Y%m%d).img  // 壓縮映像檔

# 還原映像檔 // 將備份寫回 SD 卡
sudo dd if=/backup/pos-image-20240101.img.gz \  // 來源:壓縮映像檔
  of=/dev/sdX \                        // 目標:新的 SD 卡
  bs=4M \                              // 區塊大小 4MB
  status=progress                      // 顯示進度

自動備份腳本

#!/bin/bash                              // 指定直譯器
# auto_backup.sh // 每週自動備份 POS 資料

BACKUP_DIR="/backup"                   // 備份目錄
DB_FILE="/opt/pos-app/pos.db"          // SQLite 資料庫
REMOTE_SERVER="backup@cloud-server.com" // 遠端備份伺服器
LOG_FILE="/var/log/backup.log"         // 備份日誌

echo "$(date): 開始備份..." >> $LOG_FILE  // 記錄開始

# 備份 SQLite 資料庫 // 使用 .backup 指令避免鎖定
sqlite3 $DB_FILE ".backup '$BACKUP_DIR/pos-db-$(date +%Y%m%d).db'"  // 備份資料庫

# 備份設定檔 // 應用程式設定
cp /opt/pos-app/appsettings.json $BACKUP_DIR/  // 複製設定檔

# 壓縮備份 // 節省空間和傳輸時間
tar czf $BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz \  // 建立壓縮檔
  $BACKUP_DIR/pos-db-*.db \            // 包含資料庫
  $BACKUP_DIR/appsettings.json         // 包含設定檔

# 上傳到遠端 // 異地備份
scp $BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz \  // 安全複製
  $REMOTE_SERVER:/backup/              // 到遠端伺服器

# 清理 7 天前的本地備份 // 避免塞滿磁碟
find $BACKUP_DIR -name "backup-*.tar.gz" -mtime +7 -delete  // 刪除舊備份

echo "$(date): 備份完成" >> $LOG_FILE  // 記錄完成

UPS 不斷電設計

UPS 監控服務

// UPS 不斷電監控服務 // 偵測停電並安全關機
public class UpsMonitorService // UPS 監控服務類別
{
    private readonly ILogger<UpsMonitorService> _logger; // 日誌記錄器

    // UPS 狀態模型 // 記錄 UPS 當前狀態
    public class UpsStatus // UPS 狀態類別
    {
        public bool IsOnBattery { get; set; } // 是否在使用電池
        public int BatteryPercent { get; set; } // 電池電量百分比
        public int EstimatedMinutes { get; set; } // 預估剩餘時間(分鐘)
        public string PowerSource { get; set; } = "AC"; // 電源來源
    }

    // 建構函式 // 注入日誌
    public UpsMonitorService( // 建構 UPS 監控
        ILogger<UpsMonitorService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 監控 UPS 狀態 // 持續檢查電源狀態
    public async Task MonitorAsync( // 監控方法
        CancellationToken ct) // 取消令牌
    {
        while (!ct.IsCancellationRequested) // 持續監控
        {
            var status = await GetUpsStatusAsync(); // 取得 UPS 狀態

            if (status.IsOnBattery) // 如果使用電池(停電了)
            {
                _logger.LogWarning( // 記錄停電警告
                    "停電中!電池 {Percent}%,預估剩餘 {Min} 分鐘", // 格式化
                    status.BatteryPercent, status.EstimatedMinutes); // 傳入參數

                if (status.BatteryPercent < 20) // 電量低於 20%
                {
                    _logger.LogCritical("電量不足,執行安全關機!"); // 緊急記錄
                    await SafeShutdownAsync(); // 安全關機
                }
            }

            await Task.Delay(10000, ct); // 每 10 秒檢查一次
        }
    }

    // 安全關機 // 先儲存資料再關機
    private async Task SafeShutdownAsync() // 安全關機方法
    {
        _logger.LogInformation("開始安全關機流程..."); // 記錄開始

        // 1. 儲存未完成的交易 // 防止資料遺失
        // 2. 同步離線佇列 // 盡量同步到雲端
        // 3. 關閉應用程式 // 優雅停止
        // 4. 執行系統關機 // 最後一步

        await Task.Delay(1000); // 等待儲存完成

        var process = new System.Diagnostics.Process(); // 建立程序
        process.StartInfo.FileName = "sudo"; // 使用 sudo
        process.StartInfo.Arguments = "shutdown -h now"; // 立即關機
        process.Start(); // 執行關機
    }

    private Task<UpsStatus> GetUpsStatusAsync() => // 取得 UPS 狀態
        Task.FromResult(new UpsStatus()); // 回傳狀態(待實作)
}

故障排除 SOP

// 故障排除服務 // 自動診斷常見問題
public class TroubleshootService // 故障排除服務類別
{
    private readonly ILogger<TroubleshootService> _logger; // 日誌記錄器

    // 診斷結果 // 記錄檢查項目和狀態
    public class DiagnosticResult // 診斷結果類別
    {
        public string CheckName { get; set; } = ""; // 檢查項目名稱
        public bool Passed { get; set; } // 是否通過
        public string Message { get; set; } = ""; // 結果訊息
        public string? Suggestion { get; set; } // 建議修復方式
    }

    // 建構函式 // 注入日誌
    public TroubleshootService( // 建構故障排除服務
        ILogger<TroubleshootService> logger) // 注入 Logger
    {
        _logger = logger; // 儲存日誌記錄器
    }

    // 執行完整診斷 // 檢查所有常見問題
    public async Task<List<DiagnosticResult>> RunDiagnosticsAsync() // 執行診斷方法
    {
        var results = new List<DiagnosticResult>(); // 建立結果列表

        // 1. 檢查網路連線 // 確認能上網
        results.Add(await CheckNetworkAsync()); // 加入網路檢查結果

        // 2. 檢查磁碟空間 // 確認還有空間
        results.Add(CheckDiskSpace()); // 加入磁碟檢查結果

        // 3. 檢查 CPU 溫度 // 確認沒過熱
        results.Add(CheckCpuTemperature()); // 加入溫度檢查結果

        // 4. 檢查服務狀態 // 確認應用程式在跑
        results.Add(CheckServiceStatus()); // 加入服務檢查結果

        // 5. 檢查資料庫連線 // 確認 DB 正常
        results.Add(await CheckDatabaseAsync()); // 加入資料庫檢查結果

        // 記錄診斷結果 // 方便事後追蹤
        foreach (var r in results) // 逐一記錄
        {
            if (r.Passed) // 如果通過
                _logger.LogInformation("✓ {Check}: {Msg}", r.CheckName, r.Message); // 記錄成功
            else // 如果失敗
                _logger.LogWarning("✗ {Check}: {Msg}", r.CheckName, r.Message); // 記錄警告
        }

        return results; // 回傳所有結果
    }

    // 檢查網路 // ping 外部主機
    private async Task<DiagnosticResult> CheckNetworkAsync() // 網路檢查方法
    {
        try // 嘗試連線
        {
            using var client = new HttpClient(); // 建立 HTTP 客戶端
            client.Timeout = TimeSpan.FromSeconds(5); // 超時 5 秒
            await client.GetAsync("https://www.google.com"); // 嘗試連線

            return new DiagnosticResult // 回傳成功
            {
                CheckName = "網路連線", // 檢查名稱
                Passed = true, // 通過
                Message = "網路連線正常" // 成功訊息
            };
        }
        catch // 連線失敗
        {
            return new DiagnosticResult // 回傳失敗
            {
                CheckName = "網路連線", // 檢查名稱
                Passed = false, // 未通過
                Message = "無法連線到網路", // 失敗訊息
                Suggestion = "檢查網路線或 Wi-Fi 設定" // 建議
            };
        }
    }

    private DiagnosticResult CheckDiskSpace() => new() // 磁碟檢查
    {
        CheckName = "磁碟空間", Passed = true, Message = "磁碟空間充足" // 預設結果
    };

    private DiagnosticResult CheckCpuTemperature() => new() // 溫度檢查
    {
        CheckName = "CPU 溫度", Passed = true, Message = "溫度正常" // 預設結果
    };

    private DiagnosticResult CheckServiceStatus() => new() // 服務檢查
    {
        CheckName = "應用服務", Passed = true, Message = "服務運行中" // 預設結果
    };

    private Task<DiagnosticResult> CheckDatabaseAsync() => // 資料庫檢查
        Task.FromResult(new DiagnosticResult // 回傳結果
        {
            CheckName = "資料庫", Passed = true, Message = "資料庫連線正常" // 預設結果
        });
}

批量佈署(Pi Imager + cloud-init)

cloud-init 設定

# user-data // cloud-init 自動設定檔
#cloud-config                          # cloud-init 標記 // 必須放在第一行

hostname: pos-store-001                # 設定主機名稱 // 每台 Pi 不同

users:                                 # 建立用戶 // 設定管理帳號
  - name: pi                           # 用戶名稱 // 預設帳號
    groups: sudo                       # 群組 // 加入 sudo 群組
    shell: /bin/bash                   # Shell // 使用 Bash
    sudo: ALL=(ALL) NOPASSWD:ALL       # sudo 權限 // 免密碼

packages:                              # 安裝套件 // 自動安裝需要的軟體
  - chromium-browser                   # Chromium 瀏覽器 // Kiosk 用
  - unclutter                          # 隱藏游標 // Kiosk 用
  - sqlite3                            # SQLite // 本地資料庫

runcmd:                                # 執行指令 // 首次開機執行
  - curl -sSL https://dot.net/v1/dotnet-install.sh | bash  # 安裝 .NET // 執行環境
  - mkdir -p /opt/pos-app              # 建立應用目錄 // POS 安裝路徑
  - systemctl enable kiosk.service     # 啟用 Kiosk // 開機自動啟動

批量燒錄腳本

#!/bin/bash                              // 指定直譯器
# batch_flash.sh // 批量燒錄 SD 卡

IMAGE_FILE="pos-base-image.img"        // 基礎映像檔
STORE_LIST=("store-001" "store-002" "store-003")  // 門市列表

echo "批量燒錄開始"                    // 顯示開始訊息

for STORE in "${STORE_LIST[@]}"; do    // 逐一處理每間門市
    echo "準備 ${STORE} 的 SD 卡..."   // 顯示進度
    echo "請插入 SD 卡後按 Enter"      // 提示插入 SD 卡
    read                                 // 等待使用者按 Enter

    # 偵測新插入的 SD 卡 // 通常是 /dev/sdX
    DEVICE=$(lsblk -dpno NAME | tail -1)  // 取得最後插入的裝置

    # 燒錄映像檔 // 寫入 SD 卡
    echo "燒錄到 ${DEVICE}..."         // 顯示燒錄目標
    sudo dd if=$IMAGE_FILE of=$DEVICE bs=4M status=progress  // 執行燒錄

    # 修改 hostname // 讓每台 Pi 有不同的名稱
    sudo mount ${DEVICE}2 /mnt          // 掛載 SD 卡分區
    echo "$STORE" | sudo tee /mnt/etc/hostname  // 寫入主機名稱
    sudo umount /mnt                    // 卸載分區

    echo "${STORE} 燒錄完成!"         // 顯示完成
done

echo "全部燒錄完成!"                  // 顯示全部完成

🤔 我這樣寫為什麼會錯?

❌ 錯誤:SD 卡直接斷電不關機

# 錯誤做法 // 直接拔電源
# 直接拔掉 Pi 的電源線 ← SD 卡可能損壞! // 正在寫入的資料會毀損
# 尤其是正在寫入資料庫的時候 // 資料庫可能整個壞掉

✅ 正確:使用 UPS + 安全關機

# 正確做法 // 透過 UPS 偵測停電後安全關機
# 1. 安裝 UPS // 不斷電系統
# 2. 偵測到停電 → 儲存資料 // 先存檔
# 3. 執行 sudo shutdown -h now // 安全關機
sudo shutdown -h now                   // 正確的關機指令

❌ 錯誤:所有 Pi 用同一個 SSH 金鑰

# 錯誤做法 // 一把鑰匙開所有門
# 把同一個 SSH 私鑰複製到所有 Pi ← 一台被駭全部中招! // 嚴重安全漏洞
ssh-copy-id -i ~/.ssh/shared_key pi@all-machines  // 共用金鑰很危險

✅ 正確:每台 Pi 獨立金鑰 + 集中管理

# 正確做法 // 每台 Pi 有自己的金鑰
for host in store-a store-b store-c; do  // 逐台處理
    ssh-keygen -t ed25519 \              // 產生 ED25519 金鑰
      -f ~/.ssh/key_${host} \            // 每台一個獨立金鑰
      -N ""                            // 無密碼(搭配 ssh-agent)
    ssh-copy-id -i ~/.ssh/key_${host}.pub pi@${host}  // 部署公鑰到 Pi
done

❌ 錯誤:日誌只存在本地 SD 卡

// 錯誤寫法 // 日誌只寫到本地 SD 卡
public class BadLogging // 錯誤的日誌設定
{
    public void Configure() // 設定方法
    {
        // 只寫到本地檔案 ← SD 卡壞了就看不到了! // 也會加速 SD 卡損壞
        Log.Logger = new LoggerConfiguration() // 建立設定
            .WriteTo.File("/var/log/app.log") // 只寫本地
            .CreateLogger(); // 建立 Logger
    }
}

✅ 正確:日誌同時寫到本地和遠端

// 正確寫法 // 本地 + 遠端雙重保險
public class GoodLogging // 正確的日誌設定
{
    public void Configure() // 設定方法
    {
        Log.Logger = new LoggerConfiguration() // 建立設定
            .WriteTo.File("/var/log/app.log", // 本地備份
                rollingInterval: RollingInterval.Day, // 每日輪替
                retainedFileCountLimit: 3) // 只保留 3 天(省 SD 卡空間)
            .WriteTo.Http("https://log-server.com/api/logs") // 傳到中央伺服器
            .CreateLogger(); // 建立 Logger
    }
}

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊