基於沉浸式體驗的健身自行車服務開發經驗談
2025/09/07 PyCon TW 2025

photo credit: PyCon TW
Slide

關於我
-
Peter, GitHub
-
活躍的開源專案貢獻者
-
也是一位講者
-
COSCUP、MOPCON......
-
-
也是一位工程師
-
DevOps
-
後端開發;PHP、Python與JavaScript
-
軟體工程、系統架構設計與分析
-
網頁應用程式安全
-
-
財團法人工業技術研究院
-
應用資通訊技術至研究智慧電網領域 (2017~2021)
-
-
財團法人資訊工業策進會
-
醫資、健康照護應用服務與碳排放等領域 (2021~迄今)
-

大綱
-
緣起與需求描述
-
使用者情境介紹
-
常見用於運動感測的通訊協定介紹與比較
-
介紹閘道器、軟硬體系統的設計與實作
-
場域應用案例探討(Demo)
-
未來展望
-
Q&A
緣起
-
故事摘要
-
組長
-
上面的主管指示說,要開發一個運動存摺服務,但是預算有限
-
並讓此服務能夠進行可行性驗證
-
讓年長者可以透過此服務去進行沉浸式健身自行車體驗
-
體驗要以遊戲的方式進行
-
-
-
專案經理
-
幫忙規劃與建立此服務情境出來
-
-
我
-
就開始規劃與開發服務,並與前端開發進行配合
-
-
前端專案人力
-
需求描述
-
使用者對象為年長者
-
使用者能夠透過我們的健身自行車服務
-
體驗遊戲方式為類似競速的遊戲
-
體驗健身自行車遊戲並獲得成績
-
競速遊戲會有單人與團體遊戲,團體遊戲目前設計上限6人
-
使用者情境介紹

選用與比較各方案
-
使用的健身自行車裝置
-
最初考慮的兩種方案
-
第一種
-
與其他部門合作,搭配其完整運動感測方案與整合
-
考慮「雄感動Siung Sport」:https://siungsport.com/zh
-
-
第二種
-
將市面上的所需要的感測裝置進行比較
-
自行研發閘道器,與感測器整合並收集/收攏需要的感測資料
-
選用與比較各方案–第一種(1/4)
-
雄感動感測與用者使用方式簡圖

選用與比較各方案–第一種(2/4)
-
感測器使用霍爾效應感測器
-
霍爾感測器,會感測磁場的存在
-
當有一塊磁鐵接近感測器的時候,因著感測器半導體內
-
電流受到磁鐵磁力的影響而轉向
-
在另外一個方向造成霍爾電壓,改變輸出的訊號
-
依照霍爾電壓輸出的大小,可以輸出數位及類比(線性)訊號
-
常被用來測定轉速(如腳踏車車輪轉速)
-
-
miniBox會收集感測器所感測的值,計算後得到km/hr速度與轉速(RPM)
-
miniBox與霍爾感測器透過RJ11線進行相互連接


選用與比較各方案–第一種(3/4)
-
miniBox除了負責感測與計算霍爾效應感測器偵測的數值之外
-
還能夠透過其使用者介面調整裝置資訊與感測參數
-
還能夠讀取miniBox上的API,包含裝置資訊與感測的資料等
-
-
考量後,不選用此方案的原因
-
成本上的考量:選用此方案的成本太高,需要採買多個霍爾感測器與接收盒
-
能夠客製化的部分較少,同時需要依賴平板去收集資料,或是自行與miniBox溝通
-
感測器與感測盒連接方式為有線
-
霍爾感測器在安裝上需要做較多的調整,避免安裝到健身自行車上感測到0的數值
-


photo credit: 迪卡儂
選用與比較各方案–第一種(4/4)
-
miniBox除了負責感測與計算霍爾效應感測器偵測的數值之外
-
還能夠透過其使用者介面調整裝置資訊與感測參數
-
還能夠讀取miniBox上的API,包含裝置資訊與感測的資料等
-
-
考量後,不選用此方案的原因
-
成本上的考量:選用此方案的成本太高,需要採買多個霍爾感測器與接收盒
-
能夠客製化的部分較少,同時需要依賴平板去收集資料,或是自行與miniBox溝通
-
感測器與感測盒連接方式為有線,預期是以無線傳遞的方式進行感測與收集
-
霍爾感測器在安裝上需要做較多的調整,避免安裝到健身自行車上感測值是0的數值
-
安裝較為複雜
-
這與霍爾感測的原理有關
-
安裝的位置取決於健身自行車,需要調整到合適的位置
-
-


