Thingy:91 Smart Car Tracker And Notification Service
About the project
I wanted to commute to work by bike but unfortunately I live some 75km away from my office. Mass transit isn't regular or convenient where I live (it's not set up for commuters) so car is the only viable alternative. I want to make sure my car is safe where I park it, so lets make it smart!
Project info
Difficulty: Easy
Platforms: Raspberry Pi, Node-RED, Nordic Semiconductor
Estimated time: 1 hour
License: GNU Lesser General Public License version 3 or later (LGPL3+)
Items used in this project
Hardware components
Software apps and online services
Story
The idea of the project was to keep any eye on my car while I was off at work, so I knew that it would be there when I got back to it. Crime isn't too bad where I leave the car, but I'd rather make sure it was in the same state that I left it in. I wanted to put a GPS tracker on the car with an alert system if it moved, and if possible set the accelerometer to alert me if there was an impact to the car. I also wanted this project to be extremely easy to replicate and as close to a standard build so I didn't have to maintain it going forward.
I used one of the default builds that came with the Thingy:91, thingy91_asset_tracker_v2_nbiot, but before taking a dip into this I would suggest that you check your coverage for NBIOT and the Arkessa/iBasis sims or get a local provider that'll do NB-IoT. There are other builds available that use LTE-m too if your network uses that instead.
After writing some basic python scripts to pick up the information from the API, I thought it might be easier not to re-invent the wheel and try using something a little more manageable long term and I haven't used Node-Red as yet so, now is as good a time as any.
A. Setup Node Red
1. Install Node-Red
There are plenty of tutorials on how to set up Node-Red on many different devices. I chose a Raspberry-Pi as I had one being used as a server at home anyway.
2. Notifications
I wanted a way to send notifications, Telegram is free and relatively easy to set up and, again, there are plenty of tutorials on how to start a bot here and how to install it into node red so I won't go through that (other than to say click the 3 lines in the top right of the UI, select Manage Palette, select Install tab and search for Telegram - simple).
B. Thingy:91 Setup
1. I'm presuming you've set up your thingy:91. If not there are a few videos and tutorials on this as well
https://www.nordicsemi.com/Products/Development-hardware/Nordic-Thingy-91/GetStarted
2. Flash the thingy91_asset_tracker_v2 pre built firmware.
3. Once you've set that up there are some details here you'll need from the nRF Cloud site https://nrfcloud.com/. You'll need the Device Name (under Devices) and the API key (available under User Account by clicking the 3 lines in the top right of the webpage).
C. Node Red Flow Setup
Set up the flow in Node Red as follows:
1. Inject Node - You'll want to set an interval to pull the information from the nRF Cloud API as you can see from the below its set to every 10 minutes
2. Change Node - I've added a change node here to set part of the payload for the API call from the following node. Make sure the string is in the format below (make sure to change your DeviceID to that of your Thingy:91
Value: {"deviceId":"[stick in your device ID here]","appId":"GPS","pageLimit":"1"}
3. HTTP Request node - add with the following (your API key is required under the Token field below)
4. Function node - this contains the javascript to convert the response from the API to GPS coordinates we can use to determine if the coordinates differ and then send in a hyperlink
Add the following to the On Message tab
- var data = msg.payload.items[0].message.data;
- var datasplit = data.split(",");
- var tempLat = datasplit[2];
- var NorS = datasplit[3];
- var tempLong = datasplit[4];
- var EorW = datasplit[5];
- var indexOfLongDecimal = tempLong.indexOf(".");
- var longMins = parseFloat(tempLong.substring(indexOfLongDecimal-2));
- longMins = longMins/60;
- var longitude = parseInt(tempLong.substring(0,indexOfLongDecimal-2)) + longMins;
- var indexOfLatDecimal = tempLat.indexOf(".");
- var latMins = parseFloat(tempLat.substring(indexOfLatDecimal-2));
- latMins = latMins/60;
- var latitude = parseInt(tempLat.substring(0,indexOfLatDecimal-2)) + latMins;
- longitude = parseFloat(longitude).toFixed(6);
- latitude = parseFloat(latitude).toFixed(6);
- if (NorS == "S") latitude = -latitude;
- if (EorW == "W") longitude = -longitude;
- let coordObj = { "latitude": latitude, "longitude":longitude };
- return coordObj;
GPS coordinates tend to jump around a bit without the device actually moving so I'd suggest rounding to the returned coordinates to smaller numbers as the difference between the farthest point to the actual location of the Thingy is some 100 metres, so it can wander from point to point over 200 metres, giving a false alert!
As a result you may need to lose some accuracy in the alerts, so change the two ".toFixed(6);"(lines 17 and 18) above to a smaller decimal place e.g. ".toFixed(3);" should do however, I ended up using 2 decimal places (1.1km as it wandered a over the 111 metres accuracy that 3 decimal places gives). As you'll see later I hyperlink the cloud api as well just to be sure!
5. Function Node
I've popped in another function here to check if the coordinates have changed passing on a boolean value and structuring a telegram message to pass on.
Insert the following (also don't forget to include your Device ID in the botMsg line 28 and Telegram Chat ID on line 29):
- var coordinatesChanged = false;
- msg.payload = {};
- var lastCoordinates = context.get("lastCoordinates");
- if (lastCoordinates == null){
- context.set("lastCoordinates", msg);
- }
- else{
- if (msg.latitude != lastCoordinates.latitude)
- {
- coordinatesChanged = true;
- }
- if (msg.longitude != lastCoordinates.longitude)
- {
- coordinatesChanged = true;
- }
- if (coordinatesChanged == true)
- {
- context.set("lastCoordinates", msg);
- }
- }
- msg.payload["coordinatesChanged"] = coordinatesChanged;
- var botMsg = "Your car has moved to https://www.google.ie/maps/place/";
- botMsg += msg.latitude;
- botMsg += ",";
- botMsg += msg.longitude;
- botMsg += " n For more information see https://nrfcloud.com/#/devices/[deviceID]"
- msg.payload.chatId = '[TelegramChatID]';
- msg.payload.type = 'message';
- msg.payload.content = botMsg;
- return msg;
6. Switch Node
This is based on whether or not the location has moved - the payload.coordinatesChanged
7. Telegram Sender Node
This is just a standard node with my bot selected
8. Debug nodes
Last nodes after the switch are both standard debug nodes with one small change to the output so that it displays the complete msg object as I've added some custom properties into the flow
That's pretty much it. Just start your initial timestamp process and check the debug to make sure it works.
Future Improvements
In my initial proposal I was hoping to have the accelerometer notify me if there was any impacts to the car (which has happened before) but unfortunately the accelerometer isn't exposed in the example code for the v2 asset tracker and sending the data to the cloud was above my skill level. If it's added to an example in a future release (or somebody can help me with that part) I'll update the script and provide a prebuilt image to load onto the Thingy:91.
The HTTP Request can be modified to get information from other sensors (see below) just change the value of the 'appId' on the Change node (C2 above) from GPS to any of those from below
{"AIR_PRESS", "TEMP", "AIR_QUAL", "LIGHT", "HUMID", "FLIP", "RSRP", "GPS", "AGPS", "CELL_POS", "MCELL", "SCELL", "WIFI", "BUTTON"}
I've also put some python functions together (below) as an example. Just make sure you populate it with your API key (there may be some errors in there though but most of it works!)
- import requests
- import time
- import json
- import requests
- from datetime import datetime,timedelta
- API_URL = "https://api.nrfcloud.com/v1/"
- api_key = ""
- device_name = ""
- from_time = datetime.now() - timedelta(days=1)
- app_ids = {"AIR_PRESS", "TEMP", "AIR_QUAL", "LIGHT", "HUMID", "FLIP", "RSRP", "GPS", "AGPS", "CELL_POS", "MCELL", "SCELL", "WIFI", "BUTTON"}
- def updateWaitTime(api_key, device_id):
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "devices/" + device_id + "/state"
- return requests.patch(req, data=payload, headers=hdr)
- def fetch_device(api_key, device_id):
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "devices/" + device_id
- return requests.get(req, headers=hdr, params=updateConfigJson)
- def fetch_devices(api_key):
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "devices"
- return requests.get(req, headers=hdr)
- def fetch_messages_by_start_time(api_key, device_id, start_time):
- global from_time
- start_time_iso = start_time.isoformat() + "Z"
- query = {'deviceId':device_id, 'inclusiveStart':start_time_iso, 'pageSort':'desc'}
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "messages"
- from_time = datetime.now()
- return requests.get(req, headers=hdr, params=query)
- def fetch_last_message_by_type(api_key, device_id, app_id):
- query = {'deviceId':device_id, 'appId':app_id, 'pageLimit':'1'}
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "messages"
- return requests.get(req, headers=hdr, params=query)
- def fetch_message_by_type(api_key, device_id, app_id):
- query = {'deviceId':device_id, 'appId':app_id}
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "messages"
- return requests.get(req, headers=hdr, params=query)
- def fetch_last_location(api_key, device_id):#cell tower and assisted GPS - need to extract the actual location
- query = {'deviceId':device_id, 'pageLimit':'1', 'pageSort':'desc'}
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "location/history"
- return requests.get(req, headers=hdr, params=query)
- def fetch_location(api_key):
- query = {"latest":"true"}
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "location/history"
- return requests.get(req, headers=hdr, params=query)
- def get_lat_long():
- responseJson = fetch_last_message_by_type(api_key, device_id, "GPS").json()
- data = responseJson['items'][0]['message']['data']
- print("Coordinates: ", data)
- dataSplit = data.split(",")
- tempLat = dataSplit[2]
- NorS = dataSplit[3]
- tempLong = dataSplit[4]
- EorW = dataSplit[5]
- indexOfLongDecimal = tempLong.index(".")
- longMins = float(tempLong[(indexOfLongDecimal-2):])
- longMins = longMins/60
- longitude = int(tempLong[0:(indexOfLongDecimal-2)]) + longMins
- indexOfLatDecimal = tempLat.index(".")
- latMins = float(tempLat[(indexOfLatDecimal-2):])
- latMins = latMins/60
- latitude = int(tempLat[0:(indexOfLatDecimal-2)]) + latMins
- if NorS == "S":
- latitude = -latitude
- if EorW == "W":
- longitude = -longitude
- return latitude, longitude
- def fetch_account_info(api_key):
- hdr = {'Authorization': 'Bearer ' + api_key}
- req = API_URL + "account"
- return requests.get(req, headers=hdr)
- response = fetch_devices(api_key)
- devicesJson = response.json()
- device_id = devicesJson['items'][0]['id']
- response = fetch_device(api_key, device_id)
- deviceJson = response.json()
Leave your feedback...