作者:Jack OmniXRI
在物聯網時代,能隨時監看家中環境變化是很常見的應用,因此各式感測器更是不可或缺,如溫度、濕度、氣壓、照度甚至空氣品質(PM2.5)等,而這些感測器通常都會使用如UART、I2C、SPI 等串列通訊方式將收集到的資訊傳送回單晶片開發板以利後續處理。
在以往Arduino Uno R3/R4時代,大家都會將感測器模組插在麵包板上,再使用杜邦線連接到主板的排插上,但常會因為線材品質不佳或接點鬆脫導致感測器信號中斷,於是Arduino UNO Q(以下簡稱 UNO Q) 借鑑 SparkFun 可快速連接並能以菊花鏈(Daisy-chain)串聯 I2C 的連接方式「QWIIC」,讓大家可更穩定連接週邊,同時推出可支援 QWIIC 的 Modulino 系列週邊。
本文將藉UNO Q App Lab範例「Home climate monitoring and storage」進行修改,使其可以搭配常見氣溫和氣壓感測器Bosch BMP280變成自定義感測器的家庭氣象站,未來大家還可依此擴展到更多的感測器上,實現更完整的智慧家庭應用。

1. I2C 、 QWIIC 與 Modulino 模組
目前市售許多感測器模組、儲存元件(EEPROM等)、OLED顯示器等都會使用 I2C (Inter-Integrated Circuit,IIC / I²C / I2C) 通訊介面來連接單晶片(MCU)或微處理器(MPU)控制板,亦可作為兩塊控制板相互通訊使用。
如圖1右上所示,傳統 I2C 只需串列資料線(SDA)、時脈線(SCL)兩條訊號線及電源(3.3或5.0V視模組元件而異)即可,多個感測器模組可當成從(Slave)裝置進行並聯。另外為了讓多個並聯裝置可以清楚的讀寫訊號,通常會在SDA及SCL上串接一個提升電阻。 所有從裝置必須設置一組固定位址(通常為7bit 0x00~0x7F 或 10bit 0x000~0x3FF)且不能重覆,工作時從裝置會一直監聽SDA ,當聽到特定從裝置位址時,就會依命令進行讀寫,很適合多組讀寫頻度不高的感測器模組交替傳輸資料使用。
從Arduino UNO R4 WIFI起就開始增加了QWIIC接頭,UNO Q也有配置。如圖1右下所示,這是借鑑 SparkFun 可快速連接並能以菊花鏈(Daisy-chain)串聯I2C的連接方式。基本上它就是I2C接頭,只是統一了接線順序、顏色及供電規格,四芯分別為GND(黑)、3.3V輸出(紅)、SDA(藍)及SCL(黃)。而每個模組中SDA / SCL已自帶提升電阻。如果串接太多模組時,由於提升電阻被多次並聯,電阻值會下降,有時會影響通訊信號品質,必須手動移除部份模組的提升電阻才能改善。
在UNO Q板子上I2C是使用左上角的SCL(D21)和SDA(D20),程式初始化部份是使用 <wire.h> 的 Wire.begin() 。而 QWIIC 則是使用獨立的接頭,程式初始化部份則是使用 Wire1.begin()。

圖1:Arduino UNO Q I2C與QWIIC介面接線定義圖 。(OmniXRI整理製作,2026/07/01)

圖2:Arduino QWIIC介面模組 ─ Modulino 家族產品 。(OmniXRI整理製作,2026/07/01)
2. BMP280氣溫氣壓模組取樣程式
為了快速建立一個家庭氣象站,Arduino UNO Q APP LAB(以下簡稱APP LAB)提供了「Home climate monitoring and storage」範例。在這個範例中使用了QWIIC介面的 Modulino Thermo,該模組主要使用了Renesas HS3003溫濕度感測器。
由於大家可能不易取得,在本文中改使用I2C 介面的Bosch BMP280氣溫和氣壓感測器來進行實驗。這樣更方便大家依此類推更換成其他I2C或QWIIC介面感測器。完整BMP280 規格可參考圖3所示,更進一步介紹可參考「南開科大智慧物聯網資料收集站工作坊【05-I2C】」。

