---
# System prepended metadata

title: Dobot Magician 手臂與滑軌的顏色辨識智慧倉儲應用

---

# Dobot Magician 手臂與滑軌的顏色辨識智慧倉儲應用

在現代智慧製造與倉儲系統中，自動化技術是提升效率與精準度的關鍵。本次研習的目標是運用 **Dobot Magician** 手臂、滑軌及影像辨識技術，打造一個 **智慧倉儲系統**，能根據物品的顏色自動完成分類與搬運工作。為了循序漸進地實現最終應用，我們設計了六個階段性任務，逐步累積技能並完成整體功能的整合：

- **階段 1：** 建立基本的串列通訊功能，讓手臂與網頁進行連線，並實現回Home操作。
- **階段 2：** 添加滑軌控制功能，啟動滑軌並能準確移動至指定位置。
- **階段 3：** 設計手臂的多段動作控制，讓手臂完成一系列如抓取與放置的分段動作。
- **階段 4：** 利用攝像頭與 Canvas 實現顏色辨識，讀取畫面中心的像素值並分類顏色。
- **階段 5：** 結合顏色辨識與手臂操作，讓手臂根據顏色自動完成分類動作。
- **階段 6：** 整合滑軌與手臂動作，實現完整的智慧倉儲應用，從顏色辨識到物品分類全程自動化。

透過這樣的階段性學習設計，參與者將能逐步掌握 Dobot 手臂的控制技巧、影像辨識技術，以及智慧倉儲系統的整體運作邏輯，最終完成一個實用性與展示效果兼備的應用範例。


## 階段一：建立基本連線功能與回 Home 操作

在智慧倉儲應用中，與硬體設備建立穩定的連線是實現自動化操作的基礎。因此，**階段一的目標**是讓網頁能夠透過 WebSerial API 與 Dobot Magician 手臂進行串列通訊，並實現手臂的回 Home 功能，確保手臂可以從任何狀態返回到初始位置。這不僅是後續功能實現的必要步驟，也為參與者理解串列通訊的原理奠定基礎。

### **功能實現步驟**
1. **建立基本串列通訊**
    - 在網頁上新增「連線到 Dobot」的按鈕，使用 WebSerial API 建立與 Dobot 的串列通訊。
    - 確保網頁可以找到可用的串列端口並成功打開。

2. **發送固定格式的回 Home 命令**
    - 定義回 Home 的 UART 資料格式，例如 `[170, 170, 6, 31, 3, 0, 0, 0, 0, 222]`。
    - 在網頁中新增「發送 Home 命令」按鈕，點擊後向 Dobot 傳送此命令。

3. **測試與調整**
    - 測試按鈕功能是否正常運作，觀察控制台是否顯示「串行端口已連接」或「命令已發送」的訊息。
    - 確認 Dobot 手臂是否成功返回初始位置。

### **完整 HTML 範例程式碼**
以下是實現階段一功能的完整 HTML 程式碼，讀者可以直接複製並上傳至 Neocities 使用：

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>階段一：建立基本連線功能與回Home操作</title>
</head>
<body>
    <h1>Dobot Magician 串列通訊示範</h1>
    <p>本頁面演示如何與 Dobot 手臂建立連線並實現回 Home 功能。</p>

    <!-- 功能按鈕 -->
    <button id="connect">連線到 Dobot</button>
    <button id="sendCommand" disabled>發送 Home 命令</button>

    <script>
        let port, writer;

        // 連線到 Dobot
        async function connectSerial() {
            if ('serial' in navigator) {
                try {
                    // 請求使用者選擇串列端口
                    port = await navigator.serial.requestPort();
                    // 打開串列端口，波特率設置為 115200
                    await port.open({ baudRate: 115200 });
                    // 獲取寫入器
                    writer = port.writable.getWriter();
                    console.log('串行端口已連接');
                    // 禁用連線按鈕並啟用發送按鈕
                    document.getElementById('connect').disabled = true;
                    document.getElementById('sendCommand').disabled = false;
                } catch (e) {
                    console.error('無法打開串行端口', e);
                }
            } else {
                alert('您的瀏覽器不支持 WebSerial API');
            }
        }

        // 發送回 Home 命令
        async function sendHomeCommand() {
            if (writer) {
                // 回 Home 的 UART 命令數據
                const data = new Uint8Array([170, 170, 6, 31, 3, 0, 0, 0, 0, 222]);
                await writer.write(data);
                console.log('回 Home 命令已發送');
            }
        }

        // 綁定按鈕事件
        document.getElementById('connect').addEventListener('click', connectSerial);
        document.getElementById('sendCommand').addEventListener('click', sendHomeCommand);
    </script>
