Do you hike with friends a lot? Ever ran into the situation you couldn’t find each other, or you want to share a path you just walked through with others but all the available tools are hard to get it done?
Hiking Pal to the rescue.
Hiking Pal is a full end-to-end realtime location sharing solution based on AT&T IoT platform and PubNub service.
System OverviewThe system consists of three part:
- Hiking Pal server
- Hiking Pal tracking device
- Hiking Pal realtime sharing map
- Server (AT&T Flow): https://flow.att.io/bowenfeng/hiking-pal
- Tracking device firmware (mbed): https://developer.mbed.org/users/bowenfeng/code/Hiking_Pal/
- Realtime sharing map (Heroku): https://hikingpal.herokuapp.com/
- Map source code: https://github.com/bowenfeng-dev/hiking-pal
The Hiking Pal v1 is a proof of concept system, so some of the interaction still need future polishing, but the overall system is working and able to finish the end to end flow. Here is how we can use the v1 system.
Step 1: Create Hiking Activity
Right now there is now UI provided for creating a new hiking activity. But a REST API is available. You can send an HTTP PUT request from command line to do this:
curl -X PUT -H "Content-Type: application/json" -H "Cache-Control: no-cache" -H "Postman-Token: 0f70aa7e-a916-1a84-5214-79230d37a0de" -d '{"name": "HIKING_ACTIVITY_NAME"}' "https://run-west.att.io/d591dcc0c690f/e8a5c1efc6e6/408869f220ca798/in/flow/hikings"
This should be the only command you need to run. And it will give you back the server response containing the new hiking ID.
{"name":"HIKING_ACTIVITY_NAME","id":1482394483909}
Here the "id" field is the hiking ID.
Step 2: Power on the Tracking Device
Currently in v1 the tracking device firmware will automatically find and join the latest hiking activity after power on. So nothing special need to do. If you want to join a newly created hiking, just power cycle the device, and it will join to it automatically.
The RGB LED will first turn red when it is initializing the cellular module. After that it will become green indicating the system is now working and able to communicate with the server.
Step 3: See Realtime Data on Map
Open the hosted map on Heroku through following URL:
https://hikingpal.herokuapp.com/
It will display a page with an hiking ID input box.
Copy and paste the hiking ID from above command line. Click "Show Map". The realtime map will show up below it.
Hiking Pal Server
Hiking Pal provides the functionalities to manage hiking activities, accepting the reported data and sharing them in realtime through PubNub channel.
The server consists two main parts:
- The REST API built on top of AT&T Flow.
- The realtime pub-sub message delivery via PubNub service.
PubNub Set Up
The PubNub channel used to support realtime location sharing was surprisingly easy to set up thank to the powerful service provided by PubNub.
Simply register a free account on PubNub.com, add a new app, and create a new keyset within that app. You are all set. This will gives you a publish key and a subscribe key. These are all you need to use the PubNub service to deliver realtime message.
In my Hiking Pal server, the reported data is immediately published to a PubNub channel. This is done by adding a PubNub output node into the Flow, and specify the publish key and a channel name.
The channel name can be anything, and all the subscribers subscribe to the same channel name will receive every single message gets published to the channel.
AT&T Flow Set Up
https://flow.att.io/bowenfeng/hiking-pal
The AT&T Flow design for Hiking Pal is grouped into three sections:
- Admin
- Hikings
- Sessions
Admin tab allows you to initialize the hikings database, dump the content of all hikings and clear everything.
The most important function here is the database initializing. Here I just use the global state to store the hiking activities. But could be extended to set up a full fledged database.
Code for database set up function node:
var hikings = global.get('hikings');
if (!hikings) {
hikings = {}
}
global.set('hikings', hikings);
return {
payload: hikings
}
Hikings tab exposes the REST API for hiking activities management:
- Create new hiking activity with a name.
- Get a list of all hiking activities.
- Get name for one hiking activity.
- Remove a hiking activity when no longer needed.
To add aa hiking, we need to initialize the data structure and added it to the database:
var hikingId = Date.now();
var hikingName = msg.payload.name;
var hikings = global.get('hikings');
node.warn(hikings);
node.warn(hikingName);
node.warn(hikingId);
hikings[hikingId] = {
name: hikingName,
sessions: {}
};
msg.payload.id = hikingId;
return msg;
Session tab exposes the REST API for managing individual hiker session and reporting data:
- Join a hiking and create a new session.
- Leave a hiking and remove the session.
- Report location data, and publish it via PubNub to enable realtime sharing map.
Here is the code for the function node to create new session:
var hid = msg.req.params.hid;
var hiking = global.get('hikings')[hid];
if (hiking) {
var sid = Date.now();
hiking.sessions[sid] = {
name: msg.payload.name
};
msg.payload.id = sid;
}
return msg;
After that, the location data can be reported under a session ID. The data is then reorganized into a JSON message and published through the PubNub channel:
var hid = msg.req.params.hid;
var sid = msg.req.params.sid;
node.warn(msg.req.params);
var hiking = global.get('hikings')[hid];
if (hiking && hiking.sessions[sid]) {
var session = hiking.sessions[sid];
return {
payload: {
hiking: hid,
session: sid,
name: session.name,
gps: msg.payload.gps,
}
}
}
return null;
And here is the adapter function node which translate HTTP GET URL parameters into a PUT request body like payload:
var lat = msg.payload.lat;
var lng = msg.payload.lng;
var gps = {
lat: parseFloat(lat),
lng: parseFloat(lng)
}
msg.payload.gps = gps;
return msg;
Note that the URL parameters are Strings, and should be convert to number/float if the desired type in JSON is number/float.
REST API Endpoints
I organized the API in a RESTful way. There are two type of entities used in the system:
- Hiking
- Session
The hiking is the top level entity which has an unique ID and an arbitrary name. Within each hiking there could be multiple sessions, each will have a ID and a name indicates a hiker who joins the hiking activity.
I use timestamp as the ID of hiking and session entities, this provides a simple solution of uniqueness, and also makes finding the latest hiking activity as easy as finding the one with largest hiking ID.
Hiking Entity API
PUT /<flow-base-url>/hikings
- Create a new hiking activity.
- Payload (JSON): {“name”: “<hiking activity name>”}
- Response (JSON): {“id”: <hikingID>}
GET /<flow-base-url>/hikings
- List all hiking activities.
- Response (JSON object array): [{“id”: <hikingID1>, “name”: “<name1>”}, {...}, ... ]
DELETE /<flow-base-url>/hikings/:hid
- Delete one hiking activity along with all its sessions.
GET /<flow-base-url>/hikings/:hid
- Get the name of a hiking activity
- Response (String): “<hiking activity name>”
Session Entity API
PUT /<flow-base-url>/hikings/:hid/sessions
- Join a hiking by creating a new hiking session.
- Payload (JSON): {“name”: “<hiker name>”}
- Response (JSON): {“id”: <sessionID>}
GET /<flow-base-url>/hikings/:hid/sessions?name=<hiker name>
- This is an alternative endpoint for reporting a hiker’s location. I found the WNC module driver didn’t handle the PUT request properly so as an alternative you can call this endpoint to join a hiking.
DELETE /<flow-base-url>/hikings/:hid/sessions/:sid
- Remove a hiker from a hiking activity and remove the session.
PUT /<flow-base-url>/hikings/:hid/sessions/:sid/moves
- Report a hiker’s location.
- Payload (JSON): {“gps”: {“lat”:<latitude>, “lng”: <longitude>}}
GET /<flow-base-url>/hikings/:hid/sessions/:sid/moves?lat=<latitude>&lng=<longitude>
- This is an alternative endpoint for reporting a hiker’s location. I found the WNC module driver didn’t handle the PUT request properly so as an alternative you can call this endpoint to report data.
Tracking Device Firmware
The tracking device was built with the AT&T IoT StartKit. This is an mbed-enabled dev board FRDM-K64F together with a Avnet M14A2A cellular shield. You can find detail tech specs on the product page: http://cloudconnectkits.org/product/att-cellular-iot-starter-kit.
The device doesn’t come with a GPS module. So I used the Xadow GPS v2 module to add the ability to track current location. You can buy one from SeeedStudio: https://www.seeedstudio.com/Xadow-GPS-v2-p-2557.html
Since this is an mbed-enabled device, the program for the Hiking Pal tracking device is written in C++ and compiled through the mbed online compiler (https://developer.mbed.org/compiler/). The code is a modified version of the IOT demo program provided by Avnet: https://developer.mbed.org/teams/Avnet/code/Avnet_ATT_Cellular_IOT/
I reused the sensor data reading code in the demo project. But replaced the HTTP request handling part with my own code to interact with the REST API.
Following are the helper functions I added to simplify the code to send request and handle the response:
void extract_longlong(const char* s, char v[]) {
long long value = strtoll(s, NULL, 0);
sprintf(v, "%lld", value);
}
long long find_longlong(const char* s, const char* token, const char** end) {
const char* tokenBegin = strstr(s, token);
if (tokenBegin != 0) {
*end = tokenBegin + strlen(token);
return strtoll(tokenBegin + strlen(token), NULL, 0);
}
return 0;
}
void find_longlong(const char* s, const char* token, char v[]) {
const char* tokenBegin = strstr(s, token);
if (tokenBegin != 0) {
extract_longlong(tokenBegin + strlen(token), v);
}
}
int send_receive(char* request, char* response) {
int result = cell_modem_Sendreceive(request, response);
SetLedColor(result ? OK_COLOR : ERROR_COLOR);
return result;
}
int send_only(char* request) {
char response[512];
return send_receive(request, response);
}
I also had to change the cell_modem_Sendreceive function in cell_modem.cpp. The one provided in the demo project doesn't support more complex JSON response format. It only extract the content between the first '{' and '}' it can find. This definitely won't work for nested JSON objects, or the JSON arrays.
So I just copy the entire body from the response to the given string buffer. This enables the client code to decide how to parse it.
int cell_modem_Sendreceive(char* tx_string, char* rx_string) {
int iStatus = 0; //error by default
PRINTF(DEF "\r\n");
PRINTF(BLU "Sending to modem : %s" DEF "\r\n", &tx_string[0]);
sockwrite_mdm(&tx_string[0]);
if (sockread_mdm(&MySocketData, 1024, 20)) {
PRINTF(DEF "\r\n");
PRINTF(YEL "Read back : %s" DEF "\r\n", MySocketData.c_str());
char stringToCharBuf[BUF_SIZE_FOR_N_MAX_SOCKREAD*MAX_WNC_SOCKREAD_PAYLOAD+1]; // WNC can return max of 1500 (per sockread)
if ((MySocketData.length() + 1) < sizeof(stringToCharBuf)) {
strcpy(stringToCharBuf, MySocketData.c_str());
char* body = strstr(stringToCharBuf, "\r\n\r\n");
sprintf(rx_string, body + 4);
iStatus = 1; //all good
} else {
PRINTF(RED "BUFFER not big enough for sock data!" DEF "\r\n");
}
} else {
PRINTF(RED "No response..." DEF "\r\n");
}
return iStatus;
}
With all above changes, I can now send request and extract the hiking ID / session ID from the JSON data in the response.
First let's find the latest hiking by finding the largest hiking ID. The hiking ID is the timestamp when they are created.
void find_latest_hiking(char hikingId[]) {
char request[512];
char response[512];
sprintf(request, "GET %s/hikings HTTP/1.1\r\nHost: %s\r\n\r\n", FLOW_BASE_URL, MY_SERVER_URL);
if (send_receive(&request[0], &response[0])) {
long long llv;
long long latestId = 0;
const char* begin = response;
PRINTF("%s\n\n", begin);
char token[] = "\"id\":";
for (;;) {
llv = find_longlong(begin, token, &begin);
PRINTF("---> %lld\n", llv);
if (llv == 0) {
break;
}
if (llv > latestId) {
latestId = llv;
}
}
sprintf(hikingId, "%lld", latestId);
PRINTF("LATEST HIKING ID: %lld", latestId);
}
}
Next up let's join the found hiking.
void join_hiking(const char* hikingId, const char* name, char sessionId[]) {
char request[512];
char response[512];
sprintf(request, "GET %s/hikings/%s/sessions?name=%s HTTP/1.1\r\nHost: %s\r\n\r\n", FLOW_BASE_URL, hikingId, name, MY_SERVER_URL);
if (send_receive(&request[0], &response[0])) {
find_longlong(response, "\"id\":", sessionId);
}
}
After that, we will have the hiking ID and session ID. Now we can read the sensor data and report our device location
void generate_move_request(char request[], const char* hid, const char* sid) {
sprintf(
request,
"GET %s/hikings/%s/sessions/%s/moves?lat=%s&lng=%s HTTP/1.1\r\nHost: %s\r\n\r\n",
FLOW_BASE_URL,
hid,
sid,
SENSOR_DATA.GPS_Latitude,
SENSOR_DATA.GPS_Longitude,
MY_SERVER_URL);
}
void report_move(const char* hid, const char* sid) {
char request[512];
generate_move_request(request, hid, sid);
send_only(&request[0]);
}
Here is the main function:
int main() {
//delay so that the debug terminal can open after power-on reset:
wait (5.0);
pc.baud(115200);
display_app_firmware_version();
PRINTF(GRN "Hiking Pal tracking device started!\r\n\r\n");
//Initialize the I2C sensors that are present
sensors_init();
read_sensors();
// Set LED to RED until init finishes
SetLedColor(0x1); //Red
// Initialize the modem
PRINTF("\r\n");
cell_modem_init();
display_wnc_firmware_rev();
// Set LED BLUE for partial init
SetLedColor(0x4); //Blue
//Create a 1ms timer tick function:
iTimer1Interval_ms = SENSOR_UPDATE_INTERVAL_MS;
OneMsTicker.attach(OneMsFunction, 0.001f) ;
sprintf(sessionName, "IoT-kit-%d", rand() % 1000);
find_latest_hiking(hid);
PRINTF("Found Hiking ID: ");
PRINTF(hid);
PRINTF("\r\n");
join_hiking(hid, sessionName, sid);
PRINTF("Allocated Session ID: ");
PRINTF(sid);
PRINTF("\r\n");
// Send and receive data perpetually
while(1) {
if (bTimerExpiredFlag) {
bTimerExpiredFlag = false;
read_sensors(); //read available external sensors from a PMOD and the on-board motion sensor
report_move(hid, sid);
}
} //forever loop
}
Realtime Sharing Map
This is the map view of a hiking activity. All the hiker sessions will be displayed in different color, and updated in realtime to reflect their most recent location.
The code is written in Javascript. It uses the PubNub client library to subscribe to the location sharing channel, receives the data published by the server Flow, and visualize them on the map through the Google Maps API.
The code to subscribe to PubNub channel:
function pubs() {
pubnub = PUBNUB.init({
subscribe_key: 'YOUR_PUBNUB_SUBSCRIBER_KEY',
ssl: true
})
pubnub.subscribe({
channel: "moves",
message: function(message, channel) {
console.log(message)
addWayPoint(message);
},
connect: function() {console.log("PubNub Connected")}
})
}
It also registered the callback function which is called whenever a new location message is received from the PubNub. We add a new line in that callback to reflect the change in realtime:
function addWayPoint(message) {
var hiking = message['hiking'];
var session = message['session'];
var name = message['name'];
var gps = message['gps'];
if (hiking != current_hiking) {
return;
}
lat = gps.lat
lng = gps.lng
if (!sessions[session]) {
var pl = new google.maps.Polyline({
geodesic: true,
strokeColor: nextColor(),
strokeOpacity: 1.0,
strokeWeight: 2
});
pl.setMap(map);
sessions[session] = pl;
}
map.setCenter({lat: lat, lng : lng, alt: 0})
map_marker.setPosition({lat: lat, lng : lng, alt: 0});
var path = sessions[session].getPath();
path.push(new google.maps.LatLng(lat, lng));
}
Host on Heroku
To make the map easy to access by any one, I decided to host it on heroku. The simplest way I could find is using a PHP script to have heroku treat the files as a PHP web server.
Here is the Hiking Pal map hosted on heroku:
https://hikingpal.herokuapp.com/
After Submission WorkThere are still a lot of things needs polishing, but consider this is the result of one-week work during which I also learned mbed eco-system, the AT&T Flow, and PubNub from scratch, I'm happy with the solid end-to-end user experience I have built on top of the powerful tool chain used in this project..
After Dec 22 project submission I'm planning to do more polishing work to existing functionalities, as well as adding more features as I mentioned in the project idea document.
Polishing
- Add data persistence support. So the realtime map will be able to show history paths.
- More polishing on the realtime map. I'm considering using PubNub EON map, which looks quite promising.
- Allow join any hiking activity, not just the last one, which may not be the one you just created.
- Consumes less power and make it battery powered.
More Features
- Send emergency message, and show it on shared map.
- Add LCD display to the tracking device.
- Show distance between team members.
- Support report more sensor data, and show them on shared map.
- Compass.
- Automatic SOS message.
- Achievement and competing within team.
- Calories calculation.
The Hiking Pal application demonstrated how to leverage the cellular network to provide an always online, realtime location sharing application. By mixing up well established infrastructures like cellular network provided by AT&T, pubsub service provided by PubNub, Map service provided by Google, and web hosting service provided by Heroku, I can quickly build a very powerful realtime application solving real-life problems with full end-to-end user experience in a quite short amount of time.
Comments