基於沉浸式體驗的健身自行車服務開發經驗談

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)

      • 設定藍牙接收器的步驟如下:

        1. ​​停用內建藍牙感測器

        2. 修改藍牙設定,改為multiple模式

        3. 維持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)

  • 考慮使用的藍牙函式庫如下:

    • pycycling

      • 授權: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接收器

運動感測的通訊協定介紹與比較(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系列的作業系統上,執行下列的指令進行設定與解決:

$ 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為前端,供使用者操作與使用,使用到的技術如下:

軟硬體系統結合的設計與實作(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)

# /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

}

應用實證案例(一)

理想與現實的差距

應用實證案例(一)

應用實證案例(一)遇到的問題(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資料庫中

應用實證案例(二)

各式不確定的因素下,反覆測試與實證

應用實證案例(二)、各實證場地

場域實證結論

  • 場地限制上,具有不可抗力的因素

    • 不可抗力的因素比預期的還要多

    • 網路延遲是整個體驗最主要的因素,因此有的實證會考慮導入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