</body>
</html>
```
## 階段二：添加滑軌控制功能

滑軌作為 Dobot Magician 的擴展設備，可以讓機器手臂實現更大範圍的移動與操作。在 **階段二**，我們將實現滑軌的啟動與基本控制功能，讓滑軌能根據指定命令移動到預定位置，並與手臂動作協同工作。

### **功能實現步驟**
1. **啟動滑軌功能**
    - 使用 WebSerial API 發送滑軌啟動命令，確保滑軌進入工作狀態。

2. **設計滑軌移動命令**
    - 定義滑軌移動的 UART 命令格式，例如滑軌移動至特定位置。

3. **測試滑軌控制**
    - 在網頁上添加滑軌控制按鈕，點擊後讓滑軌移動到指定位置。

### **完整 HTML 範例程式碼**
以下是實現階段二功能的完整 HTML 程式碼，讀者可以直接複製並上傳至 Neocities 使用：

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>階段二：滑軌控制功能</title>
</head>
<body>
    <h1>Dobot Magician 滑軌控制示範</h1>
    <p>本頁面演示如何啟動滑軌並讓其移動至指定位置。</p>

    <!-- 功能按鈕 -->
    <button id="connect">連線到 Dobot</button>
    <button id="sliderOn" disabled>啟動滑軌</button>
    <button id="moveSlider" disabled>滑軌移動到指定位置</button>

    <script>
        let port, writer;

        // 連線到 Dobot
        async function connectSerial() {
            if ('serial' in navigator) {
                try {
                    port = await navigator.serial.requestPort();
                    await port.open({ baudRate: 115200 });
                    writer = port.writable.getWriter();
                    console.log('串行端口已連接');
                    document.getElementById('connect').disabled = true;
                    document.getElementById('sliderOn').disabled = false;
                    document.getElementById('moveSlider').disabled = false;
                } catch (e) {
                    console.error('無法打開串行端口', e);
                }
            } else {
                alert('您的瀏覽器不支持 WebSerial API');
            }
        }

        // 啟動滑軌
        async function sliderOn() {
            if (writer) {
                // 滑軌啟動命令數據
                const data = new Uint8Array([170, 170, 3, 3, 1, 1, 248]);
                await writer.write(data);
                console.log('滑軌已啟動');
            }
        }

        // 滑軌移動到指定位置
        async function moveSliderToPosition(position) {
            if (writer) {
                // 定義滑軌目標位置的命令（position 為目標位置，單位 mm）
                const length = 8;
                const ID = 10;
                const Control = 3;
                const payload = [ID, Control, position & 0xFF, (position >> 8) & 0xFF, 0, 0];
                const Checksum = 256 - payload.reduce((a, b) => a + b, 0) & 0xFF;
                const command = [170, 170, length, ...payload, Checksum];
                const data = new Uint8Array(command);
                await writer.write(data);
                console.log(`滑軌移動到位置 ${position}`);
            }
        }

        // 綁定按鈕事件
        document.getElementById('connect').addEventListener('click', connectSerial);
        document.getElementById('sliderOn').addEventListener('click', sliderOn);
        document.getElementById('moveSlider').addEventListener('click', () => moveSliderToPosition(200));
    </script>
</body>
</html>
```

## 階段三：手臂多段動作控制

在前兩個階段中，我們已經成功建立與 Dobot 的連線、實現回 Home 動作以及控制滑軌位置。接下來的 **階段三**，目標是透過多次分段指令，讓手臂執行一系列動作，形成一個完整的流程。例如：  
1. 移動到某個預設位置上方  
2. 降低 Z 軸以模擬「抓取」動作  
3. 再將手臂抬起、移動到另一個位置並釋放物件

透過這些多段指令，參與者可以更靈活地規劃手臂的工作流程，為後續結合影像辨識與自動分類動作做準備。

