Compare commits
3 Commits
opencv_and
...
chart
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b73003316d | ||
|
|
2bbbd0b9bf | ||
|
|
1f03a5e10b |
2
.github/FUNDING.yml
vendored
@@ -7,6 +7,6 @@ ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: cagnulein
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://www.buymeacoffee.com/cagnulein'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -32,10 +32,5 @@ If applicable, add screenshots to help explain your problem.
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Append a debug log**
|
||||
|
||||
Follow this guide https://github.com/cagnulein/qdomyos-zwift/wiki/How-do-i-get-the-debug-log-in-case-something-doesn't-work%3F
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[REQ]"
|
||||
labels: enhancement
|
||||
assignees: cagnulein
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
17
.github/stale.yml
vendored
@@ -1,17 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 15
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
#closeComment: false
|
||||
533
.github/workflows/main.yml
vendored
@@ -2,276 +2,18 @@
|
||||
|
||||
name: CI
|
||||
|
||||
env:
|
||||
DISPLAY: ':99'
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the master branch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master, github-workflow-playground ]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
# schedule:
|
||||
# - cron: "0 */12 * * *"
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
window-build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- {python: true}
|
||||
- {python: false}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: microsoft/MSIX-Toolkit
|
||||
path: "src/MSIX-Toolkit/"
|
||||
ref: b82af826d29e93e4c85d34fad8a405b6c49905e7
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.10.11
|
||||
- name: download python and paddleocr
|
||||
run: |
|
||||
python -VV
|
||||
python -m pip install opencv-python
|
||||
python -m pip install pywin32
|
||||
python -m pip install paddlepaddle-gpu==2.4.2.post117 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html
|
||||
python -m pip install https://files.pythonhosted.org/packages/03/ac/13fbe0ebf110d57a89f055a292d4fe430fee3fb22c56f8c077e63e0c5a4e/paddlepaddle-2.4.2-cp310-cp310-win_amd64.whl
|
||||
python -m pip install paddleocr>=2.0.1
|
||||
if: matrix.config.python
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
install: mingw-w64-x86_64-toolchain mingw-w64-x86_64-qt5-webview
|
||||
msystem: mingw64
|
||||
release: false
|
||||
|
||||
- name: Setup cmake
|
||||
uses: jwlawson/actions-setup-cmake@v1.9
|
||||
with:
|
||||
cmake-version: '3.20.x'
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: '5.15.2'
|
||||
host: 'windows'
|
||||
modules: 'qtnetworkauth qtcharts'
|
||||
target: "desktop"
|
||||
arch: win64_mingw81
|
||||
dir: "${{github.workspace}}/qt/"
|
||||
install-deps: "true"
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-windows'
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
qmake
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
cd ..
|
||||
make -j8
|
||||
cd src/debug
|
||||
mkdir output
|
||||
mkdir appx
|
||||
cp qdomyos-zwift.exe output/
|
||||
cd output
|
||||
windeployqt --qmldir ../../ qdomyos-zwift.exe
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libwinpthread-1.dll" .
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libgcc_s_seh-1.dll" .
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libstdc++-6.dll" .
|
||||
cp ../../../icons/iOS/iTunesArtwork@2x.png .
|
||||
cp ../../AppxManifest.xml .
|
||||
cp ../../windows/*.py .
|
||||
mkdir adb
|
||||
mkdir python
|
||||
Copy-Item -Path C:\hostedtoolcache\windows\Python\3.10.11\x64 -Destination python -Recurse
|
||||
cp ../../adb/* adb/
|
||||
cd ..
|
||||
cd appx
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Build without python
|
||||
run: |
|
||||
qmake
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
cd ..
|
||||
make -j8
|
||||
cd src/debug
|
||||
mkdir output
|
||||
mkdir appx
|
||||
cp qdomyos-zwift.exe output/
|
||||
cd output
|
||||
windeployqt --qmldir ../../ qdomyos-zwift.exe
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libwinpthread-1.dll" .
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libgcc_s_seh-1.dll" .
|
||||
cp "C:/ProgramData/Chocolatey/lib/mingw/tools/install/mingw64/bin/libstdc++-6.dll" .
|
||||
cp ../../../icons/iOS/iTunesArtwork@2x.png .
|
||||
cp ../../AppxManifest.xml .
|
||||
mkdir adb
|
||||
cp ../../adb/* adb/
|
||||
cd ..
|
||||
cd appx
|
||||
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
|
||||
if: matrix.config.python == false
|
||||
|
||||
- name: Zip artifact for deployment
|
||||
run: Compress-Archive src/debug/output release.zip
|
||||
|
||||
- name: Archive windows binary
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: windows-binary
|
||||
path: release.zip
|
||||
if: matrix.config.python
|
||||
|
||||
- name: Archive windows binary
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: windows-binary-no-python
|
||||
path: release.zip
|
||||
if: ${{ ! matrix.config.python }}
|
||||
|
||||
# window-steam-build:
|
||||
# runs-on: windows-latest
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Checkout submodule repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# repository: bluetiger9/SmtpClient-for-Qt
|
||||
# path: "src/smtpclient/"
|
||||
# ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
#
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Checkout submodule repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# repository: cagnulein/qmdnsengine
|
||||
# path: "src/qmdnsengine/"
|
||||
# ref: "zwift"
|
||||
#
|
||||
# - uses: msys2/setup-msys2@v2
|
||||
# with:
|
||||
# install: mingw-w64-x86_64-toolchain
|
||||
# msystem: mingw64
|
||||
# release: false
|
||||
#
|
||||
# - name: Setup cmake
|
||||
# uses: jwlawson/actions-setup-cmake@v1.9
|
||||
# with:
|
||||
# cmake-version: '3.20.x'
|
||||
#
|
||||
# - name: Install Qt
|
||||
# uses: jurplel/install-qt-action@v2
|
||||
# with:
|
||||
# version: '5.15.2'
|
||||
# host: 'windows'
|
||||
# modules: 'qtnetworkauth qtcharts'
|
||||
# target: "desktop"
|
||||
# arch: win64_mingw81
|
||||
# dir: "${{github.workspace}}/qt/"
|
||||
# install-deps: "true"
|
||||
#
|
||||
# - name: Build
|
||||
# run: |
|
||||
# qmake
|
||||
# cd src
|
||||
# echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
# echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
# echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
# echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
# echo "#define STEAM_STORE" >> secret.h
|
||||
# cd ..
|
||||
# make -j8
|
||||
# cd src/debug
|
||||
# mkdir output
|
||||
# mkdir appx
|
||||
# cp qdomyos-zwift.exe output/
|
||||
# cd output
|
||||
# windeployqt --qmldir ../../ qdomyos-zwift.exe
|
||||
# cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libwinpthread-1.dll" .
|
||||
# cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libgcc_s_seh-1.dll" .
|
||||
# cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libstdc++-6.dll" .
|
||||
#
|
||||
# - uses: game-ci/steam-deploy@v1
|
||||
# with:
|
||||
# username: ${{ secrets.STEAM_USERNAME }}
|
||||
# password: ${{ secrets.STEAM_PASSWORD }}
|
||||
# configVdf: ${{ secrets.STEAM_CONFIG_VDF}}
|
||||
# ssfnFileName: ${{ secrets.STEAM_SSFN_FILE_NAME }}
|
||||
# ssfnFileContents: ${{ secrets.STEAM_SSFN_FILE_CONTENTS }}
|
||||
# appId: 2267200
|
||||
# buildDescription: 2.12
|
||||
# rootPath: src/debug/output
|
||||
# depot1Path: ./
|
||||
# #depot2Path: StandaloneLinux64
|
||||
# releaseBranch: prerelease
|
||||
|
||||
# This workflow contains a single job called "build"
|
||||
linux-x86-build:
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -291,94 +33,28 @@ jobs:
|
||||
# path: '${{ github.workspace }}/output/android/'
|
||||
# key: ${{ runner.os }}-QtCache-Android
|
||||
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
# Runs a set of commands using the runners shell
|
||||
- name: Install Qt Linux Desktop
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout qHttpServer
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: qt-labs/qthttpserver
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: '5.15.2'
|
||||
version: '5.12.9'
|
||||
host: 'linux'
|
||||
modules: 'qtnetworkauth qtcharts'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-linux'
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: |
|
||||
cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Build qthttpserver
|
||||
run: |
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
target: 'desktop'
|
||||
modules: 'qtcharts debug_info qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/linux-desktop/'
|
||||
# cached: ${{ steps.cache-qt-linux-desktop.outputs.cache-hit }}
|
||||
|
||||
- name: Compile Linux Desktop
|
||||
run: qmake; make -j8
|
||||
|
||||
run: cd src; qmake; make -j4
|
||||
|
||||
- name: Archive linux-desktop binary
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: linux-desktop-binary
|
||||
path: src/qdomyos-zwift
|
||||
|
||||
- name: Test
|
||||
run: cd tst; GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests; cd ..
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: test_results_xml
|
||||
path: tst/test-results/**/*.xml
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
# run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-peloton -peloton-username ${{ secrets.peloton_username }} -peloton-password ${{ secrets.peloton_password }}
|
||||
# timeout-minutes: 2
|
||||
|
||||
# - name: Test Home Fitness Buddy API
|
||||
# run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-hfb
|
||||
# timeout-minutes: 2
|
||||
path: src/qdomyos-zwift
|
||||
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
@@ -410,7 +86,7 @@ jobs:
|
||||
# modules: 'qtcharts debug_info'
|
||||
# dir: '${{ github.workspace }}/output/android/'
|
||||
# cached: ${{ steps.cache-qt-android.outputs.cache-hit }}
|
||||
|
||||
|
||||
# - name: Compile Android
|
||||
# run: cd src; qmake; make -j4
|
||||
|
||||
@@ -422,183 +98,6 @@ jobs:
|
||||
# target: 'desktop'
|
||||
# modules: 'qtcharts debug_info'
|
||||
# dir: '${{ github.workspace }}/output/macos/'
|
||||
|
||||
|
||||
# - name: Compile MacOS
|
||||
# run: cd src; qmake; make -j4
|
||||
|
||||
|
||||
# This workflow contains a single job called "build"
|
||||
android-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# - name: Cache Qt Linux Desktop
|
||||
# id: cache-qt-linux-desktop
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: '${{ github.workspace }}/output/linux-desktop/'
|
||||
# key: ${{ runner.os }}-QtCache-Linux-Desktop
|
||||
|
||||
# - name: Cache Qt Linux Android
|
||||
# id: cache-qt-android
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: '${{ github.workspace }}/output/android/'
|
||||
# key: ${{ runner.os }}-QtCache-Android
|
||||
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
# run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-peloton -peloton-username ${{ secrets.peloton_username }} -peloton-password ${{ secrets.peloton_password }}
|
||||
# timeout-minutes: 2
|
||||
|
||||
# - name: Test Home Fitness Buddy API
|
||||
# run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-hfb
|
||||
# timeout-minutes: 2
|
||||
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
# repository: nttld/setup-ndk
|
||||
# path: setup-ndk
|
||||
# The packages.json in nttld/setup-ndk has already been updated,
|
||||
# https://github.com/nttld/setup-ndk/commit/831db5b02a0f0cab80614619efe461a3dcc140e6
|
||||
# but `dist/*` has not been rebuilt yet. Build it.
|
||||
# https://github.com/nttld/setup-ndk/tree/main/dist
|
||||
# - name: Locally rebuilt setup-ndk
|
||||
# run: |
|
||||
# npm -prefix ./setup-ndk install
|
||||
# npm -prefix ./setup-ndk run all
|
||||
# Install using locally rebuilt setup-ndk
|
||||
# - name: Setup Android NDK r21d
|
||||
# uses: ./setup-ndk
|
||||
#- uses: nttld/setup-ndk@v1
|
||||
# with:
|
||||
# ndk-version: r21d
|
||||
|
||||
# waiting github.com/jurplel/install-qt-action/issues/63
|
||||
- name: Install Qt Android
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: '5.15.2'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11'
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
|
||||
|
||||
ios-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: bluetiger9/SmtpClient-for-Qt
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: google/googletest
|
||||
path: "tst/googletest/"
|
||||
ref: "release-1.12.1"
|
||||
|
||||
- name: Install Qt iOS
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: '5.15.2'
|
||||
host: 'mac'
|
||||
target: 'ios'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/ios/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-ios'
|
||||
|
||||
- name: fix qt
|
||||
run: find ${{ github.workspace }}/output/ios/ -name 'ios.conf' -exec sed -i '' 's/ios-simulator/iphonesimulator/g' {} \;
|
||||
|
||||
- name: fix qt
|
||||
run: find ${{ github.workspace }}/output/ios/ -name 'devices.py' -exec sed -i '' 's/\/usr\/bin\/python/\/usr\/bin\/python3/g' {} \;
|
||||
|
||||
- name: fix qt
|
||||
run: find ./ -name 'qdomyos-zwift-lib.pro' -exec sed -i '' 's/TARGET = qdomyos-zwift/TARGET = qdomyoszwift/g' {} \;
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/ios/5.15.2/binary/*.* ${{ github.workspace }}/output/ios/Qt/5.15.2/ios/lib/
|
||||
|
||||
- name: Build
|
||||
run: qmake CONFIG+=debug && make -j4
|
||||
|
||||
|
||||
35
.gitignore
vendored
@@ -13,39 +13,4 @@ src/qdomyos-zwift
|
||||
src/ui_charts.h
|
||||
src/ui_mainwindow.h
|
||||
|
||||
src/build/*
|
||||
src/build/*
|
||||
|
||||
src/debug-*
|
||||
|
||||
src/secret.h
|
||||
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
build-qdomyos-zwift-Android_Qt_5_15_2_Clang_Multi_Abi-Debug/*
|
||||
**/node_modules/*
|
||||
|
||||
template-examples/youtube-viewer/node_modules/*
|
||||
template-examples/youtube-viewer/*.json
|
||||
template-examples/youtube-viewer/.eslintrc.js
|
||||
template-examples/youtube-viewer/.jshintrc
|
||||
template-examples/youtube-viewer/debug.js
|
||||
|
||||
|
||||
template-examples/train-program-saver/node_modules/*
|
||||
template-examples/train-program-saver/*.json
|
||||
template-examples/train-program-saver/.eslintrc.js
|
||||
template-examples/train-program-saver/.jshintrc
|
||||
template-examples/train-program-saver/debug.js
|
||||
|
||||
google_test/*
|
||||
|
||||
# Qt-es
|
||||
*.pro.user
|
||||
*build-*
|
||||
!build-qdomyos-zwift-Qt_*_for_iOS-Debug # Needed for Apple Watch
|
||||
src/inner_templates/googlemaps/cesium-key.js
|
||||
*.autosave
|
||||
.vscode/settings.json
|
||||
/tst/Devices/.vs
|
||||
|
||||
12
.gitmodules
vendored
@@ -1,15 +1,3 @@
|
||||
[submodule "android_openssl"]
|
||||
path = android_openssl
|
||||
url = https://github.com/KDAB/android_openssl.git
|
||||
[submodule "src/smtpclient"]
|
||||
path = src/smtpclient
|
||||
url = https://github.com/cagnulein/SmtpClient-for-Qt.git
|
||||
branch = cagnulein-patch-2
|
||||
[submodule "src/qmdnsengine"]
|
||||
path = src/qmdnsengine
|
||||
url = https://github.com/cagnulein/qmdnsengine.git
|
||||
branch = zwift
|
||||
[submodule "tst/googletest"]
|
||||
path = tst/googletest
|
||||
url = https://github.com/google/googletest.git
|
||||
branch = tags/release-1.12.1
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
roberto.viola83@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -1,437 +0,0 @@
|
||||
/** NimBLE_Server Demo:
|
||||
*
|
||||
This is working to broadcast Power and Cadence under the Cycling Power Service Profile
|
||||
Data tested against Edge and Phone
|
||||
*
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
|
||||
short powerInstantaneous = 0;
|
||||
short cadenceInstantaneous = 0;
|
||||
short speedInstantaneous = 0;
|
||||
float powerScale = 1.28; // incoming power is multiplied by this value for correction
|
||||
short resistance = 0; //Not currently doing anything with this value after receiving it
|
||||
bool notify = false;
|
||||
|
||||
// Define stuff for the Client that will receive data from Fitness Machine
|
||||
// The remote service we wish to connect to.
|
||||
static BLEUUID serviceUUID("1826"); // Fitness Machine
|
||||
// The characteristic of the remote service we are interested in.
|
||||
static BLEUUID charUUID("2ad2"); // Indoor Bike (Fitness Machine)
|
||||
|
||||
|
||||
static BLEUUID HRserviceUUID("180D"); // HR Service
|
||||
static BLEUUID HRcharUUID("2a37"); // HR Measuremente
|
||||
|
||||
static boolean doConnect = false;
|
||||
static boolean connected = false;
|
||||
static boolean doScan = false;
|
||||
static BLERemoteCharacteristic *pRemoteCharacteristic;
|
||||
static BLEAdvertisedDevice *myDevice;
|
||||
/*
|
||||
* Server Stuff
|
||||
*/
|
||||
static NimBLEServer *pServer;
|
||||
/** None of these are required as they will be handled by the library with defaults. **
|
||||
** Remove as you see fit for your needs */
|
||||
class ServerCallbacks : public NimBLEServerCallbacks
|
||||
{
|
||||
void onConnect(NimBLEServer *pServer)
|
||||
{
|
||||
Serial.println("Client connected");
|
||||
Serial.println("Multi-connect support: start advertising");
|
||||
NimBLEDevice::startAdvertising();
|
||||
};
|
||||
/** Alternative onConnect() method to extract details of the connection.
|
||||
* See: src/ble_gap.h for the details of the ble_gap_conn_desc struct.
|
||||
*/
|
||||
void onConnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
|
||||
{
|
||||
Serial.print("Client address: ");
|
||||
Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str());
|
||||
/** We can use the connection handle here to ask for different connection parameters.
|
||||
* Args: connection handle, min connection interval, max connection interval
|
||||
* latency, supervision timeout.
|
||||
* Units; Min/Max Intervals: 1.25 millisecond increments.
|
||||
* Latency: number of intervals allowed to skip.
|
||||
* Timeout: 10 millisecond increments, try for 5x interval time for best results.
|
||||
*/
|
||||
pServer->updateConnParams(desc->conn_handle, 24, 48, 0, 60);
|
||||
};
|
||||
void onDisconnect(NimBLEServer *pServer)
|
||||
{
|
||||
Serial.println("Client disconnected - start advertising");
|
||||
NimBLEDevice::startAdvertising();
|
||||
};
|
||||
void onMTUChange(uint16_t MTU, ble_gap_conn_desc *desc)
|
||||
{
|
||||
Serial.printf("MTU updated: %u for connection ID: %u\n", MTU, desc->conn_handle);
|
||||
};
|
||||
};
|
||||
|
||||
/** Handler class for characteristic actions */
|
||||
class CharacteristicCallbacks : public NimBLECharacteristicCallbacks
|
||||
{
|
||||
void onRead(NimBLECharacteristic *pCharacteristic)
|
||||
{
|
||||
Serial.print(pCharacteristic->getUUID().toString().c_str());
|
||||
Serial.print(": onRead(), value: ");
|
||||
Serial.println(pCharacteristic->getValue().c_str());
|
||||
};
|
||||
|
||||
void onWrite(NimBLECharacteristic *pCharacteristic)
|
||||
{
|
||||
Serial.print(pCharacteristic->getUUID().toString().c_str());
|
||||
Serial.print(": onWrite(), value: ");
|
||||
Serial.println(pCharacteristic->getValue().c_str());
|
||||
};
|
||||
/** Called before notification or indication is sent,
|
||||
* the value can be changed here before sending if desired.
|
||||
*/
|
||||
void onNotify(NimBLECharacteristic *pCharacteristic)
|
||||
{
|
||||
Serial.println("Sending notification to clients");
|
||||
};
|
||||
|
||||
/** The status returned in status is defined in NimBLECharacteristic.h.
|
||||
* The value returned in code is the NimBLE host return code.
|
||||
*/
|
||||
void onStatus(NimBLECharacteristic *pCharacteristic, Status status, int code)
|
||||
{
|
||||
String str = ("Notification/Indication status code: ");
|
||||
str += status;
|
||||
str += ", return code: ";
|
||||
str += code;
|
||||
str += ", ";
|
||||
str += NimBLEUtils::returnCodeToString(code);
|
||||
Serial.println(str);
|
||||
};
|
||||
|
||||
void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue)
|
||||
{
|
||||
String str = "Client ID: ";
|
||||
str += desc->conn_handle;
|
||||
str += " Address: ";
|
||||
str += std::string(NimBLEAddress(desc->peer_ota_addr)).c_str();
|
||||
if (subValue == 0)
|
||||
{
|
||||
str += " Unsubscribed to ";
|
||||
}
|
||||
else if (subValue == 1)
|
||||
{
|
||||
str += " Subscribed to notifications for ";
|
||||
}
|
||||
else if (subValue == 2)
|
||||
{
|
||||
str += " Subscribed to indications for ";
|
||||
}
|
||||
else if (subValue == 3)
|
||||
{
|
||||
str += " Subscribed to notifications and indications for ";
|
||||
}
|
||||
str += std::string(pCharacteristic->getUUID()).c_str();
|
||||
|
||||
Serial.println(str);
|
||||
};
|
||||
};
|
||||
|
||||
/** Handler class for descriptor actions */
|
||||
class DescriptorCallbacks : public NimBLEDescriptorCallbacks
|
||||
{
|
||||
void onWrite(NimBLEDescriptor *pDescriptor)
|
||||
{
|
||||
std::string dscVal((char *)pDescriptor->getValue(), pDescriptor->getLength());
|
||||
Serial.print("Descriptor witten value:");
|
||||
Serial.println(dscVal.c_str());
|
||||
};
|
||||
|
||||
void onRead(NimBLEDescriptor *pDescriptor)
|
||||
{
|
||||
Serial.print(pDescriptor->getUUID().toString().c_str());
|
||||
Serial.println(" Descriptor read");
|
||||
};
|
||||
};
|
||||
/*
|
||||
* Client Stuff
|
||||
*/
|
||||
// This callback is for when data is received from Server
|
||||
static void notifyCallback(
|
||||
BLERemoteCharacteristic *pBLERemoteCharacteristic,
|
||||
uint8_t *pData,
|
||||
size_t length,
|
||||
bool isNotify)
|
||||
{
|
||||
powerInstantaneous = pData[8] | pData[9] << 8; // 2 bytes of power
|
||||
cadenceInstantaneous = 60; //(pData[4] | pData[5] << 8) / 2; // 2 bytes of power in 0.5 resolution RPM, convert to RPM
|
||||
resistance = pData[6]; // 1 byte of resistance
|
||||
Serial.printf("Power = %d | Cadence = %d | Resistance = %d\n", powerInstantaneous, cadenceInstantaneous, resistance);
|
||||
}
|
||||
|
||||
/** None of these are required as they will be handled by the library with defaults. **
|
||||
** Remove as you see fit for your needs */
|
||||
class MyClientCallback : public BLEClientCallbacks
|
||||
{
|
||||
void onConnect(BLEClient *pclient)
|
||||
{
|
||||
}
|
||||
|
||||
void onDisconnect(BLEClient *pclient)
|
||||
{
|
||||
connected = false;
|
||||
Serial.println("onDisconnect");
|
||||
}
|
||||
};
|
||||
|
||||
bool connectToServer()
|
||||
{
|
||||
Serial.print("Forming a connection to ");
|
||||
Serial.println(myDevice->getAddress().toString().c_str());
|
||||
|
||||
BLEClient *pClient = BLEDevice::createClient();
|
||||
Serial.println(" - Created client");
|
||||
|
||||
pClient->setClientCallbacks(new MyClientCallback());
|
||||
|
||||
// Connect to the remove BLE Server.
|
||||
pClient->connect(myDevice); // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
|
||||
Serial.println(" - Connected to server");
|
||||
|
||||
// Obtain a reference to the service we are after in the remote BLE server.
|
||||
BLERemoteService *pRemoteService = pClient->getService(serviceUUID);
|
||||
if (pRemoteService == nullptr)
|
||||
{
|
||||
Serial.print("Failed to find our service UUID: ");
|
||||
Serial.println(serviceUUID.toString().c_str());
|
||||
pClient->disconnect();
|
||||
return false;
|
||||
}
|
||||
Serial.println(" - Found our service");
|
||||
|
||||
// Obtain a reference to the characteristic in the service of the remote BLE server.
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
|
||||
if (pRemoteCharacteristic == nullptr)
|
||||
{
|
||||
Serial.print("Failed to find our characteristic UUID: ");
|
||||
Serial.println(charUUID.toString().c_str());
|
||||
pClient->disconnect();
|
||||
return false;
|
||||
}
|
||||
Serial.println(" - Found our characteristic");
|
||||
|
||||
// Read the value of the characteristic.
|
||||
if (pRemoteCharacteristic->canRead())
|
||||
{
|
||||
std::string value = pRemoteCharacteristic->readValue();
|
||||
Serial.print("The characteristic value was: ");
|
||||
Serial.println(value.c_str());
|
||||
}
|
||||
|
||||
if (pRemoteCharacteristic->canNotify())
|
||||
pRemoteCharacteristic->registerForNotify(notifyCallback);
|
||||
|
||||
connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for BLE servers and find the first one that advertises the service we are looking for.
|
||||
*/
|
||||
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
|
||||
{
|
||||
/**
|
||||
* Called for each advertising BLE server.
|
||||
*/
|
||||
|
||||
/*** Only a reference to the advertised device is passed now
|
||||
void onResult(BLEAdvertisedDevice advertisedDevice) { **/
|
||||
void onResult(BLEAdvertisedDevice *advertisedDevice)
|
||||
{
|
||||
Serial.print("BLE Advertised Device found: ");
|
||||
Serial.println(advertisedDevice->toString().c_str());
|
||||
|
||||
// We have found a device, let us now see if it contains the service we are looking for.
|
||||
/********************************************************************************
|
||||
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
|
||||
********************************************************************************/
|
||||
if (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(serviceUUID))
|
||||
{
|
||||
|
||||
BLEDevice::getScan()->stop();
|
||||
/*******************************************************************
|
||||
myDevice = new BLEAdvertisedDevice(advertisedDevice);
|
||||
*******************************************************************/
|
||||
myDevice = advertisedDevice; /** Just save the reference now, no need to copy the object */
|
||||
doConnect = true;
|
||||
doScan = true;
|
||||
|
||||
} // Found our server
|
||||
} // onResult
|
||||
}; // MyAdvertisedDeviceCallbacks
|
||||
|
||||
//delays for X ms, should not block execution
|
||||
void softDelay(unsigned long delayTime)
|
||||
{
|
||||
unsigned long startTime = millis();
|
||||
while ((millis() - startTime) < delayTime)
|
||||
{
|
||||
//wait
|
||||
}
|
||||
}
|
||||
|
||||
/** Define callback instances globally to use for multiple Characteristics \ Descriptors */
|
||||
// This section is for the Server that will broadcast the data as Cycling Power
|
||||
static DescriptorCallbacks dscCallbacks;
|
||||
static CharacteristicCallbacks chrCallbacks;
|
||||
NimBLECharacteristic *CyclingPowerFeature = NULL;
|
||||
NimBLECharacteristic *CyclingPowerMeasurement = NULL;
|
||||
NimBLECharacteristic *CyclingPowerSensorLocation = NULL;
|
||||
NimBLECharacteristic *HRMeasurement = NULL;
|
||||
unsigned char bleBuffer[8];
|
||||
unsigned char slBuffer[1];
|
||||
unsigned char fBuffer[4];
|
||||
unsigned short revolutions = 0;
|
||||
unsigned short timestamp = 0;
|
||||
unsigned short flags = 0x20;
|
||||
byte sensorlocation = 0x0D;
|
||||
long lastNotify = 0;
|
||||
long lastRevolution = 0;
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
Serial.println("Starting NimBLE Server");
|
||||
|
||||
/** sets device name */
|
||||
NimBLEDevice::init("QZESP");
|
||||
/** Optional: set the transmit power, default is 3db */
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
|
||||
|
||||
pServer = NimBLEDevice::createServer();
|
||||
pServer->setCallbacks(new ServerCallbacks());
|
||||
|
||||
fBuffer[0] = 0x00;
|
||||
fBuffer[1] = 0x00;
|
||||
fBuffer[2] = 0x00;
|
||||
fBuffer[3] = 0x08;
|
||||
|
||||
slBuffer[0] = sensorlocation & 0xff;
|
||||
|
||||
NimBLEService *pDeadService = pServer->createService("1818");
|
||||
CyclingPowerFeature = pDeadService->createCharacteristic(
|
||||
"2A65",
|
||||
NIMBLE_PROPERTY::READ);
|
||||
CyclingPowerSensorLocation = pDeadService->createCharacteristic(
|
||||
"2A5D",
|
||||
NIMBLE_PROPERTY::READ);
|
||||
CyclingPowerMeasurement = pDeadService->createCharacteristic(
|
||||
"2A63",
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
|
||||
|
||||
CyclingPowerFeature->setValue(fBuffer, 4);
|
||||
CyclingPowerSensorLocation->setValue(slBuffer, 1);
|
||||
CyclingPowerMeasurement->setValue(slBuffer, 1);
|
||||
|
||||
/** Start the services when finished creating all Characteristics and Descriptors */
|
||||
pDeadService->start();
|
||||
|
||||
#if 0
|
||||
// HR service
|
||||
NimBLEService *pHRService = pServer->createService("180D");
|
||||
HRMeasurement = pHRService->createCharacteristic(
|
||||
"2A37",
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
|
||||
|
||||
HRMeasurement->setValue(fBuffer, 2);
|
||||
|
||||
/** Start the services when finished creating all Characteristics and Descriptors */
|
||||
pHRService->start();
|
||||
#endif
|
||||
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
/** Add the services to the advertisement data **/
|
||||
// pAdvertising->addServiceUUID(pHRService->getUUID());
|
||||
pAdvertising->addServiceUUID(pDeadService->getUUID());
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->start();
|
||||
|
||||
Serial.println("Advertising Started");
|
||||
|
||||
Serial.println("Starting Arduino BLE Client application...");
|
||||
BLEDevice::init("");
|
||||
|
||||
// Retrieve a Scanner and set the callback we want to use to be informed when we
|
||||
// have detected a new device. Specify that we want active scanning and start the
|
||||
// scan to run for 5 seconds.
|
||||
BLEScan *pBLEScan = BLEDevice::getScan();
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
|
||||
pBLEScan->setInterval(1349);
|
||||
pBLEScan->setWindow(449);
|
||||
pBLEScan->setActiveScan(true);
|
||||
pBLEScan->start(5, false);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// If the flag "doConnect" is true then we have scanned for and found the desired
|
||||
// BLE Server with which we wish to connect. Now we connect to it. Once we are
|
||||
// connected we set the connected flag to be true.
|
||||
if (doConnect == true)
|
||||
{
|
||||
if (connectToServer())
|
||||
{
|
||||
Serial.println("We are now connected to the BLE Server.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.println("We have failed to connect to the server; there is nothing more we will do.");
|
||||
}
|
||||
doConnect = false;
|
||||
}
|
||||
// If we are connected to a peer BLE Server, update the characteristic each time we are reached
|
||||
// with the current time since boot.
|
||||
if (connected)
|
||||
{
|
||||
//Stuff to do when connected to Client
|
||||
}
|
||||
else if (doScan)
|
||||
{
|
||||
BLEDevice::getScan()->start(0); // this is just sample to start scan after disconnect, most likely there is better way to do it in arduino
|
||||
}
|
||||
|
||||
// convert RPM to timestamp
|
||||
if (cadenceInstantaneous != 0 && (millis()) >= (lastRevolution + (60000 / cadenceInstantaneous)))
|
||||
{
|
||||
revolutions++; // One crank revolution should have passed, add one revolution
|
||||
timestamp = (unsigned short)(((millis() * 1024) / 1000) % 65536); // create timestamp and format
|
||||
lastRevolution = millis();
|
||||
}
|
||||
|
||||
if (millis() - lastNotify >= 1000) // do this every second
|
||||
{
|
||||
//if (pServer->getConnectedCount() > 0)
|
||||
{
|
||||
bleBuffer[0] = flags & 0xff;
|
||||
bleBuffer[1] = (flags >> 8) & 0xff;
|
||||
bleBuffer[2] = powerInstantaneous & 0xff;
|
||||
bleBuffer[3] = (powerInstantaneous >> 8) & 0xff;
|
||||
bleBuffer[4] = revolutions & 0xff;
|
||||
bleBuffer[5] = (revolutions >> 8) & 0xff;
|
||||
bleBuffer[6] = timestamp & 0xff;
|
||||
bleBuffer[7] = (timestamp >> 8) & 0xff;
|
||||
CyclingPowerMeasurement->setValue(bleBuffer, 8);
|
||||
CyclingPowerMeasurement->notify();
|
||||
|
||||
/*bleBuffer[0] = 0;
|
||||
bleBuffer[1] = powerInstantaneous;
|
||||
|
||||
HRMeasurement->setValue(bleBuffer, 2);
|
||||
HRMeasurement->notify();*/
|
||||
lastNotify = millis();
|
||||
}
|
||||
}
|
||||
/*if (pServer->getConnectedCount() == 0)
|
||||
{
|
||||
powerInstantaneous = 0;
|
||||
}*/
|
||||
}
|
||||
34
README.md
@@ -1,8 +1,6 @@
|
||||
# qdomyos-zwift
|
||||
Zwift bridge for Treadmills and Bike!
|
||||
|
||||
## QZ is not affiliated with or endorsed by any subscription service or maker of exercise equipment.
|
||||
|
||||
[<img src="docs/img/google_play.png">](https://play.google.com/store/apps/details?id=org.cagnulen.qdomyoszwift&fbclid=IwAR3CVoYb0scvGf7gb0Y20VFh5Na5fDWwe7VACk-2c45Tm0x5s8sXpIGhGyw)
|
||||
[<img src="docs/img/app_store.png">](https://apps.apple.com/app/id1543684531?fbclid=IwAR10H6y3mEgwkTlGJON3e8voYOh2wt3kLFOpFzoIXaYZ_N0y0pDvKxHMUaM)
|
||||
<a href="https://www.buymeacoffee.com/cagnulein" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
@@ -41,9 +39,35 @@ Read the [installation procedure](docs/10_Installation.md)
|
||||
|
||||
### Tested on
|
||||
|
||||
You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IOS and Android are also supported.
|
||||
- Raspberry PI 0W and Domyos Intense Run
|
||||
|
||||
QDomyos-Zwift works on every [FTMS-compatible application](docs/20_supported_devices_and_applications.md), and virtually any [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
|
||||
- MacBook Air 2011 and Domyos Intense Run
|
||||
|
||||
- Raspberry 3b+ and Domyos T900C
|
||||
|
||||
- Raspberry 3b+ and Toorx TRX Route Key
|
||||
|
||||
- Android Pixel 2 and Echelon Connect Sport
|
||||
|
||||
- Raspberry Pi 0W and SportsTech ESX500
|
||||
|
||||
### Your machine is not compatible?
|
||||
|
||||
Open an issue and follow these steps!
|
||||
|
||||
1. first of all you need an android device (phone or tablet)
|
||||
2. you need to become developer on your phone https://wccftech.com/how-to/how-to-enable-developer-options-on-android-10-tutorial/
|
||||
3. Go to Settings
|
||||
4. Go into developer options
|
||||
5. Enable the option Enable Bluetooth HCI snoop log
|
||||
6. restart your phone
|
||||
7. open your machine app and play with it collecting inclination and speed
|
||||
8. Disable the option Enable Bluetooth HCI snoop log
|
||||
9. in Developer Options: Bug report->Full report
|
||||
10. wait a random amount of time (10-20 seconds)
|
||||
11. A notification will appear at the top of the device. Click on it, share, email it to yourself
|
||||
12. You'll get a zip file with the entire report. In the FS/Data/Log/bt directory of the zipfile is the file you want.
|
||||
13. attach the log file in a new issue with a short description of the steps you did in the app when you used it
|
||||
|
||||
### No gui version
|
||||
|
||||
@@ -57,8 +81,6 @@ https://github.com/ProH4Ck/treadmill-bridge
|
||||
|
||||
https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/
|
||||
|
||||
Icons used in this documentation comes from [flaticon.com](https://www.flaticon.com)
|
||||
|
||||
### Blog
|
||||
|
||||
https://robertoviola.cloud
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.6.18" android:versionCode="128" android:installLocation="auto">
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.location" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="qdomyos-zwift" android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- Application arguments -->
|
||||
<!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ -->
|
||||
<!-- Application arguments -->
|
||||
<meta-data android:name="android.app.lib_name" android:value="qdomyos-zwift"/>
|
||||
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
|
||||
<!-- Deploy Qt libs as part of package -->
|
||||
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
|
||||
<!-- Run with local libs -->
|
||||
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
|
||||
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
|
||||
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
|
||||
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
|
||||
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
||||
<!-- Used to specify custom system library path to run with local system libs -->
|
||||
<!-- <meta-data android:name="android.app.system_libs_prefix" android:value="/system/lib/"/> -->
|
||||
<!-- Messages maps -->
|
||||
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
|
||||
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
|
||||
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
|
||||
<meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/>
|
||||
<!-- Messages maps -->
|
||||
<!-- Splash screen -->
|
||||
<!-- Orientation-specific (portrait/landscape) data is checked first. If not available for current orientation,
|
||||
then android.app.splash_screen_drawable. For best results, use together with splash_screen_sticky and
|
||||
use hideSplashScreen() with a fade-out animation from Qt Android Extras to hide the splash screen when you
|
||||
are done populating your window with content. -->
|
||||
<!-- meta-data android:name="android.app.splash_screen_drawable_portrait" android:resource="@drawable/logo_portrait" / -->
|
||||
<!-- meta-data android:name="android.app.splash_screen_drawable_landscape" android:resource="@drawable/logo_landscape" / -->
|
||||
<!-- meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/ -->
|
||||
<!-- meta-data android:name="android.app.splash_screen_sticky" android:value="true"/ -->
|
||||
<!-- Splash screen -->
|
||||
<!-- Background running -->
|
||||
<!-- Warning: changing this value to true may cause unexpected crashes if the
|
||||
application still try to draw after
|
||||
"applicationStateChanged(Qt::ApplicationSuspended)"
|
||||
signal is sent! -->
|
||||
<meta-data android:name="android.app.background_running" android:value="true"/>
|
||||
<!-- Background running -->
|
||||
<!-- auto screen scale factor -->
|
||||
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="full"/>
|
||||
<!-- auto screen scale factor -->
|
||||
<!-- extract android style -->
|
||||
<!-- available android:values :
|
||||
* default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
|
||||
* full - useful QWidget & Quick Controls 1 apps
|
||||
* minimal - useful for Quick Controls 2 apps, it is much faster than "full"
|
||||
* none - useful for apps that don't use any of the above Qt modules
|
||||
-->
|
||||
<meta-data android:name="android.app.extract_android_style" android:value="default"/>
|
||||
<!-- extract android style -->
|
||||
<meta-data android:name="com.amazon.input.cursor" android:value="pointer"/>
|
||||
</activity>
|
||||
<service android:name=".ChannelService"></service>
|
||||
<activity android:name="org.cagnulen.qdomyoszwift.MyActivity" />
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_CHECKIN_PROPERTIES"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
</manifest>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="ipad12_9rounded" orientation="landscape" layout="splitview2_3" appearance="light"/>
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
@@ -13,32 +13,32 @@
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="981" height="1024"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
|
||||
<rect key="frame" x="0.0" y="1004" width="981" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="876" width="414" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="launcher" translatesAutoresizingMaskIntoConstraints="NO" id="VVq-0c-S8O">
|
||||
<rect key="frame" x="20" y="114" width="941" height="776"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<rect key="frame" x="20" y="348" width="374" height="201"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" red="0.10980264100000001" green="0.11007446799999999" blue="0.1176523939" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="50.625" y="373.75"/>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
|
||||
@@ -54,10 +54,8 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "2"
|
||||
BundleIdentifier = "com.apple.Carousel"
|
||||
RemotePath = "/qdomyoszwift">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "876E4E102594747F00BD5714"
|
||||
@@ -65,7 +63,7 @@
|
||||
BlueprintName = "watchkit"
|
||||
ReferencedContainer = "container:qdomyoszwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</RemoteRunnable>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -73,10 +71,8 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "2"
|
||||
BundleIdentifier = "com.apple.Carousel"
|
||||
RemotePath = "/qdomyoszwift">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "876E4E102594747F00BD5714"
|
||||
@@ -84,14 +80,7 @@
|
||||
BlueprintName = "watchkit"
|
||||
ReferencedContainer = "container:qdomyoszwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableName = "watchkit.app"
|
||||
BlueprintName = "watchkit"
|
||||
ReferencedContainer = "container:qdomyoszwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -193,38 +193,6 @@
|
||||
endingLineNumber = "57"
|
||||
landmarkName = "BLEPeripheralManager"
|
||||
landmarkType = "3">
|
||||
<Locations>
|
||||
<Location
|
||||
uuid = "16D24B27-D0FB-4EC3-BAE8-56101FE7949B - 1c798ec95ff8d4b7"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "qdomyoszwift.BLEPeripheralManager.crankRevolutions.modify : Swift.Optional<Swift.UInt16>"
|
||||
moduleName = "qdomyoszwift"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/%3Ccompiler-generated%3E"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "0"
|
||||
endingLineNumber = "0"
|
||||
offsetFromSymbolStart = "16">
|
||||
</Location>
|
||||
<Location
|
||||
uuid = "16D24B27-D0FB-4EC3-BAE8-56101FE7949B - 5ebbef0dc9913f07"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "qdomyoszwift.BLEPeripheralManager.init() -> qdomyoszwift.BLEPeripheralManager"
|
||||
moduleName = "qdomyoszwift"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/cagnulein/qdomyos-zwift/src/ios/BLEPeripheralManager.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "57"
|
||||
endingLineNumber = "57"
|
||||
offsetFromSymbolStart = "132">
|
||||
</Location>
|
||||
</Locations>
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
@@ -367,7 +335,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "38"
|
||||
endingLineNumber = "38"
|
||||
landmarkName = "lockscreen::stepCadence()"
|
||||
landmarkName = "lockscreen::virtualbike_setHeartRate(heartRate)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
@@ -375,7 +343,7 @@
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "FE5697FF-F44C-43C2-A98D-C400EE56F047"
|
||||
shouldBeEnabled = "No"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "../src/ios/lockscreen.mm"
|
||||
@@ -383,8 +351,8 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "44"
|
||||
endingLineNumber = "44"
|
||||
landmarkName = "unknown"
|
||||
landmarkType = "0">
|
||||
landmarkName = "lockscreen::virtualbike_setCadence(crankRevolutions, lastCrankEventTime)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
@@ -399,7 +367,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "37"
|
||||
endingLineNumber = "37"
|
||||
landmarkName = "lockscreen::stepCadence()"
|
||||
landmarkName = "lockscreen::virtualbike_setHeartRate(heartRate)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
@@ -407,7 +375,7 @@
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "3DBE0495-050A-4979-85D4-28B78676F212"
|
||||
shouldBeEnabled = "No"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "../src/ios/lockscreen.mm"
|
||||
@@ -415,7 +383,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "43"
|
||||
endingLineNumber = "43"
|
||||
landmarkName = "lockscreen::setKcal(kcal)"
|
||||
landmarkName = "lockscreen::virtualbike_setCadence(crankRevolutions, lastCrankEventTime)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
@@ -431,7 +399,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "32"
|
||||
endingLineNumber = "32"
|
||||
landmarkName = "lockscreen::heartRate()"
|
||||
landmarkName = "lockscreen::virtualbike_ios()"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
@@ -463,7 +431,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "35"
|
||||
endingLineNumber = "35"
|
||||
offsetFromSymbolStart = "32">
|
||||
offsetFromSymbolStart = "22">
|
||||
</Location>
|
||||
<Location
|
||||
uuid = "18F27065-9FB2-44A2-99D0-7D41061141A3 - 4daffae51fb2d733"
|
||||
@@ -478,7 +446,7 @@
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "35"
|
||||
endingLineNumber = "35"
|
||||
offsetFromSymbolStart = "36">
|
||||
offsetFromSymbolStart = "28">
|
||||
</Location>
|
||||
</Locations>
|
||||
</BreakpointContent>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"identifier" : "2816EB89",
|
||||
"nonRenewingSubscriptions" : [
|
||||
|
||||
],
|
||||
"products" : [
|
||||
|
||||
],
|
||||
"settings" : {
|
||||
|
||||
},
|
||||
"subscriptionGroups" : [
|
||||
{
|
||||
"id" : "F012E388",
|
||||
"localizations" : [
|
||||
|
||||
],
|
||||
"name" : "Swag Bag",
|
||||
"subscriptions" : [
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "1.99",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 1,
|
||||
"internalID" : "F108BD35",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Swag Bag",
|
||||
"displayName" : "Swag Bag",
|
||||
"locale" : "en_US"
|
||||
},
|
||||
{
|
||||
"description" : "Swag Bag",
|
||||
"displayName" : "Swag Bag",
|
||||
"locale" : "en_GB"
|
||||
},
|
||||
{
|
||||
"description" : "Swag Bag",
|
||||
"displayName" : "Swag Bag",
|
||||
"locale" : "it"
|
||||
}
|
||||
],
|
||||
"productID" : "org.cagnulein.qdomyoszwift.swagbag",
|
||||
"recurringSubscriptionPeriod" : "P1M",
|
||||
"referenceName" : "SwagBag",
|
||||
"subscriptionGroupID" : "F012E388",
|
||||
"type" : "RecurringSubscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version" : {
|
||||
"major" : 1,
|
||||
"minor" : 2
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "circular38mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"filename" : "circular40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "circular42mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"filename" : "circular44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "extra-large38mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"filename" : "extra-large40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "extra-large42mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"filename" : "extra-large44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "graphic-bezel40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-bezel44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "graphic-circular40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-circular44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "graphic-corner40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-corner44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "graphic-extra-large38mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-extra-large40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-extra-large42mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-extra-large44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "graphic-large-rectangular40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "graphic-large-rectangular44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "modular38mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"filename" : "modular40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "modular42mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"filename" : "modular44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "utility38mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"filename" : "utility40mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"filename" : "utility42mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"filename" : "utility44mm@2x.png",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -13,59 +13,6 @@ class ComplicationController: NSObject, CLKComplicationDataSource {
|
||||
|
||||
// MARK: - Timeline Configuration
|
||||
|
||||
private func templateForComplication(complication: CLKComplication) -> CLKComplicationTemplate? {
|
||||
// Init default output:
|
||||
var template: CLKComplicationTemplate? = nil
|
||||
|
||||
// Graphic Complications are only availably since watchOS 5.0:
|
||||
if #available(watchOSApplicationExtension 5.0, *) {
|
||||
// NOTE: Watch faces that support graphic templates are available only on Apple Watch Series 4 or later. So the binary on older devices (e.g. Watch Series 3) will not contain the images.
|
||||
if complication.family == .graphicCircular {
|
||||
let imageTemplate = CLKComplicationTemplateGraphicCircularImage()
|
||||
// Check if asset exists, to prevent crash on non-supported devices:
|
||||
if let fullColorImage = UIImage(named: "Complication/Graphic Circular") {
|
||||
let imageProvider = CLKFullColorImageProvider.init(fullColorImage: fullColorImage)
|
||||
imageTemplate.imageProvider = imageProvider
|
||||
template = imageTemplate
|
||||
}
|
||||
}
|
||||
else if complication.family == .graphicCorner {
|
||||
let imageTemplate = CLKComplicationTemplateGraphicCornerCircularImage()
|
||||
// Check if asset exists, to prevent crash on non-supported devices:
|
||||
if let fullColorImage = UIImage(named: "Complication/Graphic Corner") {
|
||||
let imageProvider = CLKFullColorImageProvider.init(fullColorImage: fullColorImage)
|
||||
imageTemplate.imageProvider = imageProvider
|
||||
template = imageTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For all watchOS versions:
|
||||
if complication.family == .circularSmall {
|
||||
let imageTemplate = CLKComplicationTemplateCircularSmallSimpleImage()
|
||||
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/Circular")!)
|
||||
imageProvider.tintColor = UIColor.blue
|
||||
imageTemplate.imageProvider = imageProvider
|
||||
template = imageTemplate
|
||||
}
|
||||
else if complication.family == .modularSmall {
|
||||
let imageTemplate = CLKComplicationTemplateModularSmallSimpleImage()
|
||||
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/Modular")!)
|
||||
imageProvider.tintColor = UIColor.blue
|
||||
imageTemplate.imageProvider = imageProvider
|
||||
template = imageTemplate
|
||||
}
|
||||
else if complication.family == .utilitarianSmall {
|
||||
let imageTemplate = CLKComplicationTemplateUtilitarianSmallSquare()
|
||||
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/Utilitarian")!)
|
||||
imageProvider.tintColor = UIColor.blue
|
||||
imageTemplate.imageProvider = imageProvider
|
||||
template = imageTemplate
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
|
||||
handler([.forward, .backward])
|
||||
}
|
||||
@@ -86,9 +33,7 @@ class ComplicationController: NSObject, CLKComplicationDataSource {
|
||||
|
||||
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
|
||||
// Call the handler with the current timeline entry
|
||||
let template = templateForComplication(complication: complication)
|
||||
let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template!)
|
||||
handler(timelineEntry)
|
||||
handler(nil)
|
||||
}
|
||||
|
||||
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
|
||||
@@ -101,15 +46,11 @@ class ComplicationController: NSObject, CLKComplicationDataSource {
|
||||
handler(nil)
|
||||
}
|
||||
|
||||
func getPlaceholderTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
|
||||
// This method will be called once per supported complication, and the results will be cached
|
||||
handler(templateForComplication(complication: complication))
|
||||
}
|
||||
// MARK: - Placeholder Templates
|
||||
|
||||
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
|
||||
// This method will be called once per supported complication, and the results will be cached
|
||||
handler(templateForComplication(complication: complication))
|
||||
handler(nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
<string>watchkit Extension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>NSMotionUsageDescription</key>
|
||||
<string>access to step cadence in order to show it in the application</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
@@ -24,21 +22,6 @@
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
|
||||
<key>CLKComplicationSupportedFamilies</key>
|
||||
<array>
|
||||
<string>CLKComplicationFamilyModularSmall</string>
|
||||
<string>CLKComplicationFamilyModularLarge</string>
|
||||
<string>CLKComplicationFamilyUtilitarianSmall</string>
|
||||
<string>CLKComplicationFamilyUtilitarianSmallFlat</string>
|
||||
<string>CLKComplicationFamilyUtilitarianLarge</string>
|
||||
<string>CLKComplicationFamilyCircularSmall</string>
|
||||
<string>CLKComplicationFamilyExtraLarge</string>
|
||||
<string>CLKComplicationFamilyGraphicCorner</string>
|
||||
<string>CLKComplicationFamilyGraphicBezel</string>
|
||||
<string>CLKComplicationFamilyGraphicCircular</string>
|
||||
<string>CLKComplicationFamilyGraphicRectangular</string>
|
||||
<string>CLKComplicationFamilyGraphicExtraLarge</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
||||
@@ -8,56 +8,26 @@
|
||||
|
||||
import WatchKit
|
||||
import HealthKit
|
||||
import CoreMotion
|
||||
|
||||
class MainController: WKInterfaceController {
|
||||
@IBOutlet weak var userNameLabel: WKInterfaceLabel!
|
||||
@IBOutlet weak var stepCountsLabel: WKInterfaceLabel!
|
||||
@IBOutlet weak var caloriesLabel: WKInterfaceLabel!
|
||||
@IBOutlet weak var distanceLabel: WKInterfaceLabel!
|
||||
@IBOutlet weak var heartRateLabel: WKInterfaceLabel!
|
||||
@IBOutlet weak var startButton: WKInterfaceButton!
|
||||
@IBOutlet weak var cmbSports: WKInterfacePicker!
|
||||
static var start: Bool! = false
|
||||
let pedometer = CMPedometer()
|
||||
var sport: Int = 0
|
||||
|
||||
override func awake(withContext context: Any?) {
|
||||
super.awake(withContext: context)
|
||||
let sports: [WKPickerItem] = [WKPickerItem(),WKPickerItem(),WKPickerItem(),WKPickerItem(),WKPickerItem()]
|
||||
sports[0].title = "Bike"
|
||||
sports[1].title = "Run"
|
||||
sports[2].title = "Walk"
|
||||
sports[3].title = "Elliptical"
|
||||
sports[4].title = "Rowing"
|
||||
cmbSports.setItems(sports)
|
||||
sport = UserDefaults.standard.value(forKey: "sport") as? Int ?? 0
|
||||
cmbSports.setSelectedItemIndex(sport)
|
||||
|
||||
// Configure interface objects here.
|
||||
print("AWAKE")
|
||||
}
|
||||
|
||||
@IBAction func changeSport(_ value: Int) {
|
||||
self.sport = value
|
||||
UserDefaults.standard.set(value, forKey: "sport")
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
override func willActivate() {
|
||||
// This method is called when watch view controller is about to be visible to user
|
||||
super.willActivate()
|
||||
print("WILL ACTIVE")
|
||||
WorkoutTracking.shared.fetchStepCounts()
|
||||
if CMPedometer.isStepCountingAvailable() {
|
||||
pedometer.startUpdates(from: Date()) { pedometerData, error in
|
||||
guard let pedometerData = pedometerData, error == nil else { return }
|
||||
self.stepCountsLabel.setText("\(Int(((pedometerData.currentCadence?.doubleValue ?? 0) * 60.0 / 2.0))) STEP CAD.")
|
||||
WatchKitConnection.stepCadence = Int(((pedometerData.currentCadence?.doubleValue ?? 0) * 60.0 / 2.0))
|
||||
WatchKitConnection.shared.sendMessage(message: ["stepCadence":
|
||||
"\(WatchKitConnection.stepCadence)" as AnyObject])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didDeactivate() {
|
||||
@@ -74,7 +44,6 @@ extension MainController {
|
||||
MainController.start = true
|
||||
startButton.setTitle("Stop")
|
||||
WorkoutTracking.authorizeHealthKit()
|
||||
WorkoutTracking.shared.setSport(sport)
|
||||
WorkoutTracking.shared.startWorkOut()
|
||||
WorkoutTracking.shared.delegate = self
|
||||
|
||||
@@ -90,35 +59,14 @@ extension MainController {
|
||||
}
|
||||
|
||||
extension MainController: WorkoutTrackingDelegate {
|
||||
|
||||
func didReceiveHealthKitDistanceCycling(_ distanceCycling: Double) {
|
||||
|
||||
}
|
||||
func didReceiveHealthKitActiveEnergyBurned(_ activeEnergyBurned: Double) {
|
||||
|
||||
}
|
||||
|
||||
func didReceiveHealthKitHeartRate(_ heartRate: Double) {
|
||||
heartRateLabel.setText("\(heartRate) BPM")
|
||||
WatchKitConnection.shared.sendMessage(message: ["heartRate":
|
||||
"\(heartRate)" as AnyObject])
|
||||
WorkoutTracking.distance = WatchKitConnection.distance
|
||||
WorkoutTracking.kcal = WatchKitConnection.kcal
|
||||
|
||||
if Locale.current.measurementSystem != "Metric" {
|
||||
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))")
|
||||
} else {
|
||||
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance * 1.60934))")
|
||||
}
|
||||
self.caloriesLabel.setText("KCal \(Int(WorkoutTracking.kcal))")
|
||||
//WorkoutTracking.cadenceSteps = pedometer.
|
||||
}
|
||||
|
||||
func didReceiveHealthKitStepCounts(_ stepCounts: Double) {
|
||||
//stepCountsLabel.setText("\(stepCounts) STEPS")
|
||||
}
|
||||
func didReceiveHealthKitStepCadence(_ stepCadence: Double) {
|
||||
|
||||
stepCountsLabel.setText("\(stepCounts) STEPS")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +75,3 @@ extension MainController: WatchKitConnectionDelegate {
|
||||
userNameLabel.setText(userName)
|
||||
}
|
||||
}
|
||||
|
||||
extension Locale
|
||||
{
|
||||
var measurementSystem : String?
|
||||
{
|
||||
return (self as NSLocale).object(forKey: NSLocale.Key.measurementSystem) as? String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ protocol WatchKitConnectionProtocol {
|
||||
|
||||
class WatchKitConnection: NSObject {
|
||||
static let shared = WatchKitConnection()
|
||||
public static var distance = 0.0
|
||||
public static var kcal = 0.0
|
||||
public static var stepCadence = 0
|
||||
weak var delegate: WatchKitConnectionDelegate?
|
||||
|
||||
private override init() {
|
||||
@@ -62,10 +59,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
{
|
||||
validReachableSession?.sendMessage(message, replyHandler: { (result) in
|
||||
print(result)
|
||||
let dDistance = Double(result["distance"] as! Double)
|
||||
WatchKitConnection.distance = dDistance
|
||||
let dKcal = Double(result["kcal"] as! Double)
|
||||
WatchKitConnection.kcal = dKcal
|
||||
}, errorHandler: { (error) in
|
||||
print(error)
|
||||
})
|
||||
|
||||
529
build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift
Executable file → Normal file
@@ -1,330 +1,199 @@
|
||||
//
|
||||
// WatchWorkoutTracking.swift
|
||||
// ElecDemo WatchKit Extension
|
||||
//
|
||||
// Created by NhatHM on 8/12/19.
|
||||
// Copyright © 2019 GST.PID. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
protocol WorkoutTrackingDelegate: class {
|
||||
func didReceiveHealthKitHeartRate(_ heartRate: Double)
|
||||
func didReceiveHealthKitStepCounts(_ stepCounts: Double)
|
||||
func didReceiveHealthKitStepCadence(_ stepCadence: Double)
|
||||
func didReceiveHealthKitDistanceCycling(_ distanceCycling: Double)
|
||||
func didReceiveHealthKitActiveEnergyBurned(_ activeEnergyBurned: Double)
|
||||
}
|
||||
|
||||
protocol WorkoutTrackingProtocol {
|
||||
static func authorizeHealthKit()
|
||||
func startWorkOut()
|
||||
func stopWorkOut()
|
||||
func fetchStepCounts()
|
||||
}
|
||||
|
||||
class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
public static var distance = Double()
|
||||
public static var kcal = Double()
|
||||
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
|
||||
public static var cadenceLastSteps = Double()
|
||||
public static var cadenceSteps = 0
|
||||
var sport: Int = 0
|
||||
let healthStore = HKHealthStore()
|
||||
let configuration = HKWorkoutConfiguration()
|
||||
var workoutSession: HKWorkoutSession!
|
||||
var workoutBuilder: HKLiveWorkoutBuilder!
|
||||
|
||||
weak var delegate: WorkoutTrackingDelegate?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking {
|
||||
private func handleSendStatisticsData(_ statistics: HKStatistics) {
|
||||
switch statistics.quantityType {
|
||||
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
|
||||
let distanceUnit = HKUnit.mile()
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
|
||||
let energyUnit = HKUnit.kilocalorie()
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .heartRate):
|
||||
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitHeartRate(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .stepCount):
|
||||
guard let stepCounts = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
}
|
||||
var resultCount = 0.0
|
||||
guard let result = result else {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
let now = NSDate().timeIntervalSince1970
|
||||
let deltaT = now - WorkoutTracking.cadenceTimeStamp
|
||||
let deltaC = resultCount - WorkoutTracking.cadenceLastSteps
|
||||
WorkoutTracking.cadenceLastSteps = resultCount
|
||||
WorkoutTracking.cadenceTimeStamp = now
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCadence((deltaC / deltaT) * 60)
|
||||
} else {
|
||||
print("Failed to fetch steps rate 2")
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setSport(_ sport: Int) {
|
||||
self.sport = sport
|
||||
}
|
||||
|
||||
private func configWorkout() {
|
||||
var activityType = HKWorkoutActivityType.cycling
|
||||
if self.sport == 1 {
|
||||
activityType = HKWorkoutActivityType.running
|
||||
} else if self.sport == 2 {
|
||||
activityType = HKWorkoutActivityType.walking
|
||||
} else if self.sport == 3 {
|
||||
activityType = HKWorkoutActivityType.elliptical
|
||||
} else if self.sport == 4 {
|
||||
activityType = HKWorkoutActivityType.rowing
|
||||
}
|
||||
|
||||
configuration.activityType = activityType
|
||||
configuration.locationType = .indoor
|
||||
|
||||
do {
|
||||
workoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
|
||||
workoutBuilder = workoutSession?.associatedWorkoutBuilder()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
workoutSession.delegate = self
|
||||
workoutBuilder.delegate = self
|
||||
|
||||
workoutBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
static func authorizeHealthKit() {
|
||||
if HKHealthStore.isHealthDataAvailable() {
|
||||
let infoToRead = Set([
|
||||
HKSampleType.quantityType(forIdentifier: .stepCount)!,
|
||||
HKSampleType.quantityType(forIdentifier: .heartRate)!,
|
||||
/*HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,*/
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
|
||||
let infoToShare = Set([
|
||||
HKSampleType.quantityType(forIdentifier: .stepCount)!,
|
||||
HKSampleType.quantityType(forIdentifier: .heartRate)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
|
||||
HKHealthStore().requestAuthorization(toShare: infoToShare, read: infoToRead) { (success, error) in
|
||||
if success {
|
||||
print("Authorization healthkit success")
|
||||
} else if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("HealthKit not avaiable")
|
||||
}
|
||||
}
|
||||
|
||||
func startWorkOut() {
|
||||
print("Start workout")
|
||||
configWorkout()
|
||||
workoutSession.startActivity(with: Date())
|
||||
workoutBuilder.beginCollection(withStart: Date()) { (success, error) in
|
||||
print(success)
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
|
||||
if self.sport > 0 {
|
||||
self.workoutBuilder.dataSource?.enableCollection(for: HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!, predicate: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopWorkOut() {
|
||||
print("Stop workout")
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
|
||||
guard let quantityType = HKQuantityType.quantityType(
|
||||
forIdentifier: .activeEnergyBurned) else {
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit.kilocalorie()
|
||||
let totalEnergyBurned = WorkoutTracking.kcal
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
|
||||
let unitDistance = HKUnit.mile()
|
||||
let miles = WorkoutTracking.distance
|
||||
let quantityMiles = HKQuantity(unit: unitDistance,
|
||||
doubleValue: miles)
|
||||
|
||||
if(sport == 0) {
|
||||
|
||||
guard let quantityTypeDistance = HKQuantityType.quantityType(
|
||||
forIdentifier: .distanceCycling) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
guard let quantityTypeDistance = HKQuantityType.quantityType(
|
||||
forIdentifier: .distanceWalkingRunning) else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func fetchStepCounts() {
|
||||
guard let stepCounts = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
}
|
||||
var resultCount = 0.0
|
||||
guard let result = result else {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
} else {
|
||||
print("Failed to fetch steps rate 2")
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
|
||||
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
|
||||
print("GET DATA: \(Date())")
|
||||
for type in collectedTypes {
|
||||
guard let quantityType = type as? HKQuantityType else {
|
||||
return
|
||||
}
|
||||
|
||||
if let statistics = workoutBuilder.statistics(for: quantityType) {
|
||||
handleSendStatisticsData(statistics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: HKWorkoutSessionDelegate {
|
||||
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
|
||||
|
||||
}
|
||||
|
||||
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
|
||||
|
||||
}
|
||||
}
|
||||
//
|
||||
// WatchWorkoutTracking.swift
|
||||
// ElecDemo WatchKit Extension
|
||||
//
|
||||
// Created by NhatHM on 8/12/19.
|
||||
// Copyright © 2019 GST.PID. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
protocol WorkoutTrackingDelegate: class {
|
||||
func didReceiveHealthKitHeartRate(_ heartRate: Double)
|
||||
func didReceiveHealthKitStepCounts(_ stepCounts: Double)
|
||||
}
|
||||
|
||||
protocol WorkoutTrackingProtocol {
|
||||
static func authorizeHealthKit()
|
||||
func startWorkOut()
|
||||
func stopWorkOut()
|
||||
func fetchStepCounts()
|
||||
}
|
||||
|
||||
class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
let healthStore = HKHealthStore()
|
||||
let configuration = HKWorkoutConfiguration()
|
||||
var workoutSession: HKWorkoutSession!
|
||||
var workoutBuilder: HKLiveWorkoutBuilder!
|
||||
|
||||
weak var delegate: WorkoutTrackingDelegate?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking {
|
||||
private func handleSendStatisticsData(_ statistics: HKStatistics) {
|
||||
switch statistics.quantityType {
|
||||
case HKQuantityType.quantityType(forIdentifier: .heartRate):
|
||||
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitHeartRate(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .stepCount):
|
||||
guard let stepCounts = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
}
|
||||
var resultCount = 0.0
|
||||
guard let result = result else {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
} else {
|
||||
print("Failed to fetch steps rate 2")
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func configWorkout() {
|
||||
configuration.activityType = .cycling
|
||||
configuration.locationType = .indoor
|
||||
|
||||
do {
|
||||
workoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
|
||||
workoutBuilder = workoutSession?.associatedWorkoutBuilder()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
workoutSession.delegate = self
|
||||
workoutBuilder.delegate = self
|
||||
|
||||
workoutBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
static func authorizeHealthKit() {
|
||||
if HKHealthStore.isHealthDataAvailable() {
|
||||
let infoToRead = Set([
|
||||
HKSampleType.quantityType(forIdentifier: .stepCount)!,
|
||||
HKSampleType.quantityType(forIdentifier: .heartRate)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
|
||||
let infoToShare = Set([
|
||||
HKSampleType.quantityType(forIdentifier: .stepCount)!,
|
||||
HKSampleType.quantityType(forIdentifier: .heartRate)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
|
||||
HKHealthStore().requestAuthorization(toShare: infoToShare, read: infoToRead) { (success, error) in
|
||||
if success {
|
||||
print("Authorization healthkit success")
|
||||
} else if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("HealthKit not avaiable")
|
||||
}
|
||||
}
|
||||
|
||||
func startWorkOut() {
|
||||
print("Start workout")
|
||||
configWorkout()
|
||||
workoutSession.startActivity(with: Date())
|
||||
workoutBuilder.beginCollection(withStart: Date()) { (success, error) in
|
||||
print(success)
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopWorkOut() {
|
||||
print("Stop workout")
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStepCounts() {
|
||||
guard let stepCounts = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
}
|
||||
var resultCount = 0.0
|
||||
guard let result = result else {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
} else {
|
||||
print("Failed to fetch steps rate 2")
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
|
||||
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
|
||||
print("GET DATA: \(Date())")
|
||||
for type in collectedTypes {
|
||||
guard let quantityType = type as? HKQuantityType else {
|
||||
return
|
||||
}
|
||||
|
||||
if let statistics = workoutBuilder.statistics(for: quantityType) {
|
||||
handleSendStatisticsData(statistics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking: HKWorkoutSessionDelegate {
|
||||
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
|
||||
|
||||
}
|
||||
|
||||
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,6 @@
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "33x33",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
@@ -47,13 +40,6 @@
|
||||
"size" : "44x44",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "46x46",
|
||||
"subtype" : "41mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
@@ -61,13 +47,6 @@
|
||||
"size" : "50x50",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
@@ -89,13 +68,6 @@
|
||||
"size" : "108x108",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch-marketing",
|
||||
"scale" : "1x",
|
||||
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -103,7 +103,7 @@
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "ItunesArtwork-1.png",
|
||||
"filename" : "iTunesArtwork@2x.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
@@ -138,13 +138,6 @@
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "33x33",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "80x80-1.png",
|
||||
"idiom" : "watch",
|
||||
@@ -161,13 +154,6 @@
|
||||
"size" : "44x44",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "46x46",
|
||||
"subtype" : "41mm"
|
||||
},
|
||||
{
|
||||
"filename" : "50@2x-1.png",
|
||||
"idiom" : "watch",
|
||||
@@ -176,13 +162,6 @@
|
||||
"size" : "50x50",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "86@2x-1.png",
|
||||
"idiom" : "watch",
|
||||
@@ -207,13 +186,6 @@
|
||||
"size" : "108x108",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "1024@1x.png",
|
||||
"idiom" : "watch-marketing",
|
||||
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="20037" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Tpn-rd-UUX">
|
||||
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="17506" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Tpn-rd-UUX">
|
||||
<device id="watch38"/>
|
||||
<dependencies>
|
||||
<deployment identifier="watchOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="20006"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="17500"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Main-->
|
||||
@@ -12,26 +12,16 @@
|
||||
<objects>
|
||||
<controller identifier="Main" hidesWhenLoading="NO" id="Tpn-rd-UUX" customClass="MainController" customModule="watchkit_Extension">
|
||||
<items>
|
||||
<label width="136" alignment="left" text="QZ Fitness" textAlignment="center" id="SlU-M7-WGB"/>
|
||||
<label width="136" alignment="left" text="qdomyos-zwift" textAlignment="center" id="SlU-M7-WGB"/>
|
||||
<label width="136" alignment="left" text="Heart Rate" id="Nda-m1-XRw"/>
|
||||
<label width="136" alignment="left" text="Step Counts" id="HpA-e9-6YV"/>
|
||||
<button width="1" alignment="left" title="Start" id="vZg-X8-uY5">
|
||||
<connections>
|
||||
<action selector="startWorkout" destination="Tpn-rd-UUX" id="UaW-pR-tn6"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label width="136" alignment="left" text="Heart Rate" id="Nda-m1-XRw"/>
|
||||
<label width="136" alignment="left" text="Step Counts" id="HpA-e9-6YV"/>
|
||||
<label width="136" alignment="left" text="Calories" id="Szi-Jp-J3S"/>
|
||||
<label width="136" alignment="left" text="Distance" id="eRf-NJ-6If"/>
|
||||
<picker height="100" alignment="left" id="OTR-HF-vYb">
|
||||
<connections>
|
||||
<action selector="changeSport:" destination="Tpn-rd-UUX" id="3vY-lq-IhZ"/>
|
||||
</connections>
|
||||
</picker>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="caloriesLabel" destination="Szi-Jp-J3S" id="trd-YS-bJy"/>
|
||||
<outlet property="cmbSports" destination="OTR-HF-vYb" id="Ws5-w9-ZT8"/>
|
||||
<outlet property="distanceLabel" destination="eRf-NJ-6If" id="ZE2-OB-jqN"/>
|
||||
<outlet property="heartRateLabel" destination="Nda-m1-XRw" id="1la-8R-3jG"/>
|
||||
<outlet property="startButton" destination="vZg-X8-uY5" id="pJc-09-kfV"/>
|
||||
<outlet property="stepCountsLabel" destination="HpA-e9-6YV" id="Z88-ej-6oG"/>
|
||||
|
||||
15
defaults.pri
@@ -1,15 +0,0 @@
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts
|
||||
|
||||
unix:android: QT += androidextras gui-private
|
||||
|
||||
android: include(android_openssl/openssl.pri)
|
||||
|
||||
INCLUDEPATH += $$PWD/src/qmdnsengine/src/include
|
||||
|
||||
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/src/android
|
||||
|
||||
ANDROID_ABIS = armeabi-v7a arm64-v8a x86 x86_64
|
||||
|
||||
#QMAKE_CXXFLAGS += -Werror=suggest-override
|
||||
@@ -1,24 +1,10 @@
|
||||
FROM ubuntu:latest
|
||||
FROM debian:stable
|
||||
MAINTAINER cagnulein
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Europe/Moscow
|
||||
ENV MAKEFLAGS -j8
|
||||
WORKDIR /usr/local/src
|
||||
|
||||
RUN apt-get update && apt-get install -y tzdata
|
||||
# utils
|
||||
RUN apt -y update
|
||||
RUN apt -y upgrade
|
||||
RUN apt update -y && apt-get install -y git qt5-default libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev build-essential
|
||||
|
||||
RUN git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
WORKDIR /usr/local/src/qdomyos-zwift
|
||||
|
||||
RUN git submodule update --init src/smtpclient/
|
||||
RUN git submodule update --init src/qmdnsengine/
|
||||
WORKDIR /usr/local/src/qdomyos-zwift/src
|
||||
RUN qmake
|
||||
RUN make -j4
|
||||
|
||||
WORKDIR /usr/local/src/qdomyos-zwift/src
|
||||
CMD ["./qdomyos-zwift","-no-gui"]
|
||||
RUN apt -y install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default
|
||||
@@ -1,40 +1,29 @@
|
||||
# Installation
|
||||
|
||||
QDomyos-Zwift can be installed from source on MacOs, Linux, Android and IOS.
|
||||
|
||||
Once you've installed QDomyos-Zwift, you can access the [operation guide](30_usage.md) for more information.
|
||||
|
||||
These instructions build the app itself, not the test project.
|
||||
|
||||
## On a Linux System (from source)
|
||||
|
||||
```buildoutcfg
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
|
||||
$ sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default libqt5networkauth5-dev
|
||||
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
$ cd qdomyos-zwift
|
||||
$ git submodule update --init src/smtpclient/
|
||||
$ git submodule update --init src/qmdnsengine/
|
||||
$ git submodule update --init tst/googletest/
|
||||
$ cd src
|
||||
$ qmake qdomyos-zwift.pro
|
||||
$ qmake
|
||||
$ make -j4
|
||||
$ sudo ./qdomyos-zwift
|
||||
```
|
||||
|
||||
|
||||
## MacOs installation
|
||||
|
||||
You will need to (at a minimum) to install the xcode Command Line Tools (CLI) thanks to @richardwait
|
||||
https://developer.apple.com/download/more/?=xcode
|
||||
|
||||
Download and install http://download.qt.io/official_releases/qt/5.12/5.12.9/qt-opensource-mac-x64-5.12.9.dmg and simply run the qdomyos-zwift release for MacOs
|
||||
Download and install http://download.qt.io/official_releases/qt/5.12/5.12.9/qt-opensource-mac-x64-5.12.9.dmg and simply run the qdomyos-zwift relase for MacOs
|
||||
|
||||
## On Raspberry Pi Zero W
|
||||
|
||||

|
||||
|
||||
This guide will walk you through steps to setup an autonomous, headless raspberry bridge.
|
||||
This guide will walk you through steps to setup an autonomous, headless raspberry brigde.
|
||||
|
||||
|
||||
### Initial System Preparation
|
||||
@@ -105,17 +94,12 @@ This operation takes a moment to complete.
|
||||
|
||||
#### Install qdomyos-zwift from sources
|
||||
|
||||
```bash
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd qdomyos-zwift
|
||||
git submodule update --init src/smtpclient/
|
||||
git submodule update --init src/qmdnsengine/
|
||||
git submodule update --init tst/googletest/
|
||||
cd src
|
||||
qmake qdomyos-zwift.pro
|
||||
make
|
||||
```
|
||||
`sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-default libqt5networkauth5-dev`
|
||||
|
||||
`git clone https://github.com/cagnulein/qdomyos-zwift.git`
|
||||
`cd src`
|
||||
`qmake`
|
||||
`make`
|
||||
|
||||
Please note :
|
||||
- Don't build the application with `-j4` option (this will fail)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Supported applications
|
||||
|
||||
QDomyos-Zwift should support every application able to read a [FTMS](specs/FTMS_v1.0.pdf) bluetooth signal.
|
||||
|
||||
This list is not exhaustive. Please report any application known to be working with QDomyos-Zwift !
|
||||
|
||||
|Application|Sport|Platform|Speed|RPM|Power|HRM |Resistance Control|
|
||||
|-----------|-----|--------|------------|---|-----|-----|----------------|
|
||||
|[Zwift](21_applications_detail.md#zwift)| |  | Yes|Yes|Yes| Yes, no FTMS support | Yes |
|
||||
|[BKool](21_applications_detail.md#bkool) ||  |Yes|Yes|Yes|Yes| Yes |
|
||||
|[Fulgaz](21_applications_detail.md#fulgaz)||  |Yes|Yes|Yes|Yes, no FTMS support (see note)|Yes (see note) |
|
||||
|
||||
# Supported devices
|
||||
Check the full list https://github.com/cagnulein/qdomyos-zwift/wiki/Equipment-Compatibility
|
||||
|
||||
This list is not exhaustive.
|
||||
Try the qdomyos app with your fitness appliance and report how it is going.
|
||||
If it's not working, you can [ask for your device to be supported](#ask-for-device-support)
|
||||
|
||||
# Ask for device support
|
||||
|
||||
You can ask for supporting a device by opening an issue and following these steps.
|
||||
We need to "spy" the bluetooth activity from your fitness device and it's application, in order to guess how they communicate.
|
||||
|
||||
An android device is required for this operation.
|
||||
|
||||
## Android device
|
||||
|
||||
1. first of all you need an android device (phone or tablet)
|
||||
2. you need to become developer on your phone https://wccftech.com/how-to/how-to-enable-developer-options-on-android-10-tutorial/
|
||||
3. Go to Settings
|
||||
4. Go into developer options
|
||||
5. Enable the option Enable Bluetooth HCI snoop log
|
||||
6. restart your phone
|
||||
7. open your machine app and play with it collecting inclination and speed
|
||||
8. Disable the option Enable Bluetooth HCI snoop log
|
||||
9. in Developer Options: Bug report->Full report
|
||||
10. wait a random amount of time (10-20 seconds)
|
||||
11. A notification will appear at the top of the device. Click on it, share, email it to yourself. If it doesn't appear you need to use ADB to pull the file from the phone itself
|
||||
12. You'll get a zip file with the entire report. In the FS/Data/Log/bt directory of the zipfile is the file you want.
|
||||
13. attach the log file in a new issue with a short description of the steps you did in the app when you used it
|
||||
|
||||
## Android Device (oppo based OS : oppo, ColorOS, RealMe, ...)
|
||||
1. Dial *#800# on the phone app. A special menu should appear.
|
||||
2. Go to Bluetooth and press "Start capture" (green button)
|
||||
3. open your machine app and play with it collecting inclination and speed
|
||||
4. Go back to the special menu by dialing *#800# on the phone app. Stop the bluetooth capture.
|
||||
5. A new debug directory is stored onto `/oppo_log/` with a time stamp.
|
||||
6. Fetch the CFA file stored in the `btsnoop_hci` folder.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Applications supported
|
||||
|
||||
This list is not exhaustive !
|
||||
Open a discussion to tell us if a missing application is supported.
|
||||
|
||||
## BKool
|
||||
Everything is working out of the box (read FTMS data).
|
||||
|
||||
|
||||
## Fulgaz
|
||||
|
||||
### HRM Support
|
||||
The application do not read the FTMS value. It is required to start the application with `-heart-service` or `bike_heartrate_service: true` in settings.
|
||||
The HRM captor will not be shown by default, you need to go in the `Application Settings > Advanced > Disable Bluetooth Filter`.
|
||||
|
||||
### Resistance management
|
||||
Fulgaz is known to be very severe by default in resistance adjustment. It is advised to adjust [application settings](https://tempocyclist.com/2020/04/29/fulgaz-resistance-too-hard/).
|
||||
The resistance is automatically adjusted with the slope. However, you can override it using
|
||||
|
||||
### Additional notes
|
||||
You can have ae true 4k video stream while you ride ("extreme quality" setting) however it requires about 10 gb per hour.
|
||||
|
||||
|
||||
## Zwift
|
||||
### HRM support
|
||||
The application do not read the FTMS value. It is required to start the application with `-heart-service` or `bike_heartrate_service: true` in settings.
|
||||
|
||||
### Resistance management
|
||||
You can adjust resistance using arrows [up and down](img/21_zwift-resistance-buttons.jpg) rom the riding screen.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Devices detail
|
||||
|
||||
## Echelon
|
||||
|
||||
|
||||
## Sportstech
|
||||
|
||||
### ESX 500
|
||||
#### HRM
|
||||
The [cardio belt provided](https://www.sports-tech.uk.com/chest-strap-sportstech-uncoded) with the bike is also supported, the bike reads the value and forwards it in its bluetooth signal.
|
||||
The cardio captors are reported to not be accurate.
|
||||
#### Resistance
|
||||
The resistance is adjusted after a few seconds delay (time for the engine to adapt magnetic resistance).
|
||||
@@ -1,54 +0,0 @@
|
||||
# QDomyos-Zwift operation guide
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
The QDomyos-Zwift can be started in two modes : QML or NativeQT.
|
||||
The main difference is the configuration management : you can change settings within the application with QML, where you need to specify settings at startup in NativeQT.
|
||||
|
||||
**Note:** Android and IOS are always running in QML mode.
|
||||
|
||||
On MacOS and Linux, you start QDomyos-Zwift in NativeQT mode (where settings are defined in commandline switches).
|
||||
|
||||
You can start the application in QML mode with the command-line switch -qml.
|
||||
|
||||
## Configuration in QML mode
|
||||
Please refer to this article for more information under [QML Operations](https://robertoviola.cloud/qdomyos-zwift-guide/) with several useful information.
|
||||
|
||||
|
||||
## Configuration in NativeQT mode
|
||||
|
||||
This is the list of settings available in the application. These settings needs to be appended to the binary command line.
|
||||
*Example :* `sudo ./qdomyos-zwift -no-gui` for disabling any graphical interface.
|
||||
|
||||
| **Option** | **Type** | **Default** | **Function** |
|
||||
|:------------------------------|:---------|:------------|:-----------------------------------------------------------------------------|
|
||||
| -no-gui | Boolean | False | Disable GUI |
|
||||
| -qml | Boolean | True | Enables the QML interface |
|
||||
| -noqml | Boolean | False | Enables the NativeQT interface |
|
||||
| -miles | Boolean | False | Switches to Imperial Units System |
|
||||
| -no-console | Boolean | False | Not in use |
|
||||
| -test-resistance | Boolean | False | |
|
||||
| -no-log | Boolean | False | Disable Logging |
|
||||
| -no-write-resistance | Boolean | False | Disable resistance instructions from QZ to your fitness equipment |
|
||||
| -no-heart-service | Boolean | False | Do not simulate external HR monitor, use only FTMS |
|
||||
| -heart-service | Boolean | True | Simulate HR service (required for applications not reading FTMS) |
|
||||
| -only-virtualbike | Boolean | False | |
|
||||
| -only-virtualtreadmill | Boolean | False | |
|
||||
| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipement if enabled |
|
||||
| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipement |
|
||||
| -bike-cadence-sensor | Boolean | False | |
|
||||
| -bike-power-sensor | Boolean | False | |
|
||||
| -battery-service | Boolean | False | |
|
||||
| -service-changed | Boolean | False | |
|
||||
| -bike-wheel-revs | Boolean | False | |
|
||||
| -run-cadence-sensor | Boolean | False | |
|
||||
| -nordictrack-10-treadmill | Boolean | False | Enable NordicTrack compatibility mode |
|
||||
| -train | String | | Force training program |
|
||||
| -name | String | | Force bluetooth device name (if QZ struggles finding your fitness equipment) |
|
||||
| -poll-device-time | Int | 200 (ms) | Frequency to refresh information from QZ to Fitness equipment |
|
||||
| -bike-resistance-gain | Int | | Adjust resistance from the fitness application |
|
||||
| -bike-resistance-offset | Int | | Set another resistance point than default |
|
||||
|
||||
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
# QDomyos-Zwift WebSocket API Installation & Operation guide
|
||||
|
||||
# Installation
|
||||
## About
|
||||
|
||||
The QDomyos-Zwift WebSocket API can be installed from source on Linux, Raspberry Pi (4, 3, zero W), macOS, Android and IOS.
|
||||
|
||||
However, this guide will only focus on the Linux (Debian 11) Installation and Raspberry Pi cause there are the most useful case in headless control.
|
||||
|
||||
If you already install the Web Socket, feel free to [skip to the Usage section](#usage).
|
||||
|
||||
## Requirement
|
||||
|
||||
To Install QDomyos-Zwift with WebSocket API you will need Qt 5.12.2+ and the following modules :
|
||||
- Qt Bluetooth
|
||||
- Qt Widgets
|
||||
- Qt Positioning
|
||||
- Qt XML
|
||||
- Qt Charts
|
||||
- Qt Network
|
||||
- Qt Network Authorization
|
||||
- Qt WebSockets
|
||||
- Qt Assistant
|
||||
|
||||
Unfortunately under Debian 11 (or Raspbian 11) the Qt 5 packages are not recent enough for compilation however this guide will explain how to manually compile the latest version of Qt (5.12.12)
|
||||
|
||||
If you already had Qt 5.12.2 or more, feel free to [skip to Install Qt Httpserver](#install-qt-httpserver).
|
||||
|
||||
## Install Qt 5.12.2
|
||||
|
||||
*If you compile for a Raspberry Pi Zero, it's* ***faster and easy*** *to do all the Raspberry Pi task on a Raspberry Pi 4 and after copy compiled binary files toe the Raspberry Pi Zero*
|
||||
|
||||
For more info on the steps [please refer to the source](#source)
|
||||
|
||||
Before do anything. Make sure all your packages are updated :
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
```
|
||||
|
||||
After download last version of Qt Source and extract them :
|
||||
```bash
|
||||
wget https://download.qt.io/official_releases/qt/5.12/5.12.12/single/qt-everywhere-src-5.12.12.tar.xz
|
||||
```
|
||||
|
||||
If you compile for a Raspberry Pi you will need the Raspberry Pi Qt Configuration for raspberry pi and install it in the source :
|
||||
|
||||
```bash
|
||||
git clone https://github.com/oniongarlic/qt-raspberrypi-configuration.git
|
||||
cd qt-raspberrypi-configuration && make install DESTDIR=../qt-everywhere-src-5.12.12
|
||||
```
|
||||
|
||||
Install the bare minimum required development packages for building Qt 5 with apt :
|
||||
```bash
|
||||
apt install build-essential libfontconfig1-dev libdbus-1-dev libfreetype6-dev libicu-dev libinput-dev libxkbcommon-dev libsqlite3-dev libssl-dev libpng-dev libjpeg-dev libglib2.0-dev libraspberrypi-dev
|
||||
```
|
||||
|
||||
*For raspberry Pi install `libraspberrypi-dev` package* :
|
||||
```bash
|
||||
apt install libraspberrypi-dev
|
||||
```
|
||||
|
||||
|
||||
Now install all required development packages for building all Qt 5 modules:
|
||||
```bash
|
||||
apt install bluez libgbm-dev
|
||||
apt install libudev-dev libinput-dev libts-dev libxcb-xinerama0-dev libxcb-xinerama0 gdbserver
|
||||
apt install libegl1-mesa libegl1-mesa-dev libgles2-mesa libgles2-mesa-dev
|
||||
apt install wiringpi libnfc-bin libnfc-dev fonts-texgyre libts-dev
|
||||
apt install libbluetooth-dev bluez-tools gstreamer1.0-plugins* libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libopenal-data libopenal1 libopenal-dev pulseaudio
|
||||
apt install libgstreamer*-dev
|
||||
apt install gstreamer*-dev
|
||||
apt install libasound2-dev libavcodec-dev libavformat-dev libswscale-dev libgstreamer0.10-dev libgstreamer-plugins-base0.10-dev gstreamer-tools libgstreamer-plugins-*
|
||||
apt install qtdeclarative5-dev
|
||||
apt install libvlc-dev
|
||||
```
|
||||
|
||||
On Raspbian Stretch/Buster/Bullseye the OpenGL library files have been renamed so that they wouldn't conflict with Mesa installed ones. Unfortunately Qt configure script is still looking for the old names.
|
||||
So ***on your target Raspberry Pi*** you need to symlink those file to make sure Qt run correctly.
|
||||
```bash
|
||||
ln -s /usr/lib/arm-linux-gnueabihf/libGLESv2.so /usr/lib/libbrcmGLESv2.so
|
||||
ln -s /usr/lib/arm-linux-gnueabihf/libEGL.so /usr/lib/libbrcmEGL.so
|
||||
```
|
||||
|
||||
Now all dependency are installed. It's time to create build folder and compiled.
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
|
||||
# For Raspberry Pi Zero or 3
|
||||
PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig ../qt-everywhere-src-5.12.12/configure -platform linux-rpi-g++ -v -opengl es2 -eglfs -no-gtk -opensource -confirm-license -release -reduce-exports -force-pkg-config -nomake examples -no-compile-examples -skip qtwayland -skip qtwebengine -no-feature-geoservices_mapboxgl -qt-pcre -no-pch -ssl -evdev -system-freetype -fontconfig -glib -prefix /opt/Qt/5.12.12 -qpa eglfs
|
||||
CFLAGS="-march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp" make -j3 # Remove -j3 if you compiled directly on Raspberry Pi Zero
|
||||
|
||||
# For Raspberry Pi 4
|
||||
PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig ../qt-everywhere-src-5.12.12/configure -platform linux-rpi4-v3d-g++ -v -opengl es2 -eglfs -no-gtk -opensource -confirm-license -release -reduce-exports -force-pkg-config -nomake examples -no-compile-examples -skip qtwayland -skip qtwebengine -no-feature-geoservices_mapboxgl -qt-pcre -no-pch -ssl -evdev -system-freetype -fontconfig -glib -prefix /opt/Qt/5.12.12 -qpa eglfs
|
||||
CFLAGS="-march=armv8-a -mtune=cortex-a72 -mfpu=crypto-neon-fp-armv8" make -j3
|
||||
|
||||
# For Debian 11 x64 (Not tested)
|
||||
../qt-everywhere-src-5.12.12/configure -v -opengl es2 -eglfs -no-gtk -opensource -confirm-license -release -reduce-exports -force-pkg-config -nomake examples -no-compile-examples -skip qtwayland -skip qtwebengine -no-feature-geoservices_mapboxgl -qt-pcre -no-pch -ssl -evdev -system-freetype -fontconfig -glib -prefix /opt/Qt/5.12.12 -qpa eglfs
|
||||
make
|
||||
```
|
||||
|
||||
Finally, if you cross compiled you can transfer the build folder to other machine and then just run as root in the build folder :
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
# Install Qt Httpserver
|
||||
|
||||
Like explain in PR #252, to make work the Http Server you will need to manually compile `qthttpserver` module.
|
||||
|
||||
For that just run following commands in your home directory :
|
||||
```bash
|
||||
cd ~
|
||||
git clone https://github.com/qt-labs/qthttpserver
|
||||
cd ~/qthttpserver/src/3rdparty/http-parser
|
||||
wget https://raw.githubusercontent.com/nodejs/http-parser/main/http_parser.h
|
||||
wget https://raw.githubusercontent.com/nodejs/http-parser/main/http_parser.c
|
||||
cd ~/qthttpserver/src
|
||||
qmake # Please note if you compiled Qt you need to specify /opt/Qt/5.12.12/bin/qmake
|
||||
make
|
||||
# Wait...
|
||||
sudo make install
|
||||
```
|
||||
|
||||
***You have successfully installed Qt Httpserver***
|
||||
|
||||
# Install QDomyos-Zwift
|
||||
|
||||
If you already compile QDomyos-Zwift and you just compiled a new version of Qt.
|
||||
Please delete the whole QDomyos-Zwift folder and restart from scratch to prevent linking issues.
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd ~/qdomyos-zwift
|
||||
git submodule update --init ~/qdomyos-zwift/src/smtpclient/
|
||||
cd ~/qdomyos-zwift/src
|
||||
qmake # Please note if you compiled Qt you need to specify /opt/Qt/5.12.12/bin/qmake
|
||||
make -j4 # Remove -j4 if you compiled on Raspberry Pi Zero
|
||||
```
|
||||
|
||||
Now installed you need to compile like say in PR #252 and issue #572 template/debug in the same directory of source file of QDomyos-Zwift.
|
||||
```bash
|
||||
cp -r ~/qdomyos-zwift/src/templates/debug ~/qdomyos-zwift/src/.
|
||||
cp -r ~/qdomyos-zwift/src/templates/debug/* ~/qdomyos-zwift/src/.
|
||||
```
|
||||
|
||||
Last if you can't run QML version (probably because you don't had a X11 Server.) you need to manually edit the configuration file in `/root/.config/Roberto Viola/qDomyos-Zwift.conf` and add :
|
||||
```
|
||||
template_inner_QZWS_enabled=true
|
||||
template_inner_QZWS_folders=:/inner_templates//chartjs
|
||||
template_inner_QZWS_ips=192.168.1.42
|
||||
template_inner_QZWS_port=34107
|
||||
template_inner_QZWS_type=WebServer
|
||||
```
|
||||
|
||||
In this config file we open an HTTP Server on port 34107 with bind to 192.168.1.42 but feel free to change these values.
|
||||
|
||||
Finally, ***do not move `qdomyos-zwift` from src folder*** and run it as Root
|
||||
|
||||
# Usage
|
||||
|
||||
The way that [WebSocket](https://developer.mozilla.org/docs/Web/API/WebSockets_API) work in QDomyos-Zwift is by sending commands and listen events.
|
||||
|
||||
## Workout Event
|
||||
|
||||
The workout Event is the default message send almost every second by QDomyos-Zwift to inform you which state is your equipment.
|
||||
|
||||
Here what is look like :
|
||||
```json
|
||||
{
|
||||
"BIKE_TYPE": 2,
|
||||
"ELLIPTICAL_TYPE": 4,
|
||||
"ROWING_TYPE": 3,
|
||||
"TREADMILL_TYPE": 1,
|
||||
"UNKNOWN_TYPE": 0,
|
||||
"deviceId": "0B:54:49:D1:BC:DA",
|
||||
"deviceName": "Domyos-TC-0314",
|
||||
"deviceRSSI": 0,
|
||||
"deviceType": 1,
|
||||
"deviceConnected": false,
|
||||
"devicePaused": false,
|
||||
"elapsed_s": 0,
|
||||
"elapsed_m": 0,
|
||||
"elapsed_h": 0,
|
||||
"pace_s": 0,
|
||||
"pace_m": 0,
|
||||
"pace_h": 0,
|
||||
"moving_s": 0,
|
||||
"moving_m": 0,
|
||||
"moving_h": 0,
|
||||
"speed": 0,
|
||||
"speed_avg": 0,
|
||||
"calories": 0,
|
||||
"distance": 0,
|
||||
"heart": 0,
|
||||
"heart_avg": 0,
|
||||
"heart_max": 0,
|
||||
"jouls": 0,
|
||||
"elevation": 0,
|
||||
"difficult": 1,
|
||||
"watts": 0,
|
||||
"watts_avg": 0,
|
||||
"watts_max": 0,
|
||||
"kgwatts": 0,
|
||||
"kgwatts_avg": 0,
|
||||
"kgwatts_max": 0,
|
||||
"workoutName": "",
|
||||
"workoutStartDate": "",
|
||||
"instructorName": "",
|
||||
"latitude": null,
|
||||
"longitude": null,
|
||||
"nickName": "N/A",
|
||||
"inclination": 0,
|
||||
"inclination_avg": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
To send commands you will need to send a socket message in JSON format like :
|
||||
```json
|
||||
{
|
||||
"msg": "pause"
|
||||
}
|
||||
```
|
||||
|
||||
which `msg` is always the name of the command. Command also return on WebSocket message like to acknowledge command :
|
||||
```json
|
||||
{
|
||||
"msg": "R_pause"
|
||||
}
|
||||
```
|
||||
|
||||
Here is a list of the most "useful" commands
|
||||
|
||||
### Start
|
||||
#### Description :
|
||||
Allows you to start the bike / treadmill (Reset Timer if bike / treadmill is stopped)
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "start"
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_start"
|
||||
}
|
||||
```
|
||||
|
||||
### Pause
|
||||
#### Description :
|
||||
Allows you to stop (pause) the bike / treadmill without reset timer.
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "pause"
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_pause"
|
||||
}
|
||||
```
|
||||
|
||||
### Stop
|
||||
#### Description :
|
||||
Allows you to stop the bike / treadmill and reset timer.
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "stop"
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_stop"
|
||||
}
|
||||
```
|
||||
|
||||
### SetSpeed
|
||||
#### Description :
|
||||
Allows you to control the treadmill speed.
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "setspeed",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_setspeed",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SetResistance
|
||||
#### Description :
|
||||
Allows you to control the resistance bike or the treadmill incline.
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "setresistance",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_setresistance",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SetFanSpeed
|
||||
#### Description :
|
||||
Allows you to control the fan bike / treadmill speed.
|
||||
|
||||
#### Send :
|
||||
```json
|
||||
{
|
||||
"msg": "setfanspeed",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Response :
|
||||
```json
|
||||
{
|
||||
"msg": "R_setfanspeed",
|
||||
"content": {
|
||||
"value": 8.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Source
|
||||
How compile Qt 5.12.10 on Raspberry Pi : https://www.tal.org/tutorials/building-qt-512-raspberry-pi
|
||||
|
||||
How cross compile Qt 5.12.5 on Raspberry Pi (in French) : https://wiki.logre.eu/index.php/Cross-compilation_Qt_5.12.5_pour_Raspberry_Pi
|
||||
|
||||
Issue [REQ] Add to qdomyos an API for remote access to treadmill #572
|
||||
|
||||
PR "Templated" connections and Web server #252
|
||||
|
Before Width: | Height: | Size: 491 B |
|
Before Width: | Height: | Size: 427 B |