Skip to content

Commit f2221b0

Browse files
committed
Initial commit
0 parents  commit f2221b0

28 files changed

+4222
-0
lines changed

.flake8

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[flake8]
2+
max-line-length = 88
3+
extend-ignore = E203, W503

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# Environments
7+
env/
8+
9+
# vscode
10+
.vscode/

.isort.cfg

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[settings]
2+
multi_line_output = 3
3+
include_trailing_comma = True
4+
force_grid_wrap = 0
5+
use_parentheses = True
6+
ensure_newline_before_comments = True
7+
line_length = 88

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Reverse engineering of the Horizon 7.4 AT (-02) Treadmill

btsnoop_hci.log

120 KB
Binary file not shown.

horizon/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# flake8: noqa
2+
from .state import State
3+
from .consts import HEADER_LENGTH
4+
from .workout_data import WorkoutData
5+
from .user_info import UserInfo
6+
from .message import Message
7+
from .device_info import DeviceInfo
8+
from .set_speed import SetSpeed
9+
from .set_incline import SetIncline
10+
from .workout_summary import WorkoutSummary

horizon/consts.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
HEADER_LENGTH = 10

horizon/device_info.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from .message import Message
2+
3+
4+
class DeviceInfo(Message):
5+
machineType: int = 0
6+
modelType: int = 0
7+
securitySwitch: int = 0
8+
runStatus: int = 0
9+
10+
def __init__(self, msg: bytes) -> None:
11+
super().__init__(msg)
12+
13+
if self.length > 0:
14+
self.machineType = msg[11]
15+
self.modelType = msg[12]
16+
self.securitySwitch = msg[141]
17+
self.runStatus = msg[142]
18+
19+
def format_body(self) -> str:
20+
return (
21+
"Device Info:\n"
22+
f" Machine Type: {self.machineType:>3d} Model Type: {self.modelType:>3d}\n"
23+
f" Security Switch: {self.securitySwitch:>3d} Run Status: {self.runStatus:>3d}"
24+
)