### **功能實現步驟**
1. **設計多段座標控制指令**
    - 使用 `toPositionWithSlider(x, y, z, r, l)` 函數傳送多組指令到 Dobot，使手臂按照指定順序移動。
    - 每個座標點可以代表一個流程步驟（如「移動到目標上方」→「向下移動」→「返回起始位置」）。

2. **建立多段動作的按鈕操作**
    - 在網頁上新增一組按鈕，一次性執行多段動作流程。
    - 透過函數呼叫順序實現連續動作的執行。

3. **測試與校正**
    - 實際觀察手臂動作是否與預期一致。
    - 若有偏差，調整座標或增加中間步驟確保動作平順、安全。

### **完整 HTML 範例程式碼**
以下為實現階段三功能的完整 HTML 程式碼範例，讀者可直接複製上傳到 Neocities 使用。在此範例中，我們會在連線成功後使用 `toPositionWithSlider` 函數定義多段動作流程，並透過按鈕觸發。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>階段三：手臂多段動作控制</title>
</head>
<body>
    <h1>Dobot Magician 多段動作示範</h1>
    <p>本頁面示範如何透過多段指令來控制 Dobot 手臂，執行類似抓取與放置的動作流程。</p>

    <!-- 功能按鈕區 -->
    <button id="connect">連線到 Dobot</button>
    <button id="sendHome" disabled>回 Home</button>
    <button id="runSequence" disabled>執行多段動作</button>

    <script>
        let port, writer;

        // 連線到 Dobot
        async function connectSerial() {
            if ('serial' in navigator) {
                try {
                    port = await navigator.serial.requestPort();
                    await port.open({ baudRate: 115200 });
                    writer = port.writable.getWriter();
                    console.log('串行端口已連接');
                    document.getElementById('connect').disabled = true;
                    document.getElementById('sendHome').disabled = false;
                    document.getElementById('runSequence').disabled = false;
                } catch (e) {
                    console.error('無法打開串行端口', e);
                }
            } else {
                alert('您的瀏覽器不支持 WebSerial API');
            }
        }

        // 發送回 Home 命令
        async function sendHomeCommand() {
            if (writer) {
                const data = new Uint8Array([170, 170, 6, 31, 3, 0, 0, 0, 0, 222]);
                await writer.write(data);
                console.log('回 Home 命令已發送');
            }
        }

        // 將浮點數轉為小端字節序陣列的函數
        function floatToBytes(float) {
            let buffer = new ArrayBuffer(4);
            let view = new DataView(buffer);
            view.setFloat32(0, float, false); // 大端寫入
            return Array.from(new Uint8Array(buffer)).reverse(); 
        }

        // 傳送手臂移動指令的函數 (包含滑軌參數)
        async function toPositionWithSlider(x, y, z, r, l) {
            if (!writer) return;

            // 命令頭
            let uartCommand = [170, 170];
            const length = 23;
            const ID = 86;
            const Ctrl = 3;
            const ptpmode = 2;

            const Dx = floatToBytes(x);
            const Dy = floatToBytes(y);
            const Dz = floatToBytes(z);
            const Dr = floatToBytes(r);
            const Dl = floatToBytes(l);

            const payload = [ID, Ctrl, ptpmode].concat(Dx, Dy, Dz, Dr, Dl);
            let Checksum = (256 - payload.reduce((a, b) => a + b, 0)) & 0xFF;

            uartCommand.push(length, ...payload, Checksum);

            const data = new Uint8Array(uartCommand);
            await writer.write(data);
            console.log(`移動到 (X:${x}, Y:${y}, Z:${z}, R:${r}, L:${l})`);
        }

        // 示範多段動作: 移動→下降→上升→回到初始位置
        async function runMultiStepSequence() {
            // 假設我們的起始位置為 (180, 0, 0, 0, 0)
            // 1. 移動到指定位置上方
            await toPositionWithSlider(180, 0, 0, 0, 0);
            
            // 2. 向下移動，以模擬抓取（Z 軸下降到 -130）
            await toPositionWithSlider(180, 0, -130, 0, 0);

            // 3. 返回上方位置（Z 軸回到 0）
            await toPositionWithSlider(180, 0, 0, 0, 0);

            // 4. 移動到另一個位置（如 x=200）模擬放置物件
            await toPositionWithSlider(200, 0, 0, 0, 0);

            // 5. 再回 Home 或回到初始位置
            sendHomeCommand();
        }

        // 綁定事件
        document.getElementById('connect').addEventListener('click', connectSerial);
        document.getElementById('sendHome').addEventListener('click', sendHomeCommand);
        document.getElementById('runSequence').addEventListener('click', runMultiStepSequence);
    </script>
