2 Implementation of the protocol used by zx303 GPS+GPRS module
3 Description from https://github.com/tobadia/petGPS/tree/master/resources
6 from datetime import datetime, timezone
7 from inspect import isclass
8 from logging import getLogger
9 from struct import pack, unpack
21 "GPS_OFFLINE_POSITIONING",
26 "WIFI_OFFLINE_POSITIONING",
31 "SYNCHRONOUS_WHITELIST",
37 "CHARGER_DISCONNECTED",
39 "POSITION_UPLOAD_INTERVAL",
42 log = getLogger("gps303")
49 def __init__(self, *args, **kwargs):
51 for k, v in kwargs.items():
55 return "{}({})".format(
56 self.__class__.__name__,
60 'bytes.fromhex("{}")'.format(v.hex())
61 if isinstance(v, bytes)
64 for k, v in self.__dict__.items()
65 if not k.startswith("_")
70 def from_packet(cls, length, proto, payload):
71 return cls(proto=proto, payload=payload, length=length)
73 def response(self, *args):
76 assert len(args) == 1 and isinstance(args[0], bytes)
78 length = len(payload) + 1
81 return b"xx" + pack("BB", length, self.proto) + payload + b"\r\n"
84 class UNKNOWN(_GT06pkt):
88 class LOGIN(_GT06pkt):
92 def from_packet(cls, length, proto, payload):
93 self = super().from_packet(length, proto, payload)
94 self.imei = payload[:-1].hex()
95 self.ver = unpack("B", payload[-1:])[0]
99 return super().response(b"")
102 class SUPERVISION(_GT06pkt):
106 class HEARTBEAT(_GT06pkt):
110 class _GPS_POSITIONING(_GT06pkt):
112 def from_packet(cls, length, proto, payload):
113 self = super().from_packet(length, proto, payload)
114 self.dtime = payload[:6]
115 if self.dtime == b"\0\0\0\0\0\0":
118 self.devtime = datetime(
119 *unpack("BBBBBB", self.dtime), tzinfo=timezone.utc
121 self.gps_data_length = payload[6] >> 4
122 self.gps_nb_sat = payload[6] & 0x0F
123 lat, lon, speed, flags = unpack("!IIBH", payload[7:18])
124 self.gps_is_valid = bool(flags & 0b0001000000000000) # bit 3
125 flip_lon = bool(flags & 0b0000100000000000) # bit 4
126 flip_lat = not bool(flags & 0b0000010000000000) # bit 5
127 self.heading = flags & 0b0000001111111111 # bits 6 - last
128 self.latitude = lat / (30000 * 60) * (-1 if flip_lat else 1)
129 self.longitude = lon / (30000 * 60) * (-2 if flip_lon else 1)
135 return super().response(self.dtime)
138 class GPS_POSITIONING(_GPS_POSITIONING):
142 class GPS_OFFLINE_POSITIONING(_GPS_POSITIONING):
146 class STATUS(_GT06pkt):
150 def from_packet(cls, length, proto, payload):
151 self = super().from_packet(length, proto, payload)
152 if len(payload) == 5:
153 self.batt, self.ver, self.intvl, self.signal, _ = unpack(
156 elif len(payload) == 4:
157 self.batt, self.ver, self.intvl, _ = unpack("BBBB", payload)
162 class HIBERNATION(_GT06pkt):
166 class RESET(_GT06pkt):
170 class WHITELIST_TOTAL(_GT06pkt):
174 class _WIFI_POSITIONING(_GT06pkt):
176 def from_packet(cls, length, proto, payload):
177 self = super().from_packet(length, proto, payload)
178 self.dtime = payload[:6]
179 if self.dtime == b"\0\0\0\0\0\0":
182 self.devtime = datetime.strptime(
183 self.dtime.hex(), "%y%m%d%H%M%S"
184 ).astimezone(tz=timezone.utc)
186 for i in range(self.length): # length has special meaning here
187 slice = payload[6 + i * 7 : 13 + i * 7]
188 self.wifi_aps.append(
189 (":".join([format(b, "02X") for b in slice[:6]]), -slice[6])
191 gsm_slice = payload[6 + self.length * 7 :]
192 ncells, self.mcc, self.mnc = unpack("!BHB", gsm_slice[:4])
194 for i in range(ncells):
195 slice = gsm_slice[4 + i * 5 : 9 + i * 5]
196 locac, cellid, sigstr = unpack(
197 "!HHB", gsm_slice[4 + i * 5 : 9 + i * 5]
199 self.gsm_cells.append((locac, cellid, -sigstr))
203 class WIFI_OFFLINE_POSITIONING(_WIFI_POSITIONING):
207 return super().response(self.dtime)
210 class TIME(_GT06pkt):
214 payload = pack("!HBBBBB", *datetime.utcnow().timetuple()[:6])
215 return super().response(payload)
218 class MOM_PHONE(_GT06pkt):
222 class STOP_ALARM(_GT06pkt):
226 class SETUP(_GT06pkt):
231 uploadIntervalSeconds=0x0300,
232 binarySwitch=0b00110001,
239 phoneNumbers=["", "", ""],
242 return pack("!I", x)[1:]
246 pack("!H", uploadIntervalSeconds),
247 pack("B", binarySwitch),
249 + [pack3b(el) for el in alarms]
251 pack("B", dndTimeSwitch),
253 + [pack3b(el) for el in dndTimes]
255 pack("B", gpsTimeSwitch),
256 pack("!H", gpsTimeStart),
257 pack("!H", gpsTimeStop),
259 + [b";".join([el.encode() for el in phoneNumbers])]
261 return super().response(payload)
264 class SYNCHRONOUS_WHITELIST(_GT06pkt):
268 class RESTORE_PASSWORD(_GT06pkt):
272 class WIFI_POSITIONING(_WIFI_POSITIONING):
276 payload = b"" # TODO fill payload
277 return super().response(payload)
280 class MANUAL_POSITIONING(_GT06pkt):
284 class BATTERY_CHARGE(_GT06pkt):
288 class CHARGER_CONNECTED(_GT06pkt):
292 class CHARGER_DISCONNECTED(_GT06pkt):
296 class VIBRATION_RECEIVED(_GT06pkt):
300 class POSITION_UPLOAD_INTERVAL(_GT06pkt):
304 def from_packet(cls, length, proto, payload):
305 self = super().from_packet(length, proto, payload)
306 self.interval = unpack("!H", payload[:2])
310 return super().response(pack("!H", self.interval))
313 # Build a dict protocol number -> class
315 if True: # just to indent the code, sorry!
318 for name, cls in globals().items()
320 and issubclass(cls, _GT06pkt)
321 and not name.startswith("_")
323 if hasattr(cls, "PROTO"):
324 CLASSES[cls.PROTO] = cls
327 def make_object(length, proto, payload):
329 return CLASSES[proto].from_packet(length, proto, payload)
331 return UNKNOWN.from_packet(length, proto, payload)
334 def handle_packet(packet, addr, when):
336 return UNKNOWN.from_packet(0, 0, packet)
338 xx, length, proto = unpack("!2sBB", packet[:4])
340 payload = packet[4:-2]
341 adjust = 2 if proto == STATUS.PROTO else 4 # Weird special case
344 not in (WIFI_POSITIONING.PROTO, WIFI_OFFLINE_POSITIONING.PROTO)
346 and len(payload) + adjust != length
349 "With proto %d length is %d but payload length is %d+%d",
355 if xx != b"xx" or crlf != b"\r\n":
356 return UNKNOWN.from_packet(length, proto, packet) # full packet
358 return make_object(length, proto, payload)
361 def make_response(msg):
362 return msg.response()
365 def set_config(config): # Note that we are setting _class_ attribute
366 _GT06pkt.CONFIG = config