Source: org/terraswarm/accessor/accessors/web/services/RedPinLocation.js

// Copyright (c) 2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/**
 *  This accessor obtains the location corresponding to a WiFi fingerprint
 *  from a RedPin localalization server http://redpin.org/. The RedPin server
 *  must first be configured with a map and trained with
 *  (fingerprint, location) matchings before it can be used with this accessor.
 *  For now, the easiest way to accomplish this is by walking around and
 *  tagging locations with the RedPin Android app available at
 *  https://github.com/MaKeAppDev/Redpin. Unfortunately, the
 *  iPhone app has difficulty obtaining fingerprint values from the phone.
 *
 *  This accessor is triggered by an input to the wifiReadings input, at which
 *  time a getLocation action is sent to the RedPin server over a raw TCP
 *  socket with specified host URL and port. These values default to those for
 *  the RedPin server running at "terra.eecs.berkeley.edu" on port 8090.
 *  Upon receiving a response from the server, the accessor parses out the
 *  "symbolicID" and "mapName" fields and outputs them on the location
 *  and MapName outputs respectively.
 *
 *  It appears the RedPin server doesn't validate its input
 *  and will return a location response to this accessor even if
 *  the wifiReadings array is ill formed. However, this accessor will
 *  raise an error if an element of the wifiReadings array is missing a
 *  field.
 *
 *  To obtain WiFi scan data on a mac, you can use the command line tool
 *  $/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s
 *
 *  The format of the wifiReadings parameter is consistent with the
 *  RedPin interface specification at
 *  http://redpin.org/resources/docu/interface_spec.pdf for the
 *  "getLocation" action.  E.g. [{ "ssid": "AirBears2", "bssid":
 *  "e8:65:49:e9:16:3b", "wepEnabled": false, "rssi": -88,
 *  "isInfrastructure": true }, {...} ]. This input triggers a raw TCP
 *  socket connection to the RedPin server running at host and port.
 *
 *  @accessor services/RedPinLocation
 *  @input wifiReadings A JSON string array of RSSI readings. See the comment above.
 *  @input {string} host The IP address or domain name of the RedPin server.
 *    Defaults to 'terra.eecs.berkeley.edu' for the Berkeley server.
 *  @input {int} port The port on the RedPin server to connect to. Defaults to
 *    8090 for the Berkeley server.
 *
 *  @output {string} mapName  The name of the map determined by the RedPin
 *    server to be the best fit for wifiReadings.
 *  @output {string} location The symbolic name of the location determined
 *    by the RedPin server to be the best match for wifiReadings according to
 *    its kNN/SVM algorithm.
 *  @author Matt Weber and Edward A. Lee
 *  @version $$Id$$
 */

// Stop extra messages from jslint and jshint.  Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals addInputHandler, console, get, getParameter, error, exports, extend, get, input, output, parameter, require, send */
/*jshint globalstrict: true*/
'use strict';

exports.setup = function () {
    this.extend('net/TCPSocketClient');
    this.input('wifiReadings');
    this.output('location');
    this.output('mapName');
    this.output('received', {
        'visibility': 'expert'
    });
    this.output('connected', {
        'visibility': 'expert'
    });
    this.input('rawBytes', {
        'visibility': 'expert',
        'value': true
    });
    this.input('toSend', {
        'visibility': 'expert'
    });
    this.input('host', {
        'value': "terra.eecs.berkeley.edu"
    });
    this.input('port', {
        'value': 8090
    });
};

exports.initialize = function () {
    exports.ssuper.initialize.call(this);
    this.addInputHandler('wifiReadings', this.exports.wifiReadingsInputHandler.bind(this));
    //exports.ssuper.tempFunc.call(this);// .call(this);
};

exports.wifiReadingsInputHandler = function () {
    //The RedPin server closes its connection after each location request
    //so it must be reestablished whenever we wish to make a new one.

    var wifiArray = this.get('wifiReadings');
    //Throw an error if input is incorrectly structured
    try {
        var wifiTest = JSON.parse(wifiArray);
        if(! Array.isArray(wifiTest) ){
            throw "the wifiArray input is not an array";
        }

        for (var i = 0; i < wifiTest.length; i += 1) {
            if (typeof wifiTest[i].ssid !== "string") {
                throw "missing ssid string in " + i + "th element of wifiReadings: " + wifiArray;
            }
            if (typeof wifiTest[i].bssid !== "string") {
                throw "missing bssid string in " + i + "th element of wifiReadings: " + wifiArray;
            }
            if (typeof wifiTest[i].wepEnabled !== "boolean") {
                throw "missing wepEnabled boolean in " + i + "th element of wifiReadings: " + wifiArray;
            }
            if (typeof wifiTest[i].rssi !== "number") {
                throw "missing rssi number in " + i + "th element of wifiReadings: " + wifiArray;
            }
            if (typeof wifiTest[i].isInfrastructure !== "boolean") {
                throw "missing isInfrastructure boolean in " + i + "th element of wifiReadings: " + wifiArray;
            }
        }
    } catch (error) {
        this.error(error);
    }
    //Format of request matches the protocol document here:
    //http://redpin.org/resources/docu/interface_spec.pdf
    var redPinRequest = '{ "action": "getLocation", "data": { "wifiReadings":' + wifiArray + '}}' + '\n';
    //This function must be called in the superAccessor context because it uses
    //the client variable.
    console.log(redPinRequest);
    exports.ssuper.send.call(this, redPinRequest);
};


//Override the dataReceivedHandler from TCPSocketClient to
//send server response over location and mapName output ports
exports.dataReceivedHandler = function (data) {
    var serverResponse = JSON.parse(data);
    var locationID = serverResponse.data.symbolicID;
    var mapName = serverResponse.data.map.mapName;
    this.send('location', locationID);
    this.send('mapName', mapName);
};