</body>
</html>
```

## 階段四：影像擷取與顏色辨識

在前面三個階段中，我們已經能夠：
- 與 Dobot 連線並實現回 Home 的基本操作（階段一）
- 控制滑軌啟動與移動（階段二）
- 執行手臂的多段動作指令（階段三）

**階段四** 的目標是加入影像擷取與顏色辨識功能。我們將透過相機（WebCam）取得影像，並利用 Canvas 擷取畫面中心的像素RGB值，進而判定該像素的顏色。未來，我們可以根據辨識到的顏色來決定手臂與滑軌的動作，達成智慧倉儲的自動分類。

### **功能實現步驟**
1. **相機畫面串接**
    - 使用 `navigator.mediaDevices.getUserMedia({ video: true })` 取得相機影像，並顯示於 `<video>` 元素中。
    
2. **Canvas 擷取像素**
    - 利用定時器（`setInterval`）每隔一段時間將 `<video>` 畫面繪製至 `<canvas>` 中。
    - 在 Canvas 中取得中心點的像素值 (RGB)。
    
3. **顏色分類與顯示**
    - 實作 `getColor(rgb)` 函數，根據 RGB 值分類顏色。
    - 將結果顯示於網頁中，方便使用者觀察顏色辨識結果。

### **完整 HTML 範例程式碼**
以下程式碼在前幾階段的基礎上加入相機影像顯示與顏色辨識功能。請直接將下列程式碼複製並上傳至 Neocities 使用。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>階段四：影像擷取與顏色辨識</title>
</head>
<body>
    <h1>Dobot Magician 顏色辨識示範</h1>
    <p>本範例在前面階段功能（連線、回Home、滑軌啟動、多段動作控制）基礎上，加入相機影像擷取與畫面中心像素的顏色辨識功能。</p>

    <!-- Dobot 功能按鈕 (延續前面階段) -->
    <button id="connect">連線到 Dobot</button>
    <button id="sendCommand" disabled>發送 Home 命令</button>
    <button id="sliderOnBtn" disabled>啟動滑軌</button>
    <button id="runSequence" disabled>執行多段動作</button>
    <hr>

    <!-- 相機與Canvas顯示區 -->
    <video id="video" width="640" height="480" autoplay hidden></video>
    <canvas id="canvas" width="640" height="480"></canvas>
    <div>中心像素顏色：<span id="color"></span></div>

    <script>
        let port, writer;

        // ---------------------
        // 階段一功能: 連線與回Home
        // ---------------------
        async function connectSerial() {
            if ('serial' in navigator) {
                try {
                    port = await navigator.serial.requestPort();
                    await port.open({ baudRate: 115200 });
                    writer = port.writable.getWriter();
                    console.log('串行端口已連接');
                    document.getElementById('connect').disabled = true;
                    document.getElementById('sendCommand').disabled = false;
                    document.getElementById('sliderOnBtn').disabled = false;
                    document.getElementById('runSequence').disabled = false;
                } catch (e) {
                    console.error('無法打開串行端口', e);
                }
            } else {
                alert('您的瀏覽器不支援 WebSerial API');
            }
        }

        async function sendHomeCommand() {
            if (writer) {
                // 回 Home 的固定命令
                const data = new Uint8Array([170, 170, 6, 31, 3, 0, 0, 0, 0, 222]);
                await writer.write(data);
                console.log('回 Home 命令已發送');
            }
        }

        // ---------------------
        // 階段二功能: 滑軌控制(示意)
        // ---------------------
        async function sliderOn() {
            if (writer) {
                // 啟動滑軌命令
                const data = new Uint8Array([170, 170, 3, 3, 1, 1, 248]);
                await writer.write(data);
                console.log('滑軌已啟動');
            }
        }

        // ---------------------
        // 階段三功能: 多段動作控制(示意)
        // ---------------------
        // 將浮點數轉為小端序列byte
        function floatToBytes(float) {
            let buffer = new ArrayBuffer(4);
            let view = new DataView(buffer);
            view.setFloat32(0, float, false); 
            return Array.from(new Uint8Array(buffer)).reverse();
        }

        async function toPositionWithSlider(x, y, z, r, l) {
            if (!writer) return;
            let uartCommand = [170, 170];
            const length = 23;
            const ID = 86;
            const Ctrl = 3;
            const ptpmode = 2;

            const Dx = floatToBytes(x);
            const Dy = floatToBytes(y);
            const Dz = floatToBytes(z);
            const Dr = floatToBytes(r);
            const Dl = floatToBytes(l);

            const payload = [ID, Ctrl, ptpmode, ...Dx, ...Dy, ...Dz, ...Dr, ...Dl];
            let Checksum = (256 - payload.reduce((a, b) => a + b, 0)) & 0xFF;

            uartCommand.push(length, ...payload, Checksum);

            const data = new Uint8Array(uartCommand);
            await writer.write(data);
            console.log(`移動到 (X:${x}, Y:${y}, Z:${z}, R:${r}, L:${l})`);
        }

        async function runMultiStepSequence() {
            // 範例多段動作流程 (可自行修改)
            await toPositionWithSlider(180, 0, 0, 0, 0);
            await toPositionWithSlider(180, 0, -130, 0, 0);
            await toPositionWithSlider(180, 0, 0, 0, 0);
            await toPositionWithSlider(200, 0, 0, 0, 0);
            sendHomeCommand();
        }

        // ---------------------
        // 階段四功能: 影像擷取與顏色辨識
        // ---------------------
        const video = document.getElementById('video');
        const canvas = document.getElementById('canvas');
        const context = canvas.getContext('2d');
        const colorDisplay = document.getElementById('color');

        // 啟用相機
        if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
            navigator.mediaDevices.getUserMedia({ video: true }).then(function(stream) {
                video.srcObject = stream;
                video.play();
            });
        }

        function getPixelRGB(x, y) {
            var pixel = context.getImageData(x, y, 1, 1).data;
            return {
                r: pixel[0],
                g: pixel[1],
                b: pixel[2]
            };
        }

        function getColor(rgb) {
            // 簡單的色碼分類邏輯，可根據需求調整
            if (rgb.r >= 128 && rgb.g >= 128 && rgb.b >= 128) {
                return 'W'; // White
            } else if (rgb.r >= 128 && rgb.g < 128 && rgb.b < 128) {
                return 'R'; // Red
            } else if (rgb.r < 128 && rgb.g >= 128 && rgb.b < 128) {
                return 'G'; // Green
            } else if (rgb.r < 128 && rgb.g < 128 && rgb.b >= 128) {
                return 'B'; // Blue
            } else if (rgb.r >= 128 && rgb.g >= 128 && rgb.b < 128) {
                return 'Y'; // Yellow
            } else if (rgb.r >= 128 && rgb.g < 128 && rgb.b >= 128) {
                return 'M'; // Magenta
            } else if (rgb.r < 128 && rgb.g >= 128 && rgb.b >= 128) {
                return 'C'; // Cyan
            } else {
                return 'K'; // Black
            }
        }

        function drawTargetCircle() {
            var centerX = canvas.width / 2;
            var centerY = canvas.height / 2;
            context.beginPath();
            context.arc(centerX, centerY, 10, 0, 2 * Math.PI, false);
            context.fillStyle = 'transparent';
            context.fill();
            context.lineWidth = 2;
            context.strokeStyle = 'red';
            context.stroke();
        }

        setInterval(function(){
            // 將video影像畫到canvas
            context.drawImage(video, 0, 0, 640, 480);
            drawTargetCircle();
            var centerX = canvas.width / 2;
            var centerY = canvas.height / 2;
            var rgb = getPixelRGB(centerX, centerY);
            var detectedColor = getColor(rgb);
            colorDisplay.innerText = detectedColor;
        }, 100); // 每100毫秒更新一次畫面與顏色辨識

        // 綁定事件
        document.getElementById('connect').addEventListener('click', connectSerial);
        document.getElementById('sendCommand').addEventListener('click', sendHomeCommand);
        document.getElementById('sliderOnBtn').addEventListener('click', sliderOn);
        document.getElementById('runSequence').addEventListener('click', runMultiStepSequence);
    </script>
</body>
</html>
```