horizon/message.py

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
class Message:
2+
msg: bytes = b""
3+
type: str = ""
4+
signature: bytes = b""
5+
seq_num: int = 0
6+
msg4: int = 0
7+
msg5: int = 0
8+
length: int = 0
9+
10+
# Note crc is a unreflected CRC-16 checksum using the CCITT CRC16 table
11+
crc: int = 0
12+
13+
def __init__(self, msg: bytes) -> None:
14+
self.msg = msg
15+
self.signature = msg[0:2]
16+
self.seq_num = int.from_bytes(msg[2:4], "little")
17+
self.msg4 = msg[4]
18+
self.msg5 = msg[5]
19+
self.length = int.from_bytes(msg[6:8], "little")
20+
self.crc = int.from_bytes(msg[8:10], "little")
21+
22+
self.parse_msg_type()
23+
24+
def format_header(self) -> str:
25+
return (
26+
"Header\n"
27+
f" signature: 0x{self.signature.hex()} sequence number: 0x{self.seq_num:04x}\n"
28+
f" msg[4]: 0x{self.msg4:02x}\n"
29+
f" msg[5]: 0x{self.msg5:02x}\n"
30+
f" length: 0x{self.length:04x}\n"
31+
f" CRC: 0x{self.crc:04x}"
32+
)
33+
34+
def format_type(self) -> str:
35+
return f"Message Type: {self.type}"
36+
37+
def format_body(self) -> str:
38+
return ""
39+
40+
def format_raw_msg(self) -> str:
41+
string = ""
42+
while self.msg:
43+
line = self.msg[0:20]
44+
while line:
45+
string += line[0:2].hex() + " "
46+
line = line[2:]
47+
string += "\n"
48+
self.msg = self.msg[20:]
49+
return string
50+
51+
def __str__(self) -> str:
52+
return self.format_type() + "\n" + self.format_header() + "\n" + self.format_body()
53+
54+
def parse_msg_type(self) -> None:
55+
msg_type = ""
56+
if self.msg[5] == 0x00:
57+
if self.msg[10] == 0x01:
58+
msg_type = "response_selectUser"
59+
elif self.msg[10] == 0x02:
60+
msg_type = "response_startWorkout"
61+
elif self.msg[10] == 0x03:
62+
if self.msg[11] == 0x00 or self.msg[11] == 0x02:
63+
msg_type = "response_pauseWorkout"
64+
else:
65+
msg_type = "response_continueWorkout"
66+
elif self.msg[10] == 0x04:
67+
msg_type = "response_changeWorkout"
68+
elif self.msg[10] == 0x05:
69+
msg_type = "response_setSpeed"
70+
elif self.msg[10] == 0x06:
71+
msg_type = "response_setIncline"
72+
elif self.msg[10] == 0x07:
73+
msg_type = "response_setResistance"
74+
elif self.msg[10] == 0x0F:
75+
msg_type = "response_setUser"
76+
elif self.msg[10] == 0x10:
77+
msg_type = "response_selectProgram"
78+
elif self.msg[10] == 0x12:
79+
msg_type = "response_deleteUser"
80+
elif self.msg[10] == 0x13:
81+
msg_type = "response_setHeartValue"
82+
elif self.msg[10] == 0x14:
83+
msg_type = "response_stopWorkout"
84+
elif self.msg[10] == 0x17:
85+
msg_type = "response_setMyFirst5k"
86+
elif self.msg[10] == 0x18:
87+
msg_type = "response_customProgram"
88+
elif self.msg[10] == 0x19:
89+
msg_type = "response_setCustomHRProgram"
90+
elif self.msg[10] == 0x1A:
91+
msg_type = "response_workoutProgram"
92+
elif self.msg[10] == 0x1B:
93+
msg_type = "response_setPopupInfo"
94+
95+
elif self.msg[5] == 0x01:
96+
if self.msg[4] == 0x03:
97+
msg_type = "selectUser"
98+
99+
elif self.msg[5] == 0x02:
100+
if self.msg[4] == 0x01:
101+
msg_type = "getMachineInfo"
102+
elif self.msg[4] == 0x03:
103+
msg_type = "startWorkout"
104+
105+
elif self.msg[5] == 0x03:
106+
if self.msg[10] == 0x01:
107+
msg_type = "continueWorkout"
108+
else:
109+
msg_type = "pauseWorkout"
110+
111+
elif self.msg[5] == 0x04:
112+
if self.msg[4] == 0x03:
113+
msg_type = "changeWorkout"
114+
115+
elif self.msg[5] == 0x05:
116+
if self.msg[4] == 0x03:
117+
msg_type = "setSpeed"
118+
119+
elif self.msg[5] == 0x06:
120+
if self.msg[4] == 0x03:
121+
msg_type = "setIncline"
122+
123+
elif self.msg[5] == 0x07:
124+
if self.msg[4] == 0x03:
125+
msg_type = "setResistance"
126+
127+
elif self.msg[5] == 0x10:
128+
if self.msg[4] == 0x03:
129+
msg_type = "selectProgram"
130+
131+
elif self.msg[5] == 0x11:
132+
msg_type = "keyPress"
133+
134+
elif self.msg[5] == 0x12:
135+
msg_type = "workoutData"
136+
137+
elif self.msg[5] == 0x13:
138+
if self.msg[4] == 0x03:
139+
msg_type = "setHeartValue"
140+
141+
elif self.msg[5] == 0x14:
142+
if self.msg[4] == 0x01:
143+
msg_type = "stopWorkout"
144+
elif self.msg[4] == 0x03:
145+
msg_type = "timeSync"
146+
147+
elif self.msg[5] == 0x16:
148+
msg_type = "userInfo"
149+
150+
elif self.msg[5] == 0x17:
151+
msg_type = "setMyFirst5k"
152+
153+
elif self.msg[5] == 0x18:
154+
msg_type = "setCustomProgram"
155+
156+
elif self.msg[5] == 0x19:
157+
msg_type = "setCustomHRProgram"
158+
159+
elif self.msg[5] == 0x1A:
160+
if self.msg[4] == 0x01:
161+
msg_type = "workoutSummary"
162+
163+
elif self.msg[5] == 0x1B:
164+
msg_type = "setPopupInfo"
165+
166+
if msg_type == "":
167+
msg_type = (
168+
f"Unknown: msg[4]: {self.msg[4:5].hex()} msg[5]: {self.msg[5:6].hex()} "
169+
f"msg[10]: {self.msg[10:11].hex()} msg[11]: {self.msg[11:12].hex()}"
170+
)
171+
172+
self.type = msg_type

0 commit comments

Comments
 (0)