選用與比較各方案–第二種(1/3)
-
將需要的感測器與接收器進行拆開來看,需要的感測器與裝置有:
-
速度或踏頻感測器,市面上的廠牌眾多,需要進行比較
-
與霍爾感測器不同的是,下列均為「無磁鐵設計」(或稱無磁石設計)的感測器
-
安裝方式較為方便與簡易,缺點是計算速度與踏頻的值誤差較大,但不影響我們設計的服務
-
速度與踏頻感測器的廠牌比較表如下:
-
廠牌名稱 | 支援方式與產品說明摘要 | 支援通訊協定 | 是否採用? | 價格(台幣) |
---|---|---|---|---|
Magene |
感測器分為速度與踏頻模式,需要透過App切換 | 藍牙與ANT+ | 不採用,為大陸廠牌 |
500~650 |
Wahoo | 美國廠牌,速度與踏頻分開,對於健身自行車靈敏度較差 | 藍牙與ANT+ | 不採用,靈敏度問題 | 1,550 |
Bryton | 臺灣廠牌,速度與踏頻分開 | 藍牙與ANT+ | 不採用,無經費做細部測試 | 1,080 |
Garmin | 臺灣廠牌,速度與踏頻分開,對於健身自行車靈敏度較差 | 藍牙與ANT+ | 先採用,後續進行細部測試 | 1,365 |
ALATECH | 臺灣廠牌,速度與踏頻號稱能夠自動判斷模式 | 藍牙與ANT+ | 不採用,無費做細部測試 | 1,080 |
Arofly | 臺灣廠牌,速度與踏頻分開,對於感測靈敏度較高,由於無市面上無零售,僅以專案方式進行採購 | 藍牙與ANT+ | 先採用,後續進行細部測試 | 1,280 |
選用與比較各方案–第二種(2/3)
將採用的踏頻器篩選出來並做更細緻的比較,相關的比較表如下:
廠牌名稱 | 採用的感測模式 | 對於健身自行車感測靈敏度 | 支援通訊協定 | 是否採用? |
---|---|---|---|---|
Garmin | 踏頻 | 感測數值會出現0的情況 | 藍牙與ANT+ | 不採用,原因是感測靈敏度較低 |
Arofly | 踏頻 | 感測較靈敏,不會有0的數值 | 藍牙與ANT+ | 採用,感測靈敏度較高且有機會客製化 |
-
最後採用的踏頻器為Arofly,綜合的原因如下:
-
對於非正常的「曲柄」健身自行車,感測的靈敏度較高
-
因為與廠商合作,後續客製化的可能性較高
-

photo credit: 迪卡儂
photo credit: 單車時代

photo credit: 新豪億科技

photo credit: Yahoo拍賣
選用與比較各方案–總結
-
感測接收方法之比較
-
第一種
-
先選用此種方法進行實作
-
使用樹莓派4自行打造接收感測器的閘道器
-
需要選擇使用藍牙或ANT接收器,並安裝到此樹莓派上
-
-
運動感測的通訊協定介紹與比較(1/10)
-
先選前述的第一種方法進行設計與實作
-
由於感測器能夠接收藍牙與ANT通訊協定,因此針對兩者進行比較

通訊協定名稱 | 通訊協定摘要 | 通訊協定頻率 | 通訊距離 |
---|---|---|---|
藍牙Bluetooth | 有探索、交握與傳輸等過程,目前版本至6.x;版本4.x與5.x較為常見 | 2.4GHZ | 無障礙物時,200至300公尺 |
ANT | 全名為「Adaptive Network Topology」,是一種低功率的無線通訊協定與多廣播之無線感測網路技術。ANT+則是依據ANT通訊協訂制定出的超低功耗短距離無線傳輸標準,但是自2025年起,Garmin停止ANT裝置認證,後續可能會遭到棄用 | 2.4GHZ | 無障礙物時,同時看感測器天線距離,短距離約10公尺,若安裝增益天線則可以將距離達50至100公尺 |