## 階段五：結合顏色辨識與手臂動作實現自動分類

經過前四個階段的努力，我們已經能夠：
- 與 Dobot 連線並實現回 Home（階段一）
- 控制滑軌啟動與移動（階段二）
- 執行多段手臂動作（階段三）
- 從相機取得影像並辨識中心像素的顏色（階段四）

**階段五** 的目標是將「顏色辨識」結果與「手臂多段動作」結合起來。當影像中心點的顏色被辨識出後，程式將根據不同顏色執行對應的分類動作，使手臂與滑軌能自動將物品移動至對應的分類位置。這是邁向智慧倉儲系統的關鍵一步。

### **功能實現步驟**
1. **顏色對應動作規劃**
    - 為每種顏色指定一個分類位置或流程。例如：  
      - R (紅)：移動到分類區1  
      - Y (黃)：移動到分類區2  
      - G (綠)：移動到分類區3  
      - C (青)：移動到分類區4  
      依此類推，可自行定義。

2. **自動分類按鈕**
    - 在網頁上新增「自動分類」按鈕。
    - 點擊此按鈕後，系統會讀取目前的顏色辨識結果，並自動執行對應的手臂與滑軌動作流程。

3. **整合測試**
    - 放置不同顏色的物體於相機中心。
    - 點擊「自動分類」按鈕觀察手臂與滑軌動作是否如預期執行。

