<repo_name>jasonwiener/PostCycla<file_sep>/gpx/simplifyPosData.js
/*
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js

Copyright (c) 2017, <NAME>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:

   1. Redistributions of source code must retain the above copyright notice, this list of
      conditions and the following disclaimer.

   2. Redistributions in binary form must reproduce the above copyright notice, this list
      of conditions and the following disclaimer in the documentation and/or other materials
      provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


Changes:
 Updated to fit PosData
*/

var xName = 'x';
var yName = 'y';

exports.SetXName = function(name)
{
    xName = name;
}

exports.SetYName = function(name)
{
    yName = name;
}

// to suit your point format, run search/replace for '[xName]' and '[yName]';
// for 3D version, see 3d branch (configurability would draw significant performance overhead)

// square distance between 2 points
function getSqDist(p1, p2) {

    var dx = p1[xName] - p2[xName],
        dy = p1[yName] - p2[yName];

    return dx * dx + dy * dy;
}

// square distance from a point to a segment
function getSqSegDist(p, p1, p2) {

    var x = p1[xName],
        y = p1[yName],
        dx = p2[xName] - x,
        dy = p2[yName] - y;

    if (dx !== 0 || dy !== 0) {

        var t = ((p[xName] - x) * dx + (p[yName] - y) * dy) / (dx * dx + dy * dy);

        if (t > 1) {
            x = p2[xName];
            y = p2[yName];

        } else if (t > 0) {
            x += dx * t;
            y += dy * t;
        }
    }

    dx = p[xName] - x;
    dy = p[yName] - y;

    return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format

// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {

    var prevPoint = points[0],
        newPoints = [prevPoint],
        point;

    for (var i = 1, len = points.length; i < len; i++) {
        point = points[i];

        if (getSqDist(point, prevPoint) > sqTolerance) {
            newPoints.push(point);
            prevPoint = point;
        }
    }

    if (prevPoint !== point) newPoints.push(point);

    return newPoints;
}

// simplification using optimized Douglas-Peucker algorithm with recursion elimination
function simplifyDouglasPeucker(points, sqTolerance) {

    var len = points.length,
        MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array,
        markers = new MarkerArray(len),
        first = 0,
        last = len - 1,
        stack = [],
        newPoints = [],
        i, maxSqDist, sqDist, index;

    markers[first] = markers[last] = 1;

    while (last) {

        maxSqDist = 0;

        for (i = first + 1; i < last; i++) {
            sqDist = getSqSegDist(points[i], points[first], points[last]);

            if (sqDist > maxSqDist) {
                index = i;
                maxSqDist = sqDist;
            }
        }

        if (maxSqDist > sqTolerance) {
            markers[index] = 1;
            stack.push(first, index, index, last);
        }

        last = stack.pop();
        first = stack.pop();
    }

    for (i = 0; i < len; i++) {
        if (markers[i]) newPoints.push(points[i]);
    }

    return newPoints;
}

// both algorithms combined for awesome performance
exports.simplify = function(points, tolerance, highestQuality) 
{

    var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;

    points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
    points = simplifyDouglasPeucker(points, sqTolerance);

    return points;
}

<file_sep>/HW_Controller/mbed/main.cpp
/**
* PostCycla stm32 sensor and server controller
* 	Used to read a CNY 70 reflex sensor to sense rotational speed of the trainers roller
*   and set the servo position (0..100%) to change the resistance 
*   (servo will be mounted to the trainer actuating the trainers bowden cable)
*	Input:
*		any character between 32...132 (0..100%) and newline as delimiter
*
*	Output:
*		(will be triggered every time an input was sent)
*		Will send a JSON string containing:
*		{ "distance" : xxx, "speedms" : xxx, "servoPos" : xxx, "charSent" : xxx }
*
*	distance => dinstance since the last time the sensor was read
*	speedms => 	current speed in meters per second
*	servoPos => current position of the servo setting the trainer resistence
*	sentChar => the servo position can be set from 0...100% and will be sent to the STM as an character followed by a newline
*				this sentChar was implemented for debugging and will show the last sent character (should be between 32 and 132)
*
* @author <NAME>
* @version 1.0
*/

#include "mbed.h"
#include "Servo.h"
 
Servo servo(D3);				// servo on pin 3
Serial pc(USBTX, USBRX);		// starting serial port for communication with PostCycla sensor module
DigitalOut  ledOut(LED1);		// previously used for debugging
InterruptIn lightBarrier(D12);	// pin 12 will sence the voltage output of the CNY 70
Timeout distanceTimer;
Timer stopwatch;

float roller_circum = 0.1;  // roller circumference in m
int rotation_Cnt = 0;       // roller rotation counter

int timer_end = 0;          // stopwatch timer end time
int timer_begin = 0;        // stopwatch timer begin time

float distanceTotal = 0;    // distance in m since last request
float speed = 0;            // current speed in m/s
int charSent = 0;

float position = 0.5;       // current servo position
float range = 0.0005;       // current servo range
 
/*
    ligth Barrier counter
    Counts light barrier hits (rotations of roller)
    Writes:
        rotation_Cnt    - adds one
*/
void lightBarriercallback()
{
    rotation_Cnt++;
}

/*
    Send request data
    Send the requested data via serial output and resets total distance
*/
void sendRequestData()
{
    pc.printf("{ \"distance\" : %.1f, \"speedms\" : %0.2f, \"servoPos\" : %.3f, \"charSent\" : %d }\n", distanceTotal, speed, position, charSent);
    
}


/*
    distance timeout
    Gets called every second and calculates distance and speed travelled.
    Reads: 
        stopwatch       - to get accurate time since last call (should be every second)
        rotation_Cnt    - rotations since last call of distance timeout
    Writes:
        distance        - in m from rotation_Cnt and roller_circum
        totalDistance   - in m by addin distance
        speed           - in m/s calculated from stopwatch time and distance
        rotation_Cnt    - resets to 0 after read
        timer_end       - used for accurate timeing
        timer_begin     - used for accurate timeing
    
*/
void distanceTimeoutCallback()
{   
    // gets time passed since last distance update     
    timer_end = stopwatch.read_us();    
    int usPassed = timer_end - timer_begin;
    
    float distance = (roller_circum * rotation_Cnt);   // gets distance in m since last distanceTimer call
    speed = (distance * usPassed) /1000000;          // calcs speed in m/s from passed time and distance
    distanceTotal += distance;                  // adds current distance to total distance
     
//    sendRequestData();
    // resets all for next timeout
    rotation_Cnt = 0;                                   // resets rotationCounter    
    distanceTimer.attach(&distanceTimeoutCallback,1);   // restarts distanceTimer    
    stopwatch.start();                                  // restarts Stopwatch
    timer_begin = stopwatch.read_us();                  // stores last distance update time
}   

/*
    Set Servo
    Sets requested servo postion
*/
void setServo(float pos)
{
    position = pos;
    servo.calibrate(range, 45.0); 
    servo = position;    
}


int main() 
{
    // lightBarrier counter
    lightBarrier.fall(&lightBarriercallback);
    
    // checks Speed and distance ever second
    distanceTimer.attach(&distanceTimeoutCallback,1);
    stopwatch.start();
    timer_begin = stopwatch.read_us();
   
    int tmpChar = 0;
    // checks input via searial for new request
    while(1) 
    {               
        while(tmpChar != 10) // read till newLine
        {        
            tmpChar = pc.getc();
            if (tmpChar >= 32 && tmpChar <= 133) // only get valid chars and only the last one
            {
                charSent = tmpChar;
            }
        }
        tmpChar = 0;
        float newPos = ((float)(charSent - 32)) / 100.0; 
        
        setServo(newPos);
    
        sendRequestData();
        distanceTotal = 0;   //always reset
            
        
    }
}<file_sep>/test/hardware/testAntplusSensors.js
/**
* Ant+ Sensor test
* 	This test will try and get data from an Ant+ stick with connected heart rate and cadence sensors
*   Should return a console ouput with the heart rate and cadence
*	
*	Example:
*		{ HeartRate: 95, Cadence : 80 }
*
*   For further information see folder: "antplus"
*   Will not give an error if one of the sensors is not connected/paired.. it will only return 0
*
* @author <NAME>
* @version 1.0
*/

var AntSensors = require('../../antplus/AntSensors.js');

var FgRed = "\x1b[31m";
var FgGreen = "\x1b[32m";
var readData = 10; // times to read data (reading once per second)

var showResults =  function (data)
{    
    if (data.Success)
    {
        console.log(FgGreen);        
    } else 
    {
        console.log(FgRed);
    }
    console.log(data);
    console.log("\x1b[0m");    
};

