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..b05a144c0a 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": "knob_control",
+ "name": "Knob Control",
+ "descriptor": {
+ "type": "rpc",
+ "sizeX": 5,
+ "sizeY": 4.5,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": "",
+ "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 \"title\": {\n \"title\": \"Knob title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"getValueMethod\": {\n \"title\": \"Get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"Set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"minValue\", \"maxValue\", \"getValueMethod\", \"setValueMethod\", \"requestTimeout\"]\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n \"initialValue\",\n \"getValueMethod\",\n \"setValueMethod\",\n \"title\",\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,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}"
+ }
}
]
}
\ 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/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js
index 1efbf7f8ea..f9ff7a6f5f 100644
--- a/ui/src/app/components/widget/widget.controller.js
+++ b/ui/src/app/components/widget/widget.controller.js
@@ -811,14 +811,17 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
return (val - parseFloat( val ) + 1) >= 0;
}
- function formatValue(value, dec, units) {
+ function formatValue(value, dec, units, showZeroDecimals) {
if (angular.isDefined(value) &&
value !== null && isNumeric(value)) {
var formatted = Number(value);
if (angular.isDefined(dec)) {
formatted = formatted.toFixed(dec);
}
- formatted = (formatted * 1).toString();
+ if (!showZeroDecimals) {
+ formatted = (formatted * 1);
+ }
+ formatted = formatted.toString();
if (angular.isDefined(units) && units.length > 0) {
formatted += ' ' + units;
}
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index e51c558fdf..08a54d818a 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js
@@ -95,6 +95,15 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
options.value = options.minValue;
}
+ if (options.gaugeType === 'donut') {
+ if (!options.donutStartAngle) {
+ options.donutStartAngle = 1.5 * Math.PI;
+ }
+ if (!options.donutEndAngle) {
+ options.donutEndAngle = options.donutStartAngle + 2 * Math.PI;
+ }
+ }
+
var colorsCount = options.levelColors.length;
var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
options.colorsRange = [];
@@ -232,6 +241,21 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
return this;
}
+ getValueColor() {
+ if (this.contextProgressClone) {
+ var color = this.contextProgressClone.currentColor;
+ if (!color) {
+ if (this.options.neonGlowBrightness) {
+ color = getProgressColor(0, this.options.neonColorsRange);
+ } else {
+ color = getProgressColor(0, this.options.colorsRange);
+ }
+ }
+ return color;
+ } else {
+ return '#000';
+ }
+ }
}
/* eslint-disable angular/document-service */
@@ -473,7 +497,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.donutEndAngle);
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 +629,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, donutEndAngle) {
context.setLineDash([]);
var strokeWidth = Ro - Ri;
var blur = 0.55;
@@ -623,7 +647,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 + (donutEndAngle - donutStartAngle) * progress + e);
} else {
context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e);
}
@@ -660,9 +684,9 @@ function drawBarGlow(context, startX, startY, endX, endY, color, strokeWidth, is
function drawProgress(context, options, progress) {
var neonColor;
if (options.neonGlowBrightness) {
- neonColor = getProgressColor(progress, options.neonColorsRange);
+ context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange);
} else {
- context.strokeStyle = getProgressColor(progress, options.colorsRange);
+ context.currentColor = context.strokeStyle = getProgressColor(progress, options.colorsRange);
}
let {barLeft, barRight, barTop, baseX, width, barBottom, Cx, Cy, Rm, Ro, Ri, strokeWidth} =
@@ -682,10 +706,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 + (options.donutEndAngle - options.donutStartAngle) * 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, options.donutEndAngle);
}
} 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..ba07eee559
--- /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 tbKnob from './knob.directive';
+
+export default angular.module('thingsboard.widgets.rpc', [
+ tbKnob
+]).name;
diff --git a/ui/src/app/widget/lib/rpc/knob.directive.js b/ui/src/app/widget/lib/rpc/knob.directive.js
new file mode 100644
index 0000000000..d62d0fbba8
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/knob.directive.js
@@ -0,0 +1,388 @@
+/*
+ * 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 './knob.scss';
+
+import CanvasDigitalGauge from './../CanvasDigitalGauge';
+import tinycolor from 'tinycolor2';
+
+import { isNumber } from '../widget-utils';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import knobTemplate from './knob.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.rpc.knob', [])
+ .directive('tbKnob', Knob)
+ .name;
+
+/*@ngInject*/
+function Knob() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ ctx: '='
+ },
+ controller: KnobController,
+ controllerAs: 'vm',
+ templateUrl: knobTemplate
+ };
+}
+
+/*@ngInject*/
+function KnobController($element, $scope, $document) {
+ let vm = this;
+
+ vm.value = 0;
+ vm.error = '';
+
+ var knob = angular.element('.knob', $element),
+ knobContainer = angular.element('#knob-container', $element),
+ knobTopPointerContainer = knob.find('.top-pointer-container'),
+ knobTopPointer = knob.find('.top-pointer'),
+ knobValueContainer = knob.find('.value-container'),
+ knobValue = knob.find('.knob-value'),
+ knobErrorContainer = knob.find('.error-container'),
+ knobError = knob.find('.knob-error'),
+ knobTitleContainer = knob.find('.title-container'),
+ knobTitle = knob.find('.knob-title'),
+ knobMinmaxContainer = knob.find('.minmax-container'),
+ minmaxLanel = knob.find('.minmax-label'),
+ textMeasure = knob.find('#text-measure'),
+ startDeg = -1,
+ currentDeg = 0,
+ rotation = 0,
+ lastDeg = 0,
+ moving = false;
+
+ var minDeg = -45;
+ var maxDeg = 225;
+
+ var canvasBarElement = angular.element('#canvasBar', $element);
+
+ var levelColors = ['#19ff4b', '#ffff19', '#ff3232'];
+
+ 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.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : '';
+
+ vm.darkTheme = vm.ctx.settings.theme == 'dark';
+
+ var canvasBarData = {
+ renderTo: canvasBarElement[0],
+ hideValue: true,
+ neonGlowBrightness: 0,
+ gaugeWidthScale: 0.4,
+ gaugeColor: 'rgba(0, 0, 0, 0)',
+ levelColors: levelColors,
+ minValue: vm.minValue,
+ maxValue: vm.maxValue,
+ gaugeType: 'donut',
+ dashThickness: 2,
+ donutStartAngle: 3/4*Math.PI,
+ donutEndAngle: 9/4*Math.PI,
+ animation: false
+ };
+
+ canvasBar = new CanvasDigitalGauge(canvasBarData).draw();
+
+ knob.on('click', (e) => {
+ if (moving) {
+ moving = false;
+ return false;
+ }
+ e.preventDefault();
+
+ var offset = knob.offset();
+ var center = {
+ y : offset.top + knob.height()/2,
+ x: offset.left + knob.width()/2
+ };
+ var a, b, deg,
+ rad2deg = 180/Math.PI;
+
+ 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 (deg > maxDeg) {
+ if (deg - 360 > minDeg) {
+ deg = deg - 360;
+ } else {
+ return false;
+ }
+ }
+ currentDeg = deg;
+ lastDeg = deg;
+ knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)');
+ turn(degreeToRatio(currentDeg));
+ rotation = currentDeg;
+ startDeg = -1;
+ });
+
+ 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) => {
+ moving = true;
+ 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 (tmp > maxDeg) {
+ if (tmp - 360 > minDeg) {
+ tmp = tmp - 360;
+ } else {
+ var deltaMax = Math.abs(maxDeg - lastDeg);
+ var deltaMin = Math.abs(minDeg - lastDeg);
+ if (deltaMax < deltaMin) {
+ tmp = maxDeg;
+ } else {
+ tmp = minDeg;
+ }
+ }
+ }
+ if(Math.abs(tmp - lastDeg) > 180){
+ startDeg = deg;
+ rotation = currentDeg;
+ return false;
+ }
+
+ currentDeg = tmp;
+ lastDeg = tmp;
+
+ knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)');
+ turn(degreeToRatio(currentDeg));
+ });
+
+ $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);
+
+ var subscription = vm.ctx.defaultSubscription;
+ var rpcEnabled = subscription.rpcEnabled;
+
+ vm.isSimulated = $scope.widgetEditMode;
+
+ vm.requestTimeout = 500;
+ if (vm.ctx.settings.requestTimeout) {
+ vm.requestTimeout = vm.ctx.settings.requestTimeout;
+ }
+ vm.getValueMethod = 'getValue';
+ if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) {
+ vm.getValueMethod = vm.ctx.settings.getValueMethod;
+ }
+ vm.setValueMethod = 'setValue';
+ if (vm.ctx.settings.setValueMethod && vm.ctx.settings.setValueMethod.length) {
+ vm.setValueMethod = vm.ctx.settings.setValueMethod;
+ }
+ if (!rpcEnabled) {
+ onError('Target device is not set!');
+ } else {
+ if (!vm.isSimulated) {
+ rpcRequestValue();
+ }
+ }
+ }
+
+ function ratioToDegree(ratio) {
+ return minDeg + ratio*(maxDeg-minDeg);
+ }
+
+ function degreeToRatio(degree) {
+ return (degree-minDeg)/(maxDeg-minDeg);
+ }
+
+ 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});
+ setFontSize(knobTitle, vm.title, knobTitleContainer.height(), knobTitleContainer.width());
+ setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width());
+ var minmaxHeight = knobMinmaxContainer.height();
+ minmaxLanel.css({'fontSize': minmaxHeight+'px', 'lineHeight': minmaxHeight+'px'});
+ checkValueSize();
+ }
+
+ function turn(ratio) {
+ var value = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(vm.ctx.decimals);
+ if (canvasBar.value != value) {
+ canvasBar.value = value;
+ }
+ updateColor(canvasBar.getValueColor());
+ onValue(value);
+ }
+
+ function setValue(value) {
+ var ratio = (value-vm.minValue) / (vm.maxValue - vm.minValue);
+ rotation = lastDeg = currentDeg = ratioToDegree(ratio);
+ knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)');
+ if (canvasBar.value != value) {
+ canvasBar.value = value;
+ }
+ updateColor(canvasBar.getValueColor());
+ vm.value = formatValue(value);
+ checkValueSize();
+ }
+
+ function updateColor(color) {
+ var glowColor = tinycolor(color).brighten(30).toHexString();
+ knobValue.css({'color': glowColor});
+ var textShadow = `${color} 1px 1px 10px, ${glowColor} 1px 1px 10px`;
+ knobValue.css({'textShadow': textShadow});
+ knobTopPointer.css({'backgroundColor': glowColor});
+ var boxShadow = `inset 1px 0 2px #040404, 1px 1px 8px 2px ${glowColor}`;
+ knobTopPointer.css({'boxShadow': boxShadow});
+ }
+
+ function onValue(value) {
+ $scope.$applyAsync(() => {
+ vm.value = formatValue(value);
+ checkValueSize();
+ rpcUpdateValue(value);
+ });
+ }
+
+ function onError(error) {
+ $scope.$applyAsync(() => {
+ vm.error = error;
+ setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width());
+ });
+ }
+
+ function formatValue(value) {
+ return vm.ctx.utils.formatValue(value, vm.ctx.decimals, vm.ctx.units, true);
+ }
+
+ function checkValueSize() {
+ var fontSize = knobValueContainer.height()/3.3;
+ var containerWidth = knobValueContainer.width();
+ setFontSize(knobValue, vm.value, fontSize, containerWidth);
+ }
+
+ function setFontSize(element, text, fontSize, maxWidth) {
+ var textWidth = measureTextWidth(text, fontSize);
+ while (textWidth > maxWidth) {
+ fontSize--;
+ textWidth = measureTextWidth(text, fontSize);
+ }
+ element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'});
+ }
+
+ function measureTextWidth(text, fontSize) {
+ textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'});
+ textMeasure.text(text);
+ return textMeasure.width();
+ }
+
+ function rpcRequestValue() {
+ vm.error = '';
+ vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then(
+ (responseBody) => {
+ if (isNumber(responseBody)) {
+ var numValue = Number(responseBody).toFixed(vm.ctx.decimals);
+ setValue(numValue);
+ } else {
+ var errorText = `Unable to parse response: ${responseBody}`;
+ onError(errorText);
+ }
+ },
+ () => {
+ var errorText = vm.ctx.defaultSubscription.rpcErrorText;
+ onError(errorText);
+ }
+ );
+ }
+
+ function rpcUpdateValue(value) {
+ if (vm.executingUpdateValue) {
+ vm.scheduledValue = value;
+ return;
+ } else {
+ vm.scheduledValue = null;
+ vm.rpcValue = value;
+ vm.executingUpdateValue = true;
+ }
+ vm.error = '';
+ vm.ctx.controlApi.sendOneWayCommand(vm.setValueMethod, value, vm.requestTimeout).then(
+ () => {
+ vm.executingUpdateValue = false;
+ if (vm.scheduledValue != null && vm.scheduledValue != vm.rpcValue) {
+ rpcUpdateValue(vm.scheduledValue);
+ }
+ },
+ () => {
+ vm.executingUpdateValue = false;
+ var errorText = vm.ctx.defaultSubscription.rpcErrorText;
+ onError(errorText);
+ }
+ );
+ }
+}
diff --git a/ui/src/app/widget/lib/rpc/knob.scss b/ui/src/app/widget/lib/rpc/knob.scss
new file mode 100644
index 0000000000..3e57f663c0
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/knob.scss
@@ -0,0 +1,157 @@
+/**
+ * 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.svg');
+
+$bars-margin-pct: percentage(0.033);
+$background-margin-pct: percentage(0.05);
+$value-container-margin-pct: percentage(0.35);
+$error-height: percentage(0.05);
+$title-height: percentage(0.066);
+$title-container-margin-pct: percentage(0.2);
+$title-container-margin-bottom-pct: percentage(0.05);
+$minmax-height: percentage(0.04);
+$minmax-container-margin-pct: percentage(0.18);
+$minmax-container-margin-bottom-pct: percentage(0.12);
+
+$background-color: #e6e7e8;
+
+.tb-knob {
+ width:100%;
+ height:100%;
+ background: $background-color;
+ &.dark {
+ background: #000;
+ }
+
+ .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;
+ z-index: 2;
+ }
+ .canvas-background {
+ position:absolute;
+ top: $background-margin-pct;
+ left: $background-margin-pct;
+ bottom: $background-margin-pct;
+ right: $background-margin-pct;
+ border-radius: 50%;
+ background: #3f4346;
+ z-index:2;
+ }
+ .value-container {
+ position:absolute;
+ top: $value-container-margin-pct;
+ left: $value-container-margin-pct;
+ bottom: $value-container-margin-pct;
+ right: $value-container-margin-pct;
+ z-index:4;
+ .knob-value {
+ color: #fff;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+ }
+ .error-container {
+ position:absolute;
+ top: 1%;
+ left: 0;
+ right: 0;
+ z-index:4;
+ height: $error-height;
+ .knob-error {
+ color: #ff3315;
+ white-space: nowrap;
+ }
+ }
+ .title-container {
+ position:absolute;
+ left: $title-container-margin-pct;
+ bottom: $title-container-margin-bottom-pct;
+ right: $title-container-margin-pct;
+ z-index:4;
+ height: $title-height;
+ .knob-title {
+ color: #757575;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+ }
+ .minmax-container {
+ position:absolute;
+ left: $minmax-container-margin-pct;
+ bottom: $minmax-container-margin-bottom-pct;
+ right: $minmax-container-margin-pct;
+ z-index:4;
+ height: $minmax-height;
+ .minmax-label {
+ color: #757575;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+ }
+ .top-pointer-container {
+ position:absolute;
+ top: $bars-margin-pct;
+ left: $bars-margin-pct;
+ bottom: $bars-margin-pct;
+ right: $bars-margin-pct;
+ z-index:3;
+ cursor:pointer !important;
+ .top-pointer {
+ content:'';
+ width:5%;
+ height:5%;
+ background-color:#b5b5b5;
+ position:absolute;
+ top:50%;
+ left:22%;
+ margin-top:-2.5%;
+ border-radius: 50%;
+ cursor:pointer !important;
+ box-shadow: 1px 0 2px #040404;
+ }
+ }
+ .top{
+ position:absolute;
+ top: $bars-margin-pct;
+ left: $bars-margin-pct;
+ bottom: $bars-margin-pct;
+ right: $bars-margin-pct;
+ background:$knob-img no-repeat;
+ background-size: contain;
+ z-index:2;
+ cursor:pointer !important;
+ }
+ #text-measure {
+ position: absolute;
+ visibility: hidden;
+ height: auto;
+ width: auto;
+ white-space: nowrap;
+ }
+ }
+}
diff --git a/ui/src/app/widget/lib/rpc/knob.svg b/ui/src/app/widget/lib/rpc/knob.svg
new file mode 100644
index 0000000000..81fe9949b7
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/knob.svg
@@ -0,0 +1,36 @@
+
diff --git a/ui/src/app/widget/lib/rpc/knob.tpl.html b/ui/src/app/widget/lib/rpc/knob.tpl.html
new file mode 100644
index 0000000000..93276b44ed
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/knob.tpl.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ {{ vm.value }}
+
+
+
+
+
+
+ {{ vm.error }}
+
+
+ {{ vm.title }}
+
+
+ min
+
+ max
+
+
+
+
+
\ No newline at end of file