]> www.average.org Git - loctrkd.git/blob - gps303/GT06mod.py
d10c9de6a6ed6bfd7f592d81548992af197e8b95
[loctrkd.git] / gps303 / GT06mod.py
1 """
2 Implementation of the protocol used by zx303 GPS+GPRS module
3 Description from https://github.com/tobadia/petGPS/tree/master/resources
4 """
5
6 from inspect import isclass
7 from logging import getLogger
8 from struct import pack, unpack
9
10 __all__ = ("handle_packet", "make_response")
11
12 log = getLogger("gps303")
13
14
15 class _GT06pkt:
16     PROTO: int
17
18     def __init__(self, *args, **kwargs):
19         assert len(args) == 0
20         for k, v in kwargs.items():
21             setattr(self, k, v)
22
23     def __repr__(self):
24         return "{}({})".format(
25             self.__class__.__name__,
26             ", ".join(
27                 "{}={}".format(
28                     k,
29                     'bytes.fromhex("{}")'.format(v.hex())
30                     if isinstance(v, bytes)
31                     else v.__repr__(),
32                 )
33                 for k, v in self.__dict__.items()
34                 if not k.startswith("_")
35             ),
36         )
37
38     @classmethod
39     def from_packet(cls, length, proto, payload):
40         adjust = 2 if proto == STATUS.PROTO else 4  # Weird special case
41         if length > 1 and len(payload) + adjust != length:
42             log.warning(
43                 "length is %d but payload length is %d", length, len(payload)
44             )
45         return cls(length=length, proto=proto, payload=payload)
46
47     def response(self, *args):
48         if len(args) == 0:
49             return None
50         assert len(args) == 1 and isinstance(args[0], bytes)
51         payload = args[0]
52         length = len(payload) + 1
53         if length > 6:
54             length -= 6
55         return b"xx" + pack("BB", length, self.proto) + payload + b"\r\n"
56
57
58 class UNKNOWN(_GT06pkt):
59     pass
60
61
62 class LOGIN(_GT06pkt):
63     PROTO = 0x01
64
65     @classmethod
66     def from_packet(cls, length, proto, payload):
67         self = super().from_packet(length, proto, payload)
68         self.imei = payload[:-1].hex()
69         self.ver = unpack("B", payload[-1:])[0]
70         return self
71
72     def response(self):
73         return super().response(b"")
74
75
76 class SUPERVISION(_GT06pkt):
77     PROTO = 0x05
78
79
80 class HEARTBEAT(_GT06pkt):
81     PROTO = 0x08
82
83
84 class GPS_POSITIONING(_GT06pkt):
85     PROTO = 0x10
86
87
88 class GPS_OFFLINE_POSITIONING(_GT06pkt):
89     PROTO = 0x11
90
91
92 class STATUS(_GT06pkt):
93     PROTO = 0x13
94
95     @classmethod
96     def from_packet(cls, length, proto, payload):
97         self = super().from_packet(length, proto, payload)
98         if len(payload) == 5:
99             self.batt, self.ver, self.intvl, self.signal, _ = unpack(
100                 "BBBBB", payload
101             )
102         elif len(payload) == 4:
103             self.batt, self.ver, self.intvl, _ = unpack("BBBB", payload)
104             self.signal = None
105         return self
106
107
108 class HIBERNATION(_GT06pkt):
109     PROTO = 0x14
110
111
112 class RESET(_GT06pkt):
113     PROTO = 0x15
114
115
116 class WHITELIST_TOTAL(_GT06pkt):
117     PROTO = 0x16
118
119
120 class WIFI_OFFLINE_POSITIONING(_GT06pkt):
121     PROTO = 0x17
122
123
124 class TIME(_GT06pkt):
125     PROTO = 0x30
126
127
128 class MOM_PHONE(_GT06pkt):
129     PROTO = 0x43
130
131
132 class STOP_ALARM(_GT06pkt):
133     PROTO = 0x56
134
135
136 class SETUP(_GT06pkt):
137     PROTO = 0x57
138
139     def response(
140         self,
141         uploadIntervalSeconds=0x0300,
142         binarySwitch=0b00110001,
143         alarms=[0, 0, 0],
144         dndTimeSwitch=0,
145         dndTimes=[0, 0, 0],
146         gpsTimeSwitch=0,
147         gpsTimeStart=0,
148         gpsTimeStop=0,
149         phoneNumbers=["", "", ""],
150     ):
151         def pack3b(x):
152             return pack("!I", x)[1:]
153
154         payload = b"".join(
155             [
156                 pack("!H", uploadIntervalSeconds),
157                 pack("B", binarySwitch),
158             ]
159             + [pack3b(el) for el in alarms]
160             + [
161                 pack("B", dndTimeSwitch),
162             ]
163             + [pack3b(el) for el in dndTimes]
164             + [
165                 pack("B", gpsTimeSwitch),
166                 pack("!H", gpsTimeStart),
167                 pack("!H", gpsTimeStop),
168             ]
169             + [b";".join([el.encode() for el in phoneNumbers])]
170         )
171         return super().response(payload)
172
173
174 class SYNCHRONOUS_WHITELIST(_GT06pkt):
175     PROTO = 0x58
176
177
178 class RESTORE_PASSWORD(_GT06pkt):
179     PROTO = 0x67
180
181
182 class WIFI_POSITIONING(_GT06pkt):
183     PROTO = 0x69
184
185
186 class MANUAL_POSITIONING(_GT06pkt):
187     PROTO = 0x80
188
189
190 class BATTERY_CHARGE(_GT06pkt):
191     PROTO = 0x81
192
193
194 class CHARGER_CONNECTED(_GT06pkt):
195     PROTO = 0x82
196
197
198 class CHARGER_DISCONNECTED(_GT06pkt):
199     PROTO = 0x83
200
201
202 class VIBRATION_RECEIVED(_GT06pkt):
203     PROTO = 0x94
204
205
206 class POSITION_UPLOAD_INTERVAL(_GT06pkt):
207     PROTO = 0x98
208
209
210 # Build a dict protocol number -> class
211 CLASSES = {}
212 if True:  # just to indent the code, sorry!
213     for cls in [
214         cls
215         for name, cls in globals().items()
216         if isclass(cls)
217         and issubclass(cls, _GT06pkt)
218         and not name.startswith("_")
219     ]:
220         if hasattr(cls, "PROTO"):
221             CLASSES[cls.PROTO] = cls
222
223
224 def handle_packet(packet, addr, when):
225     if len(packet) < 6:
226         msg = UNKNOWN.from_packet(0, 0, packet)
227     else:
228         xx, length, proto = unpack("!2sBB", packet[:4])
229         crlf = packet[-2:]
230         payload = packet[4:-2]
231         if xx != b"xx" or crlf != b"\r\n" or proto not in CLASSES:
232             msg = UNKNOWN.from_packet(length, proto, packet)
233         else:
234             msg = CLASSES[proto].from_packet(length, proto, payload)
235     return msg
236
237 def make_response(msg):
238     return msg.response()