Created
November 7, 2024 00:01
-
-
Save victorypoint/b814601d37754ee1da35ff8a15ce2788 to your computer and use it in GitHub Desktop.
Zwift workouts auto-incline and auto-speed control of treadmill via ADB and OCR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ' iFit2-Zwift-Workout - Zwift workouts auto-incline and auto-speed control of treadmill via ADB and OCR | |
| ' Author: Al Udell | |
| ' Revised: November 6, 2024 | |
| ' To debug - enable wscript.echo and run by cscript in command line | |
| ' On Error Resume Next | |
| ' Display startup message | |
| createobject("wscript.shell").popup "Ensure treadmill is in manual workout mode with onscreen speed and incline controls visible. " _ | |
| & vbCrLf & vbCrLf & "Also ensure Zwift is running in game with workout dashboard showing.", 10, "Warning", 64 | |
| ' Initialize | |
| Set wso = CreateObject("wscript.shell") | |
| Set fso = CreateObject("Scripting.FileSystemObject") | |
| buttonCount = 8 ' Number of displayed buttons | |
| swipeAmount = 400 ' Y swipe amount | |
| swipeSpeed = 300 ' Swipe speed in ms | |
| oldzwiftSpeed = 0 | |
| oldzwiftIncline = 0 | |
| ' iFit2 wolflog filename (static name, no date included) | |
| infilename2 = "/sdcard/android/data/com.ifit.glassos_service/files/.valinorlogs/log.latest.txt" | |
| ' *** Start - NordicTrack C2950 Treadmill *** | |
| ' Define button arrays for speed and incline | |
| speedButtons = Array(2, 3, 4, 6, 8, 10, 12, 14, 16, 19) | |
| inclineButtons = Array(-3, 0, 1, 2, 4, 6, 8, 10, 12, 15) | |
| minSpeed = 1 | |
| maxSpeed = 19.3 | |
| ' Define button parameters | |
| speedX = 1808 ' x position of middle of speed buttons | |
| inclineX = 114 ' X position of middle of incline buttons | |
| ' Define button positions and layout | |
| Select Case buttonCount | |
| Case 10 | |
| buttonBottomY = 977 ' Y position of middle of bottom button (swipe up) | |
| swipeBottomY = 0 ' Y position of middle of bottom button (swipe down) | |
| buttonTopY = 186 ' Y position of middle of top button (swipe up) | |
| swipeShift = 0 ' Y button shift for swipe | |
| Case 9 | |
| buttonBottomY = 915 | |
| swipeBottomY = 889 | |
| buttonTopY = 211 | |
| swipeShift = 63 | |
| Case 8 | |
| buttonBottomY = 827 | |
| swipeBottomY = 801 | |
| buttonTopY = 212 | |
| swipeShift = 150 | |
| Case 7 | |
| buttonBottomY = 752 | |
| swipeBottomY = 714 | |
| buttonTopY = 227 | |
| swipeShift = 225 | |
| Case 6 | |
| buttonBottomY = 672 | |
| swipeBottomY = 625 | |
| buttonTopY = 233 | |
| swipeShift = 305 | |
| End Select | |
| ' *** End - NordicTrack C2950 Treadmill *** | |
| buttonSpacingY = (buttonBottomY - buttonTopY) / (buttonCount - 1) ' Vertical spacing between buttons | |
| Do ' Process treadmill and Zwift workout metrics | |
| ' Query Zwift for workout speed and incline | |
| workout = GetZwiftWorkout() | |
| zwiftSpeed = workout(0) | |
| zwiftIncline = workout(1) | |
| ' Auto-speed ---------- | |
| ' Query treadmill for speed | |
| cmdstring = "adb shell tail -n5000 " & infilename2 & " | grep -a ""Changed KPH""" & " | tail -n1 | grep -oP ""(?<=to\s)\d+(\.\d+)?(?=\skph)""" | |
| ' Use synchronous exec | |
| set oexec = wso.exec("cmd /c " & cmdstring) ' Execute adb command | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| wscript.sleep 100 | |
| Loop | |
| sValue = oexec.stdout.readline | |
| ' Process treadmill speed | |
| If sValue <> "" then | |
| treadmillSpeed = formatnumber(csng(sValue),1) | |
| WScript.Echo "Treadmill speed: " & treadmillSpeed | |
| Else | |
| WScript.Echo "Waiting for treadmill to come online..." | |
| End If | |
| If IsEmpty(zwiftSpeed) Then | |
| WScript.Echo "Waiting for Zwift to come online..." | |
| Else | |
| WScript.Echo "Zwift speed: " & zwiftSpeed | |
| WScript.Echo "Old Zwift speed: " & oldzwiftSpeed | |
| ' Initialize variables for finding the closest speed | |
| closestSpeed = speedButtons(0) | |
| minSpeedDifference = Abs(zwiftSpeed - speedButtons(0)) | |
| closestSpeedIndex = 0 | |
| ' Loop through the speed buttons to find the closest speed | |
| For i = 1 To UBound(speedButtons) | |
| currentSpeedDifference = Abs(zwiftSpeed - speedButtons(i)) | |
| If currentSpeedDifference < minSpeedDifference Then | |
| minSpeedDifference = currentSpeedDifference | |
| closestSpeed = speedButtons(i) | |
| closestSpeedIndex = i | |
| End If | |
| Next | |
| WScript.Echo "Closest speed: " & closestSpeed | |
| ' Calculate pixel location for center of the closest speed button | |
| If (closestSpeedIndex + 1) > buttonCount Then | |
| speedY = Round(buttonBottomY - (buttonSpacingY * closestSpeedIndex) + swipeShift) ' Center y-coordinate of the speed button | |
| Else | |
| speedY = Round(buttonBottomY - (buttonSpacingY * closestSpeedIndex)) ' Center y-coordinate of the speed button | |
| End If | |
| ' Process speed commands if Zwift speed has changed | |
| If zwiftSpeed <> oldzwiftSpeed Then | |
| ' Process speed swipe for button visibility | |
| If buttonCount <> 10 Then | |
| If (closestSpeedIndex + 1) > buttonCount Then | |
| cmdString = "adb shell input swipe " & speedX & " " & buttonTopY & " " & speedX & " " & (buttonTopY + swipeAmount) & " " & swipeSpeed ' Swipe down | |
| Else | |
| cmdString = "adb shell input swipe " & speedX & " " & buttonBottomY & " " & speedX & " " & (buttonBottomY - swipeAmount) & " " & swipeSpeed ' Swipe up | |
| End If | |
| ' Use synchronous exec | |
| set oexec = wso.exec("cmd /c " & cmdstring) ' Execute adb command | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| wscript.sleep 100 | |
| Loop | |
| sValue = oexec.stdout.readline | |
| wscript.echo "Speed swipe: " & cmdString | |
| End If | |
| ' Process speed button click | |
| cmdString = "adb shell input tap " & speedX & " " & speedY | |
| ' Use synchronous exec | |
| set oexec = wso.exec("cmd /c " & cmdstring) ' Execute adb command | |
| 'wait for completion | |
| Do While oexec.Status = 0 | |
| wscript.sleep 100 | |
| Loop | |
| sValue = oexec.stdout.readline | |
| WScript.Echo "Speed tap: " & cmdString | |
| oldzwiftSpeed = zwiftSpeed | |
| End If | |
| End If | |
| WScript.Echo | |
| ' Auto-incline ---------- | |
| ' Query treadmill for incline | |
| cmdString = "adb shell tail -n5000 " & infilename2 & " | grep -a ""Changed INCLINE""" & " | tail -n1 | grep -oP ""(?<=to\s)[+-]?\d+(\.\d+)?(?=\s%)""" | |
| ' Use synchronous exec | |
| Set oexec = wso.Exec("cmd /c " & cmdString) ' Execute adb command | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| WScript.Sleep 100 | |
| Loop | |
| sValue = oexec.StdOut.ReadLine | |
| ' Process treadmill incline | |
| If sValue <> "" Then | |
| treadmillIncline = FormatNumber(CSng(sValue), 1) | |
| WScript.Echo "Treadmill incline: " & treadmillIncline | |
| Else | |
| WScript.Echo "Waiting for treadmill to come online..." | |
| End If | |
| If IsEmpty(zwiftIncline) Then | |
| WScript.Echo "Waiting for Zwift to come online..." | |
| Else | |
| WScript.Echo "Zwift incline: " & zwiftIncline | |
| WScript.Echo "Old Zwift incline: " & oldzwiftIncline | |
| ' Find the closest incline | |
| closestIncline = inclineButtons(0) | |
| minInclineDifference = Abs(zwiftIncline - inclineButtons(0)) | |
| closestInclineIndex = 0 | |
| For i = 1 To UBound(inclineButtons) | |
| currentInclineDifference = Abs(zwiftIncline - inclineButtons(i)) | |
| If currentInclineDifference < minInclineDifference Then | |
| minInclineDifference = currentInclineDifference | |
| closestIncline = inclineButtons(i) | |
| closestInclineIndex = i | |
| End If | |
| Next | |
| WScript.Echo "Closest incline: " & closestIncline | |
| ' Calculate pixel location for center of the closest incline button | |
| inclineY = Round(buttonBottomY - (buttonSpacingY * closestInclineIndex)) | |
| If (closestInclineIndex + 1) > buttonCount Then | |
| inclineY = Round(inclineY + swipeShift) | |
| End If | |
| ' Process incline commands if Zwift incline has changed | |
| If zwiftIncline <> oldzwiftIncline Then | |
| ' Process incline swipe for button visibility | |
| If buttonCount <> 10 Then | |
| If (closestInclineIndex + 1) > buttonCount Then | |
| cmdString = "adb shell input swipe " & inclineX & " " & buttonTopY & " " & inclineX & " " & (buttonTopY + swipeAmount) & " " & swipeSpeed | |
| Else | |
| cmdString = "adb shell input swipe " & inclineX & " " & buttonBottomY & " " & inclineX & " " & (buttonBottomY - swipeAmount) & " " & swipeSpeed | |
| End If | |
| ' Use synchronous exec | |
| Set oexec = wso.Exec("cmd /c " & cmdString) | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| WScript.Sleep 100 | |
| Loop | |
| WScript.Echo "Incline swipe: " & cmdString | |
| End If | |
| ' Process incline button click | |
| cmdString = "adb shell input tap " & inclineX & " " & inclineY | |
| ' Use synchronous exec | |
| Set oexec = wso.Exec("cmd /c " & cmdString) | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| WScript.Sleep 100 | |
| Loop | |
| WScript.Echo "Incline tap: " & cmdString | |
| oldzwiftIncline = zwiftIncline | |
| End If | |
| End If | |
| WScript.Echo | |
| Loop ' Process treadmill and Zwift workout metrics | |
| '--- Functions --- | |
| Function GetZwiftWorkout() | |
| ' Take a screenshot of Zwift, save it to disk, then OCR image for workout metrics | |
| Set wshShell = WScript.CreateObject("WScript.Shell") | |
| Set fso = WScript.CreateObject("Scripting.FileSystemObject") | |
| strComputer = "." | |
| FindProc = "zwiftapp.exe" | |
| ' Is Zwift running? | |
| Set objWMIService = GetObject("winmgmts:" _ | |
| & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2") | |
| Set colProcessList = objWMIService.ExecQuery _ | |
| ("Select Name from Win32_Process WHERE Name='" & FindProc & "'") | |
| ' Process Zwift metrics | |
| If colProcessList.count > 0 then | |
| cmdString = "python zwift-workout.py" | |
| ' Use synchronous exec | |
| Set oexec = wso.Exec("cmd /c " & cmdString) | |
| ' Wait for completion | |
| Do While oexec.Status = 0 | |
| Wscript.sleep 100 | |
| Loop | |
| sValue = oexec.StdOut.ReadLine | |
| metrics = Split(sValue, ";") | |
| If UBound(metrics) = 0 Then metrics = array(Empty,Empty) | |
| sSpeed = metrics(0) | |
| sIncline = metrics(1) | |
| ' Validate speed is numeric | |
| If IsNumeric(sSpeed) Then | |
| nSpeed = FormatNumber(CDbl(sSpeed), 1) | |
| 'WScript.Echo "Zwift speed: " & nSpeed | |
| End If | |
| ' Validate incline is numeric | |
| If IsNumeric(sIncline) Then | |
| nIncline = FormatNumber(CDbl(sIncline), 1) | |
| 'WScript.Echo "Zwift incline: " & nIncline | |
| End If | |
| End If | |
| ' Return metrics array | |
| GetZwiftWorkout = array(nSpeed, nIncline) | |
| Set objWMIService = Nothing | |
| Set colProcessList = Nothing | |
| End Function |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment