├── .env.example ├── logs └── .gitignore ├── resources ├── Communication Protocol.xlsx ├── GT06_GPS_Tracker_Communication_Protocol_v1.8.1.pdf ├── ZhongXun Topin Locator Communication Protocol-180612.docx ├── ~$ongXun Topin Locator Communication Protocol-180612.docx ├── http-headers.py ├── tcp_server.py └── python_chat_server.py ├── .gitignore ├── Pipfile ├── LICENSE ├── README.md ├── identify_packet_standalone.py ├── Pipfile.lock └── gps_tcp_server.py /.env.example: -------------------------------------------------------------------------------- 1 | GMAPS_API_KEY='YOUR_GOOGLE_MAPS_API_TOKEN_HERE' 2 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /resources/Communication Protocol.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobadia/petGPS/HEAD/resources/Communication Protocol.xlsx -------------------------------------------------------------------------------- /resources/GT06_GPS_Tracker_Communication_Protocol_v1.8.1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobadia/petGPS/HEAD/resources/GT06_GPS_Tracker_Communication_Protocol_v1.8.1.pdf -------------------------------------------------------------------------------- /resources/ZhongXun Topin Locator Communication Protocol-180612.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobadia/petGPS/HEAD/resources/ZhongXun Topin Locator Communication Protocol-180612.docx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | logs/server_log.txt 4 | logs/location_log.txt 5 | logs/location_log_old.txt 6 | logs/location_log_old_newformat.txt 7 | .Rproj.user 8 | PetGPS_UI_RShiny/ 9 | -------------------------------------------------------------------------------- /resources/~$ongXun Topin Locator Communication Protocol-180612.docx: -------------------------------------------------------------------------------- 1 | Thomas Obadia Thomas ObadiaDocuments/GitHub/PetGPS/resources/~$ong -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | googlemaps = "*" 10 | datetime = "*" 11 | python-dotenv = "*" 12 | python-dateutil = "*" 13 | python-math = "*" 14 | 15 | [requires] 16 | python_version = "3.8" 17 | -------------------------------------------------------------------------------- /resources/http-headers.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def main(): 7 | print(request.headers) 8 | #print(request.headers['User-Agent']) 9 | return('This is the root of the web server') 10 | 11 | @app.route('/hello') 12 | def hello(): 13 | return('Hello, world!') 14 | 15 | if __name__ == '__main__': 16 | app.run() 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thomas Obadia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/tcp_server.py: -------------------------------------------------------------------------------- 1 | # Original source: https://medium.com/swlh/lets-write-a-chat-app-in-python-f6783a9ac170 2 | 3 | """Server for multithreaded (asynchronous) application.""" 4 | from socket import AF_INET, socket, SOCK_STREAM 5 | from threading import Thread 6 | 7 | 8 | def accept_incoming_connections(): 9 | """Accepts any incoming client connexion 10 | and starts a dedicated thread for each client.""" 11 | while True: 12 | client, client_address = SERVER.accept() 13 | print("%s:%s has connected." % client_address) 14 | # No need to send anything 15 | #client.send(bytes("Greetings from the cave! Now type your name and press enter!", "utf8")) 16 | addresses[client] = client_address 17 | Thread(target=handle_client, args=(client,)).start() 18 | 19 | 20 | def handle_client(client): # Takes client socket as argument. 21 | """Handles a single client connection.""" 22 | 23 | # Keep receiving and analyzing packets 24 | while True: 25 | msg = client.recv(BUFSIZ) 26 | print("IN: %s" % msg) 27 | 28 | 29 | addresses = {} 30 | 31 | HOST = '' 32 | PORT = 5023 33 | BUFSIZ = 1024 34 | ADDR = (HOST, PORT) 35 | 36 | SERVER = socket(AF_INET, SOCK_STREAM) 37 | SERVER.bind(ADDR) 38 | 39 | if __name__ == "__main__": 40 | SERVER.listen(5) 41 | print("Waiting for connection...") 42 | ACCEPT_THREAD = Thread(target=accept_incoming_connections) 43 | ACCEPT_THREAD.start() 44 | ACCEPT_THREAD.join() 45 | SERVER.close() -------------------------------------------------------------------------------- /resources/python_chat_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Server for multithreaded (asynchronous) chat application.""" 3 | from socket import AF_INET, socket, SOCK_STREAM 4 | from threading import Thread 5 | 6 | 7 | def accept_incoming_connections(): 8 | """Sets up handling for incoming clients.""" 9 | while True: 10 | client, client_address = SERVER.accept() 11 | print("%s:%s has connected." % client_address) 12 | client.send(bytes("Greetings from the cave! Now type your name and press enter!", "utf8")) 13 | addresses[client] = client_address 14 | Thread(target=handle_client, args=(client,)).start() 15 | 16 | 17 | def handle_client(client): # Takes client socket as argument. 18 | """Handles a single client connection.""" 19 | 20 | name = client.recv(BUFSIZ).decode("utf8") 21 | welcome = 'Welcome %s! If you ever want to quit, type {quit} to exit.' % name 22 | client.send(bytes(welcome, "utf8")) 23 | msg = "%s has joined the chat!" % name 24 | broadcast(bytes(msg, "utf8")) 25 | clients[client] = name 26 | 27 | while True: 28 | msg = client.recv(BUFSIZ) 29 | if msg != bytes("{quit}", "utf8"): 30 | broadcast(msg, name+": ") 31 | else: 32 | client.send(bytes("{quit}", "utf8")) 33 | client.close() 34 | del clients[client] 35 | broadcast(bytes("%s has left the chat." % name, "utf8")) 36 | break 37 | 38 | 39 | def broadcast(msg, prefix=""): # prefix is for name identification. 40 | """Broadcasts a message to all the clients.""" 41 | 42 | for sock in clients: 43 | sock.send(bytes(prefix, "utf8")+msg) 44 | 45 | 46 | clients = {} 47 | addresses = {} 48 | 49 | HOST = '' 50 | PORT = 33000 51 | BUFSIZ = 1024 52 | ADDR = (HOST, PORT) 53 | 54 | SERVER = socket(AF_INET, SOCK_STREAM) 55 | SERVER.bind(ADDR) 56 | 57 | if __name__ == "__main__": 58 | SERVER.listen(5) 59 | print("Waiting for connection...") 60 | ACCEPT_THREAD = Thread(target=accept_incoming_connections) 61 | ACCEPT_THREAD.start() 62 | ACCEPT_THREAD.join() 63 | SERVER.close() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PetGPS 2 | 3 | This is a DIY-project to equip my cat (and hopefully, yours) with a small GPS tracker that is fitted with a SIM card to enable real-time location. There are many commercial alternatives to such a project, but they basically all rely on: 4 | * 2G chipset with nanoSIM slot 5 | * GPS chipset 6 | * Sometimes, WiFi chipset capable of listening to nearby SSIDs (for location-based algorithms) 7 | 8 | These commercial alternatives are absolutely overpriced (device 80 EUR as of today + usually charging a montly service fee, ranging from 3 to 8 EUR). In fact, the design is based on cheap knockoff IoT devices available from a well-known chinese wholesale website. The amount of data used by this kind of tracker is minimal (like, __REALLY small__, maybe up to 1 or 2 Mb __per day of use__), and as such, there must be cheaper alternatives to these commercial things... M'kaaaay ? 9 | 10 | On the other hand, the chinese alternatives are provided with ugly UI for their ad-hoc services... and I don't want anyone in China to know where my cat goes out! 11 | 12 | This repo hosts some developments I have made while using these devices called ZX612 and ZX303 from AliExpress. As of now, it is a complete WIP project, and the code is ugly, but hopefully decently documented. The ultimate goal is to have: 13 | 1. A stand-alone Python server that communicates with the device 14 | 2. A web UI that reads location logs from the server and shows where my cat has been and when. 15 | 16 | Link for purchasing these devices: [ZX612](https://www.aliexpress.com/store/product/Topin-DIY-PCBA-612-Micro-Hidden-Mini-GPS-Tracker-Positioner-Personal-Locator-SOS-Button-Double-Positioning/2968012_32804101835.html) and [ZX303](https://www.aliexpress.com/store/product/New-ZX303-PCBA-GPS-Tracker-GSM-GPS-Wifi-LBS-Locator-SOS-Alarm-Web-APP-Tracking-TF/2968012_32826849478.html) (The ZX303 has more feature for the same size and I'd go with that one). If you want a battery included, order the versions with small plastic casing. 17 | 18 | ## Protocol documentation 19 | The protocol documentation for these devices is extremely poorly written. It was sent to me by the seller in the form of a Word document, available in the [resources folder](resources/ZhongXun%20Topin%20Locator%20Communication%20Protocol-180612.docx). It seems to be derived from the [GT06 protocol](resources/GT06_GPS_Tracker_Communication_Protocol_v1.8.1.pdf), also documented in the same directory. I have _somewhat_ re-written the documentation into an Excel document where each column represents a byte, for each kind of packet sent or received by the device. 20 | 21 | Basically: 22 | 1. Device starts and sends a 'hello' packet to the server 23 | 2. Server acknowledges the device 24 | 3. Device send location-based data (either direct GPS coordinates or proxy informations) 25 | 4. Server send proxy informations to a location API; GPS coordinates are acknowledged 26 | 5. Repeat 27 | 28 | These devices can be controlled by sending them SMS or data packets in the form of hexadecimal strings. The general format is 29 | `7878 XX YY ZZZZ 0D0A` 30 | * `7878`: (2 bytes) start bytes 31 | * `XX`: (1 byte) Data length. For some protocol numbers (see `YY` this is not the length but a parameter, e.g. number of SSIDs for WiFi location-based data) 32 | * `YY`: (1 byte) Protocol number (defining what the data will be) 33 | * `ZZZZ`: (varying length) Long chain of hex data that will be interpreted according to the value of `YY` 34 | * `0D0A`: (2 bytes) Stop bytes 35 | 36 | ## Google Maps API key with dotenv 37 | Your Google Maps API key is **strictly private**. It should **NEVER** be shared with anyone, as it enables querying the API without further login. An API key made public exposes you to unauthorized use, breach of Google's API Terms of Services, or massive querying possibly rsulting in you having to pay the bill once free queries have been exhausted. You don't want that to happen, do you ? 38 | 39 | The `dotenv` Python library is used to import the content of a `.env` file into the environment upon starting the main script. This is convenient to set your private API key in a file that will not make it into the Git repository. 40 | An empty exemple of this file is available [here](https://github.com/tobadia/petGPS/blob/master/.env.example) in the Git repository. The .gitignore file from this repository is set to *not track* the real `.env` file. As such, you should add your API key in a local copy of .env derived from the example file, e.g. with: 41 | 42 | ``` 43 | cp .env.example .env 44 | vi .env 45 | ``` 46 | 47 | Remember to **not** remove `.env` from the `.gitignore` file ! 48 | 49 | # Running the server 50 | ## Port forwarding 51 | The server is set to run on port TCP 5023. Remember to redirect that port towards the machine that will run the server. 52 | 53 | ## Start 54 | After you've created the actual `.env` file, you're all set. Just run: 55 | ``` 56 | python gps_tcp_server.py 57 | ``` 58 | Data should now be coming in. 59 | 60 | ## Stop 61 | Ctrl+C twice will kill the current connection and then kill the server. 62 | 63 | # Some more README 64 | ...to be written here 65 | -------------------------------------------------------------------------------- /identify_packet_standalone.py: -------------------------------------------------------------------------------- 1 | # Local tests with hard-coded content for requests, to test 2 | # identification of data while my sockets may be busy 3 | 4 | def identify_packet(packet): 5 | print("Original packet :", packet) 6 | print("Length of packet :", len(packet)) 7 | #print("Bytes from Hex :", bytes.fromhex(packet)) 8 | #print("Length of Bytes from Hex :", len(bytes.fromhex(packet))) 9 | #print("ByteArray from Hex :", bytearray.fromhex(packet)) 10 | #print("Length of ByteArray from Hex :", len(bytearray.fromhex(packet))) 11 | print("Hex from Bytes :", packet.hex()) 12 | print("Length of Hex from Bytes :", len(packet.hex())) 13 | 14 | # DEBUG: Manually set client value and dictionary 15 | client = "tmp" 16 | 17 | # Explore Bytes 18 | #print("-----BYTES-----") 19 | #for b in bytes.fromhex(packet): 20 | #print(b, "Type : ", type(b)) 21 | #print(bytes(b)) 22 | #print("-----BYTEARRAY-----") 23 | #for b in bytearray.fromhex(packet): 24 | # print(b, "Type : ", type(b)) 25 | # print(bytes(b)) 26 | 27 | # Convert hex string into list for convenience 28 | # Strip packet of bits 1 and 2 (start 0x78 0x78) and n-1 and n (end 0x0d 0x0a) 29 | packet_list = [packet.hex()[i:i+2] for i in range(4, len(packet.hex())-4, 2)] 30 | 31 | # DEBUG: Print packet 32 | #print("-----HEX LIST-----") 33 | #for h in packet_list: 34 | # print(h) 35 | 36 | # DEBUG: Print the role of current packet 37 | protocol_name = protocol_dict['protocol'][packet_list[1]] 38 | protocol_method = protocol_dict['response_method'][protocol_name] 39 | print("The current packet is for protocol: " + protocol_name + " which has method: " + protocol_method) 40 | # If the packet requires a specific response, run the associated function 41 | if (protocol_method == "login"): 42 | r = login(client, packet_list) 43 | elif (protocol_method == "setup"): 44 | # TODO: HANDLE NON-DEFAULT VALUES 45 | r = setup(packet_list, '0300', '00110001', '000000', '000000', '000000', '00', '000000', '000000', '000000', '00', '0000', '0000', ['', '', '']) 46 | 47 | # Otherwise, return a generic packet based on the current protocol number 48 | else: 49 | r = generic_response(packet_list[1]) 50 | 51 | # DEBUG: Run login function manually 52 | #r = login(packet_list) 53 | 54 | print("OUT Hex : ", r) 55 | print("OUT Bytes: ", bytes.fromhex(r)) 56 | 57 | def login(client, q): 58 | """This function extracts IMEI and Software Version from the login packet. 59 | The IMEI and Software Version will be stored into a client dictionary to 60 | allow handling of multiple devices at once, in the future. 61 | 62 | The client socket is passed as an argument because it is in this packet 63 | that IMEI is sent and will be stored in the address dictionary. 64 | """ 65 | 66 | # Initialize the dictionary 67 | addresses[client] = {} 68 | 69 | # Read data: Bits 2 through 9 are IMEI and 10 is software version 70 | protocol = q[1] 71 | addresses[client]['imei'] = q[2] + q[3] + q[4] + q[5] + q[6] + q[7] + q[8] + q[9] 72 | addresses[client]['software_version'] = q[10] 73 | 74 | # DEBUG: Print IMEI and software version 75 | print("IMEI : ", addresses[client]['imei']) 76 | print("Sw v. : ", addresses[client]['software_version']) 77 | 78 | # Prepare response: in absence of control values, 79 | # always accept the client 80 | response = '01' 81 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2']) 82 | return(r) 83 | 84 | 85 | def setup(q, uploadIntervalSeconds, binarySwitch, alarm1, alarm2, alarm3, dndTimeSwitch, dndTime1, dndTime2, dndTime3, gpsTimeSwitch, gpsTimeStart, gpsTimeStop, phoneNumbers): 86 | """Synchronous setup is initiated by the device who asks the server for 87 | instructions. 88 | These instructions will consists of bits for different flags as well as 89 | alarm clocks ans emergency phone numbers.""" 90 | 91 | # Read protocol 92 | protocol = q[1] 93 | 94 | # Convert binarySwitch from byte to hex 95 | binarySwitch = format(int(binarySwitch, base=2), '02X') 96 | 97 | # Convert phone numbers to 'ASCII' (?) by padding each digit with 3's and concatenate 98 | for n in range(len(phoneNumbers)): 99 | phoneNumbers[n] = bytes(phoneNumbers[n], 'UTF-8').hex() 100 | phoneNumbers = '3B'.join(phoneNumbers) 101 | 102 | # Build response 103 | response = uploadIntervalSeconds + binarySwitch + alarm1 + alarm2 + alarm3 + dndTimeSwitch + dndTime1 + dndTime2 + dndTime3 + gpsTimeSwitch + gpsTimeStart + gpsTimeStop + phoneNumbers 104 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2']) 105 | return(r) 106 | 107 | 108 | def generic_response(protocol): 109 | """Many queries made by the device do not expect a complex 110 | response: most of the times, the device expects the exact same packet. 111 | Here, we will answer with the same value of protocol that the device sent, 112 | not using any content.""" 113 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, None, hex_dict['stop_1'] + hex_dict['stop_2']) 114 | return(r) 115 | 116 | 117 | def make_content_response(start, protocol, content, stop): 118 | """This is just a wrapper to generate the complete response 119 | to a query, goven its content. 120 | It will apply to all packets where response is of the format: 121 | start-start-length-protocol-content-stop_1-stop_2. 122 | Other specific packets where length is replaced by counters 123 | will be treated separately.""" 124 | return(start + format((len(bytes.fromhex(content)) if content else 0)+1, '02X') + protocol + (content if content else '') + stop) 125 | 126 | 127 | # Declare common Hex codes for packets 128 | hex_dict = { 129 | 'start': '78', 130 | 'stop_1': '0D', 131 | 'stop_2': '0A' 132 | } 133 | 134 | protocol_dict = { 135 | 'protocol': { 136 | '01': 'login', 137 | '05': 'supervision', 138 | '08': 'heartbeat', 139 | '10': 'gps_positioning', 140 | '11': 'gps_offline_positioning', 141 | '13': 'status', 142 | '14': 'hibernation', 143 | '15': 'factory', 144 | '16': 'whitelist_total', 145 | '17': 'offline_wifi', 146 | '30': 'time', 147 | '56': 'stop_alarm', 148 | '57': 'setup', 149 | '58': 'synchronous_whitelist', 150 | '67': 'restore_password', 151 | '69': 'wifi_positioning', 152 | '80': 'manual_positioning', 153 | '81': 'battery_charge', 154 | '82': 'charger_connected', 155 | '83': 'charger_disconnected', 156 | '94': 'vibration_received' 157 | }, 158 | 'response_method': { 159 | 'login': 'login', 160 | 'supervision': '', 161 | 'heartbeat': '', 162 | 'gps_positioning': 'datetime_response', 163 | 'gps_offline_positioning': 'datetime_response', 164 | 'status': '', 165 | 'hibernation': '', 166 | 'factory': '', 167 | 'whitelist_total': '', 168 | 'offline_wifi': '', 169 | 'time': '', 170 | 'stop_alarm': '', 171 | 'setup': 'setup', 172 | 'synchronous_whitelist': '', 173 | 'restore_password': '', 174 | 'wifi_positioning': '', 175 | 'manual_positioning': '', 176 | 'battery_charge': '', 177 | 'charger_connected': '', 178 | 'charger_disconnected': '', 179 | 'vibration_received': '' 180 | } 181 | } 182 | 183 | 184 | addresses = {} 185 | if __name__ == "__main__": 186 | hex_login_q = "78780d010359339075016807420d0a" 187 | identify_packet(bytes.fromhex(hex_login_q)) 188 | hex_setup_q = "787801570d0a" 189 | identify_packet(bytes.fromhex(hex_setup_q)) -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "72756721844b0abffc4f75951b7b3b8b7d62f43f8a8db0343613c59fa2ad78f4" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", 22 | "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" 23 | ], 24 | "index": "pypi", 25 | "markers": "python_version >= '3.6'", 26 | "version": "==2024.7.4" 27 | }, 28 | "charset-normalizer": { 29 | "hashes": [ 30 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 31 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 32 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 33 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 34 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 35 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 36 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 37 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 38 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 39 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 40 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 41 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 42 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 43 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 44 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 45 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 46 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 47 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 48 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 49 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 50 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 51 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 52 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 53 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 54 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 55 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 56 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 57 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 58 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 59 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 60 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 61 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 62 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 63 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 64 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 65 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 66 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 67 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 68 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 69 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 70 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 71 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 72 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 73 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 74 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 75 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 76 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 77 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 78 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 79 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 80 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 81 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 82 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 83 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 84 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 85 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 86 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 87 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 88 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 89 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 90 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 91 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 92 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 93 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 94 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 95 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 96 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 97 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 98 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 99 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 100 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 101 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 102 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 103 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 104 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 105 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 106 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 107 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 108 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 109 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 110 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 111 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 112 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 113 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 114 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 115 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 116 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 117 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 118 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 119 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 120 | ], 121 | "markers": "python_full_version >= '3.7.0'", 122 | "version": "==3.3.2" 123 | }, 124 | "datetime": { 125 | "hashes": [ 126 | "sha256:371dba07417b929a4fa685c2f7a3eaa6a62d60c02947831f97d4df9a9e70dfd0", 127 | "sha256:5cef605bab8259ff61281762cdf3290e459fbf0b4719951d5fab967d5f2ea0ea" 128 | ], 129 | "index": "pypi", 130 | "version": "==4.3" 131 | }, 132 | "googlemaps": { 133 | "hashes": [ 134 | "sha256:8e13cbecc9b5b0462d53a78074c166753027be285db0c9fff70f5b40241ce500" 135 | ], 136 | "index": "pypi", 137 | "markers": "python_version >= '3.5'", 138 | "version": "==4.4.2" 139 | }, 140 | "idna": { 141 | "hashes": [ 142 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 143 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 144 | ], 145 | "markers": "python_version >= '3.5'", 146 | "version": "==3.7" 147 | }, 148 | "python-dateutil": { 149 | "hashes": [ 150 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 151 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 152 | ], 153 | "index": "pypi", 154 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 155 | "version": "==2.8.1" 156 | }, 157 | "python-dotenv": { 158 | "hashes": [ 159 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 160 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 161 | ], 162 | "index": "pypi", 163 | "version": "==0.14.0" 164 | }, 165 | "python-math": { 166 | "hashes": [ 167 | "sha256:897efb087bc81bbb32277046830c1e2407203c637a43aa0834dcd4de822024c8" 168 | ], 169 | "index": "pypi", 170 | "version": "==0.0.1" 171 | }, 172 | "pytz": { 173 | "hashes": [ 174 | "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", 175 | "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" 176 | ], 177 | "version": "==2023.3.post1" 178 | }, 179 | "requests": { 180 | "hashes": [ 181 | "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5", 182 | "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8" 183 | ], 184 | "index": "pypi", 185 | "markers": "python_version >= '3.8'", 186 | "version": "==2.32.0" 187 | }, 188 | "setuptools": { 189 | "hashes": [ 190 | "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", 191 | "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" 192 | ], 193 | "index": "pypi", 194 | "markers": "python_version >= '3.8'", 195 | "version": "==70.0.0" 196 | }, 197 | "six": { 198 | "hashes": [ 199 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 200 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 201 | ], 202 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 203 | "version": "==1.16.0" 204 | }, 205 | "urllib3": { 206 | "hashes": [ 207 | "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", 208 | "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" 209 | ], 210 | "index": "pypi", 211 | "markers": "python_version >= '3.8'", 212 | "version": "==2.2.2" 213 | }, 214 | "zope.interface": { 215 | "hashes": [ 216 | "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", 217 | "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c", 218 | "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac", 219 | "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", 220 | "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d", 221 | "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", 222 | "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", 223 | "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179", 224 | "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", 225 | "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941", 226 | "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d", 227 | "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", 228 | "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b", 229 | "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", 230 | "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f", 231 | "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3", 232 | "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d", 233 | "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", 234 | "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", 235 | "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", 236 | "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", 237 | "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40", 238 | "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", 239 | "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1", 240 | "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", 241 | "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", 242 | "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", 243 | "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43", 244 | "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", 245 | "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", 246 | "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379", 247 | "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", 248 | "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83", 249 | "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56", 250 | "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9", 251 | "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de" 252 | ], 253 | "markers": "python_version >= '3.7'", 254 | "version": "==6.1" 255 | } 256 | }, 257 | "develop": {} 258 | } 259 | -------------------------------------------------------------------------------- /gps_tcp_server.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | """ 4 | TCP Server for multithreaded (asynchronous) application. 5 | 6 | This server implements the protocol documented by the chinese 7 | company TOPIN to handle communication with their GPS trackers, 8 | sending and receiving TCP packets over 2G network. 9 | 10 | This program will create a TCP socket and each client will have 11 | its dedicated thread created, so that multipe clients can connect 12 | simultaneously should this be necessary someday. 13 | 14 | This server is based on the work from: 15 | https://medium.com/swlh/lets-write-a-chat-app-in-python-f6783a9ac170 16 | """ 17 | 18 | from dotenv import load_dotenv 19 | from socket import AF_INET, socket, SOCK_STREAM 20 | from threading import Thread 21 | from datetime import datetime 22 | from dateutil import tz 23 | import googlemaps 24 | import math 25 | import os 26 | 27 | 28 | def accept_incoming_connections(): 29 | """ 30 | Accepts any incoming client connexion 31 | and starts a dedicated thread for each client. 32 | """ 33 | 34 | while True: 35 | client, client_address = SERVER.accept() 36 | print('%s:%s has connected.' % client_address) 37 | 38 | # Initialize the dictionaries 39 | addresses[client] = {} 40 | positions[client] = {} 41 | 42 | # Add current client address into adresses 43 | addresses[client]['address'] = client_address 44 | Thread(target=handle_client, args=(client,)).start() 45 | 46 | def LOGGER(event, filename, ip, client, type, data): 47 | """ 48 | A logging function to store all input packets, 49 | as well as output ones when they are generated. 50 | 51 | There are two types of logs implemented: 52 | - a general (info) logger that will keep track of all 53 | incoming and outgoing packets, 54 | - a position (location) logger that will write to a 55 | file contianing only results og GPS or LBS data. 56 | """ 57 | 58 | with open(os.path.join('./logs/', filename), 'a+') as log: 59 | if (event == 'info'): 60 | # TSV format of: Timestamp, Client IP, IN/OUT, Packet 61 | logMessage = datetime.now().strftime('%Y/%m/%d %H:%M:%S') + '\t' + ip + '\t' + client + '\t' + type + '\t' + data + '\n' 62 | elif (event == 'location'): 63 | # TSV format of: Timestamp, Client IP, Location DateTime, GPS/LBS, Validity, Nb Sat, Latitude, Longitude, Accuracy, Speed, Heading 64 | logMessage = datetime.now().strftime('%Y/%m/%d %H:%M:%S') + '\t' + ip + '\t' + client + '\t' + '\t'.join(list(str(x) for x in data.values())) + '\n' 65 | log.write(logMessage) 66 | 67 | 68 | def handle_client(client): 69 | """ 70 | Takes client socket as argument. 71 | Handles a single client connection, by listening indefinitely for packets. 72 | """ 73 | 74 | # Initialize dictionaries for that client 75 | positions[client]['wifi'] = [] 76 | positions[client]['gsm-cells'] = [] 77 | positions[client]['gsm-carrier'] = {} 78 | positions[client]['gps'] = {} 79 | 80 | # Keep receiving and analyzing packets until end of time 81 | # or until device sends disconnection signal 82 | keepAlive = True 83 | while (True): 84 | 85 | # Handle socket errors with a try/except approach 86 | try: 87 | packet = client.recv(BUFSIZ) 88 | 89 | # Only process non-empty packets 90 | if (len(packet) > 0): 91 | print('[', addresses[client]['address'][0], ']', 'IN Hex :', packet.hex(), '(length in bytes =', len(packet), ')') 92 | keepAlive = read_incoming_packet(client, packet) 93 | LOGGER('info', 'server_log.txt', addresses[client]['address'][0], addresses[client]['imei'], 'IN', packet.hex()) 94 | 95 | # Disconnect if client sent disconnect signal 96 | #if (keepAlive is False): 97 | # print('[', addresses[client]['address'][0], ']', 'DISCONNECTED: socket was closed by client.') 98 | # client.close() 99 | # break 100 | 101 | # Close socket if recv() returns 0 bytes, i.e. connection has been closed 102 | else: 103 | print('[', addresses[client]['address'][0], ']', 'DISCONNECTED: socket was closed for an unknown reason.') 104 | client.close() 105 | break 106 | 107 | # Something went sideways... close the socket so that it does not hang 108 | except Exception as e: 109 | print('[', addresses[client]['address'][0], ']', 'ERROR: socket was closed due to the following exception:') 110 | print(e) 111 | client.close() 112 | break 113 | print("This thread is now closed.") 114 | 115 | 116 | def read_incoming_packet(client, packet): 117 | """ 118 | Handle incoming packets to identify the protocol they are related to, 119 | and then redirects to response functions that will generate the apropriate 120 | packet that should be sent back. 121 | Actual sending of the response packet will be done by an external function. 122 | """ 123 | 124 | # Convert hex string into list for convenience 125 | # Strip packet of bits 1 and 2 (start 0x78 0x78) and n-1 and n (end 0x0d 0x0a) 126 | packet_list = [packet.hex()[i:i+2] for i in range(4, len(packet.hex())-4, 2)] 127 | 128 | # DEBUG: Print the role of current packet 129 | protocol_name = protocol_dict['protocol'][packet_list[1]] 130 | protocol_method = protocol_dict['response_method'][protocol_name] 131 | print('The current packet is for protocol:', protocol_name, 'which has method:', protocol_method) 132 | 133 | # Prepare the response, initialize as empty 134 | r = '' 135 | 136 | # Get the protocol name and react accordingly 137 | if (protocol_name == 'login'): 138 | r = answer_login(client, packet_list) 139 | 140 | elif (protocol_name == 'gps_positioning' or protocol_name == 'gps_offline_positioning'): 141 | r = answer_gps(client, packet_list) 142 | 143 | elif (protocol_name == 'status'): 144 | # Status can sometimes carry signal strength and sometimes not 145 | if (packet_list[0] == '06'): 146 | print('[', addresses[client]['address'][0], ']', 'STATUS : Battery =', int(packet_list[2], base=16), '; Sw v. =', int(packet_list[3], base=16), '; Status upload interval =', int(packet_list[4], base=16)) 147 | elif (packet_list[0] == '07'): 148 | print('[', addresses[client]['address'][0], ']', 'STATUS : Battery =', int(packet_list[2], base=16), '; Sw v. =', int(packet_list[3], base=16), '; Status upload interval =', int(packet_list[4], base=16), '; Signal strength =', int(packet_list[5], base=16)) 149 | # Exit function without altering anything 150 | return(True) 151 | 152 | elif (protocol_name == 'hibernation'): 153 | # Exit function returning False to break main while loop in handle_client() 154 | print('[', addresses[client]['address'][0], ']', 'STATUS : Sent hibernation packet. Disconnecting now.') 155 | return(False) 156 | 157 | elif (protocol_name == 'setup'): 158 | # TODO: HANDLE NON-DEFAULT VALUES 159 | r = answer_setup(packet_list, '0300', '00110001', '000000', '000000', '000000', '00', '000000', '000000', '000000', '00', '0000', '0000', ['', '', '']) 160 | 161 | elif (protocol_name == 'time'): 162 | r = answer_time(packet_list) 163 | 164 | elif (protocol_name == 'wifi_positioning' or protocol_name == 'wifi_offline_positioning'): 165 | r = answer_wifi_lbs(client, packet_list) 166 | 167 | elif (protocol_name == 'position_upload_interval'): 168 | r = answer_upload_interval(client, packet_list) 169 | 170 | # Else, prepare a generic response with only the protocol number 171 | # else: 172 | # r = generic_response(packet_list[1]) 173 | 174 | # Send response to client, if it exists 175 | if (r != ''): 176 | print('[', addresses[client]['address'][0], ']', 'OUT Hex :', r, '(length in bytes =', len(bytes.fromhex(r)), ')') 177 | send_response(client, r) 178 | 179 | # Return True to avoid failing in main while loop in handle_client() 180 | return(True) 181 | 182 | 183 | def answer_login(client, query): 184 | """ 185 | This function extracts IMEI and Software Version from the login packet. 186 | The IMEI and Software Version will be stored into a client dictionary to 187 | allow handling of multiple devices at once, in the future. 188 | 189 | The client socket is passed as an argument because it is in this packet 190 | that IMEI is sent and will be stored in the address dictionary. 191 | """ 192 | 193 | # Read data: Bits 2 through 9 are IMEI and 10 is software version 194 | protocol = query[1] 195 | addresses[client]['imei'] = ''.join(query[2:10])[1:] 196 | addresses[client]['software_version'] = int(query[10], base=16) 197 | 198 | # DEBUG: Print IMEI and software version 199 | print("Detected IMEI :", addresses[client]['imei'], "and Sw v. :", addresses[client]['software_version']) 200 | 201 | # Prepare response: in absence of control values, 202 | # always accept the client 203 | response = '01' 204 | # response = '44' 205 | # r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False) 206 | r = generic_response(response) 207 | return(r) 208 | 209 | 210 | def answer_setup(query, uploadIntervalSeconds, binarySwitch, alarm1, alarm2, alarm3, dndTimeSwitch, dndTime1, dndTime2, dndTime3, gpsTimeSwitch, gpsTimeStart, gpsTimeStop, phoneNumbers): 211 | """ 212 | Synchronous setup is initiated by the device who asks the server for 213 | instructions. 214 | These instructions will consists of bits for different flags as well as 215 | alarm clocks ans emergency phone numbers. 216 | """ 217 | 218 | # Read protocol 219 | protocol = query[1] 220 | 221 | # Convert binarySwitch from byte to hex 222 | binarySwitch = format(int(binarySwitch, base=2), '02X') 223 | 224 | # Convert phone numbers to 'ASCII' (?) by padding each digit with 3's and concatenate 225 | for n in range(len(phoneNumbers)): 226 | phoneNumbers[n] = bytes(phoneNumbers[n], 'UTF-8').hex() 227 | phoneNumbers = '3B'.join(phoneNumbers) 228 | 229 | # Build response 230 | response = uploadIntervalSeconds + binarySwitch + alarm1 + alarm2 + alarm3 + dndTimeSwitch + dndTime1 + dndTime2 + dndTime3 + gpsTimeSwitch + gpsTimeStart + gpsTimeStop + phoneNumbers 231 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False) 232 | return(r) 233 | 234 | 235 | def answer_time(query): 236 | """ 237 | Time synchronization is initiated by the device, which expects a response 238 | contianing current datetime over 7 bytes: YY YY MM DD HH MM SS. 239 | This function is a wrapper to generate the proper response 240 | """ 241 | 242 | # Read protocol 243 | protocol = query[1] 244 | 245 | # Get current date and time into the pretty-fied hex format 246 | response = get_hexified_datetime(truncatedYear=False) 247 | 248 | # Build response 249 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False) 250 | return(r) 251 | 252 | 253 | def answer_gps(client, query): 254 | """ 255 | GPS positioning can come into two packets that have the exact same structure, 256 | but protocol can be 0x10 (GPS positioning) or 0x11 (Offline GPS positioning)... ? 257 | Anyway: the structure of these packets is constant, not like GSM or WiFi packets 258 | """ 259 | 260 | # Reset positions lists (Wi-Fi, and LBS) and dictionary (carrier) for that client 261 | positions[client]['gps'] = {} 262 | 263 | # Read protocol 264 | protocol = query[1] 265 | 266 | # Extract datetime from incoming query to put into the response 267 | # Datetime is in HEX format here, contrary to LBS packets... 268 | # That means it's read as HEX(YY) HEX(MM) HEX(DD) HEX(HH) HEX(MM) HEX(SS)... 269 | dt = ''.join([ format(int(x, base = 16), '02d') for x in query[2:8] ]) 270 | # GPS DateTime is at UTC timezone: we need to store that information in the object 271 | if (dt != '000000000000'): 272 | dt = datetime.strptime(dt, '%y%m%d%H%M%S').replace(tzinfo=tz.tzutc()) 273 | 274 | 275 | # Read in the incoming GPS positioning 276 | # Byte 8 contains length of packet on 1st char and number of satellites on 2nd char 277 | gps_data_length = int(query[8][0], base=16) 278 | gps_nb_sat = int(query[8][1], base=16) 279 | # Latitude and longitude are both on 4 bytes, and were multiplied by 30000 280 | # after being converted to seconds-of-angle. Let's convert them back to degree 281 | gps_latitude = int(''.join(query[9:13]), base=16) / (30000 * 60) 282 | gps_longitude = int(''.join(query[13:17]), base=16) / (30000 * 60) 283 | # Speed is on the next byte 284 | gps_speed = int(query[17], base=16) 285 | # Last two bytes contain flags in binary that will be interpreted 286 | gps_flags = format(int(''.join(query[18:20]), base=16), '0>16b') 287 | position_is_valid = gps_flags[3] 288 | # Flip sign of GPS latitude if South, longitude if West 289 | if (gps_flags[4] == '1'): 290 | gps_longitude = -gps_longitude 291 | if (gps_flags[5] == '0'): 292 | gps_latitude = -gps_latitude 293 | gps_heading = int(''.join(gps_flags[6:]), base = 2) 294 | 295 | # Store GPS information into the position dictionary and print them 296 | positions[client]['gps']['method'] = 'GPS' 297 | # In some cases dt is empty with value '000000000000': let's avoid that because it'll crash strptime 298 | positions[client]['gps']['datetime'] = (datetime.strptime(datetime.now().strftime('%y%m%d%H%M%S') if dt == '000000000000' else dt.astimezone(tz.tzlocal()).strftime('%y%m%d%H%M%S'), '%y%m%d%H%M%S').strftime('%Y/%m/%d %H:%M:%S')) 299 | # Special value for 'valid' flag when dt is '000000000000' which may be an invalid position after all 300 | positions[client]['gps']['valid'] = (2 if (dt == '000000000000' and position_is_valid == 1) else position_is_valid) 301 | positions[client]['gps']['nb_sat'] = gps_nb_sat 302 | positions[client]['gps']['latitude'] = gps_latitude 303 | positions[client]['gps']['longitude'] = gps_longitude 304 | positions[client]['gps']['accuracy'] = 0.0 305 | positions[client]['gps']['speed'] = gps_speed 306 | positions[client]['gps']['heading'] = gps_heading 307 | print('[', addresses[client]['address'][0], ']', "POSITION/GPS : Valid =", position_is_valid, "; Nb Sat =", gps_nb_sat, "; Lat =", gps_latitude, "; Long =", gps_longitude, "; Speed =", gps_speed, "; Heading =", gps_heading) 308 | LOGGER('location', 'location_log.txt', addresses[client]['address'][0], addresses[client]['imei'], '', positions[client]['gps']) 309 | 310 | # Get current datetime for answering 311 | # TEST: Return datetime that was extracted from packet instead of current server datetime 312 | # response = get_hexified_datetime(truncatedYear=True) 313 | response = ''.join(query[2:8]) 314 | 315 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False, forceLengthToValue=0) 316 | return(r) 317 | 318 | 319 | def answer_wifi_lbs(client, query): 320 | """ 321 | iFi + LBS data can come into two packets that have the exact same structure, 322 | but protocol can be 0x17 or 0x69. Likely similar to GPS/offline GPS... ? 323 | According to documentation 0x17 is an "offline" (cached?) query, which may be 324 | preserved and queried again until the right answer is returned. 325 | 0x17 expects only datetime as an answer 326 | 0x69 extepects datetime, followed by a decoded position 327 | 328 | Packet structure is variable and consist in N WiFi hotspots (3 <= N <= 8) and 329 | N (2 <= N <= ?) GSM towers. 330 | 331 | WiFi hotspots are identified by BSSID (mac address; 6 bytes) and RSSI (1 byte). 332 | 333 | GSM is firest defined as MCCMNC (2+1 bytes) and nearby towers are then 334 | identified by LAC (2 bytes), Cell ID (2 bytes) and MCISS (1 byte). 335 | 336 | This function will not return anything but write to a dictionary that is accessible 337 | outside of the function. This is because WiFi/LBS packets expect two responses : 338 | - hexified datetime 339 | - decoded positions as latitude and longitude, based from transmitted elements. 340 | """ 341 | 342 | # Reset positions lists (Wi-Fi, and LBS) and dictionary (carrier) for that client 343 | positions[client]['wifi'] = [] 344 | positions[client]['gsm-cells'] = [] 345 | positions[client]['gsm-carrier'] = {} 346 | positions[client]['gps'] = {} 347 | 348 | # Read protocol 349 | protocol = query[1] 350 | 351 | # Datetime is BCD-encoded in bytes 2:7, meaning it's read *directly* as YY MM DD HH MM SS 352 | # and does not need to be decoded from hex. YY value above 2000. 353 | dt = ''.join(query[2:8]) 354 | # WiFi DateTime seems to be UTC timezone: add that info to the object 355 | if (dt != '000000000000'): 356 | dt = datetime.strptime(dt, '%y%m%d%H%M%S').replace(tzinfo=tz.tzutc()) 357 | 358 | # WIFI 359 | n_wifi = int(query[0]) 360 | if (n_wifi > 0): 361 | for i in range(n_wifi): 362 | current_wifi = {'macAddress': ':'.join(query[(8 + (7 * i)):(8 + (7 * (i + 1)) - 2 + 1)]), # That +1 is because l[start:stop] returnes elements from start to stop-1... 363 | 'signalStrength': -int(query[(8 + (7 * (i + 1)) - 1)], base = 16)} 364 | positions[client]['wifi'].append(current_wifi) 365 | 366 | # Print Wi-Fi hotspots into the logs 367 | print('[', addresses[client]['address'][0], ']', "POSITION/WIFI : BSSID =", current_wifi['macAddress'], "; RSSI =", current_wifi['signalStrength']) 368 | 369 | # GSM Cell towers 370 | n_gsm_cells = int(query[(8 + (7 * n_wifi))]) 371 | # The first three bytes after n_lbs are MCC(2 bytes)+MNC(1 byte) 372 | gsm_mcc = int(''.join(query[((8 + (7 * n_wifi)) + 1):((8 + (7 * n_wifi)) + 2 + 1)]), base=16) 373 | gsm_mnc = int(query[((8 + (7 * n_wifi)) + 3)], base=16) 374 | positions[client]['gsm-carrier']['n_gsm_cells'] = n_gsm_cells 375 | positions[client]['gsm-carrier']['MCC'] = gsm_mcc 376 | positions[client]['gsm-carrier']['MNC'] = gsm_mnc 377 | 378 | if (n_gsm_cells > 0): 379 | for i in range(n_gsm_cells): 380 | current_gsm_cell = {'locationAreaCode': int(''.join(query[(((8 + (7 * n_wifi)) + 4) + (5 * i)):(((8 + (7 * n_wifi)) + 4) + (5 * i) + 1 + 1)]), base=16), 381 | 'cellId': int(''.join(query[(((8 + (7 * n_wifi)) + 4) + (5 * i) + 1 + 1):(((8 + (7 * n_wifi)) + 4) + (5 * i) + 2 + 1 + 1)]), base=16), 382 | 'signalStrength': -int(query[(((8 + (7 * n_wifi)) + 4) + (5 * i) + 2 + 1 + 1)], base=16)} 383 | positions[client]['gsm-cells'].append(current_gsm_cell) 384 | 385 | # Print LBS data into logs as well 386 | print('[', addresses[client]['address'][0], ']', "POSITION/LBS : LAC =", current_gsm_cell['locationAreaCode'], "; CellID =", current_gsm_cell['cellId'], "; MCISS =", current_gsm_cell['signalStrength']) 387 | 388 | # Build first stage of response with dt and send it to devices 389 | r_1 = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, dt.strftime('%y%m%d%H%M%S'), hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False, forceLengthToValue=0) 390 | 391 | # Build second stage of response, which requires decoding the positioning data 392 | print("Decoding location-based data using Google Maps Geolocation API...") 393 | decoded_position = GoogleMaps_geolocation_service(gmaps, positions[client]) 394 | 395 | # Handle errors in decoding location 396 | if (list(decoded_position.keys())[0] == 'error'): 397 | # Google API returned an error 398 | positions[client]['gps']['method'] = 'LBS' 399 | positions[client]['gps']['datetime'] = '' 400 | positions[client]['gps']['valid'] = 0 401 | positions[client]['gps']['nb_sat'] = '' 402 | positions[client]['gps']['latitude'] = '' 403 | positions[client]['gps']['longitude'] = '' 404 | positions[client]['gps']['accuracy'] = '' 405 | positions[client]['gps']['speed'] = '' 406 | positions[client]['gps']['heading'] = '' 407 | 408 | else: 409 | # Google API returned a location 410 | if (len(positions[client]['wifi']) > 0): 411 | positions[client]['gps']['method'] = 'LBS-GSM-WIFI' 412 | else: 413 | positions[client]['gps']['method'] = 'LBS-GSM' 414 | # In some cases dt is empty with value '000000000000': let's avoid that because it'll crash strptime 415 | positions[client]['gps']['datetime'] = (datetime.strptime(datetime.now().strftime('%y%m%d%H%M%S') if dt == '000000000000' else dt.astimezone(tz.tzlocal()).strftime('%y%m%d%H%M%S'), '%y%m%d%H%M%S').strftime('%Y/%m/%d %H:%M:%S')) 416 | # Special value for 'valid' flag when dt is '000000000000' which may be an invalid position after all 417 | positions[client]['gps']['valid'] = (2 if dt == '000000000000' else 1) 418 | positions[client]['gps']['nb_sat'] = '' 419 | # We will need to pad latitude and longitude with + sign if missing 420 | positions[client]['gps']['latitude'] = '{0:{1}}'.format(decoded_position['location']['lat'], '+' if decoded_position['location']['lat'] else '') 421 | positions[client]['gps']['longitude'] = '{0:{1}}'.format(decoded_position['location']['lng'], '+' if decoded_position['location']['lng'] else '') 422 | positions[client]['gps']['accuracy'] = decoded_position['accuracy'] 423 | positions[client]['gps']['speed'] = '' 424 | positions[client]['gps']['heading'] = '' 425 | LOGGER('location', 'location_log.txt', addresses[client]['address'][0], addresses[client]['imei'], '', positions[client]['gps']) 426 | 427 | # And return the second stage of response, which will be sent in the handle_package() function 428 | # The latitudes and longitudes are truncated to the 6th digit after decimal separator but must preserve the sign 429 | response = '2C'.join( 430 | [ bytes(positions[client]['gps']['latitude'][0] + str(round(float(positions[client]['gps']['latitude'][1:]), 6)), 'UTF-8').hex(), 431 | bytes(positions[client]['gps']['longitude'][0] + str(round(float(positions[client]['gps']['longitude'][1:]), 6)), 'UTF-8').hex() ]) 432 | r_2 = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False, forceLengthToValue=0) 433 | 434 | # Send the response corresponding to what is expected by the protocol 435 | # 0x17 : only send r_1 (returned and handled by send_content_response()) 436 | if (protocol == '17'): 437 | return(r_1) 438 | 439 | elif (protocol == '69'): 440 | print('[', addresses[client]['address'][0], ']', 'OUT Hex :', r_1, '(length in bytes =', len(bytes.fromhex(r_1)), ')') 441 | send_response(client, r_1) 442 | return(r_2) 443 | 444 | 445 | def answer_upload_interval(client, query): 446 | """ 447 | Whenever the device received an SMS that changes the value of an upload interval, 448 | it sends this information to the server. 449 | The server should answer with the exact same content to acknowledge the packet. 450 | """ 451 | 452 | # Read protocol 453 | protocol = query[1] 454 | 455 | # Response is new upload interval reported by device (HEX formatted, no need to alter it) 456 | response = ''.join(query[2:4]) 457 | 458 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, response, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False) 459 | return(r) 460 | 461 | 462 | def generic_response(protocol): 463 | """ 464 | Many queries made by the device do not expect a complex 465 | response: most of the times, the device expects the exact same packet. 466 | Here, we will answer with the same value of protocol that the device sent, 467 | not using any content. 468 | """ 469 | r = make_content_response(hex_dict['start'] + hex_dict['start'], protocol, None, hex_dict['stop_1'] + hex_dict['stop_2'], ignoreDatetimeLength=False, ignoreSeparatorLength=False) 470 | return(r) 471 | 472 | 473 | def make_content_response(start, protocol, content, stop, ignoreDatetimeLength, ignoreSeparatorLength, forceLengthToValue=None): 474 | """ 475 | This is just a wrapper to generate the complete response 476 | to a query, given its content. 477 | It will apply to all packets where response is of the format: 478 | start-start-length-protocol-content-stop_1-stop_2. 479 | Other specific packets where length is replaced by counters 480 | will be treated separately. 481 | 482 | The forceLengthToValue flag allows bypassing calculation of content length, 483 | in case the expected response should contain the length that was in the query, 484 | and not the actual length of the response 485 | """ 486 | 487 | # Length is easier that of content (minus some stuff) or fixed to 1, supposedly 488 | if (forceLengthToValue is None): 489 | length = (len(bytes.fromhex(content))+1 if content else 1) 490 | 491 | # Length is computed either on the full content or by discarding datetime and separators 492 | # This is really a wild guess, because documentation is poor... 493 | if (ignoreDatetimeLength and length >= 6): 494 | length = length - 6 495 | # When latitude/longitude are returned, the separator 2C isn't counted in length, apparently 496 | if (ignoreSeparatorLength and length >= 1): 497 | length = length - 1 498 | 499 | # Handle case of length forced to a given value 500 | else: 501 | length = int(forceLengthToValue) 502 | 503 | # Convert length to hexadecimal value 504 | length = format(length, '02X') 505 | 506 | return(start + length + protocol + (content if content else '') + stop) 507 | 508 | 509 | def send_response(client, response): 510 | """ 511 | Function to send a response packet to the client. 512 | """ 513 | LOGGER('info', 'server_log.txt', addresses[client]['address'][0], addresses[client]['imei'], 'OUT', response) 514 | client.send(bytes.fromhex(response)) 515 | 516 | 517 | def get_hexified_datetime(truncatedYear): 518 | """ 519 | Make a fancy function that will return current GMT datetime as hex 520 | concatenated data, using 2 bytes for year and 1 for the rest. 521 | The returned string is YY YY MM DD HH MM SS if truncatedYear is False, 522 | or just YY MM DD HH MM SS if truncatedYear is True. 523 | """ 524 | 525 | # Get current GMT time into a list 526 | if (truncatedYear): 527 | dt = datetime.utcnow().strftime('%y-%m-%d-%H-%M-%S').split("-") 528 | else: 529 | dt = datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S').split("-") 530 | 531 | # Then convert to hex with 2 bytes for year and 1 for the rest 532 | dt = [ format(int(x), '0'+str(len(x))+'X') for x in dt ] 533 | return(''.join(dt)) 534 | 535 | 536 | def GoogleMaps_geolocation_service(gmapsClient, positionDict): 537 | """ 538 | This wrapper function will query the Google Maps API with the list 539 | of cell towers identifiers and WiFi SSIDs that the device detected. 540 | It requires a Google Maps API key. 541 | 542 | For now, the radio_type argument is forced to 'gsm' because there are 543 | no CDMA cells in France (at least that's what I believe), and since the 544 | GPS device only handles 2G, it's the only option available. 545 | The carrier is forced to 'Free' since that's the one for the SIM card 546 | I'm using, but again this would need to be tweaked (also it probbaly 547 | doesn't make much of a difference to feed it to the function or not!) 548 | 549 | These would need to be tweaked depending on where you live. 550 | 551 | A nice source for such data is available at https://opencellid.org/ 552 | """ 553 | print('Google Maps Geolocation API queried with:', positionDict) 554 | geoloc = gmapsClient.geolocate(home_mobile_country_code=positionDict['gsm-carrier']['MCC'], 555 | home_mobile_network_code=positionDict['gsm-carrier']['MCC'], 556 | radio_type='gsm', 557 | carrier='Free', 558 | consider_ip='true', 559 | cell_towers=positionDict['gsm-cells'], 560 | wifi_access_points=positionDict['wifi']) 561 | 562 | print('Google Maps Geolocation API returned:', geoloc) 563 | return(geoloc) 564 | 565 | """ 566 | This is a debug block to test the GeoLocation API 567 | 568 | gmaps.geolocate(home_mobile_country_code='208', home_mobile_network_code='01', radio_type=None, carrier=None, consider_ip=False, cell_towers=cell_towers, wifi_access_points=None) 569 | 570 | ## DEBUG: USE DATA FROM ONE PACKET 571 | # Using RSSI 572 | cell_towers = [ 573 | { 574 | 'locationAreaCode': 832, 575 | 'cellId': 51917, 576 | 'signalStrength': -90 577 | }, 578 | { 579 | 'locationAreaCode': 768, 580 | 'cellId': 64667, 581 | 'signalStrength': -100 582 | }, 583 | { 584 | 'locationAreaCode': 1024, 585 | 'cellId': 24713, 586 | 'signalStrength': -100 587 | }, 588 | { 589 | 'locationAreaCode': 768, 590 | 'cellId': 53851, 591 | 'signalStrength': -100 592 | }, 593 | { 594 | 'locationAreaCode': 1024, 595 | 'cellId': 8021, 596 | 'signalStrength': -100 597 | }, 598 | { 599 | 'locationAreaCode': 1024, 600 | 'cellId': 62216, 601 | 'signalStrength': -100 602 | } 603 | ] 604 | 605 | # Using dummy values in dBm 606 | cell_towers = [ 607 | { 608 | 'locationAreaCode': 832, 609 | 'cellId': 51917, 610 | 'signalStrength': -50 611 | }, 612 | { 613 | 'locationAreaCode': 768, 614 | 'cellId': 64667, 615 | 'signalStrength': -30 616 | }, 617 | { 618 | 'locationAreaCode': 1024, 619 | 'cellId': 24713, 620 | 'signalStrength': -30 621 | }, 622 | { 623 | 'locationAreaCode': 768, 624 | 'cellId': 53851, 625 | 'signalStrength': -30 626 | }, 627 | { 628 | 'locationAreaCode': 1024, 629 | 'cellId': 8021, 630 | 'signalStrength': -30 631 | }, 632 | { 633 | 'locationAreaCode': 1024, 634 | 'cellId': 62216, 635 | 'signalStrength': -30 636 | } 637 | ] 638 | """ 639 | 640 | # Declare common Hex codes for packets 641 | hex_dict = { 642 | 'start': '78', 643 | 'stop_1': '0D', 644 | 'stop_2': '0A' 645 | } 646 | 647 | protocol_dict = { 648 | 'protocol': { 649 | '01': 'login', 650 | '05': 'supervision', 651 | '08': 'heartbeat', 652 | '10': 'gps_positioning', 653 | '11': 'gps_offline_positioning', 654 | '13': 'status', 655 | '14': 'hibernation', 656 | '15': 'reset', 657 | '16': 'whitelist_total', 658 | '17': 'wifi_offline_positioning', 659 | '30': 'time', 660 | '43': 'mom_phone_WTFISDIS?', 661 | '56': 'stop_alarm', 662 | '57': 'setup', 663 | '58': 'synchronous_whitelist', 664 | '67': 'restore_password', 665 | '69': 'wifi_positioning', 666 | '80': 'manual_positioning', 667 | '81': 'battery_charge', 668 | '82': 'charger_connected', 669 | '83': 'charger_disconnected', 670 | '94': 'vibration_received', 671 | '98': 'position_upload_interval' 672 | }, 673 | 'response_method': { 674 | 'login': 'login', 675 | 'logout': 'logout', 676 | 'supervision': '', 677 | 'heartbeat': '', 678 | 'gps_positioning': 'datetime_response', 679 | 'gps_offline_positioning': 'datetime_response', 680 | 'status': '', 681 | 'hibernation': '', 682 | 'reset': '', 683 | 'whitelist_total': '', 684 | 'wifi_offline_positioning': 'datetime_response', 685 | 'time': 'time_response', 686 | 'stop_alarm': '', 687 | 'setup': 'setup', 688 | 'synchronous_whitelist': '', 689 | 'restore_password': '', 690 | 'wifi_positioning': 'datetime_position_response', 691 | 'manual_positioning': '', 692 | 'battery_charge': '', 693 | 'charger_connected': '', 694 | 'charger_disconnected': '', 695 | 'vibration_received': '', 696 | 'position_upload_interval': 'upload_interval_response' 697 | } 698 | } 699 | 700 | 701 | # Import dotenv with API keys and initialize API connections 702 | load_dotenv() 703 | GMAPS_API_KEY = os.getenv('GMAPS_API_KEY') 704 | gmaps = googlemaps.Client(key=GMAPS_API_KEY) 705 | 706 | # Details about host server 707 | HOST = '' 708 | PORT = 5023 709 | BUFSIZ = 4096 710 | ADDR = (HOST, PORT) 711 | 712 | # Initialize socket 713 | SERVER = socket(AF_INET, SOCK_STREAM) 714 | SERVER.bind(ADDR) 715 | 716 | # Store client data into dictionaries 717 | addresses = {} 718 | positions = {} 719 | 720 | if __name__ == '__main__': 721 | SERVER.listen(5) 722 | print("Waiting for connection...") 723 | ACCEPT_THREAD = Thread(target=accept_incoming_connections) 724 | ACCEPT_THREAD.start() 725 | ACCEPT_THREAD.join() 726 | SERVER.close() --------------------------------------------------------------------------------