try
{
// sensor id's for my heartrate and cadence sensor
antSensors = new AntSensors(30250, 59056); 
antSensors.Open();
var i = 0;
var interval = setInterval(function () 
{ 
    console.log(antSensors.Data);
    if (i > readData)
    {
        antSensors.Close();
        clearInterval( interval);
    }
    i++;
}, 1000);

} catch(err)
{
    showResults({Success: false, Data: err});
}<file_sep>/test/hardware/testSTMSensor.js
/**
* STM32 sensor test
* 	This test will try and get data from the STM32/Arduino and check the output
*   Should return a console ouput with the data from the module
*	
*	Example:
*		{ "distance" : 0, "speedms" : 0, "servoPos" : 0, "sentChar" : 0 }
*
*   For further information see folder: "stm"
*   
* @author <NAME>
* @version 1.0
*/

var os = require('os');
var stm = require('../../stm/newstm.js');
var FgRed = "\x1b[31m";
var FgGreen = "\x1b[32m";
var readData = 10; // times to read data (reading once per second)
var i = 0;
var interval= null;

var getComPort = function()
{
    if (os.platform() == "win32")
    {
        return "COM3"; // change this to your std com port
    } else 
    {
        return "/dev/ttyACM0"; // change this to your std com port
    }
}


var showResults =  function (data)
{    
    if (data.Success)
    {
        console.log(FgGreen);        
    } else 
    {
        console.log(FgRed);
    }
    console.log(data);
    console.log("\x1b[0m");    
};

try
{
    var stmSensors = new stm(getComPort());    
        
    stmSensors.on("SensorReady", function (data)
    {    
        console.log("Sensor Ready... sending data");    
        setTimeout(function() 
		{
            interval = setInterval(function () 
            { 
                stmSensors.SetLevel((i/10), false);	
                if (i > readData)
                {
                    stmSensors.Disconnect();
                    clearInterval(interval);
                }
                i++;
            }, 1000);
        },3000);  // need to wait at least 2 sec. ?!?!  otherwise it wont work
    });

    stmSensors.on("SensorData", function (data)
    {
        // expected data = { distance: 0.1, speedms: 0, servoPos: 0.1 }
        console.log(data);   
      
    });
    
    stmSensors.Connect();    

} catch(err)
{
    showResults({Success: false, Data: err});
}


<file_sep>/stm/stmMock.js
/**
* STM32 sensor mockup
* 	This mockup will provide sensor data for testing
*	Constant speed can be set with the testMS variable (meters per second).. currently set to 24 km/h
*   Should provide the same data as the STM32 hardware
*	Example:
*		{ "distance" : 0, "speedms" : 0, "servoPos" : 0, "sentChar" : 0 }
*
*	distance => dinstance since the last time the sensor was read
*	speedms => 	current speed in meters per second
*	servoPos => current position of the servo setting the trainer resistence
*	sentChar => the servo position can be set from 0...100% and will be sent to the STM as an character followed by a newline
*				this sentChar was implemented for debugging and will show the last sent character (should be between 32 and 132)
*
*	For more details regarding the functionality of this module look at the "newstm.js" containing the real implementation.
* @author <NAME>
* @version 1.0
*/

var events = require('events');
var Settings = require('../settings.js');
var calc = require('../util/calc.js');


var testMS = 6.666;
var ServoOffset = { Min : 0.2, Max: 0.9};

var STM = function(comPort, script, tcpPort, ip)
{
	events.EventEmitter.call(this);
    this.ServerPos = 0;

	this.SensorReady = function()
	{		
		this.emit('SensorReady');	
	}

	this.SensorData = function(strdata)
	{
		var data = JSON.parse(strdata);
		this.emit('SensorData', data);
	}
}

STM.prototype.__proto__ = events.EventEmitter.prototype;

STM.prototype.Connect = function()
{
	this.Init();
}

STM.prototype.Disconnect = function()
{

}

STM.prototype.Destroy = function()
{
	
}

STM.prototype.Init = function()
{
	ServoOffset = typeof Settings.Data.Sensors.ServoOffset  !== 'undefined' ? Settings.Data.Sensors.ServoOffset : ServoOffset;	
	ServoOffset.Delta = ServoOffset.Max - ServoOffset.Min;
	var me = this;
	setTimeout(function(){
		me.SensorReady();
	},1000);
}

STM.prototype.SetLevel = function(pos)
{
		// servo controller expects values from 32 to 132	
	pos = checkServoEndpoints(pos);	
	var position = pos;
	pos = (pos *100) +32;		

	var send = String.fromCharCode(pos);
	if (Settings.Data.DebugOutput)	
	{
		console.log("Char: %s Pos: %d  CHR: %d", send, position, send.charCodeAt(0));
	}	
	this.SensorData("{ \"distance\" : "+testMS+", \"speedms\" : "+testMS+", \"servoPos\" : "+pos+", \"sentChar\" : "+send.charCodeAt(0)+" }");
}


// determined by enpoints of trainer bowden cable
var checkServoEndpoints = function(pos)
{
	var offestPos = calc.ceil(pos /1 * ServoOffset.Delta);

	offestPos += ServoOffset.Min;
	if (offestPos > ServoOffset.Max)
	{
		offestPos = ServoOffset.Max;
	}
	if (offestPos < ServoOffset.Min)
	{
		offestPos = ServoOffset.Min;
	}
	return offestPos;
}

// export the class
module.exports = STM;


<file_sep>/test/hardware/libs/test_antplus.js
var Ant = require('../../../antplus/antplus.js');
var events = require('events');

var resultData = {
	Test: 'AntPlus', 
	Success: false, 
	Data: null};

var Test = function()
{
	events.EventEmitter.call(this);
    var me = this;
    
	this.Result = function(success, data)
	{				
		resultData.Success = success;
		resultData.Data = data;
		this.emit('TestResult', resultData);	
    }
    
    this.Setup = function(data)
    {
		console.log('AntPlus setup');  
        try
        {
            me.stick = new Ant.GarminStick2();
            me.stick.on('startup', function () 
            {
                console.log('Ant stick started');
                try
                {                  
                    me.stick.detach_all();
                    me.stick.close();                     
                } catch(err) 
                {
                    me.Result(false,err);
                }
                

            });
            me.stick.on('shutdown', function () 
            { 
                me.Result(true, 'Ant Stick started and shut down gracefully!');                 
            });
        } catch(err) 
        {
            me.Result(false,err);
        }
	}
	
	this.Test = function()
	{
        try
        {		
            console.log("checking ant plus stick");
            if (!me.stick.open()) 
	        {
                me.Result(false, 'Stick not Found');                      	
                return;
	        } 
            
            //me.Result(true, 'Everything fine');                      	
        } catch(err) 
        {
            me.Result(false,err);
        }
	}
}

Test.prototype.__proto__ = events.EventEmitter.prototype;


// export the class
module.exports = Test;<file_sep>/antplus/AntSensorsMock.js
/**
* Ant+ sensors (heart rate and cadence) mockup
* 	This mockup will provide sensor data for testing
*	Constant cadence and heart rate can be set in the "AntSensors" function
*   Should provide the same data as the "AntSensors.js"
*	Example:
*		{ HeartRate: 95, Cadence : 80 }
**
*	For more details regarding the functionality of this module look at the "AntSensors.js" containing the real implementation.
* @author <NAME>
* @version 1.0
*/
var Settings = require('../settings.js');
var Ant = require('./antplus.js');

var AntSensors = function(HR_dev_id, CAD_dev_id)
{
	this.Data = { HeartRate: 95, Cadence : 80	};	
}

AntSensors.prototype.Open = function()
{
	 
}

AntSensors.prototype.Close = function()
{
	 
}

module.exports = AntSensors;
<file_sep>/test/hardware/libs/test_usb.js
var usbPort = require('usb');
var events = require('events');

var resultData = {
	Test: 'USBPort', 
	Success: false, 
	Data: null};

var Test = function()
{
	events.EventEmitter.call(this);
    var me = this;
    
	this.Result = function(success, data)
	{				
		resultData.Success = success;
		resultData.Data = data;
		this.emit('TestResult', resultData);	
    }
    
    this.Setup = function(data)
    {
		console.log('USB setup');  
        try
        {
			
        } catch(err) 
        {
            me.Result(false,err);
        }
	}
	
	this.Test = function()
	{
        try
        {		
            console.log("checking usb devices");
            var list = usbPort.getDeviceList();
            me.Result(true, list);                      	
        } catch(err) 
        {
            me.Result(false,err);
        }
	}
}

Test.prototype.__proto__ = events.EventEmitter.prototype;


