451 lines
15 KiB
JavaScript
451 lines
15 KiB
JavaScript
/** \file
|
|
* \brief Wrappers for the Accelerometer Sensor, LinearAccelerationSensor,
|
|
* and GravitySensor
|
|
*
|
|
* \see https://www.w3.org/TR/accelerometer/
|
|
* \see https://www.w3.org/TR/accelerometer/#linearaccelerationsensor
|
|
* \see https://www.w3.org/TR/accelerometer/#gravitysensor
|
|
*
|
|
* \author Copyright (C) 2021 Radek Hranicky
|
|
*
|
|
* \license SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
/** \file
|
|
* \ingroup wrappers
|
|
* MOTIVATION
|
|
* Readings from the Accelerometer, LinearAccelerationSensor, and GravitySensor
|
|
* of the Generic Sensor API should be secured as they provide a potentially
|
|
* valuable data for creating fingerprints. There are multiple options.
|
|
* A unique fingerprint can be obtained by describing the device's vibrations
|
|
* (See https://link.springer.com/chapter/10.1007/978-3-319-30806-7_7).
|
|
* Using trajectory inference and matching of the model to map data, one may
|
|
* use the readings from the Accelerometer to determing the device's position
|
|
* (See https://www.researchgate.net/publication/220990763_ACComplice_Location_
|
|
* inference_using_accelerometers_on_smartphones).
|
|
* Accelerometer readings can also be used for determining human walking patterns
|
|
* (See https://www.researchgate.net/publication/322835708_Classifying_Human_
|
|
* Walking_Patterns_using_Accelerometer_Data_from_Smartphone).
|
|
*
|
|
*
|
|
* WRAPPING
|
|
* The wrapper replaces the "XYZ" getters of the Accelerometer sensor,
|
|
* LinearAccelerationSensor, and GravitySensor. The wrapping's goal is to
|
|
* simulate a stationary device that can be possibly rotated. The rotation
|
|
* of the device is represented by the fake rotation matrix "orient.rotMat".
|
|
*
|
|
* The GravitySensor should provide readings of gravity acceleration applied
|
|
* to the device. This is represented by a vector made of x, y, z portions.
|
|
* To get this faked gravity vector for the device, the reference vector
|
|
* [0, 0, 9.8] is multipled with the rotation matrix. Wrappers for the
|
|
* GravitySensor's getters return x, y, z portions of the fake gravity vector.
|
|
*
|
|
* Next, the LinearAccelerationSensor should return acceleration values without
|
|
* the contribution of gravity. For a stationary device, it should be all zeroes.
|
|
* Yet, there could be vibrations that may change values a little bit, e.g.,
|
|
* spin around -0.1 to +0.1, as seen on the examined devices. This usually does
|
|
* not happed with every reading but only in intervals of seconds. And thus,
|
|
* after a few seconds we pseudo-randomly change these values.
|
|
*
|
|
* Finally, the Accelerometer sensor combines the previous two. Our wrappers thus
|
|
* return tha values from the LinearAccelerationSensor with the fake gravity
|
|
* vector portions added.
|
|
*
|
|
*
|
|
* POSSIBLE IMPROVEMENTS
|
|
* Support for simulation of a non-stationary device where the rotation
|
|
* can change. Currently, the calculation of the gravity vector is done
|
|
* only once by the initDataGenerator() where the reference vector is
|
|
* multiplied with the rotation matrix. If orient.rotMat could change,
|
|
* the dataGen would have to be updated periodically.
|
|
* Moreover, such a change should also be taken into account in wrappers
|
|
* for other movement-related sensors (Gyroscope, etc.).
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Create private namespace
|
|
*/
|
|
(function() {
|
|
/*
|
|
* \brief Initialization of data for storing sensor readings
|
|
*/
|
|
var init_data = `
|
|
var currentReading = currentReading || {orig_x: null, orig_y: null, orig_z: null, timestamp: null,
|
|
fake_x: null, fake_y: null, fake_z: null, gVector: null};
|
|
var previousReading = previousReading || {orig_x: null, orig_y: null, orig_z: null, timestamp: null,
|
|
fake_x: null, fake_y: null, fake_z: null, gVector: null};
|
|
var emulateStationaryDevice = (typeof args === 'undefined') ? true : args[0];
|
|
var debugMode = false;
|
|
|
|
const TWOPI = 2 * Math.PI;
|
|
`;
|
|
|
|
/*
|
|
* \brief Property getters of the original sensor object
|
|
*/
|
|
var orig_getters = `
|
|
var origGetX = Object.getOwnPropertyDescriptor(Accelerometer.prototype, "x").get;
|
|
var origGetY = Object.getOwnPropertyDescriptor(Accelerometer.prototype, "y").get;
|
|
var origGetZ = Object.getOwnPropertyDescriptor(Accelerometer.prototype, "z").get;
|
|
var origGetTimestamp = Object.getOwnPropertyDescriptor(Sensor.prototype, "timestamp").get;
|
|
`;
|
|
|
|
/*
|
|
* \brief Changes the value on the given axis to a new one from the given interval
|
|
*
|
|
* \param the axis object (min, max, value, and decimalPlaces properties required)
|
|
*/
|
|
function shake(axis) {
|
|
val = sen_prng() * (axis.max - axis.min) + axis.min;
|
|
|
|
var precision = Math.pow(10, -1 * axis.decimalPlaces);
|
|
if (val < precision) {
|
|
val = 0;
|
|
}
|
|
|
|
if (axis.canBeNegative) {
|
|
val *= Math.round(sen_prng()) ? 1 : -1;
|
|
}
|
|
|
|
if (val == 0) {
|
|
axis.value = 0;
|
|
} else {
|
|
axis.value = fixedNumber(val, axis.decimalPlaces);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief The data generator for creating fake accelerometer values
|
|
*/
|
|
class DataGenerator {
|
|
constructor() {
|
|
this.NEXT_CHANGE_MS_MIN = 1000;
|
|
this.NEXT_CHANGE_MS_MAX = 10000;
|
|
|
|
/* Reference gravity vector
|
|
* For a non-rotated device lying bottom-down on a flat surface,
|
|
* only axis "z" is afected by g.
|
|
*/
|
|
let referenceGravityVector = [0, 0, 9.8];
|
|
|
|
/*
|
|
* For a rotated device, the reference gravity vector needs to be
|
|
* multiplied by the rotation matrix.
|
|
* Here we calculate and store the device gravity vector
|
|
*/
|
|
this.gVector = multVectRot(referenceGravityVector, orient.rotMat);
|
|
|
|
/*
|
|
* Values for the linear acceleration are fluctuating
|
|
*/
|
|
this.x = {
|
|
name: "x",
|
|
min: 0.0,
|
|
max: 0.11,
|
|
decimalPlaces: 1,
|
|
canBeNegative: true,
|
|
value: null
|
|
};
|
|
this.y = {
|
|
name: "y",
|
|
min: 0.0,
|
|
max: 0.11,
|
|
decimalPlaces: 1,
|
|
canBeNegative: true,
|
|
value: null
|
|
};
|
|
this.z = {
|
|
name: "z",
|
|
min: 0.0,
|
|
max: 0.11,
|
|
decimalPlaces: 1,
|
|
canBeNegative: true,
|
|
value: null
|
|
};
|
|
this.nextChangeTimeX = null; // miliseconds
|
|
this.nextChangeTimeY = null;
|
|
this.nextChangeTimeZ = null;
|
|
}
|
|
|
|
/*
|
|
* \brief Updates the x/y/z axes values based on the current timestamp
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
update(currentTimestamp) {
|
|
// Simulate the Gyroscope changes
|
|
if (this.shouldWeUpdateX(currentTimestamp)) {
|
|
shake(this.x);
|
|
this.setNextChangeX(currentTimestamp);
|
|
};
|
|
if (this.shouldWeUpdateY(currentTimestamp)) {
|
|
shake(this.y);
|
|
this.setNextChangeY(currentTimestamp);
|
|
};
|
|
if (this.shouldWeUpdateZ(currentTimestamp)) {
|
|
shake(this.z);
|
|
this.setNextChangeZ(currentTimestamp);
|
|
};
|
|
}
|
|
|
|
/*
|
|
* \brief Boolean function that decides if the value on the axis X
|
|
* should be updated. Returns true if update is needed.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
shouldWeUpdateX(currentTimestamp) {
|
|
if (currentTimestamp === null || this.nextChangeTimeX === null) {
|
|
return true;
|
|
}
|
|
if (currentTimestamp >= this.nextChangeTimeX) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief Boolean function that decides if the value on the axis Y
|
|
* should be updated. Returns true if update is needed.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
shouldWeUpdateY(currentTimestamp) {
|
|
if (currentTimestamp === null || this.nextChangeTimeY === null) {
|
|
return true;
|
|
}
|
|
if (currentTimestamp >= this.nextChangeTimeY) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief Boolean function that decides if the value on the axis Z
|
|
* should be updated. Returns true if update is needed.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
shouldWeUpdateZ(currentTimestamp) {
|
|
if (currentTimestamp === null || this.nextChangeTimeZ === null) {
|
|
return true;
|
|
}
|
|
if (currentTimestamp >= this.nextChangeTimeZ) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief Sets the timestamp of the next update of value on the axis X.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
setNextChangeX(currentTimestamp) {
|
|
let interval_ms = Math.floor(
|
|
sen_prng() * (this.NEXT_CHANGE_MS_MAX - this.NEXT_CHANGE_MS_MIN + 1)
|
|
+ this.NEXT_CHANGE_MS_MIN
|
|
);
|
|
this.nextChangeTimeX = currentTimestamp + interval_ms;
|
|
}
|
|
|
|
/*
|
|
* \brief Sets the timestamp of the next update of value on the axis Y.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
setNextChangeY(currentTimestamp) {
|
|
let interval_ms = Math.floor(
|
|
sen_prng() * (this.NEXT_CHANGE_MS_MAX - this.NEXT_CHANGE_MS_MIN + 1)
|
|
+ this.NEXT_CHANGE_MS_MIN
|
|
);
|
|
this.nextChangeTimeY = currentTimestamp + interval_ms;
|
|
}
|
|
|
|
/*
|
|
* \brief Sets the timestamp of the next update of value on the axis Z.
|
|
*
|
|
* \param Current timestamp from the sensor object
|
|
*/
|
|
setNextChangeZ(currentTimestamp) {
|
|
let interval_ms = Math.floor(
|
|
sen_prng() * (this.NEXT_CHANGE_MS_MAX - this.NEXT_CHANGE_MS_MIN + 1)
|
|
+ this.NEXT_CHANGE_MS_MIN
|
|
);
|
|
this.nextChangeTimeZ = currentTimestamp + interval_ms;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief Updates the stored (both real and fake) sensor readings
|
|
* according to the data from the sensor object.
|
|
*
|
|
* \param The sensor object
|
|
*/
|
|
function updateReadings(sensorObject) {
|
|
|
|
// We need the original reading's timestamp to see if it differs
|
|
// from the previous sample. If so, we need to update the faked x,y,z
|
|
let previousTimestamp = previousReading.timestamp;
|
|
let currentTimestamp = origGetTimestamp.call(sensorObject);
|
|
|
|
if (debugMode) {
|
|
// [!] Debug mode: overriding timestamp
|
|
// This allows test suites to set a custom timestamp externally
|
|
// by modifying the property of the sensor object directly.
|
|
currentTimestamp = sensorObject.timestamp;
|
|
}
|
|
|
|
if (currentTimestamp === previousReading.timestamp) {
|
|
// No new reading, nothing to update
|
|
return;
|
|
}
|
|
|
|
// Rotate the readings: previous <- current
|
|
previousReading = JSON.parse(JSON.stringify(currentReading));
|
|
|
|
// Update current reading
|
|
// NOTE: Original values are also stored for possible future use
|
|
currentReading.orig_x = origGetX.call(sensorObject);
|
|
currentReading.orig_y = origGetY.call(sensorObject);
|
|
currentReading.orig_z = origGetZ.call(sensorObject);
|
|
currentReading.timestamp = currentTimestamp;
|
|
|
|
dataGenerator.update(currentTimestamp);
|
|
|
|
currentReading.fake_x = dataGenerator.x.value;
|
|
currentReading.fake_y = dataGenerator.y.value;
|
|
currentReading.fake_z = dataGenerator.z.value;
|
|
currentReading.fake_gVector = dataGenerator.gVector;
|
|
|
|
if (debugMode) {
|
|
}
|
|
}
|
|
|
|
/*
|
|
* \brief Initializes the related generators
|
|
*/
|
|
var generators = `
|
|
// Initialize the data generator, if not initialized before
|
|
var dataGenerator = dataGenerator || new DataGenerator();
|
|
`;
|
|
|
|
var helping_functions = sensorapi_prng_functions + device_orientation_functions
|
|
+ DataGenerator + shake + updateReadings;
|
|
var hc = init_data + orig_getters + helping_functions + generators;
|
|
|
|
var wrappers = [
|
|
{
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "x",
|
|
wrapped_objects: [],
|
|
helping_code: hc,
|
|
post_wrapping_code: [
|
|
{
|
|
code_type: "object_properties",
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "x",
|
|
wrapped_objects: [],
|
|
/** \brief replaces Sensor.prototype.x getter to return a faked value
|
|
*/
|
|
wrapped_properties: [
|
|
{
|
|
property_name: "get",
|
|
property_value: `
|
|
function() {
|
|
updateReadings(this);
|
|
if (this.__proto__.constructor.name === 'GravitySensor') {
|
|
return fixedNumber(currentReading.fake_gVector[0], 1);
|
|
} else if (this.__proto__.constructor.name === 'LinearAccelerationSensor') {
|
|
return fixedNumber(currentReading.fake_x, 1);
|
|
}
|
|
return fixedNumber(currentReading.fake_x + currentReading.fake_gVector[0], 1);
|
|
}`,
|
|
},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
{
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "y",
|
|
wrapped_objects: [],
|
|
helping_code: hc,
|
|
post_wrapping_code: [
|
|
{
|
|
code_type: "object_properties",
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "y",
|
|
wrapped_objects: [],
|
|
/** \brief replaces Sensor.prototype.y getter to return a faked value
|
|
*/
|
|
wrapped_properties: [
|
|
{
|
|
property_name: "get",
|
|
property_value: `
|
|
function() {
|
|
updateReadings(this);
|
|
if (this.__proto__.constructor.name === 'GravitySensor') {
|
|
return fixedNumber(currentReading.fake_gVector[1], 1);
|
|
} else if (this.__proto__.constructor.name === 'LinearAccelerationSensor') {
|
|
return fixedNumber(currentReading.fake_y, 1);
|
|
}
|
|
return fixedNumber(currentReading.fake_y + currentReading.fake_gVector[1], 1);
|
|
}`,
|
|
},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
{
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "z",
|
|
wrapped_objects: [],
|
|
helping_code: hc,
|
|
post_wrapping_code: [
|
|
{
|
|
code_type: "object_properties",
|
|
parent_object: "Accelerometer.prototype",
|
|
parent_object_property: "z",
|
|
wrapped_objects: [],
|
|
/** \brief replaces Sensor.prototype.z getter to return a faked value
|
|
*/
|
|
wrapped_properties: [
|
|
{
|
|
property_name: "get",
|
|
property_value: `
|
|
function() {
|
|
updateReadings(this);
|
|
if (this.__proto__.constructor.name === 'GravitySensor') {
|
|
return fixedNumber(currentReading.fake_gVector[2], 1);
|
|
} else if (this.__proto__.constructor.name === 'LinearAccelerationSensor') {
|
|
return fixedNumber(currentReading.fake_z, 1);
|
|
}
|
|
return fixedNumber(currentReading.fake_z + currentReading.fake_gVector[2], 1);
|
|
}`,
|
|
},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
]
|
|
add_wrappers(wrappers);
|
|
})()
|