// Pull in the azure-storage library let azure = require('azure-storage'); // Connect to the storage account whos connection string is saved in the AzureWebJobsStorage App Setting let tableSvc = azure.createTableService(process.env["AzureWebJobsStorage"]); // Save the table name, partitionkey and rowkey for future use let tableName = 'deviceState'; let partitionKey = "1"; let rowKey = "1"; // Create the variable that will hold the last known device state // This will allow us to update the latitude, longitude, altitude, heading, etc. // based on new x,y,z acceleromoter values passed in to the IoT Hub from the // MXChip board let last = null; module.exports = function (context, inputMessage) { context.log('----------------------------------------'); context.log(`Input Message: ${JSON.stringify(inputMessage)}\n`); // Ensure that the deviceState table exists in the Azure storage account tableSvc.createTableIfNotExists(tableName, function (error, result, response) { if (!error) { // Then retrieve the only record ( PartitionKey: "1", RowKey: "1") from the table tableSvc.retrieveEntity(tableName, partitionKey, rowKey, function (error, result, response) { // If there wasn't an error, and the resulting record has a "deviceId" property // then it looks like we succesffully retrieved the previous state from table storage if (!error && ('deviceId' in result)) { try { // Try mapping that returned record to a simpler structure // without all the table storage metadata in the way // and log the results for debugging purpose last = { deviceId: ((result.deviceId._ === null) ? "" : result.deviceId._), timestamp: ((result.timestamp._ === null) ? (new Date()).toISOString() : new Date(result.timestamp._)), airspeed: ((result.airspeed._ === null) ? 0 : parseFloat(result.airspeed._)), heading: ((result.heading._ === null) ? 0 : parseFloat(result.heading._)), altitude: ((result.altitude._ === null) ? 0 : parseFloat(result.altitude._)), latitude: ((result.latitude._ === null) ? 0 : parseFloat(result.latitude._)), longitude: ((result.longitude._ === null) ? 0 : parseFloat(result.longitude._)), pitch: ((result.pitch._ === null) ? 0 : parseFloat(result.pitch._)), roll: ((result.roll._ === null) ? 0 : parseFloat(result.roll._)), temperature: ((result.temperature._ === null) ? 0 : parseFloat(result.temperature._)), humidity: ((result.humidity._ === null) ? 0 : parseFloat(result.humidity._)), }; context.log("deviceState: Retrieved from table storage"); context.log(`Retrieved: ${JSON.stringify(result)}`); } catch (err) { // If there was an error parsing the record from storage // log that there was an error, set last to null, // and fall through to creating a new genesis state below last = null; context.log(`ERROR: There was an error parsing the table result:\n ${err}`); } // Otherwise, if we couldn't find a valid record in table storage // then assume this is the first time it has run and create a new // genesis state } else if( error || !last || !('deviceId' in result)) { context.log("deviceState: Valid state Not found in storage. Generating new genesis state"); // Get some "randomish" values using the current time as a "seed" var seed = ((new Date).valueOf()); last = { deviceId: '', timestamp: (new Date()).toISOString(), airspeed: 384, // airspeed is fixed at 384. You chould cheat and set this higher to go faster heading: seed % 360, altitude: 10000.0 + ((seed % 25) * 1000), latitude: 36.7 + (((seed / 100) % 100) / 100.0), longitude: -116.8 + ((seed % 100) / 50.0), pitch: 0, roll: 0, temperature: 0.0, humidity: 0.0 }; } context.log(`Last State: ${JSON.stringify(last)}`); // Ok, figure out how much time in milliseconds has elapsed between // the input message's time, and the last known state's time var thisTime = Date.parse(inputMessage.timestamp); var lastTime = Date.parse(last.timestamp); var milliseconds = thisTime.valueOf() - lastTime.valueOf(); // persist the deviceId from the inputMessage last.deviceId = inputMessage.deviceId; // Set the last time to the new inputMessages time last.timestamp = inputMessage.timestamp; // Compute new heading assuming hard left or right turns 10 degrees per second var delta = (milliseconds / 100.0) * (last.roll / 30.0); last.heading += delta; if (last.heading < 0.0) last.heading += 360.0; else if (last.heading >= 360.0) last.heading -= 360.0; // Constrain pitch to +/-15 degrees (positive == nose down) last.pitch = Math.max(Math.min(inputMessage.y / 11.0, 15.0), -15.0); // Constrain roll to +/-30 degrees (positive == rolling right) last.roll = Math.max(Math.min(inputMessage.x / 11.0, 30.0), -30.0); // Determine the distance travelled between the two messages // given the time distance and the airspeed // 1 MPH == 0.000277778 miles per second var distance = (milliseconds / 1000) * (last.airspeed * 0.000277778); // Compute new altitude and constrain it to 1,000 to 40,000 feet last.altitude = last.altitude - (distance * 5280.0 * Math.sin(last.pitch * Math.PI / 180.0)); last.altitude = Math.max(Math.min(last.altitude, 40000.0), 1000.0); // Compute new latitude and longitude var radians = last.heading * Math.PI / 180.0; var dx = distance * Math.sin(radians); var dy = distance * Math.cos(radians); last.latitude += (dy / 69.0); // Assume 69 miles per 1 degree of latitude last.longitude += (dx / 69.0); // Assume 69 miles per 1 degree of longitude // Persist the temperature and humidity just to make it so we can use "last" as our outputMessage last.temperature = inputMessage.temperature; last.humidity = inputMessage.humidity; // Write the message to your Event Hub so it can be viewed by the FlySim desktop app // Not used in the Express version of the Workshop // context.bindings.outputMessage = last; // Write the message to the Shared Event Hub so it can be viewed by the AirTrafficSim desktop app context.bindings.sharedOutputMessage = last; context.log(`Revised State: ${JSON.stringify(last)}`); // Create the last known state entity that we'll persist in to table storage var entity = { PartitionKey: partitionKey, RowKey: rowKey, deviceId: last.deviceId, timestamp: last.timestamp, airspeed: last.airspeed, heading: last.heading, altitude: last.altitude, latitude: last.latitude, longitude: last.longitude, pitch: last.pitch, roll: last.roll, temperature: last.temperature, humidity: last.humidity } // And inert it or update the existing entity in table storage tableSvc.insertOrReplaceEntity(tableName, entity, function (error, result, response) { if (!error) { // The device state was successfully saved to table storage context.log("deviceState: Persisted to table storage"); context.done(); } else { // Log any errors and get out of dodge context.log(`ERROR: There was an error persisting the device state:\n ${err}`); context.done(); return; } }); }); } else { // Log the table creation error and exit context.log(`ERROR: There was an error creating the deviceState table:\n ${err}`); context.done(); return; } }); };