// export the class
module.exports = Test;<file_sep>/antplus/antplus.js
/**
* Ant+ sensors (heart rate and cadence)
* 	This encapsulates the sensors to a single module 
*	The implementation was converted from TypeScript provided by: 
*		<NAME>	https://github.com/Loghorn/ant-plus
*
* @author <NAME>
* @version 1.0
*/

var Ant = require('./ant.js');

//*
var HRS = require('./HeartRateSensor.js');
var CS = require('./CadenceSensor.js');
//*/
module.exports = {
	GarminStick2: Ant.GarminStick2,
	GarminStick3: Ant.GarminStick3,
	
	//*
	HeartRateSensor: HRS.HeartRateSensor,
	HeartRateScanner: HRS.HeartRateScanner,
	
	CadenceSensor: CS.CadenceSensor,
	CadenceScanner: CS.CadenceScanner
	//*/
};<file_sep>/antplus/HeartRateSensor.js
var events = require("events"); 
var Ant = require("./ant.js"); 

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};

    var Constants = Ant.Constants;
    var Messages = Ant.Messages;
    var HeartRateSensorState = (function () {
        function HeartRateSensorState(deviceId) {
            this.DeviceID = deviceId;
        }
        return HeartRateSensorState;
    }());
    var HeartRateScannerState = (function (_super) {
        __extends(HeartRateScannerState, _super);
        function HeartRateScannerState() {
            _super.apply(this, arguments);
        }
        return HeartRateScannerState;
    }(HeartRateSensorState));
    var PageState;
    (function (PageState) {
        PageState[PageState["INIT_PAGE"] = 0] = "INIT_PAGE";
        PageState[PageState["STD_PAGE"] = 1] = "STD_PAGE";
        PageState[PageState["EXT_PAGE"] = 2] = "EXT_PAGE";
    })(PageState || (PageState = {}));
    var HeartRateSensor = (function (_super) {
        __extends(HeartRateSensor, _super);
        function HeartRateSensor(stick) {
            _super.call(this, stick);
            this.pageState = PageState.INIT_PAGE; // sets the state of the receiver - INIT, STD_PAGE, EXT_PAGE
            this.decodeDataCbk = this.decodeData.bind(this);
        }
        HeartRateSensor.prototype.attach = function (channel, deviceID) {
            _super.prototype.attach.call(this, channel, 'receive', deviceID, HeartRateSensor.deviceType, 0, 255, 8070);
            this.state = new HeartRateSensorState(deviceID);
        };
        HeartRateSensor.prototype.decodeData = function (data) {
            if (data.readUInt8(Messages.BUFFER_INDEX_CHANNEL_NUM) !== this.channel) {
                return;
            }
            switch (data.readUInt8(Messages.BUFFER_INDEX_MSG_TYPE)) {
                case Constants.MESSAGE_CHANNEL_BROADCAST_DATA:
                case Constants.MESSAGE_CHANNEL_ACKNOWLEDGED_DATA:
                case Constants.MESSAGE_CHANNEL_BURST_DATA:
                    if (this.deviceID === 0) {
                        this.write(Messages.requestMessage(this.channel, Constants.MESSAGE_CHANNEL_ID));
                    }
                    var page = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA);
                    if (this.pageState === PageState.INIT_PAGE) {
                        this.pageState = PageState.STD_PAGE; // change the state to STD_PAGE and allow the checking of old and new pages
                    }
                    else if ((page !== this.oldPage) || (this.pageState === PageState.EXT_PAGE)) {
                        this.pageState = PageState.EXT_PAGE; // set the state to use the extended page format
                        switch (page & ~HeartRateSensor.TOGGLE_MASK) {
                            case 1:
                                //decode the cumulative operating time
                                this.state.OperatingTime = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                this.state.OperatingTime |= data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2) << 8;
                                this.state.OperatingTime |= data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3) << 16;
                                this.state.OperatingTime *= 2;
                                break;
                            case 2:
                                //decode the Manufacturer ID
                                this.state.ManId = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                //decode the 4 byte serial number
                                this.state.SerialNumber = this.deviceID;
                                this.state.SerialNumber |= data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 2) << 16;
                                this.state.SerialNumber >>>= 0;
                                break;
                            case 3:
                                //decode HW version, SW version, and model number
                                this.state.HwVersion = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                this.state.SwVersion = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2);
                                this.state.ModelNum = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3);
                                break;
                            case 4:
                                //decode the previous heart beat measurement time
                                this.state.PreviousBeat = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 2);
                                break;
                            default:
                                break;
                        }
                    }
                    // decode the last four bytes of the HRM format, the first byte of this message is the channel number
                    this.DecodeDefaultHRM(data.slice(Messages.BUFFER_INDEX_MSG_DATA + 4));
                    this.oldPage = page;
                    break;
                case Constants.MESSAGE_CHANNEL_ID:
                    this.deviceID = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA);
                    this.transmissionType = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3);
                    this.state.DeviceID = this.deviceID;
                    break;
                default:
                    break;
            }
        };
        HeartRateSensor.prototype.DecodeDefaultHRM = function (pucPayload) {
            // decode the measurement time data (two bytes)
            this.state.BeatTime = pucPayload.readUInt16LE(0);
            // decode the measurement count data
            this.state.BeatCount = pucPayload.readUInt8(2);
            // decode the measurement count data
            this.state.ComputedHeartRate = pucPayload.readUInt8(3);
            this.emit('hbdata', this.state);
        };
        HeartRateSensor.deviceType = 120;
        HeartRateSensor.TOGGLE_MASK = 0x80;
        return HeartRateSensor;
    }(Ant.AntPlusSensor));
    exports.HeartRateSensor = HeartRateSensor;
    var HeartRateScanner = (function (_super) {
        __extends(HeartRateScanner, _super);
        function HeartRateScanner(stick) {
            _super.call(this, stick);
            this.states = {};
            this.pageState = PageState.INIT_PAGE;
            this.decodeDataCbk = this.decodeData.bind(this);
        }
        HeartRateScanner.prototype.scan = function () {
            _super.prototype.scan.call(this, 'receive');
        };
        HeartRateScanner.prototype.decodeData = function (data) {
            if (data.length <= Messages.BUFFER_INDEX_EXT_MSG_BEGIN || !(data.readUInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN) & 0x80)) {
                console.log('wrong message format');
                return;
            }
            var deviceId = data.readUInt16LE(Messages.BUFFER_INDEX_EXT_MSG_BEGIN + 1);
            var deviceType = data.readUInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN + 3);
            if (deviceType !== HeartRateScanner.deviceType) {
                return;
            }
            if (!this.states[deviceId]) {
                this.states[deviceId] = new HeartRateScannerState(deviceId);
            }
            if (data.readUInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN) & 0x40) {
                if (data.readUInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN + 5) === 0x20) {
                    this.states[deviceId].Rssi = data.readInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN + 6);
                    this.states[deviceId].Threshold = data.readInt8(Messages.BUFFER_INDEX_EXT_MSG_BEGIN + 7);
                }
            }
            switch (data.readUInt8(Messages.BUFFER_INDEX_MSG_TYPE)) {
                case Constants.MESSAGE_CHANNEL_BROADCAST_DATA:
                case Constants.MESSAGE_CHANNEL_ACKNOWLEDGED_DATA:
                case Constants.MESSAGE_CHANNEL_BURST_DATA:
                    var page = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA);
                    if (this.pageState === PageState.INIT_PAGE) {
                        this.pageState = PageState.STD_PAGE; // change the state to STD_PAGE and allow the checking of old and new pages
                    }
                    else if ((page !== this.oldPage) || (this.pageState === PageState.EXT_PAGE)) {
                        this.pageState = PageState.EXT_PAGE; // set the state to use the extended page format
                        switch (page & ~HeartRateScanner.TOGGLE_MASK) {
                            case 1:
                                //decode the cumulative operating time
                                this.states[deviceId].OperatingTime = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                this.states[deviceId].OperatingTime |= data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2) << 8;
                                this.states[deviceId].OperatingTime |= data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3) << 16;
                                this.states[deviceId].OperatingTime *= 2;
                                break;
                            case 2:
                                //decode the Manufacturer ID
                                this.states[deviceId].ManId = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                //decode the 4 byte serial number
                                this.states[deviceId].SerialNumber = this.deviceID;
                                this.states[deviceId].SerialNumber |= data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 2) << 16;
                                this.states[deviceId].SerialNumber >>>= 0;
                                break;
                            case 3:
                                //decode HW version, SW version, and model number
                                this.states[deviceId].HwVersion = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1);
                                this.states[deviceId].SwVersion = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2);
                                this.states[deviceId].ModelNum = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3);
                                break;
                            case 4:
                                //decode the previous heart beat measurement time
                                this.states[deviceId].PreviousBeat = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 2);
                                break;
                            default:
                                break;
                        }
                    }
                    // decode the last four bytes of the HRM format, the first byte of this message is the channel number
                    this.DecodeDefaultHRM(deviceId, data.slice(Messages.BUFFER_INDEX_MSG_DATA + 4));
                    this.oldPage = page;
                    break;
                default:
                    break;
            }
        };
        HeartRateScanner.prototype.DecodeDefaultHRM = function (deviceId, pucPayload) {
            // decode the measurement time data (two bytes)
            this.states[deviceId].BeatTime = pucPayload.readUInt16LE(0);
            // decode the measurement count data
            this.states[deviceId].BeatCount = pucPayload.readUInt8(2);
            // decode the measurement count data
            this.states[deviceId].ComputedHeartRate = pucPayload.readUInt8(3);
            this.emit('hbdata', this.states[deviceId]);
        };
        HeartRateScanner.deviceType = 120;
        HeartRateScanner.TOGGLE_MASK = 0x80;
        return HeartRateScanner;
    }(Ant.AntPlusScanner));
    exports.HeartRateScanner = HeartRateScanner;