圖3:Bosch BMP280 氣溫氣壓感測器基本規格。(OmniXRI整理製作,2026/07/01)
為了讓這個範例能順利透過I2C讀取BMP280產生的氣溫和氣壓值。如圖4所示,首先開啟APP LAB,點選左側範例(Examples),再選擇「Home climate monitoring and storage」範例,雖然開發板並沒有接範例中所需的「Modulino Thermo」QWIIC模組,但程式還是可以執行的,點擊「執行(Run)」,稍等程式編譯完,就會自動開啟網頁顯示已讀取到的溫度、濕度及計算出的露點、體感溫度及絕對濕度等資訊,如 Fi圖4右上所示。
若網頁沒有自動開啟,可自行開啟網頁瀏覽器(如Chrome、Firefox、Microsoft Edge等)輸入網址 http://localhost:7000 即可得到一樣的結果。另外亦可使用開發板動態配置的無線網路網址取代 localhost,若不清楚目前網址,可點選APP LAB右下角地球圖案,即會顯示無線網路資訊。接著在瀏覽器輸入 http://192.168.x.x:7000 亦可得到同樣資訊。
最後點選右上角複製(Copy)鍵,給定專案名稱,按下新建(Create)就能建立一個新的專案,方便後續修改。

圖4:建立「Home climate monitoring and storage」範例。(OmniXRI整理製作,2026/07/01)
由於這個專案並未支援BMP280所需Arduino LIB,如圖5所示,須先點擊左上角「新增函式庫(Add Sketch Lib)」,再輸入BMP280找到合適的驅動程式,這裡我們選擇「Adafruit BMP280」函式庫,按下「安裝(Install)」即完成。
新增LIB後,sketch.yaml 檔會自動在 libraries 下加入 Adafruit BMP280 Library 及配套的 Adafruit BusIO 及 Adafruit Unified Sensor 函式庫。原先範例已有的 Arduino Modulino 相關函式庫後續用不到可暫時略過不用處理,完整 sketch.yaml 如下所示(註:這個檔案只能在CLI模式編輯,無法從視窗模式編輯)。
profiles:
default:
fqbn:
platforms:
- platform: arduino:zephyr
libraries:
- Arduino_Modulino (0.7.0)
- dependency: ArduinoGraphics (1.1.4)
- dependency: Arduino_HS300x (1.0.0)
- dependency: Arduino_LPS22HB (1.0.2)
- dependency: Arduino_LSM6DSOX (1.1.2)
- dependency: Arduino_LTR381RGB (1.0.0)
- dependency: STM32duino VL53L4CD (1.0.5)
- dependency: STM32duino VL53L4ED (1.0.1)
- Adafruit BMP280 Library (3.0.0)
- dependency: Adafruit BusIO (1.17.4)
- dependency: Adafruit Unified Sensor (1.1.15)
default_profile: default
接下來選擇左側 sketch 路徑下的 sketch.ino,將全部內容刪除,替換成下列程式,按下左上角「執行(Run)」即可看到下方「Serial Monitor」 每隔1秒會更新顯示出現目前氣溫及氣壓值。
由於原先程式是取得溫度及濕度值,而這裡是氣溫及氣壓值,所以相關回傳給Python內容也要跟著變化,完整程式內容及說明如下所示。
// sketch.ino
// by Jack OmniXRI, 2026/07/01
// 提供 BMP280 溫度及氣壓值給 Python 顯示
#include // 導入橋接函式
#include // 導入 I2C 驅動函式
#include // 導入 BMP280 溫度氣壓感測器驅動函式
unsigned long previousMillis = 0; // 記錄最後一次取值時間
const long interval = 1000; // 每隔 1000 ms 取樣一次
Adafruit_BMP280 bmp; // 建立 BMP280 物件(預設使用 I2C 介面)
void setup() {
Bridge.begin(); // 橋接函式初始化
Serial.begin(9600); // 虛擬串列埠初始化
while ( !Serial ) delay(100); // 等待序列埠視窗開啟
Serial.println(F("BMP280 測試開始")); // 在串列埠列印開始測試字串
// 初始化感測器,大多數 BMP280 模組預設位址是 0x76
// 如果失敗,嘗試傳入不同的 I2C 位址(常見有 0x76 或 0x77)
unsigned status = bmp.begin(0x76);
if (!status) { // 若初始化失敗,顯示提示字串
Serial.println(F("找不到正確的 BMP280 感測器,請檢查:"));
Serial.println(F("1. 接線是否正確?"));
Serial.println(F("2. I2C 位址是否為 0x77?(可將 bmp.begin(0x76) 改為 0x77 試試)"));
while (1) delay(10); // 卡死程式,等待硬體重啟
}
// 設定感測器參數(使用函式庫預設值即可)
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, // 工作模式
Adafruit_BMP280::SAMPLING_X2, // 溫度過採樣
Adafruit_BMP280::SAMPLING_X16, // 氣壓過採樣
Adafruit_BMP280::FILTER_X16, // 濾波器設定
Adafruit_BMP280::STANDBY_MS_500); // 停等時間
}
void loop() {
unsigned long currentMillis = millis(); // 獲取目前時間 (ms)
// 若目前時間和前次時間差值大於設定值,則輸出溫度程氣壓值
if (currentMillis - previousMillis >= interval) {
// 更新前次時間值為目前時間值
previousMillis = currentMillis;
// 讀取目前溫度值(攝氏 ℃)
float celsius = bmp.readTemperature();
Serial.println("目前溫度:" + String(celsius) + " 度(℃)");
// 讀取目前氣壓值(帕期卡 Pa)
float pressure = bmp.readPressure();
Serial.println("目前氣壓:" + String(pressure) + " 帕斯卡(Pa)");
// 通知(輸出)目前溫度及氣壓值
Bridge.notify("record_sensor_samples", celsius, pressure);
}
}

