diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index ff920b1e32..b06c979211 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -36,6 +36,22 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC remote shell\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } + }, + { + "alias": "shiny_knob_control", + "name": "Shiny Knob Control", + "descriptor": { + "type": "rpc", + "sizeX": 2.5, + "sizeY": 3, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel md-switch {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n padding-left: 8px;\n padding-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n padding-left: 4px;\n padding-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n if (self.ctx.resize) {\n self.ctx.resize();\n }\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"theme\": {\n \"title\": \"Knob theme\",\n \"type\": \"string\",\n \"default\": \"light\"\n }, \n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"minValue\", \"maxValue\", \"requestTimeout\"]\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n \"initialValue\",\n {\n \"key\": \"theme\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"light\",\n \"label\": \"Light\"\n },\n {\n \"value\": \"dark\",\n \"label\": \"Dark\"\n }\n ]\n },\n \"requestTimeout\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"theme\":\"light\",\"initialValue\":50},\"title\":\"Shiny Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } } ] } \ No newline at end of file diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 8580b800bd..842b9768dd 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -22,6 +22,8 @@ import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-wid import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; +import thingsboardRpcWidgets from '../widget/lib/rpc'; + import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge'; @@ -39,7 +41,7 @@ import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, - thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils]) + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js index e51c558fdf..e6c2e275d2 100644 --- a/ui/src/app/widget/lib/CanvasDigitalGauge.js +++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js @@ -95,6 +95,12 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge { options.value = options.minValue; } + if (options.gaugeType === 'donut') { + if (!options.donutStartAngle) { + options.donutStartAngle = 1.5 * Math.PI; + } + } + var colorsCount = options.levelColors.length; var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; options.colorsRange = []; @@ -473,7 +479,7 @@ function drawBackground(context, options) { context.lineCap = 'round'; } if (options.gaugeType === 'donut') { - context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, 1.5 * Math.PI, 3.5 * Math.PI); + context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, options.donutStartAngle, options.donutStartAngle + 2 * Math.PI); context.stroke(); } else if (options.gaugeType === 'arc') { context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, Math.PI, 2*Math.PI); @@ -605,7 +611,7 @@ function getProgressColor(progress, colorsRange) { } } -function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut) { +function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut, donutStartAngle) { context.setLineDash([]); var strokeWidth = Ro - Ri; var blur = 0.55; @@ -623,7 +629,7 @@ function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut) { context.beginPath(); var e = 0.01 * Math.PI; if (isDonut) { - context.arc(Cx, Cy, Rm, 1.5 * Math.PI - e, 1.5 * Math.PI + 2 * Math.PI * progress + e); + context.arc(Cx, Cy, Rm, donutStartAngle - e, donutStartAngle + 2 * Math.PI * progress + e); } else { context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e); } @@ -682,10 +688,10 @@ function drawProgress(context, options, progress) { context.strokeStyle = neonColor; } context.beginPath(); - context.arc(Cx, Cy, Rm, 1.5 * Math.PI, 1.5 * Math.PI + 2 * Math.PI * progress); + context.arc(Cx, Cy, Rm, options.donutStartAngle, options.donutStartAngle + 2 * Math.PI * progress); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { - drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true); + drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, options.donutStartAngle); } } else if (options.gaugeType === 'arc') { if (options.neonGlowBrightness) { diff --git a/ui/src/app/widget/lib/rpc/index.js b/ui/src/app/widget/lib/rpc/index.js new file mode 100644 index 0000000000..9f2798974b --- /dev/null +++ b/ui/src/app/widget/lib/rpc/index.js @@ -0,0 +1,21 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import tbShinyKnob from './shiny-knob.directive'; + +export default angular.module('thingsboard.widgets.rpc', [ + tbShinyKnob +]).name; diff --git a/ui/src/app/widget/lib/rpc/knob.png b/ui/src/app/widget/lib/rpc/knob.png new file mode 100644 index 0000000000..24675b28ad Binary files /dev/null and b/ui/src/app/widget/lib/rpc/knob.png differ diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.directive.js b/ui/src/app/widget/lib/rpc/shiny-knob.directive.js new file mode 100644 index 0000000000..b1867996d6 --- /dev/null +++ b/ui/src/app/widget/lib/rpc/shiny-knob.directive.js @@ -0,0 +1,198 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './shiny-knob.scss'; + +import CanvasDigitalGauge from './../CanvasDigitalGauge'; +//import tinycolor from 'tinycolor2'; + +/* eslint-disable import/no-unresolved, import/default */ + +import shinyKnobTemplate from './shiny-knob.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.widgets.rpc.shinyKnob', []) + .directive('tbShinyKnob', ShinyKnob) + .name; + +/*@ngInject*/ +function ShinyKnob() { + return { + restrict: "E", + scope: true, + bindToController: { + ctx: '=' + }, + controller: ShinyKnobController, + controllerAs: 'vm', + templateUrl: shinyKnobTemplate + }; +} + +/*@ngInject*/ +function ShinyKnobController($element, $scope, $document) { + let vm = this; + + vm.value = 0; + + var snap = 0; + + var knob = angular.element('.knob', $element), + knobContainer = angular.element('#knob-container', $element), + knobTop = knob.find('.top'), + startDeg = -1, + currentDeg = 0, + rotation = 0, + lastDeg = 0; + + var canvasBarElement = angular.element('#canvasBar', $element); + + var levelColors = ['rgb(0, 128, 0)', 'rgb(251, 192, 45)', 'rgb(244, 67, 54)']; + var canvasBar; + + $scope.$watch('vm.ctx', () => { + if (vm.ctx) { + init(); + } + }); + + function init() { + + vm.minValue = angular.isDefined(vm.ctx.settings.minValue) ? vm.ctx.settings.minValue : 0; + vm.maxValue = angular.isDefined(vm.ctx.settings.maxValue) ? vm.ctx.settings.maxValue : 100; + + vm.darkTheme = vm.ctx.settings.theme == 'dark'; + + var canvasBarData = { + renderTo: canvasBarElement[0], + hideValue: true, + neonGlowBrightness: vm.darkTheme ? 40 : 0, + gaugeWidthScale: 0.5, + gaugeColor: vm.darkTheme ? 'rgb(23, 26, 28)' : 'rgba(0,0,0,0)', + levelColors: levelColors, + minValue: vm.minValue, + maxValue: vm.maxValue, + gaugeType: 'donut', + dashThickness: 1.5, + donutStartAngle: Math.PI, + animation: false, + animationDuration: 250, + animationRule: 'linear' + }; + + canvasBar = new CanvasDigitalGauge(canvasBarData).draw(); + + knob.on('mousedown touchstart', (e) => { + e.preventDefault(); + var offset = knob.offset(); + var center = { + y : offset.top + knob.height()/2, + x: offset.left + knob.width()/2 + }; + + var a, b, deg, tmp, + rad2deg = 180/Math.PI; + + knob.on('mousemove.rem touchmove.rem', (e) => { + + e = (e.originalEvent.touches) ? e.originalEvent.touches[0] : e; + + a = center.y - e.pageY; + b = center.x - e.pageX; + deg = Math.atan2(a,b)*rad2deg; + + if(deg<0){ + deg = 360 + deg; + } + + if(startDeg == -1){ + startDeg = deg; + } + + tmp = Math.floor((deg-startDeg) + rotation); + + if(tmp < 0){ + tmp = 360 + tmp; + } + else if(tmp > 359){ + tmp = tmp % 360; + } + + if(snap && tmp < snap){ + tmp = 0; + } + if(Math.abs(tmp - lastDeg) > 180){ + return false; + } + currentDeg = tmp; + lastDeg = tmp; + + knobTop.css('transform','rotate('+(currentDeg)+'deg)'); + turn(currentDeg/359); + }); + + $document.on('mouseup.rem touchend.rem',() => { + knob.off('.rem'); + $document.off('.rem'); + rotation = currentDeg; + startDeg = -1; + }); + + }); + + vm.ctx.resize = resize; + resize(); + + var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : vm.minValue; + + setValue(initialValue); + } + + function resize() { + var width = knobContainer.width(); + var height = knobContainer.height(); + var size = Math.min(width, height); + knob.css({width: size, height: size}); + canvasBar.update({width: size, height: size}); + } + + function turn(ratio) { + var value = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(2); + if (canvasBar.value != value) { + canvasBar.value = value; + } + onValue(value); + } + + function setValue(value) { + var ratio = (value-vm.minValue) / (vm.maxValue - vm.minValue); + rotation = lastDeg = currentDeg = ratio*360; + knobTop.css('transform','rotate('+(currentDeg)+'deg)'); + if (canvasBar.value != value) { + canvasBar.value = value; + } + vm.value = value; + } + + function onValue(value) { + console.log(`onValue ${value}`); //eslint-disable-line + $scope.$applyAsync(() => { + vm.value = value; + }); + } + +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.scss b/ui/src/app/widget/lib/rpc/shiny-knob.scss new file mode 100644 index 0000000000..7ff204c427 --- /dev/null +++ b/ui/src/app/widget/lib/rpc/shiny-knob.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +$knob-img: url('./knob.png'); + +$bars-margin-pct: percentage(0.2); +$shadow-size: 5; +$shadow-size-px: $shadow-size+px; +$shadow-offset-px: $shadow-size/2+px; + +.tb-shiny-knob { + width:100%; + height:100%; + &.dark { + background: #000;//$dark-bg-img #1f2129; + } + + .knob { + position: relative; + &[draggable] { + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + } + #canvasBar { + position:absolute; + top:0; + left:0; + bottom: 0; + right: 0; + } + .top{ + position:absolute; + top: calc(#{$bars-margin-pct} - #{$shadow-offset-px}); + left: $bars-margin-pct; + bottom: calc(#{$bars-margin-pct} + #{$shadow-offset-px}); + right: $bars-margin-pct; + background:$knob-img no-repeat; + background-size: contain; + z-index:10; + cursor:default !important; + &:after { + content:''; + width:10px; + height:10px; + background-color:#666; + position:absolute; + top:50%; + left:10px; + margin-top:-5px; + border-radius: 50%; + cursor:default !important; + box-shadow: 0 0 1px #5a5a5a inset; + } + } + .base{ + top: calc(#{$bars-margin-pct} - #{$shadow-offset-px}); + left: $bars-margin-pct; + bottom: calc(#{$bars-margin-pct} + #{$shadow-offset-px}); + right: $bars-margin-pct; + border-radius:50%; + box-shadow:0 $shadow-size-px 0 #4a5056,$shadow-size-px $shadow-size-px $shadow-size-px #000; + position:absolute; + z-index:1; + } + } +} diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html b/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html new file mode 100644 index 0000000000..18094b39c5 --- /dev/null +++ b/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html @@ -0,0 +1,30 @@ + + +
+
+ {{ vm.value }} +
+
+
+ +
+
+
+
+
\ No newline at end of file