<file_sep>/test/calculations/TrackParsingAndSmoothing_test.js
/**
* GPX track parsing and smoothing
* 	Tests the gpx file parser and smoothing algorithm
*   Needs the original example/testing track gpx file (track segment length is fixed for testing)
*   Should result in a console output either showing an error or the new smoothed segments
*
* @author <NAME>
* @version 1.0
*/

var Settings = require('../../settings.js');
var parseString = require('xml2js').parseString;
var gpxParser = require('../../gpx/parseGPX.js');
var files = require('../../files.js');


var readGPX = function (fileContent) 
{
    parseString(fileContent, function (err, result) 
    {
        var segments = gpxParser.parse(result.gpx.trk[0].trkseg[0].trkpt);
        if (segments.length >= 1003) // original length of Example/Testing track
        {
            console.log("Error track segments not smoothed.");
            return;
        }
        segments.forEach(function(element) 
        {            
            if (element)
            {                
                console.log(element);             
            } else 
            {
                console.log("Error: ", element);
            }
        }, this); 
    });		
    
}

files.ReadFileContent({ file : "Example_Testing Track.gpx"}, readGPX);<file_sep>/postcycla.js
/**
* PostCycla
* 	Will create a web server/sockets to provide & handle client (html) requests
*   
* @author <NAME>
* @version 1.0
*/

var Settings = require('./settings.js'); // this "First"
var app = require('express')();
var http = require('http').Server(app);
var path = require('path');
var drive = require('./drive.js');
var files = require('./files.js');
var States = require("./serviceState.js");
var io = require('socket.io')(http);

drive.Init();

// setting web server path for client files
app.use('/', require('express').static(path.join(__dirname, 'newclient'))); 

//*** WebSocket request handling *//*

States.SetStateCallback(function(state)
{
    io.local.emit("GetState", state);
})

// receiving from client
io.on('connection', function(socket)
{
    
    socket.on('CancelRide', function(data)
	{
        drive.CancelRide();
    });
    socket.on("StartRide", function(data)
    {   
        drive.StartRide();    
    });
    socket.on("StopRide", function(data)
    {   
        drive.StopRide()
    });
    socket.on("GetState", function(data)
    {
        io.local.emit("GetState", States.GetCurrentState());
    });
    socket.on("GetFiles", function()
    {
        files.GetFiles(function(data, err)
        {
            if (err)
            {
                io.local.emit("Error", err);
            } else 
            {
                io.local.emit("GetFiles", data);                    
            }
        });
    });

    socket.on("GetMountain", function()
    {
        io.local.emit("GetMountain", drive.GetMountain());                            
    });

    
    socket.on("LoadTrack",function(data)
    { 
        files.ReadFileContent(data, drive.readGPX); // call file Read and parse callback
    });

    socket.on("GetOptions", function()
    { 
        io.local.emit("GetOptions", Settings.Data);        
    });

    socket.on("SaveOptions", function(req, resp)
    { 
        try
        {
            if (req)
            {
                Settings.Data.BikeWeightKG = req.BikeWeightKG;
                Settings.Data.MinimumJoulperMeter = req.MinimumJoulperMeter;
                Settings.Data.RiderWeightKG = req.RiderWeightKG;
                Settings.Data.Smooth = req.Smooth;
                Settings.Save();
                io.local.emit("SaveOptions", "OK");
            }
        }
        catch(e)
        {
            console.log("Error save Options:",e)
        }
        
    });

    socket.on("GetPowerSettings", function()
    { 
        io.local.emit("GetPowerSettings", Settings.Data.PowerCalc);        
    });

    socket.on("UpdatePowerSettings", function(req, resp)
    { 
        try
        {
            if (req)
            {
                Settings.Data.PowerCalc.FrontArea = req.FrontArea;
                Settings.Data.PowerCalc.DragCoeff = req.DragCoeff;
                Settings.Data.PowerCalc.DriveDrainLoss = req.DriveDrainLoss;
                Settings.Data.PowerCalc.RollingResistance = req.RollingResistance;
                Settings.Data.PowerCalc.PowerOffset = req.PowerOffset;
                Settings.Save();
                io.local.emit("UpdatePowerSettings", "OK");
            }
        }
        catch(e)
        {
            console.log("Error save Options:",e)
        }
        
    });

    if (Settings.Data.DebugOutput)
    {
        console.log('a user connected');
    }
});

drive.SetPosUpdateCallback(function(PosData)
{
    io.local.emit("PosUpdate", PosData);
});

// WebSocket handling */


//*** WebServer request handling (REST) *//*

app.get("/GetState", function(req, resp)
{
    resp.send(States.GetCurrentState());
});

app.get("/LoadFileList", function(req, resp)
{
    resp.send(files.LoadFileList());
});
app.get("/GetFileList",  function(req, resp)
{
    resp.send(files.GetFileList());
});

app.get("/LoadTrack", function(req, resp)
{ 
    resp.send(files.ReadFileContent(req.query, drive.readGPX)); // call file Read and parse callback
});

app.get("/GetOptions", function(req, resp)
{ 
    resp.send(Settings.Data);
});

app.get("/SaveOptions", function(req, resp)
{ 
    if (req.query)
    {
        Settings.Data.BikeWeightKG = req.query.BikeWeightKG;
        Settings.Data.MinimumJoulperMeter = req.query.MinimumJoulperMeter;
        Settings.Data.RiderWeightKG = req.query.RiderWeightKG;
        Settings.Data.Smooth = req.query.Smooth;
        Settings.Save();
    }
    resp.send("OK");
});

app.get("/GetSettings", function(req, resp)
{
    resp.send(Settings.Data);
});
app.get("/CancelRide", function(req, resp)
{
    resp.send(drive.CancelRide());
});
app.get("/StartRide", function(req, resp)
{
    resp.send(drive.StartRide());
});
app.get("/StopRide", function(req, resp)
{
    resp.send(drive.StopRide());
});
app.get("/GetPos", function(req, resp)
{
    resp.send(drive.GetPositionData());
});
app.get("/GetMountain", function(req, resp)
{
    resp.send(drive.GetMountain());
});

// REST handling */

http.listen(Settings.Data.HttpPort);
console.log('Server running at http://localhost:'+Settings.Data.HttpPort+'/');
<file_sep>/settings.js
/**
* Settings module
* 	Will automatically load settings stored inside the settings.json file
*
*   Settings json Example:
*
*{
*	"UseMockups": true,                 => if "true" PostCycla will run without the need of sensors (FOR TESTING)
*	"DebugOutput": false,               => will show more information during run in the console output
*   "PowerCalc":
*	{
*      "MinimumJoulperMeter": 16,	        => minimum joul/watt per meter (for going downhill....)    
*	    "BikeWeightKG" : 10,                => weight of bike and equipment (needed for power calculation)
*	    "RiderWeightKG" : 95,               => weight of yourself (needed for power calculation)
*       "FrontArea" :  0.55,                => frontal area, rider+bike (m^2) (original 0,509)
*       "DragCoeff" : 0.7,                  => drag coefficient Cd (original 0,63)
*       "DriveDrainLoss" : 4,               => drivetrain loss Loss_dt (original 3%)
*		"RollingResistance" : 0.005,        => coefficient of rolling resistance Crr
*       "PowerOffset" : 1.1                 => used to calculate power offset for fine tuning 1 = 100% (used in PowerCalcPostCycla.js)
*   }
*	"WebSocketPort" : 9998,             => port for the web socket (needed for client communication)
*	"HttpPort" : 8125,                  => port for the web server (needed for the client )
*	"GPXDir": "./tracks",               => path to the directory where gpx files will be read and written
*	"Smooth": 0.2,                      => parameter for gpx parsing and smoothing algorithm
*	
*	"Sensors": 
*	{
*		"HeartRate_AntId" : 30250,      => heart rate sensor Ant+ id (needed for Ant+ and pairing)
*		"Cadence_AntId": 59056,         => cadence sensor Ant+ id (needed for Ant+ and pairing)
*		"STM":
*		{
*			"ComPort" : "/dev/ttyACM0", => serial port for connection with the STM sensor
*			"ServoOffset":              => offset for min/max travel of the servo (will normally go from 0..1)
*			{ 
*				"Min" : 0.2,
*				"Max" : 0.9
*			} 
*		}
*	} 
*}  
*
* For further information about this data check the related modules.
*
* @author <NAME>
* @version 1.0
*/

