Commit 78a7858e by artur.rachwal

Merge branch 'master' of gitlab.e1s.it:iot/pi.hub

parents aa535619 f16ade01
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
namespace UI.Web.Dashboard
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace UI.Web.Dashboard
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
}
}
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.2" />
</ItemGroup>
</Project>
\ No newline at end of file
......@@ -18,8 +18,13 @@ var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require
'requireLib',
'components/app/app',
'components/nav-bar/nav-bar',
'components/dashboard/dashboardViewModel',
'components/sensor/sensor',
'components/sensor/sensorViewModel',
'pages/home/home',
'pages/about/about'
'pages/test/test',
'services/mqttListener',
'services/mqttPublisher'
],
insertRequire: ['app/startup'],
bundles: {
......@@ -35,7 +40,7 @@ var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require
gulp.task('js', function () {
return rjs(requireJsOptimizerConfig)
.pipe(uglify({ preserveComments: 'some' }))
.pipe(gulp.dest('./dist/'));
.pipe(gulp.dest('./wwwroot/'));
});
// Concatenates CSS files, rewrites relative paths to Bootstrap fonts
......@@ -48,14 +53,14 @@ gulp.task('css', function () {
.pipe(replace(/url\((')?\.\.\/fonts\//g, 'url($1fonts/'));
var combinedCss = es.concat(appCss).pipe(concat('css.css'));
return es.concat(combinedCss)
.pipe(gulp.dest('./dist/'));
.pipe(gulp.dest('./wwwroot/'));
});
// Moves the bootstrap fonts to the dist-folder
gulp.task('fonts', function(){
return gulp.src('./node_modules/bootstrap/fonts/*', { base: './node_modules/bootstrap/components-bootstrap/' })
.pipe(gulp.dest('./dist/fonts'));
.pipe(gulp.dest('./wwwroot/fonts'));
});
// Copies index.html, replacing <script> and <link> tags to reference production URLs
......@@ -65,18 +70,18 @@ gulp.task('html', function() {
'css': 'css.css?' + Date.now(),
'js': 'scripts.js?' + Date.now()
}))
.pipe(gulp.dest('./dist/'));
.pipe(gulp.dest('./wwwroot/'));
});
// Removes all files from ./dist/
gulp.task('clean', function() {
return gulp.src('./dist/**/*', { read: false })
return gulp.src('./wwwroot/**/*', { read: false })
.pipe(clean());
});
gulp.task('default', ['html', 'js', 'css', 'fonts'], function(callback) {
callback();
console.log('\nPlaced optimized files in ' + chalk.magenta('dist/\n'));
console.log('\nPlaced optimized files in ' + chalk.magenta('wwwroot/\n'));
});
// Sets up a webserver with live reload for development
......
......@@ -23,8 +23,11 @@
"hasher": "^1.2.0",
"jquery": "^3.2.1",
"knockout": "^3.4.1",
"knockout-postbox": "^0.6.0",
"lodash": "^4.17.4",
"mqtt": "^2.9.0",
"requirejs": "^2.3.2",
"requirejs-text": "^2.0.12"
"requirejs-text": "^2.0.12",
"signals": "^1.0.0"
}
}
# Installation
# JS development
1. Install npm https://www.npmjs.com/
1. Run `npm install`
1. Run `gulp webserver`
# Build
1. Install .net core https://www.microsoft.com/net/core#windowscmd
1. Install npm https://www.npmjs.com/
1. Run `npm install`
1. Run `gulp`
1. Run `dotnet restore`
1. Run `dotnet run`
......@@ -3,7 +3,7 @@
pages: [
// [Scaffolded pages will be inserted here. To retain this feature, don't remove this comment.]
{ name: 'home', path: 'pages/home/home' },
{ name: 'about', path: 'pages/about/about' }
{ name: 'test', path: 'pages/test/test' }
],
// Components - knockout components intended as parts of pages, a.k.a. components
components: [
......
define({
current: {
mqtt_server_url: "mqtt://test.mosquitto.org:8080",
mqtt_topic: "e1s/iot.challange"
}
});
\ No newline at end of file
define({
sensorUpdate: "sensorUpdate"
});
\ No newline at end of file
......@@ -6,9 +6,11 @@ var require = {
"crossroads": "../node_modules/crossroads/dist/crossroads.min",
"hasher": "../node_modules/hasher/dist/js/hasher.min",
"knockout": "../node_modules/knockout/build/output/knockout-latest",
"knockout-postbox": "../node_modules/knockout-postbox/build/knockout-postbox.min",
"signals": "../node_modules/signals/dist/signals.min",
"text": "../node_modules/requirejs-text/text",
"lodash": "../node_modules/lodash/lodash",
"jquery": "../node_modules/jquery/dist/jquery.min"
"jquery": "../node_modules/jquery/dist/jquery.min",
"mqtt": "../node_modules/mqtt/dist/mqtt.min"
}
};
......@@ -12,7 +12,7 @@ define(["knockout", "crossroads", "hasher"], function(ko, crossroads, hasher) {
return new Router({
routes: [
{ url: '', params: { page: 'home' } },
{ url: 'about', params: { page: 'about' } }
{ url: 'test', params: { page: 'test' } }
]
});
......
define({
"Sensor 1": {
name: "Chill room"
},
__default__: {
minReadingsCount: 3,
readingExpiry: 3 * 60, //in seconds
lastReadings: 10 //number of last readings taken into account when calculating isOccupied
}
});
\ No newline at end of file
define([
"app/events",
"knockout",
"components/sensor/sensorViewModel",
"lodash",
"knockout-postbox"
], function (
events,
ko,
SensorViewModel,
_
) {
function DashboardViewModel() {
this.sensorsDict = {};
this.sensors = ko.observableArray();
ko.postbox.subscribe(events.sensorUpdate, _.bind(this.onSensorUpdated, this));
}
DashboardViewModel.prototype.addSensor = function (key, sensorData) {
if (!this.sensorsDict[key]) {
var sensorViewModel = new SensorViewModel(sensorData);
this.sensorsDict[key] = sensorViewModel;
this.sensors.push(sensorViewModel);
}
};
DashboardViewModel.prototype.updateSensor = function (key, sensorData) {
var sensorViewModel = this.sensorsDict[key];
if (sensorViewModel) {
sensorViewModel.update(sensorData);
}
};
DashboardViewModel.prototype.onSensorUpdated = function(data){
var key = data.id;
if(this.sensorsDict[key]){
this.updateSensor(key, data);
} else {
this.addSensor(key, data);
}
} ;
return DashboardViewModel;
});
\ No newline at end of file
......@@ -21,8 +21,8 @@
<a href="#">Home</a>
</li>
<li data-bind="css: { active: route().page === 'about' }">
<a href="#about">About</a>
<li data-bind="css: { active: route().page === 'test' }">
<a href="#test">Test</a>
</li>
</ul>
......
define(['knockout'], function(ko) {
define(['knockout', "app/sensors.config", "lodash"], function (ko, sensorsConfig, _) {
function SensorViewModel(params) {
this.isOccupied = ko.observable(params.isOccupied);
this.name = params.name;
this.timestamp = ko.observable(params.timestamp);
function SensorViewModel(sensorData) {
this.name = getName(sensorData.id);
this.readings = ko.observableArray([]);
this.config = getConfig(sensorData.id);
this.update(sensorData);
this.isOccupied = ko.computed(_.bind(calculateIsOcupied, this));
var clearReadingsPeriodically = _.bind(function () {
this.clearExpiredReadings();
setTimeout(clearReadingsPeriodically, 10 * 1000);
}, this);
clearReadingsPeriodically();
}
function calculateIsOcupied() {
var readings = this.readings();
if (readings.length < this.config.minReadingsCount)
return undefined;
var lastReadings = _.slice(_.orderBy(readings, "timestamp", "desc"), 0, this.config.lastReadings);
var occupiedReadingsCount = 0;
var notOccupiedReadingsCount = 0;
_.forEach(lastReadings, function (reading) {
if (reading.isOccupied) {
occupiedReadingsCount++;
} else {
notOccupiedReadingsCount++;
}
});
return occupiedReadingsCount > notOccupiedReadingsCount;
}
SensorViewModel.prototype.clearExpiredReadings = function () {
var currentDate = new Date();
var expiryInMiliseconds = this.config.readingExpiry * 1000;
this.readings.remove(function (reading) {
return currentDate - reading.timestamp > expiryInMiliseconds;
});
};
SensorViewModel.prototype.update = function (sensorData) {
this.readings.push(_.pick(sensorData, ["isOccupied", "timestamp"]));
this.timestamp = ko.observable(sensorData.timestamp);
};
function getName(id) {
return (sensorsConfig[id] && sensorsConfig[id].name) || id;
}
function getConfig(id) {
return _.merge(sensorsConfig[id] || {}, sensorsConfig.__default__);
}
return SensorViewModel;
......
<h2>About</h2>
<p>This component has no viewmodel. It's just an HTML template.</p>
define(['text!./about.html'], function (aboutTemplate) {
// This module could have been a template-only component.
// For examples, see earlier commits or the original
// generator-ko project.
return { viewModel: function (){}, template: aboutTemplate };
});
\ No newline at end of file
<h2>Edge1 Occupancy dashboard</h2>
<div data-bind="visible: sensors().length == 0">
<div data-bind="with: viewModel">
<div data-bind="visible: sensors().length == 0">
<h3>Waiting for sensor readings</h3>
</div>
<div class="row" data-bind="foreach: sensors">
</div>
<div class="row" data-bind="foreach: sensors">
<div class="col-xs-12 col-md-4">
<sensor params="name: name, isOccupied: isOccupied, timestamp: timestamp"></sensor>
</div>
</div>
<button type="button" class="btn btn-default btn-lg" data-bind="click: refresh, disable: isRefreshing">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh
</button>
<div data-bind="visible: isRefreshing">
Refreshing...
</div>
</div>
\ No newline at end of file
define([
"knockout",
"text!./home.html",
"components/sensor/sensorViewModel",
"queryHandlers/sensorReadingsQueryHandler",
"lodash"
"components/dashboard/dashboardViewModel",
"services/mqttListener"
], function (
ko,
homeTemplate,
SensorViewModel,
SensorReadingsQueryHandler,
_
DashboardViewModel,
MqttListener
) {
function HomeViewModel(route) {
function HomeViewModel() {
var mqttListener = new MqttListener();
mqttListener.init();
this.sensorsDict = {};
this.sensors = ko.observableArray();
this.isRefreshing = ko.observable(false);
this.queryHandler = new SensorReadingsQueryHandler();
}
HomeViewModel.prototype.addSensor = function (key, sensorData) {
if (!this.sensorsDict[key]) {
var viewModel = new SensorViewModel(sensorData);
this.sensorsDict[key] = viewModel;
this.sensors.push(viewModel);
}
}
HomeViewModel.prototype.updateSensor = function (key, sensorData) {
var viewModel = this.sensorsDict[key];
if (viewModel) {
viewModel.isOccupied(sensorData.isOccupied);
viewModel.timestamp(sensorData.timestamp);
}
}
HomeViewModel.prototype.refresh = function () {
this.isRefreshing(true);
this.queryHandler.handle().then(_.bind(function(data) {
var sensorsGroupedByName = _.groupBy(data, "name");
_.each(sensorsGroupedByName, _.bind(function(sensorReadings, key){
var latestReading = _.orderBy(sensorReadings, "timestamp").pop();
if(this.sensorsDict[key]){
this.updateSensor(key, latestReading);
} else {
this.addSensor(key, latestReading);
}
}, this));
this.isRefreshing(false);
}, this));
this.viewModel = new DashboardViewModel();
}
return { viewModel: HomeViewModel, template: homeTemplate };
......
<h2>Test publish page</h2>
<p>Input of this text box will be published over MQTT</p>
<input data-bind="textInput : inputText" type="text"/>
<button data-bind="click: publish" type="button">Publish</button>
<h3>Example data</h3>
<pre>
{ "object": { "sensorId": "Sensor 1", "isBusy": true}}
{ "object": { "sensorId": "Sensor 2", "isBusy": true}}
{ "object": { "sensorId": "Sensor 1", "isBusy": true}}
{ "object": { "sensorId": "Sensor 2", "isBusy": true}}
{ "object": { "sensorId": "Sensor 1", "isBusy": true}}
{ "object": { "sensorId": "Sensor 1", "isBusy": false}}
{ "object": { "sensorId": "Sensor 3", "isBusy": false}}}
{ "object": { "sensorId": "Sensor 3", "isBusy": true}}
</pre>
\ No newline at end of file
define(['text!./test.html',
"knockout",
"services/mqttPublisher"
], function (testTemplate, ko, MqttPublisher) {
function TestViewModel() {
this.inputText = ko.observable();
this._publisher = new MqttPublisher();
}
TestViewModel.prototype.publish = function () {
this._publisher.publish(this.inputText());
};
return {
viewModel: TestViewModel,
template: testTemplate
};
});
\ No newline at end of file
define(["jquery"], function($) {
function SensorReadingsQueryHandler() {
}
SensorReadingsQueryHandler.prototype.handle = function(){
return $.ajax('/mockData.json');
}
return SensorReadingsQueryHandler;
});
\ No newline at end of file
define(["app/config", "app/events", "mqtt", "lodash", "knockout", "knockout-postbox"], function(config, events, mqtt, _, ko) {
function MqttListener() {
}
MqttListener.prototype.init = function(){
this.client = mqtt.connect(config.current.mqtt_server_url);
console.debug("Mqtt listener connecting to: " + config.current.mqtt_server_url);
this.client.on('connect', _.bind(function () {
console.debug("Mqtt listener connected to topic: " + config.current.mqtt_topic);
this.client.subscribe(config.current.mqtt_topic);
}, this));
this.client.on('message', function (topic, message) {
// message is Buffer
try{
console.debug("Mqtt message received: " + message.toString());
var messageBody = JSON.parse(message.toString());
if(messageBody.object === undefined ||
messageBody.object.sensorId === undefined ||
messageBody.object.isBusy === undefined ){
throw "InvalidMessageFormat";
}
var model = {
id: messageBody.object.sensorId,
isOccupied: messageBody.object.isBusy,
timestamp: new Date()
};
ko.postbox.publish(events.sensorUpdate, model);
}
catch(e){
console.error("Failed to parse message into JSON: [" + message.toString() + "]: " + e);
}
});
};
MqttListener.prototype.end = function(){
if(this.client){
this.client.end();
}
};
return MqttListener;
});
\ No newline at end of file
define(["app/config", "mqtt"], function(config, mqtt) {
function MqttPublisher() {
}
MqttPublisher.prototype.publish = function(messageBody){
var client = mqtt.connect(config.current.mqtt_server_url);
client.on('connect', function () {
client.publish(config.current.mqtt_topic, messageBody);
client.end();
});
};
return MqttPublisher;
});
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>OccupancySensorDashboard</title>
<link rel="stylesheet" href="css.css?1499263535088">
<script src="scripts.js?1499263535088"></script>
</head>
<body>
<app params="route: route"></app>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment