diff --git a/Versuch 2/aufgabe_a.py b/Versuchstag-1/aufgabe_a.py similarity index 100% rename from Versuch 2/aufgabe_a.py rename to Versuchstag-1/aufgabe_a.py diff --git a/Versuch 2/aufgabe_b.py b/Versuchstag-1/aufgabe_b.py similarity index 100% rename from Versuch 2/aufgabe_b.py rename to Versuchstag-1/aufgabe_b.py diff --git a/Versuch 2/aufgabe_c.py b/Versuchstag-1/aufgabe_c.py similarity index 100% rename from Versuch 2/aufgabe_c.py rename to Versuchstag-1/aufgabe_c.py diff --git a/Versuch 1/aufgabe_e.sh b/Versuchstag-1/aufgabe_e.sh similarity index 100% rename from Versuch 1/aufgabe_e.sh rename to Versuchstag-1/aufgabe_e.sh diff --git a/Versuch 1/aufgabe_f.sh b/Versuchstag-1/aufgabe_f.sh similarity index 100% rename from Versuch 1/aufgabe_f.sh rename to Versuchstag-1/aufgabe_f.sh diff --git a/Versuch 2/aufgabe_g.py b/Versuchstag-1/aufgabe_g.py similarity index 100% rename from Versuch 2/aufgabe_g.py rename to Versuchstag-1/aufgabe_g.py diff --git a/Versuch 2/servoblaster_ctl.py b/Versuchstag-1/servoblaster_ctl.py similarity index 100% rename from Versuch 2/servoblaster_ctl.py rename to Versuchstag-1/servoblaster_ctl.py diff --git a/Versuch 1/setup.sh b/Versuchstag-1/setup.sh similarity index 100% rename from Versuch 1/setup.sh rename to Versuchstag-1/setup.sh diff --git a/Versuch 3/Pipfile b/Versuchstag-2/Pipfile similarity index 100% rename from Versuch 3/Pipfile rename to Versuchstag-2/Pipfile diff --git a/Versuch 3/Pipfile.lock b/Versuchstag-2/Pipfile.lock similarity index 100% rename from Versuch 3/Pipfile.lock rename to Versuchstag-2/Pipfile.lock diff --git a/Versuch 3/gpio_class.py b/Versuchstag-2/gpio_class.py similarity index 100% rename from Versuch 3/gpio_class.py rename to Versuchstag-2/gpio_class.py diff --git a/Versuch 3/gpio_class.pyc b/Versuchstag-2/gpio_class.pyc similarity index 100% rename from Versuch 3/gpio_class.pyc rename to Versuchstag-2/gpio_class.pyc diff --git a/Versuch 3/iot_car.py b/Versuchstag-2/iot_car.py similarity index 100% rename from Versuch 3/iot_car.py rename to Versuchstag-2/iot_car.py diff --git a/Versuch 3/keyikt_main (copy).py b/Versuchstag-2/keyikt_main (copy).py similarity index 100% rename from Versuch 3/keyikt_main (copy).py rename to Versuchstag-2/keyikt_main (copy).py diff --git a/Versuch 3/keyikt_main.py b/Versuchstag-2/keyikt_main.py similarity index 100% rename from Versuch 3/keyikt_main.py rename to Versuchstag-2/keyikt_main.py diff --git a/Versuch 3/linuxWiimoteLib.py b/Versuchstag-2/linuxWiimoteLib.py similarity index 100% rename from Versuch 3/linuxWiimoteLib.py rename to Versuchstag-2/linuxWiimoteLib.py diff --git a/Versuch 3/servo_ctrl.py b/Versuchstag-2/servo_ctrl.py similarity index 100% rename from Versuch 3/servo_ctrl.py rename to Versuchstag-2/servo_ctrl.py diff --git a/Versuch 3/servo_ctrl.pyc b/Versuchstag-2/servo_ctrl.pyc similarity index 100% rename from Versuch 3/servo_ctrl.pyc rename to Versuchstag-2/servo_ctrl.pyc diff --git a/Versuch 3/wiikt_main.py b/Versuchstag-2/wiikt_main.py similarity index 100% rename from Versuch 3/wiikt_main.py rename to Versuchstag-2/wiikt_main.py diff --git a/Versuchstag-4/ikt_car_sensorik.py b/Versuchstag-4/ikt_car_sensorik.py new file mode 100644 index 0000000..20357c9 --- /dev/null +++ b/Versuchstag-4/ikt_car_sensorik.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +import os +from time import time, sleep +import threading +import RPi.GPIO as GPIO +import smbus + +GPIO.setmode(GPIO.BCM) + +# 1 indicates /dev/i2c-1 (port I2C1) +bus = smbus.SMBus(1) + +################################################################################# +# Sensors +################################################################################# + +################################################################################# +# Ultrasonic +################################################################################# + +class Ultrasonic(): + '''This class is responsible for handling i2c requests to an ultrasonic sensor''' + + def __init__(self,address): + self.address = address + + # Aufgabe 2 + # + # Diese Methode soll ein Datenbyte an den Ultraschallsensor senden um eine Messung zu starten + def write(self,value): + return 0 + + # Aufgabe 2 + # + # Diese Methode soll den Lichtwert auslesen und zurueckgeben. + def get_brightness(self): + return 0 + + # Aufgabe 2 + # + # Diese Methode soll die Entfernung auslesen und zurueckgeben. + def get_distance(self): + return 0 + + def get_address(self): + return self.address + +class UltrasonicThread(threading.Thread): + ''' Thread-class for holding ultrasonic data ''' + + # distance to obstacle in cm + distance = 0 + + # brightness value + brightness = 0 + + # Aufgabe 4 + # + # Hier muss der Thread initialisiert werden. + def __init__(self, address): + return 0 + + # Aufgabe 4 + # + # Schreiben Sie die Messwerte in die lokalen Variablen + def run(self): + while not self.stopped: + continue + + def stop(self): + self.stopped = True + +################################################################################# +# Compass +################################################################################# + +class Compass(object): + '''This class is responsible for handling i2c requests to a compass sensor''' + + def __init__(self,address): + self.address = address + + # Aufgabe 2 + # + # Diese Methode soll den Kompasswert auslesen und zurueckgeben. + def get_bearing(self): + return 0 + +class CompassThread(threading.Thread): + ''' Thread-class for holding compass data ''' + + # Compass bearing value + bearing = 0 + + # Aufgabe 4 + # + # Hier muss der Thread initialisiert werden. + def __init__(self, address): + return 0 + + # Aufgabe 4 + # + # Diese Methode soll den Kompasswert aktuell halten. + def run(self): + while not self.stopped: + continue + + def stop(self): + self.stopped = True + +################################################################################# +# Infrared +################################################################################# + +class Infrared(object): + '''This class is responsible for handling i2c requests to an infrared sensor''' + + def __init__(self,address): + self.address = address + + # Aufgabe 2 + # + # In dieser Methode soll der gemessene Spannungswert des Infrarotsensors ausgelesen werden. + def get_voltage(self): + return 0 + + # Aufgabe 3 + # + # Der Spannungswert soll in einen Distanzwert umgerechnet werden. + def get_distance(self): + return 0 + + +class InfraredThread(threading.Thread): + ''' Thread-class for holding Infrared data ''' + + # distance to an obstacle in cm + distance = 0 + + # length of parking space in cm + parking_space_length = 0 + + # Aufgabe 4 + # + # Hier muss der Thread initialisiert werden. + def __init__(self, address, encoder=None): + return 0 + + def run(self): + while not self.stopped: + read_infrared_value() + calculate_parking_space_length() + + # Aufgabe 4 + # + # Diese Methode soll den Infrarotwert aktuell halten + def read_infrared_value(self): + return 0 + + # Aufgabe 5 + # + # Hier soll die Berechnung der Laenge der Parkluecke definiert werden + def calculate_parking_space_length(self): + return 0 + + def stop(self): + self.stopped = True + +################################################################################# +# Encoder +################################################################################# + +class Encoder(object): + ''' This class is responsible for handling encoder data ''' + + # Aufgabe 2 + # + # Wieviel cm betraegt ein einzelner Encoder-Schritt? + STEP_LENGTH = 0 # in cm + + # number of encoder steps + count = 0 + + def __init__(self, pin): + self.pin = pin + + # Aufgabe 2 + # + # Jeder Flankenwechsel muss zur Berechnung der Entfernung gezaehlt werden. + # Definieren Sie alle dazu noetigen Methoden. + + # Aufgabe 2 + # + # Diese Methode soll die gesamte zurueckgelegte Distanz zurueckgeben. + def get_travelled_dist(self): + return 0 + +class EncoderThread(threading.Thread): + ''' Thread-class for holding speed and distance data of all encoders''' + + # current speed. + speed = 0 + + # currently traversed distance. + distance = 0 + + # Aufgabe 4 + # + # Hier muss der Thread initialisiert werden. + def __init__(self, encoder): + return 0 + + def run(self): + while not self.stopped: + get_values() + + # Aufgabe 4 + # + # Diese Methode soll die aktuelle Geschwindigkeit sowie die zurueckgelegte Distanz aktuell halten. + def get_values(self): + return 0 + + + def stop(self): + self.stopped = True + +################################################################################# +# Main Thread +################################################################################# + +if __name__ == "__main__": + + # The GPIO pin to which the encoder is connected + encoder_pin = 23 + + # Aufgabe 1 + # + # Tragen Sie die i2c Adressen der Sensoren hier ein + + # The i2c addresses of front and rear ultrasound sensors + ultrasonic_front_i2c_address = 0x00; + ultrasonic_rear_i2c_address = 0x00; + + # The i2c address of the compass sensor + compass_i2c_address = 0x00 + + # The i2c address of the infrared sensor + infrared_i2c_address = 0x00 + + # Aufgabe 6 + # + # Hier sollen saemtlichen Messwerte periodisch auf der Konsole ausgegeben werden. \ No newline at end of file diff --git a/Versuchstag-4/ikt_car_webserver.py b/Versuchstag-4/ikt_car_webserver.py new file mode 100644 index 0000000..f695562 --- /dev/null +++ b/Versuchstag-4/ikt_car_webserver.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +import tornado.httpserver +import tornado.ioloop +import tornado.options +import tornado.web +import tornado.websocket +import io +import json + +import threading +from ikt_car_sensorik import * +import _servo_ctrl +from math import acos, sqrt, degrees + + + +# Aufgabe 4 +# +# Der Tornado Webserver soll die Datei index.html am Port 8081 zur Verfügung stellen + +# Aufgabe 3 +# +# Der Tornado Webserver muss eine Liste der clients verwalten. +class WebSocketHandler(tornado.websocket.WebSocketHandler): + '''Definition der Operationen des WebSocket Servers''' + + print "hello WebSocketHandler" + + def open(self): + return 0 + + def on_message(self, message): + return 0 + + def on_close(self): + return 0 + + +class DataThread(threading.Thread): + '''Thread zum Senden der Zustandsdaten an alle Clients aus der Client-Liste''' + + # Aufgabe 3 + # + # Hier muss der Thread initialisiert werden. + def __init__(self): + return 0 + + # Aufgabe 3 + # + # Erstellen Sie hier Instanzen von Klassen aus dem ersten Teilversuch + def set_sensorik(self, address_ultrasonic_front, address_ultrasonic_back, address_compass, address_infrared, encoder_pin): + return 0 + + # Aufgabe 3 + # + # Hier muessen die Sensorwerte ausgelesen und an alle Clients des Webservers verschickt werden. + def run(self): + while not self.stopped: + continue + + def stop(self): + self.stopped = True + +class DrivingThread(threading.Thread): + '''Thread zum Fahren des Autos''' + + # Einparken + # + # Hier muss der Thread initialisiert werden. + def __init__(self): + return 0 + + # Einparken + # + # Definieren Sie einen Thread, der auf die ueber den Webserver erhaltenen Befehle reagiert und den Einparkprozess durchfuehrt + def run(self): + while not self.stopped: + continue + + def stop(self): + self.stopped = True + + +if __name__ == "__main__": + print "Main Thread started" + # Aufgabe 3 + # + # Erstellen und starten Sie hier eine Instanz des DataThread und starten Sie den Webserver . + + + # Einparken + # + # Erstellen und starten Sie hier eine Instanz des DrivingThread, um das Einparken zu ermoeglichen. + diff --git a/Versuchstag-4/index.html b/Versuchstag-4/index.html new file mode 100644 index 0000000..95b3854 --- /dev/null +++ b/Versuchstag-4/index.html @@ -0,0 +1,67 @@ + + + + +
+TimeSeries with optional data options.
+ *
+ * Options are of the form (defaults shown):
+ *
+ *
+ * {
+ * resetBounds: true, // enables/disables automatic scaling of the y-axis
+ * resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+ * }
+ *
+ *
+ * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries.
+ *
+ * @constructor
+ */
+ function TimeSeries(options) {
+ this.options = Util.extend({}, TimeSeries.defaultOptions, options);
+ this.data = [];
+ this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
+ this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
+ }
+
+ TimeSeries.defaultOptions = {
+ resetBoundsInterval: 3000,
+ resetBounds: true
+ };
+
+ /**
+ * Recalculate the min/max values for this TimeSeries object.
+ *
+ * This causes the graph to scale itself in the y-axis.
+ */
+ TimeSeries.prototype.resetBounds = function() {
+ if (this.data.length) {
+ // Walk through all data points, finding the min/max value
+ this.maxValue = this.data[0][1];
+ this.minValue = this.data[0][1];
+ for (var i = 1; i < this.data.length; i++) {
+ var value = this.data[i][1];
+ if (value > this.maxValue) {
+ this.maxValue = value;
+ }
+ if (value < this.minValue) {
+ this.minValue = value;
+ }
+ }
+ } else {
+ // No data exists, so set min/max to NaN
+ this.maxValue = Number.NaN;
+ this.minValue = Number.NaN;
+ }
+ };
+
+ /**
+ * Adds a new data point to the TimeSeries, preserving chronological order.
+ *
+ * @param timestamp the position, in time, of this data point
+ * @param value the value of this data point
+ * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls
+ * whether it is replaced, or the values summed (defaults to false.)
+ */
+ TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
+ // Rewind until we hit an older timestamp
+ var i = this.data.length - 1;
+ while (i > 0 && this.data[i][0] > timestamp) {
+ i--;
+ }
+
+ if (this.data.length > 0 && this.data[i][0] === timestamp) {
+ // Update existing values in the array
+ if (sumRepeatedTimeStampValues) {
+ // Sum this value into the existing 'bucket'
+ this.data[i][1] += value;
+ value = this.data[i][1];
+ } else {
+ // Replace the previous value
+ this.data[i][1] = value;
+ }
+ } else if (i < this.data.length - 1) {
+ // Splice into the correct position to keep timestamps in order
+ this.data.splice(i + 1, 0, [timestamp, value]);
+ } else {
+ // Add to the end of the array
+ this.data.push([timestamp, value]);
+ }
+
+ this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
+ this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
+ };
+
+ TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
+ // We must always keep one expired data point as we need this to draw the
+ // line that comes into the chart from the left, but any points prior to that can be removed.
+ var removeCount = 0;
+ while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
+ removeCount++;
+ }
+ if (removeCount !== 0) {
+ this.data.splice(0, removeCount);
+ }
+ };
+
+ /**
+ * Initialises a new SmoothieChart.
+ *
+ * Options are optional, and should be of the form below. Just specify the values you
+ * need and the rest will be given sensible defaults as shown:
+ *
+ *
+ * {
+ * minValue: undefined, // specify to clamp the lower y-axis to a given value
+ * maxValue: undefined, // specify to clamp the upper y-axis to a given value
+ * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+ * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; }
+ * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs
+ * millisPerPixel: 20, // sets the speed at which the chart pans by
+ * maxDataSetLength: 2,
+ * interpolation: 'bezier' // or 'linear'
+ * timestampFormatter: null, // Optional function to format time stamps for bottom of chart. You may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+ * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ],
+ * grid:
+ * {
+ * fillStyle: '#000000', // the background colour of the chart
+ * lineWidth: 1, // the pixel width of grid lines
+ * strokeStyle: '#777777', // colour of grid lines
+ * millisPerLine: 1000, // distance between vertical grid lines
+ * sharpLines: false, // controls whether grid lines are 1px sharp, or softened
+ * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines
+ * borderVisible: true // whether the grid lines trace the border of the chart or not
+ * },
+ * labels
+ * {
+ * disabled: false, // enables/disables labels showing the min/max values
+ * fillStyle: '#ffffff', // colour for text of labels,
+ * fontSize: 15,
+ * fontFamily: 'sans-serif',
+ * precision: 2
+ * },
+ * }
+ *
+ *
+ * @constructor
+ */
+ function SmoothieChart(options) {
+ this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options);
+ this.seriesSet = [];
+ this.currentValueRange = 1;
+ this.currentVisMinValue = 0;
+ this.lastRenderTimeMillis = 0;
+ }
+
+ SmoothieChart.defaultChartOptions = {
+ millisPerPixel: 20,
+ maxValueScale: 1,
+ interpolation: 'bezier',
+ scaleSmoothing: 0.125,
+ maxDataSetLength: 2,
+ grid: {
+ fillStyle: '#000000',
+ strokeStyle: '#777777',
+ lineWidth: 1,
+ sharpLines: false,
+ millisPerLine: 1000,
+ verticalSections: 2,
+ borderVisible: true
+ },
+ labels: {
+ fillStyle: '#ffffff',
+ disabled: false,
+ fontSize: 10,
+ fontFamily: 'monospace',
+ precision: 2
+ },
+ horizontalLines: []
+ };
+
+ // Based on http://inspirit.github.com/jsfeat/js/compatibility.js
+ SmoothieChart.AnimateCompatibility = (function() {
+ var requestAnimationFrame = function(callback, element) {
+ var requestAnimationFrame =
+ window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback) {
+ return window.setTimeout(function() {
+ callback(new Date().getTime());
+ }, 16);
+ };
+ return requestAnimationFrame.call(window, callback, element);
+ },
+ cancelAnimationFrame = function(id) {
+ var cancelAnimationFrame =
+ window.cancelAnimationFrame ||
+ function(id) {
+ clearTimeout(id);
+ };
+ return cancelAnimationFrame.call(window, id);
+ };
+
+ return {
+ requestAnimationFrame: requestAnimationFrame,
+ cancelAnimationFrame: cancelAnimationFrame
+ };
+ })();
+
+ SmoothieChart.defaultSeriesPresentationOptions = {
+ lineWidth: 1,
+ strokeStyle: '#ffffff'
+ };
+
+ /**
+ * Adds a TimeSeries to this chart, with optional presentation options.
+ *
+ * Presentation options should be of the form (defaults shown):
+ *
+ *
+ * {
+ * lineWidth: 1,
+ * strokeStyle: '#ffffff',
+ * fillStyle: undefined
+ * }
+ *
+ */
+ SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+ this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
+ if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
+ timeSeries.resetBoundsTimerId = setInterval(
+ function() {
+ timeSeries.resetBounds();
+ },
+ timeSeries.options.resetBoundsInterval
+ );
+ }
+ };
+
+ /**
+ * Removes the specified TimeSeries from the chart.
+ */
+ SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ this.seriesSet.splice(i, 1);
+ break;
+ }
+ }
+ // If a timer was operating for that timeseries, remove it
+ if (timeSeries.resetBoundsTimerId) {
+ // Stop resetting the bounds, if we were
+ clearInterval(timeSeries.resetBoundsTimerId);
+ }
+ };
+
+ /**
+ * Gets render options for the specified TimeSeries.
+ *
+ * As you may use a single TimeSeries in multiple charts with different formatting in each usage,
+ * these settings are stored in the chart.
+ */
+ SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ return this.seriesSet[i].options;
+ }
+ }
+ };
+
+ /**
+ * Brings the specified TimeSeries to the top of the chart. It will be rendered last.
+ */
+ SmoothieChart.prototype.bringToFront = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ var set = this.seriesSet.splice(i, 1);
+ this.seriesSet.push(set[0]);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay.
+ *
+ * @param canvas the target canvas element
+ * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
+ * from appearing on screen, with new values flashing into view, at the expense of some latency.
+ */
+ SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
+ this.canvas = canvas;
+ this.delay = delayMillis;
+ this.start();
+ };
+
+ /**
+ * Starts the animation of this chart.
+ */
+ SmoothieChart.prototype.start = function() {
+ if (this.frame) {
+ // We're already running, so just return
+ return;
+ }
+
+ // Renders a frame, and queues the next frame for later rendering
+ var animate = function() {
+ this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
+ this.render();
+ animate();
+ }.bind(this));
+ }.bind(this);
+
+ animate();
+ };
+
+ /**
+ * Stops the animation of this chart.
+ */
+ SmoothieChart.prototype.stop = function() {
+ if (this.frame) {
+ SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
+ delete this.frame;
+ }
+ };
+
+ SmoothieChart.prototype.updateValueRange = function() {
+ // Calculate the current scale of the chart, from all time series.
+ var chartOptions = this.options,
+ chartMaxValue = Number.NaN,
+ chartMinValue = Number.NaN;
+
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ // TODO(ndunn): We could calculate / track these values as they stream in.
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (!isNaN(timeSeries.maxValue)) {
+ chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
+ }
+
+ if (!isNaN(timeSeries.minValue)) {
+ chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
+ }
+ }
+
+ // Scale the chartMaxValue to add padding at the top if required
+ if (chartOptions.maxValue != null) {
+ chartMaxValue = chartOptions.maxValue;
+ } else {
+ chartMaxValue *= chartOptions.maxValueScale;
+ }
+
+ // Set the minimum if we've specified one
+ if (chartOptions.minValue != null) {
+ chartMinValue = chartOptions.minValue;
+ }
+
+ // If a custom range function is set, call it
+ if (this.options.yRangeFunction) {
+ var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
+ chartMinValue = range.min;
+ chartMaxValue = range.max;
+ }
+
+ if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
+ var targetValueRange = chartMaxValue - chartMinValue;
+ var valueRangeDiff = (targetValueRange - this.currentValueRange);
+ var minValueDiff = (chartMinValue - this.currentVisMinValue);
+ this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
+ this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
+ this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
+ }
+
+ this.valueRange = { min: chartMinValue, max: chartMaxValue };
+ };
+
+ SmoothieChart.prototype.render = function(canvas, time) {
+ var nowMillis = new Date().getTime();
+
+ if (!this.isAnimatingScale) {
+ // We're not animating. We can use the last render time and the scroll speed to work out whether
+ // we actually need to paint anything yet. If not, we can return immediately.
+
+ // Render at least every 1/6th of a second. The canvas may be resized, which there is
+ // no reliable way to detect.
+ var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
+
+ if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
+ return;
+ }
+ }
+ this.lastRenderTimeMillis = nowMillis;
+
+ canvas = canvas || this.canvas;
+ time = time || nowMillis - (this.delay || 0);
+
+ // Round time down to pixel granularity, so motion appears smoother.
+ time -= time % this.options.millisPerPixel;
+
+ var context = canvas.getContext('2d'),
+ chartOptions = this.options,
+ dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
+ // Calculate the threshold time for the oldest data points.
+ oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
+ valueToYPixel = function(value) {
+ var offset = value - this.currentVisMinValue;
+ return this.currentValueRange === 0
+ ? dimensions.height
+ : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
+ }.bind(this),
+ timeToXPixel = function(t) {
+ return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
+ };
+
+ this.updateValueRange();
+
+ context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
+
+ // Save the state of the canvas context, any transformations applied in this method
+ // will get removed from the stack at the end of this method when .restore() is called.
+ context.save();
+
+ // Move the origin.
+ context.translate(dimensions.left, dimensions.top);
+
+ // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+ // This prevents the occasional pixels from curves near the edges overrunning and creating
+ // screen cheese (that phrase should need no explanation).
+ context.beginPath();
+ context.rect(0, 0, dimensions.width, dimensions.height);
+ context.clip();
+
+ // Clear the working area.
+ context.save();
+ context.fillStyle = chartOptions.grid.fillStyle;
+ context.clearRect(0, 0, dimensions.width, dimensions.height);
+ context.fillRect(0, 0, dimensions.width, dimensions.height);
+ context.restore();
+
+ // Grid lines...
+ context.save();
+ context.lineWidth = chartOptions.grid.lineWidth;
+ context.strokeStyle = chartOptions.grid.strokeStyle;
+ // Vertical (time) dividers.
+ if (chartOptions.grid.millisPerLine > 0) {
+ var textUntilX = dimensions.width - context.measureText(minValueString).width + 4;
+ for (var t = time - (time % chartOptions.grid.millisPerLine);
+ t >= oldestValidTime;
+ t -= chartOptions.grid.millisPerLine) {
+ var gx = timeToXPixel(t);
+ if (chartOptions.grid.sharpLines) {
+ gx -= 0.5;
+ }
+ context.beginPath();
+ context.moveTo(gx, 0);
+ context.lineTo(gx, dimensions.height);
+ context.stroke();
+ context.closePath();
+
+ // Display timestamp at bottom of this line if requested, and it won't overlap
+ if (chartOptions.timestampFormatter && gx < textUntilX) {
+ // Formats the timestamp based on user specified formatting function
+ // SmoothieChart.timeFormatter function above is one such formatting option
+ var tx = new Date(t),
+ ts = chartOptions.timestampFormatter(tx),
+ tsWidth = context.measureText(ts).width;
+ textUntilX = gx - tsWidth - 2;
+ context.fillStyle = chartOptions.labels.fillStyle;
+ context.fillText(ts, gx - tsWidth, dimensions.height - 2);
+ }
+ }
+ }
+
+ // Horizontal (value) dividers.
+ for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
+ var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
+ if (chartOptions.grid.sharpLines) {
+ gy -= 0.5;
+ }
+ context.beginPath();
+ context.moveTo(0, gy);
+ context.lineTo(dimensions.width, gy);
+ context.stroke();
+ context.closePath();
+ }
+ // Bounding rectangle.
+ if (chartOptions.grid.borderVisible) {
+ context.beginPath();
+ context.strokeRect(0, 0, dimensions.width, dimensions.height);
+ context.closePath();
+ }
+ context.restore();
+
+ // Draw any horizontal lines...
+ if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
+ for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
+ var line = chartOptions.horizontalLines[hl],
+ hly = Math.round(valueToYPixel(line.value)) - 0.5;
+ context.strokeStyle = line.color || '#ffffff';
+ context.lineWidth = line.lineWidth || 1;
+ context.beginPath();
+ context.moveTo(0, hly);
+ context.lineTo(dimensions.width, hly);
+ context.stroke();
+ context.closePath();
+ }
+ }
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ context.save();
+ var timeSeries = this.seriesSet[d].timeSeries,
+ dataSet = timeSeries.data,
+ seriesOptions = this.seriesSet[d].options;
+
+ // Delete old data that's moved off the left of the chart.
+ timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
+
+ // Set style for this dataSet.
+ context.lineWidth = seriesOptions.lineWidth;
+ context.strokeStyle = seriesOptions.strokeStyle;
+ // Draw the line...
+ context.beginPath();
+ // Retain lastX, lastY for calculating the control points of bezier curves.
+ var firstX = 0, lastX = 0, lastY = 0;
+ for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
+ var x = timeToXPixel(dataSet[i][0]),
+ y = valueToYPixel(dataSet[i][1]);
+
+ if (i === 0) {
+ firstX = x;
+ context.moveTo(x, y);
+ } else {
+ switch (chartOptions.interpolation) {
+ case "linear":
+ case "line": {
+ context.lineTo(x,y);
+ break;
+ }
+ case "bezier":
+ default: {
+ // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
+ //
+ // Assuming A was the last point in the line plotted and B is the new point,
+ // we draw a curve with control points P and Q as below.
+ //
+ // A---P
+ // |
+ // |
+ // |
+ // Q---B
+ //
+ // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+ // so adjacent curves appear to flow as one.
+ //
+ context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+ Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+ Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+ x, y); // endPoint (B)
+ break;
+ }
+ case "step": {
+ context.lineTo(x,lastY);
+ context.lineTo(x,y);
+ break;
+ }
+ }
+ }
+
+ lastX = x; lastY = y;
+ }
+
+ if (dataSet.length > 1) {
+ if (seriesOptions.fillStyle) {
+ // Close up the fill region.
+ context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+ context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+ context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+ context.fillStyle = seriesOptions.fillStyle;
+ context.fill();
+ }
+
+ if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
+ context.stroke();
+ }
+ context.closePath();
+ }
+ context.restore();
+ }
+
+ // Draw the axis values on the chart.
+ if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
+ var maxValueString = parseFloat(this.valueRange.max).toFixed(chartOptions.labels.precision),
+ minValueString = parseFloat(this.valueRange.min).toFixed(chartOptions.labels.precision);
+ context.fillStyle = chartOptions.labels.fillStyle;
+ context.fillText(maxValueString, dimensions.width - context.measureText(maxValueString).width - 2, chartOptions.labels.fontSize);
+ context.fillText(minValueString, dimensions.width - context.measureText(minValueString).width - 2, dimensions.height - 2);
+ }
+
+ context.restore(); // See .save() above.
+ };
+
+ // Sample timestamp formatting function
+ SmoothieChart.timeFormatter = function(date) {
+ function pad2(number) { return (number < 10 ? '0' : '') + number }
+ return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
+ };
+
+ exports.TimeSeries = TimeSeries;
+ exports.SmoothieChart = SmoothieChart;
+
+})(typeof exports === 'undefined' ? this : exports);
+
+