var fs = require('fs');
var settingsfile = "./settings.json";

/**
 * Data
 *  data stored inside the settingsfile
 */
exports.Data = null;

/**
 * Load
 *  Loading settings form the "settingsfile"
 */
exports.Load = function()
{
 	exports.Data = JSON.parse(fs.readFileSync(settingsfile).toString());   
}

/**
 * Rest
 *  Reload/Reset settings form the "settingsfile"
 */
exports.Reset = function()
{
    exports.Data = JSON.parse(fs.readFileSync(settingsfile).toString()); 
}

/**
 * Save
 *  Save settings to the "settingsfile"
 */
exports.Save = function()
{
    try
    {
        fs.writefileSync(settingsfile, JSON.stringify(exports.Data)); 
    } catch(e) 
    {}
}

exports.Load(); // Load settings automatically<file_sep>/util/calc.js
// Math ceiling function with the default for of 2 digits
exports.ceil = function(f, digits)
{
	
	digits = typeof digits  !== 'undefined' ? digits : 2;
	
	var div = Math.pow(10 ,digits);
	if (f > 0)
	{        
		f = Math.ceil(f * div) /div;
	} else 
	{
		f = Math.floor(f *div) /div;
	}
	return parseFloat(f.toFixed(digits));
}

// Math floor function with default the set for 2 digits
exports.floor = function(f, digits)
{
	digits = typeof digits  !== 'undefined' ? digits : 2;
	var div = Math.pow(10 ,digits);
	if (f > 0)
	{
		f = Math.floor(f *div) /div;
	} else 
	{
		f = Math.ceil(f *div) /div;
	}
	return parseFloat(f.toFixed(digits));
}<file_sep>/drive.js
/**
* Drive module
* 	Reads and writes GPX data
*   Loads the GPX file and uses sensor data to calculate 
*		- Position
*		- Power/Resistance
*		- Data for UI & file output
*
* @author <NAME>
* @version 1.0
*/
// --- required modules
var fs = require('fs');
var path = require('path');
var parseString = require('xml2js').parseString;
var calc = require('./util/calc.js');
var sensors = require('./sensors.js');
var gpxWriter = require('./gpx/GPXWriter.js');
var gpxParser = require('./gpx/parseGPX.js');
var latlon = require('./gps/latlon.js');
var Settings = require('./settings.js');
var PowerCalculator = require('./power/PowerCalcPostCycla.js'); // Implement equipment calculations here
var States = require("./serviceState.js");


// --- Fields
var sensorTimer;
var PowerCalc;
var Data = 
{
	elediv: 0,
	recordCnt: 0,
 	segments: new Array(),
	odo : 0,
	lastSegIdx : 0,
	lastWrittenSeg : 0,
	writeDir: "",
}
var PosData = 
{ 
	pos:
		{
			lat: 0, 
			lng: 0, 
			slope: 0,
			ele: 0
		},
	total: 0,
	power : 
	{
		watt: 0,
		resPerc: 0,
		resistance: 0
	},
	odo: 0,
	starttime: 0,
	time: 0,
	bike: 
		{
			speed: 0,
			cad: 0,
			pulse: 0
		}
};

// --- exports

exports.State

var PosUpdateCallback = null;
/**
 * Initialization
 *  Starts PowerCalculator & sets output directory form the saved settings
 */
exports.Init = function()
{
	PowerCalc = new PowerCalculator();
	Data.writeDir = Settings.Data.GPXDir;
}

exports.SetPosUpdateCallback = function(callback)
{
	PosUpdateCallback = callback;
}

/**
 * readGPX
 *  parses the gpx file content and starts sensors for ride
 */
exports.readGPX = function (fileContent) 
{
	Data.odo  = 0;
	Data.lastSegIdx = 0;
	Data.lastWrittenSeg = 0;	

	parseString(fileContent, function (err, result) 
	{
		Data.segments = gpxParser.parse(result.gpx.trk[0].trkseg[0].trkpt);

		States.SetCurrentState(States.States.TrackLoaded);
		States.SetCurrentState(States.States.StartingSensors);
		
		sensors.SetSensorDataCallback(sensorReader);
		sensors.Start(function()
		{	
			States.SetCurrentState(States.States.ReadyForRide);			
		});	
	});		

	
	
}

/**
 * GetPositionData
 *  returns the current position and all data for it
 */
exports.GetPositionData = function()
{
	return PosData;
}

/**
 * StartRide
 *  sets the state accordingly
 *  creates a new gpx file
 * 	sets start time
 *  creates a timer to update ride calculations every second
 */
exports.StartRide = function()
{
	States.SetCurrentState(States.States.RecordingTrack);
	gpxWriter.NewFile(Data.writeDir);
	PosData.starttime = (new Date()).getTime();
	sensorTimer = setInterval(function ()
	{
		rideinterval();
	}, 1000);	
	return "OK";
}

/**
 * StopRide
 *  stops all sensors
 *  closes gpx file
 *  stops ride calculation timer
 *  updates state
 */
exports.StopRide = function(callback)
{
	sensors.Stop();
	gpxWriter.EndFile();
	clearInterval(sensorTimer);	
	States.SetCurrentState(States.States.RideStopped);
	return "OK";
}

/**
 * CancelRide
 *  simply stops sensors and sets state
 */
exports.CancelRide = function()
{
	sensors.Stop();
	States.SetCurrentState(States.States.Ready);
	return "OK";
}

// updates the gpx file with new position data
var sensorReader = function(data)
{
	checkSensors(data);
	Data.recordCnt = ((Data.recordCnt + 1) % 2);
	if ((PosData.bike.speed > 0) && (Data.recordCnt == 1))
	{		
		gpxWriter.AddTrackPoint(PosData);
	} 
	
	// if we "overshoot" end the gpx file and ride
	if (Data.lastSegIdx > Data.segments.length)
	{
		StopRide();
	}
}

// updates the resistance during ride
// everytime the resistance is changed we also will get sensor data... see: sensors.js
var rideinterval = function()
{
	sensors.SetServoPos(PosData.power.resistance);
}



// --- Postion Ride and Sensor Calculations ---------------
var calcNewPoint = function(dist, lat1, lng1, lat2, lng2)
{
	var pos1 = latlon(lat1, lng1);
	var pos2 = latlon(lat2, lng2);
	var bearing = pos1.bearingTo(pos2);
	var newPos = pos1.destinationPoint(dist, bearing);
	return { lat: newPos.lat, lng: newPos.lon }
}

var RecalcPos = function()
{
	var seg = getSegment();	
	if (seg.distDriven > seg.dist)
	{
		PosData.pos.lat = seg.end.lat
		PosData.pos.lng = seg.end.lng;
	}
	{
		var newPos = calcNewPoint(seg.distDriven, seg.start.lat, seg.start.lng, seg.end.lat, seg.end.lng)
		PosData.pos.lat = newPos.lat;
		PosData.pos.lng = newPos.lng;  
	}
	if (PosUpdateCallback)
	{
		PosUpdateCallback(PosData);
	}
}

