創想三維(Creality)K1 SE 3D列印機(四):自製即時監控網頁

K1SE 3D列印機的觸控顯示幕裝在本體外框右下角,仰角不能調整。如果3D列印機放在桌上,而你坐著操作它,螢幕的仰角剛剛好。但若是放在稍矮的地方,或者站著操作,觸控螢幕應該放在本體頂部。

K1SE的觸控螢幕採用40針B型排線(並列傳輸)連接控制板,Reddit論壇 “orlee008” 網友分別用400mm和500mm長的排線,延伸觸控螢幕。測試結果,用超過400mm長的排線連接,顯示器將無法啟動。

用延長排線顯示器

此外,40針排線的體積比較大,無法隱藏在本體外框內側,必須暴露在外,有礙觀瞻。

有人則是設計3D列印零件,把觸控螢幕的仰角調高20°,方便在低矮的桌面操作;這個是調高40°的版本,也有可調角度的設計

抬高40度角

另一個辦法是,準備第二個觸控螢幕或其他可監控3D列印機的設備,例如,舊手機、平板或其他具備觸控螢幕和網頁瀏覽器的裝置。

透過網頁瀏覽器監控3D列印機

現今3D列印機都具備聯網功能,像K1SE就是透過Wi-Fi連線操作,所以它本身就是一台物聯網實驗設備。這類3D列印機在開機時會啟動內建的HTTP伺服器,而列印軟體以及手機App,使用WebSocket協定與3D列印機連線、即時互動。

關於WebSocket,請參閱《超圖解ESP32深度實作》第9章「使用WebSocket即時連線監控聯網裝置」;《超圖解ESP32應用實作》第4章則用WebSocket連線ESP32開發板調控PID值,用圖表呈現即時溫度變化。

換句話說,只要能跑WebSocket協定,就能監控3D列印機。例如,執行JavaScript, Python, …等程式的電腦,或者ESP32 Arduino微控器。底下以創想三維K1SE為例,其他廠商型號的3D列印機的控制方式應該都差不多,因為幾乎所有3D列印機的操控軟體都是開源專案的客製化版本。

websocket連線

打開瀏覽器連線到3D列印機的IP位址(此例為192.168.1.204),就能開啟3D列印機HTTP伺服器提供的監控網頁(可切換顯示英文或簡體中文),如下圖。網頁左上方會顯示列印中的物件的縮圖,目前沒有在列印,因此呈現灰白立體方塊圖。

3D列印機的監控頁面

網頁右上角可呈現串流影像,我將它折疊起來,方便顯示底下的噴嘴和熱床的溫度變化動態圖表。中間左側的控制鈕可調整列印頭(toolhead)的X, Y, Z軸位置、開 / 關風扇和LED燈。

檢視3D印表機串流視訊來源

在了解如何編寫程式或用ESP32 Arduino監控3D列印機之前,筆者先製作一個簡易的測試網頁,呈現串流影像、即時噴嘴和熱床的溫度變化,以及LED燈開關。

自製簡易版監控頁面

先查看串流視訊的來源網址。在瀏覽器中按F12鍵(Mac版Chrome瀏覽器請按Command (⌘) + Option (⌥) + I鍵)開啟「開發人員工具」,點擊「網路」分頁的「篩選」列裡的「圖片」,可看到有個 “?action=stream” 影像來源持續在載入資料:

串流影像來源網址

它就是串流視訊。點擊該網址可看到它的標頭,例如,等一下將會用到的請求網址 “http://列印機的IP位址/?action=stream”;點擊「預覽」則可預覽串流影像畫面。

在自製HTML網頁的內文區,設置如下的影像(img)標籤,即可呈現來自“http://192.168.1.204:8080” 網址的串流視訊:

<img src="http://192.168.1.204:8080/?action=stream" id="camera" alt="即時影像串流">

或者,先不指定影像標籤的來源(src),僅自訂唯一識別名稱“camera”:

<img id="camera" alt="即時影像串流">

再透過JavaScript動態設定它的影像來源也行:

<script>
    const printerIP = "192.168.1.204";  // 3D列印機的IP位址
    document.getElementById("camera").src = `http://${printerIP}:8080/?action=stream`;
</script>

查看WebSocket訊息內容

點擊「篩選」列的「通訊埠(socket)」,可見到由websocket.js發起的持續連線,請求網址是 “ws://列印機的IP位址:9999”。

websocket通訊網址

點擊「名稱」欄裡的IP位址,再點擊右側面板的「訊息」,就能看到WebSocket訊息內容,從底下JSON格式訊息的成員名稱,可推測其內容是噴嘴與熱床溫度。

查看WebSocket訊息內容

點擊3D列印機網頁上的控制鈕,例如LED燈的開關,它將送出開啟或關閉LED燈的訊息。

關閉LED燈的訊息

從捕捉到的畫面可得知,接收到的:

{"lightSw":0}

代表3D列印機回應「LED燈已關閉」;而它的前一則,傳送出去的訊息:

{"method":"set", "params":{"lightSw":0} }