圖5:新增 Arduino LIB – Adafruit BMP280 函式庫、Sketch程式及執行結果。(OmniXRI整理製作,2026/07/01)
3. Arduino 磚塊(Brick) ─ 資料記錄與網頁介面
如下 app.yaml 所示,在此範例中主要會使用到兩個磚塊:資料庫儲存、時間序列儲存(dbstorage_tsstore)及網頁使用者介面(web_ui),分別用於儲存及顯示收集到的感測器資料。
name: Copy of Home climate monitoring and storage
description: A simple data logger that gets temperature and humidity from the board via Modulino Thermo and stores them in a database.
ports: []
bricks:
- arduino:dbstorage_tsstore: {}
- arduino:web_ui: {}
icon: 🎛️
同樣地,為了讓 Python 程式也能正確接收並顯示感測器獲得的數值,點選左側python路徑下的 main.py,刪除並替換成下列所有內容。在這個程式中,Python處於被動狀態,當MCU更新感測器內容時,會透過Brideg呼叫 record_sensor_samples() 函式記錄氣溫及氣壓值並以send_message() 送到網頁使用者介面,此時網頁會依據一日(1D)、一小時(1h)和即時(live)選項利用 on_get_samples() 函式取回並顯示最近資料集中的內容。
# main.py
# by Jack OmniXRI, 2026/07/01
# 接收、記錄 BMP280 取得之溫度及氣壓值,並顯示在網頁上
import datetime
import math
from arduino.app_bricks.dbstorage_tsstore import TimeSeriesStore
from arduino.app_bricks.web_ui import WebUI
from arduino.app_utils import App, Bridge
db = TimeSeriesStore() # 初始化時間序列儲存器
# 當取得感測器回傳值
def on_get_samples(resource: str, start: str, aggr_window: str):
samples = db.read_samples(measure=resource, start_from=start, aggr_window=aggr_window, aggr_func="mean", limit=100)
return [{"ts": s[1], "value": s[2]} for s in samples]
ui = WebUI() # 初始化網頁介面
ui.expose_api("GET", "/get_samples/{resource}/{start}/{aggr_window}", on_get_samples)
# 記錄感測器取樣值
def record_sensor_samples(celsius: float, pressure: float):
if celsius is None or pressure is None:
print("Received invalid sensor samples: celsius=%s, pressurd=%s" % (celsius, pressure))
return
ts = int(datetime.datetime.now().timestamp() * 1000) # 取得時間戳
# 將溫度和氣壓值寫入時間序列資料庫中
db.write_sample("temperature", float(celsius), ts)
db.write_sample("pressure", float(pressure), ts)
# 將溫度和氣壓值顯示在網頁上
ui.send_message('temperature', {"value": float(celsius), "ts": ts})
ui.send_message('pressure', {"value": float(pressure), "ts": ts})
print("Registering 'record_sensor_samples' callback.")
# 宣告記錄感測器回呼函式
Bridge.provide("record_sensor_samples", record_sensor_samples)
print("Starting App...")
App.run()
原範例程式網頁使用者介面顯示內容共有五項(如圖4右上所示),而這裡只需氣溫和氣壓,如圖6所示,所以必須修正左側assets路徑下的 index.html 及 app.js。同樣地只需開啟後,將下列 index.html 及 app.js 內容全部刪除後再貼上下列內容即完成。主要修改是將濕度(Humidity,%)變成氣壓(Pressure,Pa),並刪除露點(Dew point)、體感溫度(Heat Index)及絕對濕度(Absolute Humidity)等相關內容。
<!-- index.html SPDX-FileCopyrightText: Copyright (C) Arduino s.r.l. and/or its affiliated companies SPDX-License-Identifier: MPL-2.0 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script><script src="libs/socket.io.min.js"></script><script src="app.js"></script></pre>
<div class="app-container">
<div class="header">
<h1 class="arduino-text">Home climate monitoring and storage</h1>
<img class="arduino-logo" src="./img/RGB-Arduino-Logo_Color Inline Loop.svg" alt="Arduino Logo" />
</div>
<div class="tabs"><button class="tab" data-tab="historical-1d">1 D</button>
<button class="tab" data-tab="historical-1h">1 h</button>
<button class="tab-with-circle tab active" data-tab="live">
<span id="live-circle"></span>
Live
</button></div>
<div class="main-content">
<div id="historical-1d" class="tab-content">
<div class="top-row">
<div class="container">
<div class="graph-header">
Temperature (°C)
<img class="info-btn temp" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="temperature-1d-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
<div class="container">
<div class="graph-header">
Pressure (Pa)
<img class="info-btn pressure" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="pressure-1d-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
</div>
</div>
<div id="historical-1h" class="tab-content">
<div class="top-row">
<div class="container">
<div class="graph-header">
Temperature (°C)
<img class="info-btn temp" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="temperature-1h-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
<div class="container">
<div class="graph-header">
Pressure (Pa)
<img class="info-btn pressure" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="pressure-1h-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
</div>
</div>
<div id="live" class="tab-content active">
<div class="top-row">
<div class="container">
<div class="graph-header">
Temperature (°C)
<img class="info-btn temp" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="temperature-live-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
<div class="container">
<div class="graph-header">
Pressure (Pa)
<img class="info-btn pressure" src="./img/info.svg" alt="Info" />
<div class="popover"></div>
</div>
<div id="pressure-live-chart-nodata" class="nodata-container"><img class="nodata-img" src="./img/nodata.svg" />
<span class="no-data">No data</span></div>
</div>
</div>
</div>
</div>
<div id="error-container" class="error-message" style="display: none;"></div>
</div>
<pre>
// app.js
// SPDX-FileCopyrightText: Copyright (C) Arduino s.r.l. and/or its affiliated companies
//
// SPDX-License-Identifier: MPL-2.0
const socket = io(`http://${window.location.host}`);
// Temperature and Pressure chart objects
const temperatureLive = { canvas: null, chart: null, data: newChartData('orange', 'rgba(255,165,0,0.1)'), unit: '°C' };
const pressureLive = { canvas: null, chart: null, data: newChartData('teal', 'rgba(0,128,128,0.08)'), unit: 'Pa' };
const temperature1h = { canvas: null, chart: null, data: newChartData('orange', 'rgba(255,165,0,0.1)'), unit: '°C' };
const pressure1h = { canvas: null, chart: null, data: newChartData('teal', 'rgba(0,128,128,0.08)'), unit: 'Pa' };
const temperature1d = { canvas: null, chart: null, data: newChartData('orange', 'rgba(255,165,0,0.1)'), unit: '°C' };
const pressure1d = { canvas: null, chart: null, data: newChartData('teal', 'rgba(0,128,128,0.08)'), unit: 'Pa' };
let liveCircleTimeout = null;
const noDataTimeout = 10000; // 10 seconds
let errorContainer;
document.addEventListener('DOMContentLoaded', () => {
// Initialize canvases
temperatureLive.canvas = document.getElementById('temperature-live-chart');
pressureLive.canvas = document.getElementById('pressure-live-chart');
temperature1h.canvas = document.getElementById('temperature-1h-chart');
pressure1h.canvas = document.getElementById('pressure-1h-chart');
temperature1d.canvas = document.getElementById('temperature-1d-chart');
pressure1d.canvas = document.getElementById('pressure-1d-chart');
// The live circle is hidden initially until data come
const liveCircle = document.getElementById('live-circle');
if (liveCircle) liveCircle.style.display = 'none';
errorContainer = document.getElementById('error-container');
// Tab switching logic
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
this.classList.add('active');
document.getElementById(this.dataset.tab).classList.add('active');
});
});
document.querySelector('.tab[data-tab="historical-1h"]').addEventListener('click', async () => {
const temperature_samples = await listSamples("temperature", "-1h", "5m");
renderChartData(temperature1h, temperature_samples, 12, true, false);
const pressure_samples = await listSamples("pressure", "-1h", "5m");
renderChartData(pressure1h, pressure_samples, 12, true, false);
});
document.querySelector('.tab[data-tab="historical-1d"]').addEventListener('click', async () => {
const temperature_samples = await listSamples("temperature", "-1d", "1h");
renderChartData(temperature1d, temperature_samples, 24, false, false);
const pressure_samples = await listSamples("pressure", "-1d", "1h");
renderChartData(pressure1d, pressure_samples, 24, false, false);
});
// Popover logic for Temperature and Pressure info buttons
const tempPopoverText = "Shows temperature readings in °C. Data is average per 1h (1D view) or per 1 minute (1h view)";
const pressurePopoverText = "Shows pressure readings in Pa. Data is average per 1h (1D view) or per 1 minute (1h view)";
document.querySelectorAll('.info-btn.temp').forEach(img => {
img.style.position = 'relative';
const popover = img.nextElementSibling;
img.addEventListener('mouseenter', () => {
popover.textContent = tempPopoverText;
popover.style.display = 'block';
});
img.addEventListener('mouseleave', () => {
popover.style.display = 'none';
});
});
document.querySelectorAll('.info-btn.pressure').forEach(img => {
img.style.position = 'relative';
const popover = img.nextElementSibling;
img.addEventListener('mouseenter', () => {
popover.textContent = pressurePopoverText;
popover.style.display = 'block';
});
img.addEventListener('mouseleave', () => {
popover.style.display = 'none';
});
});
initSocketIO();
});
function initSocketIO() {
socket.on('connect', () => {
if (errorContainer) {
errorContainer.style.display = 'none';
errorContainer.textContent = '';
}
});
socket.on('disconnect', (reason) => {
if (errorContainer) {
errorContainer.textContent = 'Connection to the board lost. Please check the connection.';
errorContainer.style.display = 'block';
}
});
// Temperature and Pressure live updates
socket.on('temperature', (message) => {
renderChartData(temperatureLive, [message]);
});
socket.on('pressure', (message) => {
renderChartData(pressureLive, [message]);
});
}
async function listSamples(resource, start, aggr_window) {
try {
const response = await fetch(`http://${window.location.host}/get_samples/${resource}/${start}/${aggr_window}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.error) {
console.log(`Failed to get samples: ${data.error}`);
return;
}
return data;
} catch (error) {
console.log(`Error fetching samples: ${error.message}`);
}
}
function renderChartData(obj, messages, maxPoints = 20, showMinutes = true, showSeconds = true) {
if (!messages || messages.length === 0) {
return;
}
const noDataDiv = document.getElementById((obj.canvas && obj.canvas.id) + '-nodata');
const liveCircle = document.getElementById('live-circle');
const isLiveChart = obj.canvas && obj.canvas.id && obj.canvas.id.endsWith('-live-chart');
// Only clear data for non-live charts
if (!isLiveChart) {
obj.data.labels = [];
obj.data.datasets[0].data = [];
}
for (const message of messages) {
if (!message.ts) {
console.warn('Invalid message format:', message);
continue;
}
let date = new Date(message.ts);
if (showMinutes && showSeconds) {
date = date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
} else if (showMinutes) {
date = date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
} else {
date = date.toLocaleTimeString([], {hour: '2-digit'});
}
obj.data.labels.push(date);
obj.data.datasets[0].data.push(message.value);
// Keep only the n points
if (obj.data.labels.length > maxPoints) {
obj.data.labels.shift();
obj.data.datasets[0].data.shift();
}
if (obj.data.labels.length === 0 || obj.data.datasets[0].data.length === 0) {
if (obj.canvas) obj.canvas.style.display = 'none';
if (noDataDiv) noDataDiv.style.display = 'flex';
if (isLiveChart && liveCircle) {
liveCircle.style.display = 'none';
liveCircle.classList.remove('flash');
if (liveCircleTimeout) {
clearTimeout(liveCircleTimeout);
liveCircleTimeout = null;
}
}
if (obj.chart) {
obj.chart.destroy();
obj.chart = null;
}
} else {
if (obj.canvas) obj.canvas.style.display = 'block';
if (noDataDiv) noDataDiv.style.display = 'none';
if (isLiveChart && liveCircle) {
liveCircle.style.display = 'flex';
liveCircle.classList.add('flash');
if (liveCircleTimeout) clearTimeout(liveCircleTimeout);
liveCircleTimeout = setTimeout(() => {
liveCircle.classList.remove('flash');
liveCircle.style.display = 'none';
}, noDataTimeout);
}
if (!obj.chart) {
obj.chart = newChart(obj.canvas.getContext('2d'), obj);
} else {
obj.chart.update();
}
}
}
}
function newChart(ctx, obj) {
return new Chart(ctx, {
type: 'line',
data: obj.data,
options: {
responsive: true,
animation: false,
scales: {
y: obj.unit === '%' ? { min: 0, max: 100 } : {},
x: {
grid: { display: false },
ticks: {
maxRotation: 45,
minRotation: 45
}
}
},
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: { display: false },
tooltip: {
displayColors: false,
callbacks: {
title: function() { return ''; },
label: function(context) {
const unit = context.chart && context.chart.options && context.chart.options._unit ? context.chart.options._unit : (obj.unit || '');
// store unit in chart options for future reference
if (!context.chart.options._unit) context.chart.options._unit = obj.unit;
return `${context.label} - ${context.parsed.y.toFixed(1)} ${unit}`;
}
}
},
noDataMessage: true
}
}
});
}
function newChartData(borderColor, backgroundColor) {
return {
labels: [],
datasets: [{
data: [],
borderColor: borderColor,
backgroundColor: backgroundColor,
fill: true,
}]
};
}
最後修正完的網頁使用者介面如圖6所示。

圖6:修正後即時感測器數值顯示結果。(OmniXRI整理製作,2026/07/01)
結語
透過本文及相關範例程式,可讓大家了解到Arduino UNO Q新式I2C介面「QWIIC」及如何連接、讀取各式I2C感測器內容,同時可依需求修改顯示的網頁使用者人機介面。後續依此類推就能納入更多感測器,讓自己動手完成一個智慧家庭氣象站不再是難事。
(責編:Judith Cheng;編按:本文內的程式碼可能因顯示模式而有跑版情況,完整內容也可參考作者的部落格)
- 【Arduino UNO Q專欄05】家庭氣象站 - 2026/07/07
- 【Arduino UNO Q專欄04】人臉偵測開關燈具 - 2026/06/22
- 【Arduino UNO Q專欄03】板載點矩陣LED應用 - 2026/06/08
訂閱MakerPRO知識充電報
與40000位開發者一同掌握科技創新的技術資訊!