// uses sensor data to calculate new position and resistance
var checkSensors = function(sensorData)
{
	PosData.bike.speed = calc.floor(sensorData.mps * 3.6, 2); // convert mps to kph
	PosData.bike.pulse = sensorData.hr;
	PosData.bike.cad = Math.floor(sensorData.cad);
	var DistDriven = sensorData.dist;
	
	var segment = getSegment(); // gets the current track segment we are in (inbetween two trackpoints)
	if (segment)
	{	
		// update current time/data (for gpx file)
		PosData.time = new Date(); 
		PosData.pos.ele = segment.ele;
		PosData.pos.lat = segment.lat;
		PosData.pos.lng = segment.lng;
		PosData.pos.slope = segment.slope;
		
		// calculates watts and resistance from speed slope and weight
		PowerCalc.CalcPowerData(sensorData.mps, PosData.pos.slope);
		// these are the watts we are able to generate with the trainer /  the Watts we need to perform
		PosData.power.watt = PowerCalc.PowerData.RealWatts+"/" +PowerCalc.PowerData.VirtualWatts; 
		
		// We only slowly in/decrease resistance (like in reality)
		var res = PowerCalc.PowerData.Resistance - PosData.power.resistance;
		if (Math.abs(res) > 0.1) 
		{
			res = 0.1 *(Math.abs(res)/res);
		}

		// if the "virtual"/needed watts are more then we are currently able to produce via the trainer we cut it from the distance
		if (PowerCalc.PowerData.RealWatts < PowerCalc.PowerData.VirtualWatts)		
		{
			DistDriven = (DistDriven / PowerCalc.PowerData.VirtualWatts) * PowerCalc.PowerData.RealWatts;
		}
		segment.distDriven += DistDriven // we add the driven distance to the current segment data

		// setting the "virtual data"
		PosData.power.resistance +=res;		
		PosData.power.resPerc = calc.floor(PosData.power.resistance * 100, 0);				
		PosData.bike.virtualSpeed = calc.ceil(DistDriven * 3.6, 2);
		
		// storing currently overall performance
		PosData.LastDist = DistDriven;
		Data.odo += DistDriven;
		PosData.total = calc.floor(Data.segments.Odo/1000, 2);
		PosData.odo = calc.floor(Data.odo/1000, 3);
		// calculating current elevation from startpoint slope and driven distance
		// (as we are between two trackpoints)
		PosData.pos.ele = calc.floor(PosData.pos.ele + (PosData.pos.slope * (segment.end.ODO - Data.odo)/-10),1);
		// same here for new trackpoints
		RecalcPos(segment);
	}

};

// will search for the current segment (between two track points) we are in
// needed to calculate resistance
var getSegment = function()
{
	if (Data.lastSegIdx < Data.segments.length) // check if we drove over the last segment
	{
		var tmpIdx = Data.lastSegIdx;
		var seg = Data.segments[tmpIdx];
		// search for the new segment by checking if we allready drove the distance needed to successfully finish this segment
		while (tmpIdx < Data.segments.length &&  seg.distDriven >= seg.dist)
		{			
			tmpIdx++;			
			if (tmpIdx < Data.segments.length)
			{
				// if we finished the semgent we use the "leftover" distance to use for the new segment
				var diff = seg.distDriven - seg.dist;
				seg = Data.segments[tmpIdx];
				seg.distDriven = diff;
			}
		}
		// simple check if the new found segment is the last
		if (tmpIdx >= 0)
		{
			Data.lastSegIdx = tmpIdx;
			if (Data.lastSegIdx > Data.segments.length)
			{
				Data.lastSegIdx = Data.segments.length -1;
			}
		}
	}
	return Data.segments[Data.lastSegIdx];
}


// --- Mountain SVG Data ------

exports.GetMountain = function()
{
	return SvgMountain();
}


// we create a profile of the elevation we are about to drive
var SvgMountain = function()
{	
	var path = "";
	var size = { width: 1200, height: 150 }
	var xratio = Math.ceil(Data.segments.Odo /size.width);
	var height = Math.ceil(Data.segments.MaxEle-Data.segments.MinEle);
	var yratio = Math.ceil(height / size.height);
	var lastx = 0;
	var yZero = height / yratio;
	for (var i = 0; i < Data.segments.length; i++)
	{
		var seg = Data.segments[i];	
		var newx = Math.floor(seg.end.ODO / xratio);	
		if (newx > lastx)
		{
			lastx = newx;				
			path += " L "+newx+" "+calc.ceil((Data.segments.MaxEle - seg.ele) /yratio, 2);
		}		
	}
	var tmp = '<svg id="MountainSVG" internalwidth='+size.width+' width="'+size.width+'" viewBox="0 0 '+size.width+' '+size.height+'" xmlns="http://www.w3.org/2000/svg" version="1.1">';	
	tmp += '<path d="M 0 '+yZero;
	tmp += path+'L '+size.width+' '+yZero+'" fill="gray" />';
	tmp += '<rect id="svgBikePos" x="0" y="0" width="0.1" height="'+yZero+'" stroke="red" stroke-width="0.5"/>'
	tmp +='</svg>';
	return tmp;
}

<file_sep>/test/hardware/test_serial.py
#Fallback test for serial communication

import serial #test as serial
import time
import sys

def writeSerial(data):		
	port.write(data)
	port.write("\n")	
	rcv = port.readline()
	#bytesToRead = port.inWaiting()
	#rcv = port.read(bytesToRead)
	print rcv

comport = "/dev/ttyACM0"

port = serial.Serial(comport, baudrate=9600, timeout=3.0)
port.close()
port.open()
sin = raw_input("Pos: ")
while not (sin == "exit"):
	print(sin)
	writeSerial(sin)
	sin = raw_input("Pos: ")
	
port.close()
	
<file_sep>/power/WattCalculator.js
/*
 * Cycling power vs. velocity, based on a physical model.
 *
 * Copyright 2012 by <NAME> (gribble [at] gmail (dot) com)
 * http://www.gribble.org/
 * 
 * Many thanks again for allowing me to use your code!
 */
var Settings = require('../settings.js');

var WattCalculator = function()
{
    this.Params = {
        WeightRider : 75,           // weight of rider (kg)
        WeightEquipment : 8,        // weight of bike (kg)
        FrontArea : 0.55,           // frontal area, rider+bike (m^2) (original 0,509)
        DragCoeff : 0.7,            // drag coefficient Cd (original 0,63)
        DriveDrainLoss : 4,         // drivetrain loss Loss_dt (original 3%)
        RollingResistance : 0.005,  // coefficient of rolling resistance Crr
        AirDensity : 1.226,         // air density (kg / m^3)
        Slope : 0,                  // grade of hill (%)        
        Speed : 0                   // speed in km/h
    }; 
}


/**
 * CalcPower
 *  uses the speed, slope and formula from Steve Gribble to calculate the power output needed at the wheels
 */
WattCalculator.prototype.CalcPower = function(speed, slope)
{
    this.Params.WeightRider = Settings.Data.PowerCalc.RiderWeightKG;
    this.Params.WeightEquipment = Settings.Data.PowerCalc.BikeWeightKG;
    this.Params.FrontArea = Settings.Data.PowerCalc.FrontArea;
    this.Params.DragCoeff = Settings.Data.PowerCalc.DragCoeff;
    this.Params.DriveDrainLoss = Settings.Data.PowerCalc.DriveDrainLoss;
    this.Params.RollingResistance = Settings.Data.PowerCalc.RollingResistance;    
    this.Params.Slope = slope;
    this.Params.Speed = speed;
    
    return CalculatePower(this.Params).Wheel;
}
// Calculates and returns the force components needed to achieve the
// given velocity.  <params> is a dictionary containing the rider and
// environmental parameters, as returned by ScrapeParameters(), i.e.,
// all in metric units.  'velocity' is in km/h.
function CalculateForces(params) 
{
    // calculate Fgravity
    var Fgravity = 9.8067 *
        (params.WeightRider + params.WeightEquipment) *
        Math.sin(Math.atan(params.Slope / 100.0));

    // calculate Frolling
    var Frolling = 9.8067 *
        (params.WeightRider + params.WeightEquipment) *
        Math.cos(Math.atan(params.Slope / 100.0)) *
        (params.RollingResistance);

    // calculate Fdrag
    var Fdrag = 0.5 *
        (params.FrontArea) *
        (params.DragCoeff) *
        (params.AirDensity) *
        //(params.Speed) *// * 1000.0 / 3600.0) // check if speed squared is needed (air drag)!!!
        (params.Speed);// * 1000.0 / 3600.0);

    // cons up and return the force components
    var ret = { };
    ret.Fgravity = Fgravity;
    ret.Frolling = Frolling;
    ret.Fdrag = Fdrag;    
    return ret;
}

// Calculates and returns the power needed to achieve the given
// velocity.  <params> is a dictionary containing the rider and
// environmenetal parameters, as returned by ScrapeParameters(), i.e.,
// all in metric units.  'velocity' is in km/h.  Returns power in
// watts.
function CalculatePower(params) 
{
    // calculate the forces on the rider.
    var forces = CalculateForces(params);
    var totalforce = forces.Fgravity + forces.Frolling + forces.Fdrag;

    // calculate necessary wheelpower
    var wheelpower = totalforce * params.Speed;// * 1000.0 / 3600.0);

    // calculate necessary legpower
    var legpower = wheelpower / (1.0 - (params.DriveDrainLoss/100.0));

    return { Leg: legpower, Wheel: wheelpower};
}