代表設定(set)參數,其內容是「關閉LED燈」。

此外,上圖的訊息畫面的最後一則,是3D列印機持續傳出的「心跳(heart_beat)」訊息,其值是當前日期時間字串:

{"ModeCode":"heart_beat","msg":"2026-01-16T16:44:58.9427"}

「心跳」訊息用於告知WebSocket用戶端:3D列印機仍在運作,網路也沒有斷線。如果用戶端在幾秒(如:5或7秒)內沒有收到「心跳」訊息,就代表3D列印機可能關機、網路斷線或者當機了。

依照以上方法,調整3D列印機網頁上的各種操作,對照「訊息」面板裡的收、送訊息,即可推敲出每項控制和感測功能的識別名稱和數值格式。

附帶一提,許多網頁的JavaScript程式都是經過去除多餘的留白、刪除註解、簡化識別名稱等的min壓縮版,這個3D列印機的webSocket.js原始碼未經壓縮處理,因此可看到程式設計師留下的註解:

程式註解

自製3D列印機即時資訊網頁

我們可以寫一個簡易版即時監控網頁,收 / 送上文解析的WebSocket訊息,網頁畫面如之前所示:

自製簡易版監控頁面

完整的HTML原始碼如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>K1SE列印機監控器</title>
  <style>
    body { font-family: "微軟正黑體", "黑體-繁", sans-serif;}
    .status { margin: 10px 0; }
    button { margin: 5px; }
    img { border: 1px solid #ccc; max-width: 480px; }
  </style>
</head>
<body>
  <h1>K1SE列印機監控器</h1>

  <!-- 即時影像串流 -->
  <div>
    <h2>即時影像</h2>
    <img id="camera" alt="即時影像串流">
  </div>

  <!-- 狀態顯示 -->
  <div class="status">噴嘴溫度:<span id="nozzle">--</span> °C</div>
  <div class="status">熱床溫度:<span id="bed">--</span> °C</div>
  <div class="status">列印進度:<span id="progress">--</span> %</div>

  <!-- 控制按鈕 -->
  <button onclick="turnOnLight()">開燈</button>
  <button onclick="turnOffLight()">關燈</button>

  <script>
    const printerIP = "192.168.1.204";        // 3D列印機的IP位址
    const ws = new WebSocket(`ws://${printerIP}:9999/`);     // 建立WebSocket物件
    const nozzleElm = document.getElementById("nozzle");  // 取得"nozzle"元素
    const bedElm = document.getElementById("bed");
    const progressElm= document.getElementById("progress");

    // 設定串流影像來源
    document.getElementById("camera").src = `http://${printerIP}:8080/?action=stream`;

    ws.onopen = () => {
      console.log("已連線到K1SE");
    };

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);

        if (data.nozzleTemp) {
          nozzleElm.textContent = parseFloat(data.nozzleTemp).toFixed(1);
        }
        if (data.bedTemp0) {
          bedElm.textContent = parseFloat(data.bedTemp0).toFixed(1);
        }
        if (data.printProgress !== undefined) {
          progressEl.textContent = data.printProgress;
        }
      } catch (e) {
        console.log("非JSON訊息:", event.data);
      }
    };

    ws.onclose = () => {
      console.log("連線中斷…");
    };

    // 開燈
    function turnOnLight() {
      ws.send(JSON.stringify({method:"set", params:{lightSw:1}}));
    }

    // 關燈
    function turnOffLight() {
      ws.send(JSON.stringify({method:"set", params:{lightSw:0}}));
    }
  </script>
</body>
</html>

把上面的程式碼存成k1se.html,直接雙按,用瀏覽器開啟它,即可取得3D列印機的動態資訊。

我把這個網頁存入OpenWrt路由器內建HTTP伺服器的 “/www/” 路徑,也就是 ssh 連線至路由器後,執行底下命令新建 ”k1se.html” 檔,然後貼入HTML原始碼。

nano /www/k1se.html

如此,開啟 “http://192.168.1.1/k1se.html”,即可連線至3D列印機。

偵測3D列印機心跳(heart_beat)的網頁程式

修改上文的HTML原始碼,把兩個燈光控制鈕,改用一個核取方塊(checkbox),並且用CSS樣式把它的外觀改成滑鈕型式。

改成滑鈕的燈光控制開關

另外,加上偵測WebSocket的 “heart_beat”(心跳)訊息機制,若7秒內未收到心跳或其他訊息(如:噴嘴溫度),則顯示「關機或離線」。

顯示關機或離線畫面

為了切換顯示這兩個畫面,我把網頁內容分成兩個<div>區塊:

  • <div id=”offline-ui”>:包含「關機或離線」訊息的畫面
  • <div id=”main-ui”>:包含監控元素的畫面