### **完整 HTML 範例程式碼**
以下為實現階段五功能的完整 HTML 程式碼範例，讀者可直接複製上傳至 Neocities 使用。在此範例中，我們擴充前四階段的程式碼，加入「自動分類」的邏輯。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>階段五：顏色辨識與自動分類</title>
</head>
<body>
    <h1>Dobot Magician 智慧倉儲自動分類示範</h1>
    <p>本範例結合了前面各階段的功能，當辨識出中心顏色後，點擊「自動分類」按鈕即可執行對應的手臂與滑軌動作完成分類。</p>

    <!-- Dobot 功能按鈕 -->
    <button id="connect">連線到 Dobot</button>
    <button id="sendCommand" disabled>發送 Home 命令</button>
    <button id="sliderOnBtn" disabled>啟動滑軌</button>
    <button id="runSequence" disabled>執行多段動作</button>
    <button id="autoClassify" disabled>自動分類</button>
    <hr>

    <!-- 相機與 Canvas 顯示區 -->
    <video id="video" width="640" height="480" autoplay hidden></video>
    <canvas id="canvas" width="640" height="480"></canvas>
    <div>中心像素顏色：<span id="color"></span></div>

    <script>
        let port, writer;

        // ---------------------
        // 階段一: 連線與回Home
        // ---------------------
        async function connectSerial() {
            if ('serial' in navigator) {
                try {
                    port = await navigator.serial.requestPort();
                    await port.open({ baudRate: 115200 });
                    writer = port.writable.getWriter();
                    console.log('串行端口已連接');
                    document.getElementById('connect').disabled = true;
                    document.getElementById('sendCommand').disabled = false;
                    document.getElementById('sliderOnBtn').disabled = false;
                    document.getElementById('runSequence').disabled = false;
                    document.getElementById('autoClassify').disabled = false;
                } catch (e) {
                    console.error('無法打開串行端口', e);
                }
            } else {
                alert('您的瀏覽器不支援 WebSerial API');
            }
        }

        async function sendHomeCommand() {
            if (writer) {
                const data = new Uint8Array([170, 170, 6, 31, 3, 0, 0, 0, 0, 222]);
                await writer.write(data);
                console.log('回 Home 命令已發送');
            }
        }

        // ---------------------
        // 階段二: 滑軌控制(示意)
        // ---------------------
        async function sliderOn() {
            if (writer) {
                const data = new Uint8Array([170, 170, 3, 3, 1, 1, 248]);
                await writer.write(data);
                console.log('滑軌已啟動');
            }
        }

        // ---------------------
        // 階段三: 多段動作控制(示意)
        // ---------------------
        function floatToBytes(float) {
            let buffer = new ArrayBuffer(4);
            let view = new DataView(buffer);
            view.setFloat32(0, float, false);
            return Array.from(new Uint8Array(buffer)).reverse();
        }

        async function toPositionWithSlider(x, y, z, r, l) {
            if (!writer) return;
            let uartCommand = [170, 170];
            const length = 23;
            const ID = 86;
            const Ctrl = 3;
            const ptpmode = 2;

            const Dx = floatToBytes(x);
            const Dy = floatToBytes(y);
            const Dz = floatToBytes(z);
            const Dr = floatToBytes(r);
            const Dl = floatToBytes(l);

            const payload = [ID, Ctrl, ptpmode, ...Dx, ...Dy, ...Dz, ...Dr, ...Dl];
            let Checksum = (256 - payload.reduce((a, b) => a + b, 0)) & 0xFF;

            uartCommand.push(length, ...payload, Checksum);

            const data = new Uint8Array(uartCommand);
            await writer.write(data);
            console.log(`移動到 (X:${x}, Y:${y}, Z:${z}, R:${r}, L:${l})`);
        }

        async function runMultiStepSequence() {
            // 範例多段動作
            await toPositionWithSlider(180, 0, 0, 0, 0);
            await toPositionWithSlider(180, 0, -130, 0, 0);
            await toPositionWithSlider(180, 0, 0, 0, 0);
            await toPositionWithSlider(200, 0, 0, 0, 0);
            sendHomeCommand();
        }

        // ---------------------
        // 階段四: 影像擷取與顏色辨識
        // ---------------------
        const video = document.getElementById('video');
        const canvas = document.getElementById('canvas');
        const context = canvas.getContext('2d');
        const colorDisplay = document.getElementById('color');

        if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
            navigator.mediaDevices.getUserMedia({ video: true }).then(function(stream) {
                video.srcObject = stream;
                video.play();
            });
        }

        function getPixelRGB(x, y) {
            var pixel = context.getImageData(x, y, 1, 1).data;
            return { r: pixel[0], g: pixel[1], b: pixel[2] };
        }

        function getColor(rgb) {
            if (rgb.r >= 128 && rgb.g >= 128 && rgb.b >= 128) {
                return 'W'; 
            } else if (rgb.r >= 128 && rgb.g < 128 && rgb.b < 128) {
                return 'R'; 
            } else if (rgb.r < 128 && rgb.g >= 128 && rgb.b < 128) {
                return 'G'; 
            } else if (rgb.r < 128 && rgb.g < 128 && rgb.b >= 128) {
                return 'B'; 
            } else if (rgb.r >= 128 && rgb.g >= 128 && rgb.b < 128) {
                return 'Y'; 
            } else if (rgb.r >= 128 && rgb.g < 128 && rgb.b >= 128) {
                return 'M'; 
            } else if (rgb.r < 128 && rgb.g >= 128 && rgb.b >= 128) {
                return 'C'; 
            } else {
                return 'K'; 
            }
        }

        function drawTargetCircle() {
            var centerX = canvas.width / 2;
            var centerY = canvas.height / 2;
            context.beginPath();
            context.arc(centerX, centerY, 10, 0, 2 * Math.PI, false);
            context.fillStyle = 'transparent';
            context.fill();
            context.lineWidth = 2;
            context.strokeStyle = 'red';
            context.stroke();
        }

        setInterval(function(){
            context.drawImage(video, 0, 0, 640, 480);
            drawTargetCircle();
            var centerX = canvas.width / 2;
            var centerY = canvas.height / 2;
            var rgb = getPixelRGB(centerX, centerY);
            var detectedColor = getColor(rgb);
            colorDisplay.innerText = detectedColor;
        }, 100);

        // ---------------------
        // 階段五: 自動分類邏輯
        // ---------------------
        async function autoClassify() {
            const currentColor = colorDisplay.innerText;

            // 設定不同顏色對應的分類區位置 (此處為範例座標)
            // 假設以L參數為滑軌位置來做分區，如 0、85、170、255等分佈
            let distence; 
            if (currentColor === 'R') {
                distence = 40 + 85 * 1;
            } else if (currentColor === 'Y') {
                distence = 40 + 85 * 2;
            } else if (currentColor === 'G' || currentColor === 'K') {
                distence = 40 + 85 * 3;
            } else if (currentColor === 'C') {
                distence = 40 + 85 * 4;
            } else {
                distence = 40 + 85 * 5; // 若有白色或其他顏色分配至第5區
            }

            // 將物品移動到對應分類位置的動作流程 (範例流程)
            await toPositionWithSlider(180, 0,   0, 0, 0);
            await toPositionWithSlider(180, 0,   0, 0, distence);
            await toPositionWithSlider(180, 0, -130, 0, distence);
            await toPositionWithSlider(160, 0, -130, 0, distence);
            await toPositionWithSlider(160, 0,   0, 0, distence);
            await toPositionWithSlider(180, 0,   0, 0, distence);
            await toPositionWithSlider(180, 0,   0, 0, 0);
        }

        // 綁定事件
        document.getElementById('connect').addEventListener('click', connectSerial);
        document.getElementById('sendCommand').addEventListener('click', sendHomeCommand);
        document.getElementById('sliderOnBtn').addEventListener('click', sliderOn);
        document.getElementById('runSequence').addEventListener('click', runMultiStepSequence);
        document.getElementById('autoClassify').addEventListener('click', autoClassify);
    </script>