module.exports = WattCalculator;<file_sep>/power/PowerCalcPostCycla.js
/**
* Power Calculator for PostCycla equipment
*	uses speed, slope, weight,.... to calculate the power/resistance needed to perform
*
*	Calculates:
*		Watts from speed, slope and weight in kg
*		
*		Resistance level from Watts and speed
*			resistance shall be in percentage from max resistance of the equipment 
*			Values: 0.0 - 1.0
*		
*		Raw_Resistance
*			resistance in percentage from maxResistance of equipment 
*			(was implemented because maximum resistance may be exceeded)
*
*	Every fitness equipment needs to implement it's own Resistance level calculations
* 	
*	Will create a power data object:
*		RealWatts		=> the watts we are able to produce with the trainer (may be to small)
*       VirtualWatts	=> the watts needed to perform (may exceed the resistance the trainer can provide)
*       Resistance		=> the resistance for the trainer in percent (0..100%) this will be sent to the trainer
*		Raw_Resistance	=> the resistance needed to perform in percent (0..xxx% may be more then we can provide)
*
* @author <NAME>
* @version 1.0
*/

var Settings = require('../settings.js');
var calc = require('../util/calc.js');
var WattsCalc = require('./WattCalculator.js');

// 160 Watts are needed when driving a -1% hill with 10 m/s
// calculated via https://www.gribble.org/cycling/power_v_speed.html
var MinimumJoulperMeter = 16; 

/**
 * PowerCalcPostCycla
 *  Uses the WattCalculator and a minimum setting to calculate power/resistance needed
 */
var PowerCalcPostCycla = function()
{
	this.Watts = new WattsCalc();
	this.PowerData = 
    {
		RealWatts: 0,
        VirtualWatts : 0,
        Resistance : 0,
		Raw_Resistance :0
    };    
}

/**
 * PowerCalcPostCycla
 *  Uses the WattCalculator and a minimum setting to calculate power/resistance needed
 */
PowerCalcPostCycla.prototype.CalcPowerData = function(ms, slope)
{
    this.CalcWatts(ms, slope);
    this.CalcResistenceLevel(this.PowerData.VirtualWatts,ms);
    return this.PowerData;
};


PowerCalcPostCycla.prototype.CalcResistenceLevel = function(watt, ms)
{
	kmh = ms * 3.6
		
	// This formula derived from the resistance measuring for a TACX CYCLETRAINER T1810 
	// see: http://www.fahrrad.de/fahrradzubehoer/rollentrainer-tacx-elite/tacx-cycletrainer-speedmatic-t1810/7146.html
	// THIS NEEDS TO BE CALCULATED FOR EVERY TRAINER
	var level = (((watt)-(0.05569*kmh+1.223)*kmh)/(1.3811*kmh-5.0809));

	// multiply by 0,14285714285714285714285714285714 
	// since first calculations where done by tacx resistance (0-7) and we now want 0..100%
	this.PowerData.Raw_Resistance = calc.ceil(level* Settings.Data.PowerCalc.PowerOffset* 0.14285714285714285714285714285714, 2) ;

	// cutoff "overshot"
	if (level < 1)
	{
		level = 1;
	} else if (level > 7)
	{
		level = 7;
	}

	this.PowerData.Resistance = this.PowerData.Raw_Resistance;
	// since the formula used can produce values of more/less then 0% - 100% we "cut off" wrong values.  
	if (this.PowerData.Resistance > 1)
	{
		this.PowerData.Resistance = 1;
		this.PowerData.RealWatts = calc.floor((1.3811*kmh-5.0809)*level + (0.05569*kmh+1.2233)*kmh, 0);				
	} else if (this.PowerData.Resistance < 0)
	{
		this.PowerData.Resistance = 0;
		this.PowerData.RealWatts = calc.floor((1.3811*kmh-5.0809)*level + (0.05569*kmh+1.2233)*kmh, 0);
	} 

};

// Convert Speed and slope to Power :
// Calculations used from: https://www.gribble.org/cycling/power_v_speed.html
PowerCalcPostCycla.prototype.CalcWatts = function(ms, slope)
{	
	this.PowerData.VirtualWatts = calc.ceil(this.Watts.CalcPower(ms, slope),0);	
	this.PowerData.RealWatts = this.PowerData.VirtualWatts;
};


// export the class
module.exports = PowerCalcPostCycla;
<file_sep>/HW_Controller/PostCyclaHardwareMock/PostCyclaHardwareMock.ino
/**
* Hardware Mockup for the PostCycla stm32 sensor and server controller
*	Input:
*		any character and newline as delimiter 
*   (will only evaluate inputs a-z so we are able to use the keyboard for debugging)
*
*	Output:
*		(will be triggered every time an input was sent)
*		Will send a JSON string containing:
*		{ "distance" : xxx, "speedms" : xxx, "servoPos" : xxx, "charSent" : xxx }
*
*	distance => dinstance since the last time the sensor was read
*	speedms => 	current speed in meters per second
*	servoPos => current position of the servo setting the trainer resistence
*	sentChar => the servo position can be set from 0...100% and will be sent to the STM as an character followed by a newline
*				this sentChar was implemented for debugging and will show the last sent character (should be between 32 and 132)
*
* for Debugging will use the a-z input to convert them into 0..10 and use this as an output for distance, speed and servoPos
*
* @author <NAME>
* @version 1.0
*/


char lastReceivedChar;

// main setup will just initialize the serial port
void setup() 
{
  Serial.begin(9600);
}

// main loop will read all data from serial and send back an answer
void loop() 
{
  recvOneChar();
}

// checks the serial input for 
void recvOneChar() 
{
  if (Serial.available() > 0) // checks if anything can be read
  {
    char tmp  = Serial.read();     
    if (tmp == '\n') // checks for newline delimiter
    {      
      // convert input to value between 0..10
      float serverPos = (float(lastReceivedChar) - 97)* 0.4;
      if (serverPos > 10)
      {
         serverPos = 10;
      } else if (serverPos < 0)
      {
        serverPos = 0;
      }
      //rturn output
      Serial.print("{ \"distance\" :  ");      
      Serial.print(serverPos);
      Serial.print(", \"speedms\" : ");
      Serial.print(serverPos);
      Serial.print(", \"servoPos\" : ");
      Serial.print(serverPos);
      Serial.print(", \"sentChar\" : ");
      Serial.print(int(lastReceivedChar));
      Serial.println(" }");      
    } else 
    {
      lastReceivedChar = tmp; // if not newline
    }
  }
}
<file_sep>/README.md
# PostCycla
Is a DIY project to create a smart bike trainer with "virtal ride" capability, using a Raspberry Pi and Node.js for the server and a Web UI on the client side. A STM32 MCU Nucleo with a CNY 70 sensor and a servo will get data from the trainer and set the resistance.