完整的HTML原始碼如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>K1SE列印機監控器</title>
  <style>
    body { font-family: "微軟正黑體", "黑體-繁", sans-serif; }
    .status { margin: 10px 0; }
    button { margin: 5px; }
    img { border: 1px solid #ccc; max-width: 480px; }
    /* 預設隱藏 offline-ui */
    #offline-ui { display: none; }

    /* LED燈的切換鈕*/
    .switch {
      position: relative;
      display: inline-block;
      width: 50px;
      height: 24px;
      vertical-align: middle;
    }

    /* 隱藏原本的核取方塊 */
    .switch input { opacity: 0; width: 0; height: 0; }

    /* 按鈕滑軌 */
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0; left: 0; right: 0; bottom: 0;
      background-color: #ccc;
      transition: .4s;
      border-radius: 24px;
    }

    /* 滑動的小圓點 */
    .slider:before {
      position: absolute;
      content: "";
      height: 18px; width: 18px;
      left: 3px; bottom: 3px;
      background-color: white;
      transition: .4s;
      border-radius: 50%;
    }

    /* 被勾選時的滑軌顏色 */
    input:checked + .slider { background-color: #2196F3; }

    /* 被勾選時圓點位移 */
    input:checked + .slider:before {
      transform: translateX(26px);
    }
  </style>
</head>
<body>
  <div id="offline-ui">
    <h1 style="color:#d9534f;">關機或離線</h1>
    <button class="btn" onclick="location.reload()">重新嘗試連線</button>
  </div>

  <div id="main-ui">
    <h1>K1SE控制面板</h1>
    <!-- 即時影像串流 -->
    <div>
        <img id="camera" alt="即時影像串流">
    </div>

    <!-- 狀態顯示 -->
    <div class="status">噴嘴溫度:<span id="nozzle">--</span> °C</div>
    <div class="status">熱床溫度:<span id="bed">--</span> °C</div>
    <div class="status">列印進度:<span id="progress">--</span> %</div>

    <!-- 控制按鈕 -->
    <div class="status">
        燈光控制:
        <label class="switch">
          <input type="checkbox" id="light-toggle" onchange="handleToggle(this)">
          <span class="slider"></span>
        </label>
        <span id="light-label">關</span>
    </div>
  </div>

  <script>
    const printerIP = "192.168.1.204";
    const ws = new WebSocket(`ws://${printerIP}:9999/`);   // 建立WebSocket物件
    const nozzleElm = document.getElementById("nozzle");  // 取得 “nozzle” 元素
    const bedElm = document.getElementById("bed");
    const progressElm= document.getElementById("progress");
    const lightElm = document.getElementById("light");

    const offlineUI = document.getElementById("offline-ui");
    const mainUI = document.getElementById("main-ui");

    // 設定影像串流
    document.getElementById("camera").src = `http://${printerIP}:8080/?action=stream`;

    // 心跳檢查
    let lastHeartbeat = Date.now();

    // 每秒檢查心跳是否超過 7 秒未更新
    setInterval(() => {
      if (Date.now() - lastHeartbeat > 7000) {
       // 偵測到超時,切換至離線畫面
        if (mainUI.style.display !== "none") {
          mainUI.style.display = "none";
          offlineUI.style.display = "block";
        }
      } else {
        // 恢復連線(如:lastHeartbeat更新了)則切回控制畫面
        if (mainUI.style.display === "none") {
          mainUI.style.display = "block";
          offlineUI.style.display = "none";
          var dummy = mainUI.offsetHeight;
        }
      }
    }, 1000);

    ws.onopen = () => {
      // 已連線到K1SE…
      mainUI.style.display = "block";    // 顯示控制介面
      offlineUI.style.display = "none";  // 隱藏「關機離線」畫面
      lastHeartbeat = Date.now();        // 初始化心跳時間
    };

    ws.onmessage = (event) => {
      // 只要有訊息進來,就代表持續連線中…
      lastHeartbeat = Date.now();

      try {
        const data = JSON.parse(event.data);

        if (data.nozzleTemp) {
          nozzleElm.textContent = parseFloat(data.nozzleTemp).toFixed(1);
        }
        if (data.bedTemp0) {
          bedElm.textContent = parseFloat(data.bedTemp0).toFixed(1);
        }
        if (data.printProgress !== undefined) {
          progressEl.textContent = data.printProgress;
        }
        // 修改原本解析 lightSw 的地方
        if (data.lightSw !== undefined) {
            const isOn = data.lightSw === 1;
            document.getElementById("light-toggle").checked = isOn;
            document.getElementById("light-label").textContent = isOn ? "開" : "關";
        }
        if (data.ModeCode === "heart_beat") {
          lastHeartbeat = Date.now();
        }
      } catch (e) {
        console.log("非JSON訊息:", event.data);
      }
    };

    ws.onclose = () => {
      console.log("連線中斷…");
    };

    ws.onerror = () => {
      console.log("連線錯誤");
    };

    // 處理點擊開關的函式
    function handleToggle(checkbox) {
        const targetValue = checkbox.checked ? 1 : 0;
        ws.send(JSON.stringify({
            method: "set",
            params: { lightSw: targetValue }
        }));
    }
  </script>
</body>
</html>
Posts created 529

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top