</body>
</html>
```

## 階段六：整合優化與實務應用

在第五階段，我們已成功實現透過顏色辨識自動分類的智慧倉儲雛型系統。**階段六** 將重點放在整合優化與應用層面的打磨，使整個系統在實際操作中更穩定、高效和易用。透過這些微調與加強，我們將從原型系統進一步昇華到可進行教學示範或簡易量產應用的成品。

### **功能實現步驟**
1. **動作與流程優化**
    - 檢查手臂與滑軌的運動路徑，調整座標與速度，讓動作過程更加流暢、平順。
    - 加入適當的延遲、夾取控制或安全檢查邏輯，確保在分揀過程中不會產生撞擊或遺漏。

2. **異常處理與錯誤恢復機制**
    - 若辨識不到顏色，預設將物件放到備用儲位或回傳錯誤訊息，避免系統停滯不前。
    - 若手臂或滑軌運動出現異常（如通訊中斷、物體卡住），可透過預設的回Home指令或安全停止流程快速恢復。

3. **使用者體驗與介面改良**
    - 美化網頁介面，加入狀態文字或圖示，讓使用者能清楚了解當下系統正在辨識顏色、移動手臂或滑軌。
    - 添加提示訊息（如「分類完成」、「顏色不明，使用預設儲位」等），提升使用者互動友善度。

4. **擴充與客製化**
    - 調整顏色辨識演算法，提升精準度，或根據不同物品類型增添其他分類依據（如形狀、尺寸）。
    - 擴增儲位數量，透過重新定義座標與滑軌位置，讓系統可處理更多種顏色分類或不同規格的物品。

### **完整 HTML 程式碼示意**
在此階段的程式碼不會與前階段有大規模結構變動，而是著重在細節優化、錯誤處理與介面改善。例如：
- 在自動分類邏輯中加入例外處理。
- 在影像辨識流程中添加狀態表示。
- 增加顏色判斷失敗的備援措施。

下面為示意程式碼片段（不再提供完整HTML，請讀者延用前五階段的程式碼基礎）：

```javascript
// 異常處理範例：若未偵測到顏色，使用預設儲位
async function autoClassify() {
    const currentColor = colorDisplay.innerText;
    let distence;

    switch (currentColor) {
        case 'R':
            distence = 40 + 85 * 1;
            break;
        case 'Y':
            distence = 40 + 85 * 2;
            break;
        case 'G':
        case 'K': // 將黑色(K)也分配到同一個儲位以示範處理多種條件
            distence = 40 + 85 * 3;
            break;
        case 'C':
            distence = 40 + 85 * 4;
            break;
        default:
            // 若顏色不在預期範圍內，分到預設儲位 5
            distence = 40 + 85 * 5;
            console.log('顏色未知，使用預設儲位');
    }

    // 加入更多動作前後的延遲或檢查，確保過程安全可靠
    await toPositionWithSlider(180, 0,   0, 0, 0);
    // ... 其餘動作流程與前述類似
}
```