photo credit: This is ANT
photo credit: Bluetooth.com
運動感測的通訊協定介紹與比較(2/10)
-
藍牙與樹莓派4進行整合討論
-
樹莓派4本身有內建藍牙晶片,但是是與WiFi晶片封裝在一起
-
因此傳輸與感測距離較差,需要仰賴外接的USB藍牙感測器
-
因此使用與評估Edimax BT-8500
-
使用作業系統為Raspbian Lite, Debian GNU/Linux 12 (bookworm)
-
設定藍牙接收器的步驟如下:
-
停用內建藍牙感測器
-
修改藍牙設定,改為multiple模式
-
維持Bletooth狀態是運作的
-
-
-
運動感測的通訊協定介紹與比較(3/10)
-
藍牙與樹莓派4進行整合討論
-
設定藍牙接收器的步驟如下(1/2):
-
# 加入下列內容到上述的設定檔:
# Disable built-in bluetooth
dtoverlay=disable-bt
$ sudo vim /boot/firmware/config.txt
$ sudo reboot
$ hciconfig -a
hci0: Type: Primary Bus: USB
BD Address: 08:BE:AC:3F:5F:86 ACL MTU: 1021:6 SCO MTU: 255:12
UP RUNNING
RX bytes:1905 acl:0 sco:0 events:180 errors:0
TX bytes:33414 acl:0 sco:0 commands:180 errors:0
Features: 0xff 0xff 0xff 0xfe 0xdb 0xfd 0x7b 0x87
Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
Link policy: RSWITCH HOLD SNIFF PARK
Link mode: PERIPHERAL ACCEPT
Name: 'iotdevice01'
Class: 0x000000
Service Classes: Unspecified
Device Class: Miscellaneous,
HCI Version: 5.1 (0xa) Revision: 0xdfc6
LMP Version: 5.1 (0xa) Subversion: 0xd922
Manufacturer: Realtek Semiconductor Corporation (93)
運動感測的通訊協定介紹與比較(4/10)
-
藍牙與樹莓派4進行整合討論
-
設定藍牙接收器的步驟如下(2/2):
-
# 找到MultiProfile的關鍵字設定並改成multiple
$ sudo vim /etc/bluetooth/main.conf
# 重新啟動藍牙背景服務
$ sudo systemctl restart bluetooth
# 修改/etc/bluetooth/main.conf檔,改成下列設定:
DiscoverableTimeout = 0
$ sudo vim /etc/bluetooth/main.conf
# 重新啟動Bluetooth服務
$ sudo systemctl restart bluetooth.service
# 將下列設定加入至/etc/rc.local檔案中
# 下列的設定會讓Raspiberry Pi 4開機之後,將藍牙裝置進行電源啟動,開啟可以搜尋與配對的設定
$ sudo bluetoothctl <<EOF
power on
discoverable on
pairable on
EOF
運動感測的通訊協定介紹與比較(5/10)
-
考慮使用的藍牙函式庫如下:
-
-
授權:MIT License
-
雖然支援多種踏頻感測器,但是前述採用的感測器不在支援的清單中
-
-
adafruit-circuitpython-ble-cycling-speed-and-cadence
-
授權:MIT License
-
由Adafruit公司開發的函式庫
-
一家總部位於美國紐約的開源硬體公司
-
設計、製造和銷售電子產品、電子元件、工具和配件,製作學習資源。
-
包括有關電子、技術和程式設計的即時和錄製影片
-
-
運動感測的通訊協定介紹與比較(6/10)
-
選用adafruit-circuitpython-ble-cycling-speed-and-cadence函式庫
-
安裝此套件的方式如下:
-
pip3 install adafruit-circuitpython-ble-cycling-speed-and-cadence --user
-
ble_cycling_speed_and_cadence_simpletest.py程式碼內容如下:
-
-
# SPDX-FileCopyrightText: 2020 Dan Halbert for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
Read cycling speed and cadence data from a peripheral using the standard BLE
Cycling Speed and Cadence (CSC) Service.
"""
import time
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService
from adafruit_ble_cycling_speed_and_cadence import CyclingSpeedAndCadenceService
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio() # pylint: disable=no-member
ble._adapter.ble_backend = 'hcitool'
while True:
print("Scanning...")
# Save advertisements, indexed by address
advs = {}
for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
if CyclingSpeedAndCadenceService in adv.services:
print("found a CyclingSpeedAndCadenceService advertisement")
# Save advertisement. Overwrite duplicates from same address (device).
advs[adv.address] = adv
ble.stop_scan()
print("Stopped scanning")
if not advs:
# Nothing found. Go back and keep looking.
continue
# Connect to all available CSC sensors.
cyc_connections = []
for adv in advs.values():
cyc_connections.append(ble.connect(adv))
print("Connected", len(cyc_connections))
# Print out info about each sensors.
for conn in cyc_connections:
if conn.connected:
if DeviceInfoService in conn:
dis = conn[DeviceInfoService]
try:
manufacturer = dis.manufacturer
except AttributeError:
manufacturer = "(Manufacturer Not specified)"
print("Device:", manufacturer)
else:
print("No device information")
print("Waiting for data... (could be 10-20 seconds or more)")
# Get CSC Service from each sensor.
cyc_services = []
for conn in cyc_connections:
cyc_services.append(conn[CyclingSpeedAndCadenceService])
# Read data from each sensor once a second.
# Stop if we lose connection to all sensors.
while True:
still_connected = False
for conn, svc in zip(cyc_connections, cyc_services):
if conn.connected:
still_connected = True
print(svc.measurement_values)
if not still_connected:
break
time.sleep(1)
運動感測的通訊協定介紹與比較(7/10)
-
選用adafruit-circuitpython-ble-cycling-speed-and-cadence函式庫
-
使用的方式如下:
-
$ python3 ble_cycling_speed_and_cadence_simpletest.py
Scanning...
found a CyclingSpeedAndCadenceService advertisement
found a CyclingSpeedAndCadenceService advertisement
Stopped scanning
{Address(string="F1:72:2D:B9:2C:53"): Advertisement(data=b"\x0d\xff\xff\xff\x6b\x00\x7e\xe5\x01\x03\x00\x04\x00\x74\x03\x03\x16\x18\x08\x09\x35\x38\x37\x35\x30\x2d\x31")}
Connected 1
Device: Qingdao Magene Intelligence Technology Co., Ltd
Waiting for data... (could be 10-20 seconds or more)
None
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
CSCMeasurementValues(cumulative_wheel_revolutions=None, last_wheel_event_time=None, cumulative_crank_revolutions=2, last_crank_event_time=4059)
^CTraceback (most recent call last):
File "/home/pi/ble_cycling_speed_and_cadence_simpletest.py", line 73, in <module>
time.sleep(1)
KeyboardInterrupt
運動感測的通訊協定介紹與比較(8/10)
-
Edimax BT-8500(藍牙5.0)接收器已知的問題如下:
-
啟動藍牙搜尋程式時,會讓本來藍牙接收器從powered是開啟的狀態變成關閉的狀態
-
若要修正此問題,只能在啟動藍牙搜尋程式之後
-
再透過bluetoothctl power on指令去手動開啟藍牙接收器
-
-
-
可能造成問題的原因:
-
研判是感測器驅動與Raspbian之Kernel版本之間相容性與支援的問題
-
-
解決問題的方法:
-
更換在Raspbian Lite上安裝不同的Kernel版本
-
更換不同的藍牙接收器進行交叉測試
-
運動感測的通訊協定介紹與比較(9/10)
-
考慮ANT(ANT+)
-
若要更換不同的傳輸的通訊協定進行測試,因此使用ANT通訊協定
-
此通訊協定是由Dynastream Innovations發展而來
-
原先是一間公司後來被Garmin買走,成為Garmin的子公司
-
-
若要透過此通訊協定進行傳輸,則需要使用特殊的ANT接收器
-
Garmin有推出ANT USB Stick Dongle
-
以及其他第三方採用ANT模組的USB Dongle接收器
-
ANT通訊協定說明文件(PDF檔)
-
運動感測的通訊協定介紹與比較(10/10)
-
目前已知的ANT通訊協定接收器如下:
型號名稱 | 廠牌名稱 | 傳輸距離 | 是否採用? | 價格(台幣) |
---|---|---|---|---|
ANT USB-m Stick | Garmin | 5~10公尺 | 採用,雖然距離短但為原廠 | 1,890 |
USB ANT+ Stick | Magene | 3~5公尺 | 不採用,距離短且為大陸品牌 | 360 |
ANT+ USB Stick | TinkerRider | 3~10公尺 | 距離較長且有增益天線,暫時不採用 | 1,540 |
ANT USB Adapter | hLine | 10~15公尺 | 距離較長且有增益天線,暫時不採用 | 2,750 |



photo credit: 露天拍賣
photo credit: 蝦皮
photo credit: DigiKey

photo credit: Amazon
運動感測的通訊協定總結
-
決定使用ANT通訊協定,原因如下:
-
使用Scanning Mode時,其傳輸的方式類似MQTT的協定
-
讓裝置自行決定將資料透過協定傳輸至ANT協定的接收器
-
使用ANT通訊協定的方法(1/4)
-
選定Garmin的ANT+ USB Stick,並接上樹莓派4
-
使用openant的Python套件與前述的ANT接收器進行搭配
-
作為建立ANT Master Node的方法如下:
-
使用openant指令去執行openant scan進行接收裝置發送資料的方法
-
參考此函式庫提供的scanner.py範例程式進行串接
-
選擇此方法並改成自己需要的感測資料程式
-
-
使用ANT通訊協定的方法(2/4)
-
使用openant套件與存取ANT接收器時的故障排除
-
若執行前述的scanner.py程式或openant指令時,出現下列錯誤:
-
usb.core.USBError: [Errno 13] Access denied (insufficient permissions)
-
前述的錯誤訊息為,當前使用者存取此USB之ANT接收器的權限不足
-
若要解決前述的問題,可以使用下列兩種方式:
-
假設在Raspbian系列的作業系統上,執行下列的指令進行設定與解決:
-
-
$ lsusb | grep Dynastream
Bus 003 Device 030: ID 0fcf:1008 Dynastream Innovations, Inc. ANTUSB2 Stick
$ sudo modprobe usbserial vendor=0x0fcf product=0x1008
使用ANT通訊協定的方法(3/4)
-
若執行前述的scanner.py程式或openant指令時,出現下列錯誤:
- usb.core.USBError: [Errno 13] Access denied (insufficient permissions)
-
前述的錯誤訊息為,當前使用者存取此USB之ANT接收器的權限不足
-
若要解決前述的問題,可以使用下列兩種方式:
-
假設在Ubuntu系列的作業系統上,執行下列的指令進行設定與解決:
-
-
- usb.core.USBError: [Errno 13] Access denied (insufficient permissions)
$ cat /etc/udev/rules.d/42-ant-usb-sticks.rules
# This files changes the mode of the Dynastream ANT UsbStick2 so all users
# can read and write to it.
#
# This file should go into '/etc/udev/rules.d'. Note that it should go in
# before 73-seat-late.rules for `uaccess` to work.
ACTION!="add", GOTO="openant_rules_end"
SUBSYSTEM!="usb", GOTO="openant_rules_end"
ATTR{idVendor}=="0fcf", ATTR{idProduct}=="1008", ENV{ID_ANT_DEVICE}="1", TAG+="uaccess", GROUP="plugdev", MODE="0666"
ATTR{idVendor}=="0fcf", ATTR{idProduct}=="1009", ENV{ID_ANT_DEVICE}="1", TAG+="uaccess", GROUP="plugdev", MODE="0666"
LABEL="openant_rules_end"
# 接著再執行下列的指令
$ sudo udevadm control --reload-rules && sudo udevadm trigger
使用ANT通訊協定的方法(4/4)
-
執行scanner.py範例程式之輸出結果如下:
pi@iotdevice01:~/sensor-gateway-fetcher $ pipenv run python scanner.py
Starting scanner for #0, type 0, press Ctrl-C to finish
Found new device #54939 DeviceType.BikeCadence; device_type: 122, transmission_type: 1
Found new device #58179 DeviceType.HeartRate; device_type: 120, transmission_type: 17
^CClosing ANT+ node...
-
從上述執行的程式範例可以知道,透過openant以Scanning模式能夠取得:
-
裝置編號,Device ID;例如:#54939
-
裝置的類型,Device Type;例如:DeviceType.BikeCadence,device_type: 122
-
裝置的特性,包含ANT Channel ID與特定裝置的功能特性;例如:transmission_type: 1
-
軟硬體系統結合的設計與實作(1/8)
-
透過先前的通訊協定、硬體與閘道器的設計後,接著搭配軟體系統進行設計:
-
讓閘道器秉持與接收ANT資料的模式,使用MQTT的方式讓閘道器決定傳送資料給遠端系統
-

軟硬體系統結合的設計與實作(2/8)
-
實作前述的軟體系統,實作的專案有:
-
sensor-gateway-fetcher,閘道器模式使用MQTT Publisher與ANT Subscriber
-
安裝在閘道器上,負責接收ANT裝置發送的資料以及透過MQTT協定推送資料到遠端
-
-
sensor-gateway-fetcher,遠端(後端)模式使用MQTT Subscriber,接收閘道器的感測資料
-
安裝在伺服器端,負責接收閘道器透過MQTT發送上來的感測資料
-
-
sensor-gateway-server
-
安裝在閘道器上,負責管理ANT裝置資訊,包含裝置ID與類型的資訊對應清單
-
-
sensor-data-api
-
安裝在伺服器端,讀取ClickHouse資料庫的感測資料,以及處理前端發送的各式請求
-
以及使用socketio建立持久的連線,與前端之間做遊戲房間的管理
-
-
軟硬體系統結合的設計與實作(3/8)
-
設計議題:
-
對於sensor-gateway-fetcher中,擷取裝置的感測資料的議題
-
在Scanner模式下,為了避免擷取到其他的具踏頻的ANT裝置的資料
-
因此透過sensor-gateway-server去做感測裝置的白名單管理
-
-
對於sensor-data-api中,接收感測資料與儲存策略的議題
-
由於接收到的感測資料具有時序性,且時間間隔非固定的時間
-
因此使用ClickHouse資料庫進行儲存感測資料
-
-
剩下關於關聯性的資料,例如:帳號資訊、帳號與房間資訊等資料
-
使用PostgreSQL資料庫,並針對前述的關聯性資料進行管理
-
-
前端需要實現遊戲房間情境
-
因此使用socketio建立持久的連線,與前端之間搭配進行遊戲房間管理
-
-
-
軟硬體系統結合的設計與實作(4/8)
-
sensor-data-api使用到的技術如下:
-
FastAPI框架,負責實作各式API服務
-
yoyo-migrations,對資料表做管理,包含Seed與Migration等
-
clickhouse-connect,與ClickHouse資料庫溝通並進行資料的儲存
-
psycopg2,用於與PostgreSQL資料庫溝通與儲存關聯式資料,例如:會員、使用者與裝置資料等
-
python-socketio與websockets,用於與前端socketio協定溝通的套件
-
python-dotenv,用於Dotenv檔案與環境變數管理的套件
-
redis,用於與Redis溝通,並將需要快取的資料進行操作與儲存的套件
-
-
sport_app為前端,供使用者操作與使用,使用到的技術如下:
-
React.js,https://react.dev
軟硬體系統結合的設計與實作(5/8)
-
WebBridge (運動存摺大螢幕地端應用),使用到的技術有
-
TypeScript
-
Node.js
-
pkg,用來建置跨平台的可執行檔,執行在投影螢幕地端,去偵測網頁瀏覽器
-
eventsource,建立Event Stream,持續接收sensor-data-api(後端)來的事件
-
直到網頁瀏覽器啟動與影片準備開始播放
-
-
軟硬體系統結合的設計與實作(6/8)
-
使用python-socketio與websockets實作房間管理的機制,以時序圖表示(1/2):

軟硬體系統結合的設計與實作(7/8)
-
使用python-socketio與websockets實作房間管理的機制,以時序圖表示(2/2):

軟硬體系統結合的設計與實作(8/8)
-
關於sensor-data-api(後端),Socket.IO server之高可用性議題
-
因為搭配Nginx反向代理,為了要設定負載平衡時,需要注意的問題
-
透過負載平衡的方式,在Nginx設定多個Scket.IO Server
-
參考資料:https://flask-socketio.readthedocs.io/en/latest/deployment.html
-
# /etc/nginx/sites-available/sensor_data_api
upstream socketio_nodes {
ip_hash;
server 127.0.0.1:8446;
server 127.0.0.1:8447;
server 127.0.0.1:8448;
server 127.0.0.1:8449;
server 127.0.0.1:8450;
server 127.0.0.1:8451;
server 127.0.0.1:8452;
server 127.0.0.1:8453;
server 127.0.0.1:8454;
server 127.0.0.1:8455;
server 127.0.0.1:8456;
}
upstream sse_backend {
server 127.0.0.1:8446;
server 127.0.0.1:8447;
server 127.0.0.1:8448;
server 127.0.0.1:8449;
server 127.0.0.1:8450;
server 127.0.0.1:8451;
server 127.0.0.1:8452;
server 127.0.0.1:8453;
server 127.0.0.1:8454;
server 127.0.0.1:8455;
server 127.0.0.1:8456;
}
server {
server_name sensor-data-api.sportservice.tw www.sensor-data-api.sportservice.tw;
location / {
add_header Access-Control-Allow-Origin '*' always;
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Allow-Methods '*';
if ($request_method = 'OPTIONS') {
return 204;
}
include proxy_params;
proxy_hide_header Access-Control-Allow-Origin;
proxy_pass http://127.0.0.1:8446;
}
location /socket.io {
include proxy_params;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://socketio_nodes/socket.io;
}
location /browser_status {
proxy_pass http://sse_backend/browser_status;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
chunked_transfer_encoding off;
}
listen 8445 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/sensor-data-api.sportservice.tw/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/sensor-data-api.sportservice.tw/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
應用實證案例(一)
理想與現實的差距
應用實證案例(一)
-
在指定的場域進行第一次的應用案例與場域實證
-
去驗證前述軟硬體整合的系統是否可行
-
實際活動照片網址
-
指定的場域是在Living Lab+
-
資策會共創生活實驗室Living Lab+https://maps.app.goo.gl/VF8poitZfHNW9yu78
-


應用實證案例(一)遇到的問題(1/3)
-
場域實證時遇到的問題
-
網路延遲的問題(1/2)
-
地端電腦針對270度環景螢幕做連動片,延遲導致影片未播放但是遊戲已經開始
-
加強地端電腦上的WebBridge偵測是否遊戲已經開始,避免這類的延遲
-
-
每個使用者利用自己的手機掃描健身自行車上的客製化QRCode
-
使用者能夠在體驗健身自行車時,同時能夠看到手機畫面去了解目前騎車狀態
-
最後顯示成績也是在個別的使用者螢幕顯示
-
-
-
應用實證案例(一)遇到的問題(2/3)
-
場域實證時遇到的問題
-
網路延遲的問題(2/2)
-
有時遊戲在遊玩過程中,成績在最後統計與紀錄時,每支手機的成績會不一致
-
場地提供的WiFi的訊號也較差
-
由於使用者使用自己的手機與通訊4G/5G網路,導致每個人網路延遲不一
-
讓地端電腦也能夠收集該場次體驗的感測資料,並將成績統計在環景螢幕上
-
考慮之後成績統一在環景螢幕上顯示
-
遊玩時間太久,使用者會受不了;體驗時間從5分鐘改為3分鐘
-
-
-
應用實證案例(一)遇到的問題(3/3)
-
場域實證後遇到的問題
-
閘道器收集踏頻感測資料導致延遲的問題
-
閘道器在透過ANT通訊協定,去收集踏頻感測器資料,會有延遲與感測距離議題
-
為了解決這類的問題,考慮調整閘道器與健身自行車的擺放位置
-
考慮安裝在閘道器上的ANT接收器加裝增益天線,增加接收的訊號
-
考慮使用其他的方案,例如:Arofly提供踏頻感測器、心率與ANT接收盒整合方案
-
接收盒號稱是感測距離在無障礙的環境時,能夠到20公尺以上
-
-
-
依據執行應用實證案例(一)的結果進行改良(1/4)
-
Arofly表示能夠提供完整感測方案,加強接收感測資料
-
需要先使用其提供的心率手環,與Arofly的踏頻感測器配對
-
配對完成後,心率手環以ANT協定的方式,持續發送感測資料
-
並由ANT接收盒持續接收前述的感測資料,包含心率與踏頻的感測資料
-
因此閘道器不再負責直接與踏頻感測資料進行溝通,轉而向ANT接收盒取得感測資料
-



photo credit: Amazon
依據執行應用實證案例(一)的結果進行改良(2/4)

依據執行應用實證案例(一)的結果進行改良(3/4)
-
sensor-gateway-fetcher
-
需要將ANT通訊協定溝通的Publisher改成UDP Socket接收
-
接收到的心率與踏頻感測資料,因此修改MQTT Publisher的JSON訊息內容
-
-
sensor-gateway-server
-
由於會有心率裝置的資料,因此需要修改裝置清單的對應的管理機制
-
改成心率與踏頻裝置對應的清單
-
-
sensor-data-api
-
由於多了心率的資料,因此傳送給前端的感測資料格式將會改變
-
依據執行應用實證案例(一)的結果進行改良(4/4)
-
sport_app前端
-
修改從後端取得的資料,以及修改前端輸出的畫面
-
包含心率與踏頻資料等
-
Arofly接收盒整合問題(1/5)
-
提供的接收盒是以WiFi並搭配AP的方式將收集到的資料發送出去
-
但是操控接收盒的方式較為複雜,以下是接收盒的機制
-
開關通電或是透過PIN腳開啟接收盒後,盒子上面有個黑色按鈕
-
長按之後直到另外一邊亮綠色則啟動成接收模式
-
當資料接進來之後,接收盒會透過UDP協定建立socket server
-
若附近沒有ANT的裝置將資料傳遞到接收盒,則預設接收盒會關機
-
-
同時,心率手環與踏頻器配對上,也相對來的複雜
-
首先,心率手環短按一次之後,會開始配對,之後都會讓手環持久記憶該配對的踏頻器
-
當心率手環沒有偵測到心率等生理訊號後,則手環會進入待機模式
-
若要將手環關機,則長按6秒變成白燈閃爍到沒有燈號即完成關機
-
Arofly接收盒整合問題(2/5)
-
同時,心率手環與踏頻器配對問題
-
心率手環能夠一次配對多個踏頻器
-
為了要讓心率手環能夠與踏頻器一對一配對,避免手環配對到其他已經配對的踏頻器
-
在配對的過程中,要讓手環與踏頻器遠離其他的踏頻器
-
配對後的心率手環與踏頻器在長時間未使用時,會出現下列的情況:
-
心率手環會自行轉成待機狀態,需要手動按下手環按鈕後,才會喚醒
-
廠商表示,修改過後的心率手環的韌體有問題....
-
有的心率手環在喚醒切換後,自己死機....
-
-
Arofly接收盒整合問題(3/5)
-
測試後,發現的各式議題
-
ANT+接收盒需要時常注意其行為是否有進行運作
-
有時長按仍無法切換至接收模式,是時會發生過從接收模式自己切換成非接收模式
-
-
心率手環在充電時,需要按一下手環上的按鈕,才會進入開始充電的模式
-
心率手環戴在手上時,有時也會轉成待機狀態
-
需要再按一下才會變成啟動模式回到運作狀態
-
廠商表示,還是與手環的韌體有關....
-
-
Arofly接收盒整合問題(4/5)
-
測試後,與ANT接收盒串接溝通所收集到感測資料的議題
-
在閘道器上,建立的UDP Socket Server並與ANT接收盒溝通與收集感測資料
-
從ANT接收盒收集到的感測資料,是原始的資料,並以16進位表示,詳細的解釋如下:
-
未配對的心率手環資料範例:A40E4E0080FFFFFFD1020100008E897A0531
-
心率手環與踏頻器配對後的資料範例如下:
-
心率資料:A40E4E0010150000B8223C138022201105C2
-
踏頻資料:A40E4E001900B400002F70308022201105C3
-
-

Arofly接收盒整合問題(5/5)
-
測試後,與ANT接收盒串接溝通所收集到感測資料的議題
-
為了要測試ANT接收盒與配對的狀態,因此在sensor-gateway-fetcher專案中
-
實作arofly_pairing_checker.py,檢查心率手環與踏頻感測器配對的狀態
-
實作arofly_ping_checker.py,檢查接收盒目前的狀態
-
-
-
為了要與ANT接收盒進行整合,在sensor-gateway-fetcher專案中
-
實作arofly_publisher.py,將透過UDP Socket,從ANT接收盒取得到的資料
-
透過MQTT協定的方式發送到遠端的伺服器上
-
-
實作arofly_subscriber.py並部署在遠端的伺服器上
-
透過訂閱MQTT協定上指定的Channel,將感測資料進行收集並儲存到ClickHouse資料庫中
-
-
應用實證案例(二)
各式不確定的因素下,反覆測試與實證
應用實證案例(二)、各實證場地
-
實證場地1
-
2025 TaiSPO台灣國際運動及健身展
-
南港展覽館
-
-
實證場地2
-
資策會共創生活實驗室Living Lab+
-
場域實證結論
-
場地限制上,具有不可抗力的因素
-
不可抗力的因素比預期的還要多
-
網路延遲是整個體驗最主要的因素,因此有的實證會考慮導入5G專網
-
-
在完全信任廠商的前提下,期待越大,容易失望也越大
-
本來以為導入了廠商所提供的心率手環外加感測盒之綜合解決方案
-
透過前述的解決方案,能夠解決ANT+上之感測距離問題
-
非但沒有解決,反而多了額外的問題需要進行交互驗證與測試
-
內部測試Demo影片片段(1/2)
內部測試Demo影片片段(2/2)
最後盤點那些陣亡的裝置
-
廠商提供的1支心率手環死機
-
廠商提供的心率手環是魔改過韌體版本,有機率會死機
-
-
辦公室的兩台樹莓派4直接燒毀
-
內部晶片燒毀,且有燒焦味
-
未來展望(1/2)
-
感測裝置(硬體)部分
-
將ANT+接收器加裝更強的增益天線,將感測距離拉長
-
移除廠商的解決方案(ANT+接收盒與修改過韌體的心率手環)
-
考慮將收集資料的閘道器設計成高可用性的架構方案
-
-
使用者體驗
-
考慮體驗服務的使用者與網路延遲,改造遊戲流程,例如:淡化競速的元素
-
未來展望(2/2)
-
資料收集的方式
-
考慮導入NiFi或是Airflow的工具
-
實現資料中台或是資料流(ETL)的概念
-
並讓整體收集資料的過程,更佳順暢與流程化
-
-
-
導入AI模型實現預測
-
收集每次使用者體驗的遊玩紀錄,進行使用者行為分析與預測
-
Q&A
Thank you!
基於沉浸式體驗的健身自行車服務開發經驗談
By peter279k
基於沉浸式體驗的健身自行車服務開發經驗談
PyCon TW 2025
- 101