For testing and development mockup implementations of all sensors and actuators are available at: [Running the tests](#running-the-tests)

To set up sensors and actuators you can use any microcontroller to read/control sensors and a servo.
Every component was encapsulated to allow easy swapping of sensors, controllers or provide another way of sensing distance/speed and setting resistance on the trainer.

Future modules/components will include a way to directly read data and set resistance of Ant+ FE-C capable trainers.


## Getting Started

### On Windows
* Clone the repository
* Install the [Node modules](#installing)
* [Start everything](#how-to-start) 

### On Raspberry
* Set up a Raspberry Pi with NodeJs.
* Clone the repository and copy it to the Raspberry
* Install the [Node modules](#installing)
* [Start everything](#how-to-start) 


## Prerequisites
### Hardware
* Raspberry Pi 3
* STM32 MCU Nucleo/Arduino Nano
* CNY 70
* Servo
* Ant+ USB Stick
* Ant+ heart rate monitor chest strap
* Ant+ candence sensor

(the current implementation uses this hardware setup)

### Software
* NodeJs 4.8.2 (on Raspberry Pi)

Node Modules:
* [usb](https://github.com/tessel/node-usb)
* [serialport](https://www.npmjs.com/package/serialport#module_serialport--SerialPort+event_error)
* [xml2js](https://github.com/isaacs/sax-js/)
* [socket.io](https://socket.io/get-started/chat/)
* [express](http://expressjs.com/de/)

A gpx file for testing/running the software. **The gpx file needs to contain elevation data!**
    
### Installing
**USB**
```
sudo apt-get install build-essential libudev-dev
npm install usb
```
**Serial port**
```
sudo npm install serialport --unsafe-perm --build-from-source
```
**XML Parser**
```
npm install xml2js
```
**Web sockets**
```
npm install --save socket.io
```
**Express**
```
npm install express --save
```
## Running the tests

To test PostCycla itself you can set the "UseMockups" and "DebugOutput" flags in the "settings.json" file and [start the program](#running-the-program).
```
...
	"UseMockups": true,    
	"DebugOutput": false,
...
```
In the "test" folder you will find test for the calculation and hardware components.

For hardware testing you can also use the [hardware mockup code](#arduino-hw-mockup).

**Calculations**

```
node test/calculations/PowerCalc_test.js
```
Tests the power calculation with a fixed speed and changing slope.
* Should provide a console output of the used speed, slope and calculated power data

```
node test/calculations/TrackParsingAndSmoothing_test.js
```
Tests the gpx file parser and smoothing algorithm
* Needs the original example/testing track gpx file (track segment length is fixed for testing)
* Should result in a console output either showing an error or the new smoothed segments

**Hardware**
```
node test/hardware/testConnections.js
```
Tests the Serial, USB and Ant+ hardware and connection to it
* Will try to find USB devices and the Ant+ Stick
* Will also try to connect to the SerialPort (ttyACM0 or COM3)
* Should return a console ouput regarding Testname, Success, and data/info about success or failure

```
node test/hardware/testAntplusSensors.js
```
This test will try and get data from an Ant+ stick with connected heart rate and cadence sensors
*   Should return a console ouput with the heart rate and cadence (if sensors are connected to the Ant+ stick)

```
node test/hardware/testSTMSensor.js
```
This test will try and get data from the STM32/Arduino and check the output
*   Should return a console ouput with the data from the module

For hardware testing you can also use the [hardware mockup code](#arduino-hw-mockup).

## Running the program
### Setup
To set up the hardware use the code in the "HW_Controller" folder.
#### STM32 MCU Nucleo
Contains the code currently used in the project
```
HW_Controller/mbed/main.cpp
```
#### Arduino HW mockup
Contains code for testing the hardware communication modules
```
HW_Controller/PostCyclaHardwareMock/PostCyclaHardwareMock.ino
```
#### Without hardware
Change the "settings.json" file:
```
...
	"UseMockups": true,    
	"DebugOutput": true,
...
```
If these settings are used, you can run PostCycla without any hardware attached and get additional information in the Node.js console.

### How to start
```
sudo node postcycla.js
```
The web UI can be accessed via the address shown in the nodejs output.
Deault:
```
http://localhost:8125/
```
Simply open this address in your web browser to see the client UI.

* For testing an "Example_Testing Track.gpx" was added in the "tracks" folder.
* The new GPX will be generated in the "tracks" folder (will not be available in the UI)

**Note to finish/close the new GPX file you need to "STOP" the ride (even when you reached the end)**

## Built With

* [Node.JS](https://nodejs.org/) - 
* [Visual Studio Code](https://code.visualstudio.com/) - IDE

## Contributing

Everyone is welcome to help, add, improve, ... only one rule here: "Don't be a dick".

## Authors

* **<NAME>** - *Initial idea / hardware / code* - [HackaDay project](https://hackaday.io/project/18658-postcycla)

## License

This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.

## Acknowledgments

A big thanks to the following:

* [<NAME>](https://www.gribble.org/cycling/power_v_speed.html)  - providing the power calculations
* [<NAME>](mourner.github.io/simplify-js) - and his Simplify.js scripts
* [Movable Type Ltd](http://www.movable-type.co.uk) - for their gps calculation scripts
* [<NAME> and Contributors](https://github.com/isaacs/sax-js/) - xml parsing for the gpx files
* [OpenLayers Map Viewer Library](https://openlayers.org/) - providing a solid map library
* [<NAME>](https://github.com/Loghorn/ant-plus) - Ant+ node module
* [Nonolith Labs, LLC](https://github.com/tessel/node-usb) - USB library for Node.JS
* [<NAME>](https://github.com/node-serialport/node-serialport) - Node Serialport
* [Socket.io](https://socket.io) - Web sockets
* [Express](http://expressjs.com/de/) - Web server<file_sep>/stm/newstm.js
/**
* STM32 sensor module
* 	This module connects via serial port to a connected STM32 board to read sensor data and set the servo position
*   The STM32 should return the following data:
*		{ "distance" : 0, "speedms" : 0, "servoPos" : 0, "sentChar" : 0 }\n
*
*	distance => dinstance since the last time the sensor was read
*	speedms => 	current speed in meters per second
*	servoPos => current position of the servo setting the trainer resistence
*	sentChar => the servo position can be set from 0...100% and will be sent to the STM as an character followed by a newline
*				this sentChar was implemented for debugging and will show the last sent character (should be between 32 and 132)
*
*	Every sent or received data should be delimited with a newline
*
*	The current hardware consists of a STM32 board with connected CNY70 reflex sensor to sense rotational speed of the trainers roller and a servo to move the bowden cable to set the resistance.
*
* @author <NAME>
* @version 1.0
*/
var Settings = require('../settings.js');
var SerialPort = require('serialport');
var events = require('events');
var calc = require('../util/calc.js');
var SerialPort = require('serialport');
var port = null;
const Readline = SerialPort.parsers.Readline; // newline delimiter parser for the serialport
const parser = new Readline();


var ServoOffset = { Min : 0.2, Max: 0.9}; // max travel of the servo,... needs to be checked and set for every hardware


/**
 * Constuctor
 *  Creates a new STM instance connecting to the set serial port
 */
var STM = function(comPort)
{
	events.EventEmitter.call(this);
	var me = this;
	port = new SerialPort(comPort, { baudRate: 9600, autoOpen: false });

	this.SensorReady = function()
	{				
		this.emit('SensorReady');	
	}
	// gets every data received will parse it and trigger a "SensorData" event to forward the parsed data
	this.SensorData = function(strdata)
	{		
		try
		{
			var data = JSON.parse(strdata);
			// fallback since sometimes distance was emtpy !?? (needs to be checked on STM side)
			if (data && data.distance == 0 && data.speedms > 0)
			{
				data.distance = data.speedms;
			}
			this.emit('SensorData', data);
		} catch(e)
		{
			
		}
	}
	
	// Switches the port into "flowing mode"
	parser.on('data', function(data)
	{
		if (Settings.Data.DebugOutput)		
		{
			console.log('Data: ' + data);  
		}
		if (data.trim() != "")
        {		
			me.SensorData(data);	
		}
	});	
}

STM.prototype.__proto__ = events.EventEmitter.prototype;

/**
 * Init
 *  Initializes the module and opens the serial port
 */
STM.prototype.Init = function()
{
	ServoOffset = typeof Settings.Data.Sensors.ServoOffset  !== 'undefined' ? Settings.Data.Sensors.ServoOffset : ServoOffset;	
	ServoOffset.Delta = ServoOffset.Max - ServoOffset.Min;

	var me = this;
	port.pipe(parser);
	port.open(function (err) 
	{	
		if (err) 
		{
			return console.log('Error opening COM port: ', err.message);
		} 
		me.SensorReady();		
	});
}

/**
 * Connect
 *  Initializes the module and opens the serial port
 */
STM.prototype.Connect = function()
{	
	this.Init();	
}

/**
 * Disconnect
 *  closes the serial port
 */
STM.prototype.Disconnect = function()
{
	port.close();
}

/**
 * SetLevel
 *  Sets the new resistance level (between 0..1) and sends it to the STM32
 *  should trigger a response from the SMT32
 */
STM.prototype.SetLevel = function(pos)
{	
	// servo controller expects values from 32 to 132 (0..100%)
	pos = checkServoEndpointspos(pos);	
	var position = pos;
	pos = (pos *100) +32;		
	
	// every message has to be delimmited by an newline
	var send = String.fromCharCode(pos)+ '\n';
	if (Settings.Data.DebugOutput)
	{
		console.log("Char: %s Pos: %d  CHR: %d", send, position, send.charCodeAt(0));
	}
	port.write(send, 'ascii', function()
	{
		if (Settings.Data.DebugOutput)
		{
			console.log('data written');
		}
	});
}
 
// determined by enpoints of trainer bowden cable
var checkServoEndpointspos = function(pos)
{
	var offestPos = calc.ceil(pos /1 * ServoOffset.Delta);

	offestPos += ServoOffset.Min;
	if (offestPos > ServoOffset.Max)
	{
		offestPos = ServoOffset.Max;
	}
	if (offestPos < ServoOffset.Min)
	{
		offestPos = ServoOffset.Min;
	}
	return offestPos;
}


// export the class
module.exports = STM;