diff --git a/GUI-help.md b/GUI-help.md
new file mode 100644
index 0000000..0293c51
--- /dev/null
+++ b/GUI-help.md
@@ -0,0 +1,4 @@
+# GUI help for signalk-raymarine-autopilot
+
+
+
diff --git a/GUI-help.png b/GUI-help.png
new file mode 100644
index 0000000..1e4ce38
Binary files /dev/null and b/GUI-help.png differ
diff --git a/README.md b/README.md
index f0d143a..e676046 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,11 @@
[](https://greenkeeper.io/)
+

+
+`signalk-raymarine-autopilot` signal is composed of 2 modules:
+- [A graphical interface that emulates a Raymarine remote control](./GUI-help.md "GUI help")
+- A back-end API described below.
# API
@@ -44,3 +49,60 @@ The `value` is in degrees and is the amount to change. So when in `auto` at a he
"value": 1
}
```
+
+## Change Target Heading or Wind Angle by key
+
+The `value` is a direct key (`+1`, `+10`, `-1`, `-10`, `-1-10`, `+1+10`).
+The key `+1` add +1 degree to heading, `+10` add +10 degree...
+The special key `-1-10` and `+1+10` is dedicated to take a tack to port or starbord.
+
+```json
+{
+ "action": "changeHeadingByKey",
+ "value": "+1"
+}
+```
+
+## Take a tack to port or starboard
+
+The `value` is `port` or `starboard`.
+This command must be send only when your pilot is in `auto` or `wind` mode.
+
+```json
+{
+ "action": "tackTo",
+ "value": "port"
+}
+```
+
+## Silence alarm
+
+This command silence a Raymarine alarms.
+You can silence alarm directly by the numeric code or by a `signal k` notification path.
+This command does not silence a `signal k` notification.
+It is your Raymarine equipment that receives the acknowledgement command
+and returns a new notification status on the NMEA2000 bus with the `normal` state value.
+See the code in `index.js` for the complete list of `raymarineAlarmGroupCodes` and `alarmsId` keys / values.
+
+In acknowledgement by the numeric code, the `value` is :
+
+```json
+{
+ "action": "silenceAlarm",
+ "value": {
+ "groupId": "0x01",
+ "alarmId": "0x47"
+ }
+}
+```
+
+In acknowledgement by `signal k` notification path, the `value` is the path you receive in your websocket :
+
+```json
+{
+ "action": "silenceAlarm",
+ "value": {
+ "signalkPath": "notifications.autopilot.PilotWarningWindShift"
+ }
+}
+```
diff --git a/index.js b/index.js
index a53aa0e..724e8f0 100644
--- a/index.js
+++ b/index.js
@@ -22,7 +22,134 @@ const state_commands = {
"wind": "%s,3,126208,%s,%s,17,01,63,ff,00,f8,04,01,3b,07,03,04,04,00,01,05,ff,ff",
"route": "%s,3,126208,%s,%s,17,01,63,ff,00,f8,04,01,3b,07,03,04,04,80,01,05,ff,ff",
"standby": "%s,3,126208,%s,%s,17,01,63,ff,00,f8,04,01,3b,07,03,04,04,00,00,05,ff,ff"
- }
+}
+const keys_code = {
+ "+1": "07,f8",
+ "+10": "08,f7",
+ "-1": "05,fa",
+ "-10": "06,f9",
+ "-1-10": "21,de",
+ "+1+10": "22,dd"
+}
+const raymarineAlarmGroupCodes = {
+ "instrument": 0x00,
+ "autopilot": 0x01,
+ "radar": 0x02,
+ "chartplotter": 0x03,
+ "ais": 0x04
+};
+const alarmsId = {
+ "NoAlarm": 0x0,
+ "ShallowDepth": 0x1,
+ "DeepDepth": 0x2,
+ "ShallowAnchor": 0x3,
+ "DeepAnchor": 0x4,
+ "OffCourse": 0x5,
+ "AWAHigh": 0x6,
+ "AWALow": 0x7,
+ "AWSHigh": 0x8,
+ "AWSLow": 0x9,
+ "TWAHigh": 0xa,
+ "TWALow": 0xb,
+ "TWSHigh": 0xc,
+ "TWSLow": 0xd,
+ "WPArrival": 0xe,
+ "BoatSpeedHigh": 0xf,
+ "BoatSpeedLow": 0x10,
+ "SeaTempHigh": 0x11,
+ "SeaTempLow": 0x12,
+ "PilotWatch": 0x13,
+ "PilotOffCourse": 0x14,
+ "PilotWindShift": 0x15,
+ "PilotLowBattery": 0x16,
+ "PilotLastMinuteOfWatch": 0x17,
+ "PilotNoNMEAData": 0x18,
+ "PilotLargeXTE": 0x19,
+ "PilotNMEADataError": 0x1a,
+ "PilotCUDisconnected": 0x1b,
+ "PilotAutoRelease": 0x1c,
+ "PilotWayPointAdvance": 0x1d,
+ "PilotDriveStopped": 0x1e,
+ "PilotTypeUnspecified": 0x1f,
+ "PilotCalibrationRequired": 0x20,
+ "PilotLastHeading": 0x21,
+ "PilotNoPilot": 0x22,
+ "PilotRouteComplete": 0x23,
+ "PilotVariableText": 0x24,
+ "GPSFailure": 0x25,
+ "MOB": 0x26,
+ "Seatalk1Anchor": 0x27,
+ "PilotSwappedMotorPower": 0x28,
+ "PilotStandbyTooFastToFish": 0x29,
+ "PilotNoGPSFix": 0x2a,
+ "PilotNoGPSCOG": 0x2b,
+ "PilotStartUp": 0x2c,
+ "PilotTooSlow": 0x2d,
+ "PilotNoCompass": 0x2e,
+ "PilotRateGyroFault": 0x2f,
+ "PilotCurrentLimit": 0x30,
+ "PilotWayPointAdvancePort": 0x31,
+ "PilotWayPointAdvanceStbd": 0x32,
+ "PilotNoWindData": 0x33,
+ "PilotNoSpeedData": 0x34,
+ "PilotSeatalkFail1": 0x35,
+ "PilotSeatalkFail2": 0x36,
+ "PilotWarningTooFastToFish": 0x37,
+ "PilotAutoDocksideFail": 0x38,
+ "PilotTurnTooFast": 0x39,
+ "PilotNoNavData": 0x3a,
+ "PilotLostWaypointData": 0x3b,
+ "PilotEEPROMCorrupt": 0x3c,
+ "PilotRudderFeedbackFail": 0x3d,
+ "PilotAutolearnFail1": 0x3e,
+ "PilotAutolearnFail2": 0x3f,
+ "PilotAutolearnFail3": 0x40,
+ "PilotAutolearnFail4": 0x41,
+ "PilotAutolearnFail5": 0x42,
+ "PilotAutolearnFail6": 0x43,
+ "PilotWarningCalRequired": 0x44,
+ "PilotWarningOffCourse": 0x45,
+ "PilotWarningXTE": 0x46,
+ "PilotWarningWindShift": 0x47,
+ "PilotWarningDriveShort": 0x48,
+ "PilotWarningClutchShort": 0x49,
+ "PilotWarningSolenoidShort": 0x4a,
+ "PilotJoystickFault": 0x4b,
+ "PilotNoJoystickData": 0x4c,
+ "notassigned": 0x4d,
+ "notassigned": 0x4e,
+ "notassigned": 0x4f,
+ "PilotInvalidCommand": 0x50,
+ "AISTXMalfunction": 0x51,
+ "AISAntennaVSWRfault": 0x52,
+ "AISRxchannel1malfunction": 0x53,
+ "AISRxchannel2malfunction": 0x54,
+ "AISNosensorpositioninuse": 0x55,
+ "AISNovalidSOGinformation": 0x56,
+ "AISNovalidCOGinformation": 0x57,
+ "AIS12Valarm": 0x58,
+ "AIS6Valarm": 0x59,
+ "AISNoisethresholdexceededchannelA": 0x5a,
+ "AISNoisethresholdexceededchannelB": 0x5b,
+ "AISTransmitterPAfault": 0x5c,
+ "AIS3V3alarm": 0x5d,
+ "AISRxchannel70malfunction": 0x5e,
+ "AISHeadinglost/invalid": 0x5f,
+ "AISinternalGPSlost": 0x60,
+ "AISNosensorposition": 0x61,
+ "AISLockfailure": 0x62,
+ "AISInternalGGAtimeout": 0x63,
+ "AISProtocolstackrestart": 0x64,
+ "PilotNoIPScommunications": 0x65,
+ "PilotPower-OnorSleep-SwitchResetWhileEngaged": 0x66,
+ "PilotUnexpectedResetWhileEngaged": 0x67,
+ "AISDangerousTarget": 0x68,
+ "AISLostTarget": 0x69,
+ "AISSafetyRelatedMessage(usedtosilence)": 0x6a,
+ "AISConnectionLost": 0x6b,
+ "NoFix": 0x6c
+};
+const key_command = "%s,7,126720,%s,%s,22,3b,9f,f0,81,86,21,%s,ff,ff,ff,ff,ff,c1,c2,cd,66,80,d3,42,b1,c8"
const heading_command = "%s,3,126208,%s,%s,14,01,50,ff,00,f8,03,01,3b,07,03,04,06,%s,%s"
const wind_direction_command = "%s,3,126208,%s,%s,14,01,41,ff,00,f8,03,01,3b,07,03,04,04,%s,%s"
const raymarine_ttw_Mode = "%s,3,126208,%s,%s,17,01,63,ff,00,f8,04,01,3b,07,03,04,04,81,01,05,ff,ff"
@@ -176,6 +303,31 @@ function setState(app, deviceid, command_json)
return [util.format(state_commands[state], (new Date()).toISOString(), default_src, deviceid)]
}
+function tackTo(app, deviceid, command_json)
+{
+ var tackTo = command_json["value"]
+ app.debug("tackTo: " + tackTo)
+ if (tackTo === "port")
+ {
+ return [util.format(key_command, (new Date()).toISOString(), default_src, everyone_dst, keys_code["-1-10"])]
+ }
+ else if (tackTo === "starboard")
+ {
+ return [util.format(key_command, (new Date()).toISOString(), default_src, everyone_dst, keys_code["+1+10"])]
+ }
+ else
+ {
+ app.debug("tackTo: unknown " + tackTo)
+ }
+}
+
+function changeHeadingByKey(app, deviceid, command_json)
+{
+ var key = command_json["value"]
+ app.debug("changeHeadingByKey: " + key)
+ return [util.format(key_command, (new Date()).toISOString(), default_src, everyone_dst, keys_code[key])]
+}
+
function advanceWaypoint(app, deviceid, command_json)
{
return [util.format(raymarine_ttw_Mode, (new Date()).toISOString(),
@@ -186,6 +338,17 @@ function advanceWaypoint(app, deviceid, command_json)
function silenceAlarm(app, deviceid, command_json)
{
+ if (typeof command_json.value.signalkPath !== 'undefined') {
+ var path = command_json.value.signalkPath.split('.')
+ if (path.length > 1) {
+ var groupId = raymarineAlarmGroupCodes[path[path.length - 2]] || -1
+ var alarmId = alarmsId[path[path.length - 1]] || -1
+ if ((groupId !== -1) && (alarmId !== -1)) {
+ command_json.value.groupId = groupId
+ command_json.value.alarmId = alarmId
+ }
+ }
+ }
return [ util.format(raymarine_silence, (new Date()).toISOString(),
default_src, padd(command_json.value.alarmId.toString(16),2),
padd(command_json.value.groupId.toString(16),2)) ]
@@ -212,6 +375,14 @@ function sendCommand(app, deviceid, command_json)
{
n2k_msgs = silenceAlarm(app, deviceid, command_json)
}
+ else if ( action == "tackTo" )
+ {
+ n2k_msgs = tackTo(app, deviceid, command_json)
+ }
+ else if ( action == "changeHeadingByKey" )
+ {
+ n2k_msgs = changeHeadingByKey(app, deviceid, command_json)
+ }
if ( n2k_msgs )
{
app.debug("n2k_msg: " + n2k_msgs)
diff --git a/package.json b/package.json
index 2aeb452..af37038 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"test": "$NODE $npm_package_main"
},
"keywords": [
- "signalk-node-server-plugin"
+ "signalk-node-server-plugin",
+ "signalk-webapp"
],
"author": "scott@scottbender.net",
"license": "ISC",
diff --git a/public/css/rayremote.css b/public/css/rayremote.css
new file mode 100644
index 0000000..d103634
--- /dev/null
+++ b/public/css/rayremote.css
@@ -0,0 +1,445 @@
+body,
+html {
+ margin: 0;
+ padding: 0;
+}
+
+#main {
+ margin: auto;
+ width: 100vw;
+ height: 100vh;
+ background-repeat: no-repeat;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+.bgRemoteBig {
+ margin: auto;
+ width: 238px;
+ height: 612px;
+ position: relative;
+ background-image: url("../img/bgRemoteBig.png");
+}
+
+.btnCircleWithDotRed {
+ width: 70px;
+ height: 70px;
+ background-image: url("../img/btnCircleWithDotRed.png");
+}
+.btnCircleWithDotRed:active {
+ background-position-x: -70px;
+}
+
+.btnCircleRed {
+ width: 70px;
+ height: 70px;
+ background-image: url("../img/btnCircleRed.png");
+}
+.btnCircleRed:active {
+ background-position-x: -70px;
+}
+
+.btnLeftBlack {
+ width: 50px;
+ height: 54px;
+ background-image: url("../img/btnLeftBlack.png");
+ background-size: 100px;
+}
+.btnLeftBlack:active {
+ background-position-x: -50px;
+}
+
+.btnRightBlack {
+ width: 50px;
+ height: 54px;
+ background-image: url("../img/btnRightBlack.png");
+ background-size: 100px;
+}
+.btnRightBlack:active {
+ background-position-x: -50px;
+}
+
+.btnTopBlack {
+ width: 54px;
+ height: 50px;
+ left: 90px;
+ top: 280px;
+ background-image: url("../img/btnTopBlack.png");
+ background-size: 55px;
+}
+.btnTopBlack:active {
+ background-position-y: -50px;
+}
+
+.btnBottomBlack {
+ width: 54px;
+ height: 50px;
+ left: 90px;
+ top: 378px;
+ background-image: url("../img/btnBottomBlack.png");
+ background-size: 55px;
+}
+.btnBottomBlack:active {
+ background-position-y: -50px;
+}
+
+.btnTopSmallBlack {
+ width: 52px;
+ height: 38px;
+ background-image: url("../img/btnTopSmallBlack.png");
+ background-size: 104px;
+}
+.btnTopSmallBlack:active {
+ background-position-x: -52px;
+}
+
+.btnBottomSmallBlack {
+ width: 52px;
+ height: 38px;
+ background-image: url("../img/btnBottomSmallBlack.png");
+ background-size: 104px;
+}
+.btnBottomSmallBlack:active {
+ background-position-x: -52px;
+}
+
+.defaultTextFontKey {
+ width: 100%;
+ color: white;
+ position: absolute;
+ text-align: center;
+ font-size: 20px;
+ font-weight: bold;
+}
+
+.defaultKey {
+ cursor: pointer;
+ position: absolute;
+}
+
+#logoSignalK {
+ top: 54px;
+ left: 44px;
+ width: 16px;
+ height: 16px;
+ position: absolute;
+ background-image: url("../img/signalK_16x16.png");
+ background-size: 16px;
+ background-color: white;
+}
+
+#logoText {
+ top: 54px;
+ left: 66px;
+ width: 100px;
+ height: 18px;
+ position: absolute;
+ color: white;
+}
+
+#topBarIcon {
+ top: 94px;
+ left: 38px;
+ width: 155px;
+ height: 18px;
+ position: absolute;
+ background-repeat: no-repeat;
+ background-position-x: right;
+ background-color: initial;
+}
+
+#bottomBarIcon {
+ top: 190px;
+ left: 38px;
+ width: 155px;
+ height: 18px;
+ position: absolute;
+ background-repeat: no-repeat;
+ background-position-x: right;
+ background-color: initial;
+ white-space: nowrap;
+ overflow: hidden;
+ color: red;
+ text-overflow: ellipsis;
+ text-align: center;
+}
+
+.icon18x18 {
+ width: 18px;
+ height: 18px;
+ position: absolute;
+ visibility: visible;
+}
+
+#powerOnIcon {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAeklEQVR4AdXQsQ2CUBgE4K/RxM64BNqxicQlYAMdQaZgAV0B/9AxDzUPW8ibgO/aKy5n7x46vRBKpRB6ncpKazGKTWkwWbQAheQld/KVFEBtdgSxClzMaqCRnJGXDmYNcJN8nOSekivA22IybIaHcT0cKp1fdsHdvv0B0+E2g9R6Y0QAAAAASUVORK5CYII=);
+ left: 140px;
+ visibility: hidden;
+}
+
+#powerOffIcon {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAuklEQVR4Aa3NgUZDYRzG4ac0WkJCNxAS6ICQIgQQu4MQCgyAgAiKAAhhN1AGA8M2u5ddwWzGnPONPz7APtgPwON97bRDHw4UurYyKLM7yzLb8y1lVpmYGOnpIPcjmYnTjKbmki+AK8mvffdx2gLQ9q9xAXTVjsFMMtRSGYFTtRfgVeME4mYR7BkcqXWBS40/bcCtVTB41zgH+JTMTWOp8iROPVh7Q66jZyyQKp+e2dqNZTBl1qfMHu24DRTyPk1WGp22AAAAAElFTkSuQmCC);
+ left: 140px;
+}
+
+#sendIcon {
+ left: 120px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAyklEQVQoz8XOIU4DARCF4Y+iIO02IeAxCERFRRWFW+DgAAjugK8gNXCFUlQNWHQNBDD1yGZTgmw3g9jNssmGSphR8+bN/I8/rc2aMnDkaf3RQAhX6ywtUyFMtX6z7IFDB5WpBvp0Xk5nvurQxEwIC89eLIQwk1Qtu2i7lYmiV260i00BmuuBpr5LF/qaoGeeQxPvwsrYsUb5u+HEnUx4y6FbxgUk9eDa0KO0UEa2f1J1TSzLRCEsTXTz5UYl/o5THfsyH17dS/1PfQMplUPMhfqpHwAAAABJRU5ErkJggg==);
+ transform:scaleY(-1);
+}
+
+#receiveIcon {
+ left: 100px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAyklEQVQoz8XOIU4DARCF4Y+iIO02IeAxCERFRRWFW+DgAAjugK8gNXCFUlQNWHQNBDD1yGZTgmw3g9jNssmGSphR8+bN/I8/rc2aMnDkaf3RQAhX6ywtUyFMtX6z7IFDB5WpBvp0Xk5nvurQxEwIC89eLIQwk1Qtu2i7lYmiV260i00BmuuBpr5LF/qaoGeeQxPvwsrYsUb5u+HEnUx4y6FbxgUk9eDa0KO0UEa2f1J1TSzLRCEsTXTz5UYl/o5THfsyH17dS/1PfQMplUPMhfqpHwAAAABJRU5ErkJggg==);
+}
+
+#errorIcon {
+ left: 80px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAwElEQVR4AZXRtXUDMBQAwOvDjaEMwwBhWiE0RKiy9wiWYc4W3iSoGcKRS71n9qmU9FH7+izbtWVZr5pyHsR0PMiqMOjdp2MDYMCxT++GSfKCYILEhOBVHoAbf+YA+/YBc/5cA/T4dwagpATg1L8eYFU0XfPRlGgDKIi6az7qFhWAoqgTlTXRKSoC61I6gJRuPRV+XjPSWSqcK/9mVD6a8e+qcphjSMYEQZZkyJvKtTwbUiHvsdmCgV7Ldu1a1qtdZTafSCFQvo25AAAAAElFTkSuQmCC);
+}
+
+#notificationCounter {
+ left: 0px;
+ width: 35px;
+ background-repeat: no-repeat;
+ background-position: right;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAhElEQVR4Aa3MoQ3CUABF0UNwCAbAMAMTYJkDyRxI5kCyDYoNKqqbmkdFQ2j6+wXhXPvy/OY0VGftNbRWdRFxUbHVimhtLbrJ2I2yvV7GentFDxEQ8VBwlMkojqasPGejp5WJs8xGcfZlo5FCjY2PqxSf4mq00y2OOjvgLpXucBD1Dv7mDVf3cw8sx/MwAAAAAElFTkSuQmCC);
+}
+
+#notificationCounterText {
+ padding-right: 20px;
+ text-align: right;
+}
+
+#countDownCounter {
+ left: 36px;
+ text-align: center;
+}
+
+#headingValue {
+ top: 116px;
+ left: 38px;
+ width: 155px;
+ height: 34px;
+ overflow: hidden;
+ position: absolute;
+ text-align: center;
+ font-weight: bolder;
+ font-size: 30px;
+}
+
+#pilotStatus {
+ top: 150px;
+ left: 38px;
+ width: 155px;
+ height: 34px;
+ overflow: hidden;
+ position: absolute;
+ text-align: center;
+ text-transform: uppercase;
+ font-weight: bolder;
+ font-size: 30px;
+}
+
+.messageScreen {
+ position: absolute;
+ top: 112px;
+ left: 38px;
+ width: 158px;
+ height: 100px;
+ z-index: 1;
+ visibility: hidden;
+ background-color: #adc1b3;
+}
+
+#silenceScreen {
+ text-align: left;
+}
+
+#tackScreen {
+ text-align: center;
+ font-weight: bold;
+}
+
+#silenceScreenScroll{
+ position: relative;
+ top: 0px;
+ left: 0px;
+ height: 20px;
+ width: 150px;
+ background-repeat: no-repeat;
+ background-position: right;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAIElEQVR4AWMYGDAKJoAxgn2ACDyBmgaNem3Ua4TBKAAA2Kk2AbLe+1QAAAAASUVORK5CYII=);
+
+}
+
+#silenceScreenMute {
+ position: relative;
+ top: 0px;
+ left: 0px;
+ height: 20px;
+ width: 150px;
+ background-repeat: no-repeat;
+ background-position: right;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAvElEQVR4Aa3OAUcDARzG4adUpSAQECYRoEAium8QBH2D1AkcAZxKRkjQ56hhCIQADOwLDIANwGzGf5zDbHcD+70AD14rruHBbBeuLPQopIAmcuHfgbkyIcONwL4XQz2H1WzND+DMwK+ZTiUSrYJtI5UgFS4B+BPKyfCkb8+Wic9qFHKb2q7R1a5DIbPhCB3fdei5YOwaeZs/3iwRHwVb9+XEQu8lUjJQzdhxa7yckQstr8Kd2s4dg3sNK20KQZNRkQgu8yAAAAAASUVORK5CYII=);
+}
+
+#silenceScreenText {
+ padding-top: 4px;
+ text-align: center;
+ color: red;
+}
+
+#keyPlusOne {
+ top: 296px;
+ right: 20px;
+}
+#keyPlusOneText {
+ top: 16px;
+}
+
+#keyMinusOne {
+ top: 296px;
+ left: 20px;
+}
+#keyMinusOneText {
+ top: 16px;
+}
+
+#keyPlusTen {
+ top: 296px;
+ right: 68px;
+}
+#keyPlusTenText {
+ top: 16px;
+}
+
+#keyMinusTen {
+ top: 296px;
+ left: 68px;
+}
+#keyMinusTenText {
+ top: 16px;
+}
+
+#keyMute {
+ top: 362px;
+ left: 28px;
+}
+#keyMuteText {
+ top: 10px;
+ width: 20px;
+ height: 20px;
+ left: 16px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAy0lEQVR4AayRtUICUBRA7a7Jye5RF7u+wM4vsF2s0e7O37BzkZqZ+AF2WGk4XLqZeO/dPq/TUtuoZSki7mIgFloFlv3+icgOoKE8GlsH1sVOgOgy9jGjpzIuRjqv/rgNI3/hQCuD0t+8WK7Eywx6NfSGIAX+5t90CwOF5ODgNj4EO2TzxbDkdXwlgmCdLOolr+UlEbTn37QAK0fRBz8BAImvEYwMnmiJfdRzHxTAYoAQJjqPWWxJMf+3vHEAzCf+8A4avXYR9xSpm5QAaOvUDhZuhCsAAAAASUVORK5CYII=);
+ background-size: 20px;
+}
+
+#keyScroll {
+ top: 362px;
+ right: 28px;
+}
+#keyScrollText {
+ top: 10px;
+ width: 20px;
+ height: 20px;
+ left: 16px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAI0lEQVR4AWMYADAK/v//PwGEkdgHiMATqGnQqNdGvUYYjAIAXvgfJmV31egAAAAASUVORK5CYII=);
+ background-size: 20px;
+}
+
+#keyPower {
+ top: 500px;
+ right: 28px;
+}
+#keyPowerText {
+ top: 10px;
+ width: 20px;
+ height: 20px;
+ left: 16px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAw0lEQVR4AbTQgQbCQBzH8QI2Vhh7iQEJAoLWqxSwdxhApb1FgLG9wNkDbPQGA4gNU2zgGznudleADu7/v/vc/fnN/rdISX9fLuUuELJamCTkwVZHRDT4Oplzp2c9QRt6bjraAbE1LgZWCl154VjI4clFoZxalhLJuiZXqKD6FgEVhUInRlwrFJeRs2oj4GihA7DXD0o6QiO5jnL6KqBhIMH7dB4JAw2B+blPBrQIBC2Q4b+HPf7s/1f8XwuEFcAoogkAANy1lXzi6lAZAAAAAElFTkSuQmCC);
+ background-size: 20px;
+}
+
+#keyStandby {
+ top: 486px;
+ left: 84px;
+}
+#keyStandbyText {
+ top: 28px;
+ font-size: 10px;
+}
+
+#keyAuto {
+ top: 418px;
+ left: 26px;
+}
+#keyAutoText {
+ top: 28px;
+ font-size: 10px;
+}
+
+#keyWind {
+ top: 356px;
+ left: 84px;
+}
+#keyWindText {
+ top: 28px;
+ font-size: 10px;
+}
+
+#keyTrack {
+ top: 418px;
+ right: 26px;
+}
+#keyTrackText {
+ top: 28px;
+ font-size: 10px;
+}
+
+#tackLabel {
+ top: 250px;
+ font-size: 14px;
+}
+
+#keyTackPort {
+ top: 230px;
+ left: 38px;
+ z-index: 1;
+}
+#keyTackPortText {
+ top: 16px;
+ width: 25px;
+ height: 25px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAd0lEQVRIx73WSw6AIAwE0I5H9P53qTsTFYR+ZliXvJTPACw43N3NzABgt/bIANFapCYtunjXgg1sdVIFlkgH8It0AVOkExgi3cAHYQAPhAXcCBMIx0p26JZLtvGyIyy7jLJYkQWkLOplj5bs+a1C0Vg5SbX1r9EF/au4qJQe+D0AAAAASUVORK5CYII=);
+ left: 10px;
+}
+
+#keyTackStarbord {
+ top: 230px;
+ right: 38px;
+ z-index: 1;
+}
+#keyTackStarbordText {
+ top: 16px;
+ width: 25px;
+ height: 25px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAg0lEQVRIx81WQQ7AIAwCn+j//+IuW6IeZqmyjDOIqZYW7QYCULg9Sn9AVKQacRaRZNRgxR1M3EbMihUud8RRLndvGeHyRDlW3GN1f+NS6YmsEdXmyxgx0+WqUcEH+Ee57A9v/8L2ZrTHij0g7VFvH1r28es2GLYVAFVIiirlinMVenABXi3An6ky178AAAAASUVORK5CYII=);
+ right: 10px;
+}
+
+#keyHelp {
+ top: 500px;
+ left: 28px;
+}
+#keyHelpText {
+ top: 10px;
+ width: 20px;
+ height: 20px;
+ left: 16px;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAA+0lEQVR4AZSOMQhBYRSF7QkUmJRSeiWDwYPBaBa70WBjtY8WM2Fgt5ed7GXkSVklC5/bfa8/ZHFP/fc7p9Pf9f09hKjRFdUI/S4kWPA+S+LflQxH7oxIq0sL3SXJfP7iiCyhKB1RVMjSLGlKzHlQlZ3jDCBvTlxF0rlXIciTidKGIzHRga36MU+CbqkB2EoD6rqHXHXbQMst9YDA24Up9qyUAkDPDfuA31QsLuwIK/uBvhs3AduUptyIeFwEml+Hq8tTMDwxh4uZiSl7vGbtUUnSmaLaOA4nsspt2rqzOKLXxJHDXBlrtKjA5BGmrUCPYNxJJRcIQUmFVAAAZtDpcTPc14kAAAAASUVORK5CYII=);
+ background-size: 20px;
+}
diff --git a/public/img/bgRemoteBig.png b/public/img/bgRemoteBig.png
new file mode 100644
index 0000000..b93383f
Binary files /dev/null and b/public/img/bgRemoteBig.png differ
diff --git a/public/img/btnBottomBlack.png b/public/img/btnBottomBlack.png
new file mode 100644
index 0000000..391027b
Binary files /dev/null and b/public/img/btnBottomBlack.png differ
diff --git a/public/img/btnBottomSmallBlack.png b/public/img/btnBottomSmallBlack.png
new file mode 100644
index 0000000..9b47101
Binary files /dev/null and b/public/img/btnBottomSmallBlack.png differ
diff --git a/public/img/btnCircleRed.png b/public/img/btnCircleRed.png
new file mode 100644
index 0000000..d21a2c1
Binary files /dev/null and b/public/img/btnCircleRed.png differ
diff --git a/public/img/btnCircleWithDotRed.png b/public/img/btnCircleWithDotRed.png
new file mode 100644
index 0000000..4fbd312
Binary files /dev/null and b/public/img/btnCircleWithDotRed.png differ
diff --git a/public/img/btnLeftBlack.png b/public/img/btnLeftBlack.png
new file mode 100644
index 0000000..a78fabf
Binary files /dev/null and b/public/img/btnLeftBlack.png differ
diff --git a/public/img/btnRightBlack.png b/public/img/btnRightBlack.png
new file mode 100644
index 0000000..a6f1971
Binary files /dev/null and b/public/img/btnRightBlack.png differ
diff --git a/public/img/btnTopBlack.png b/public/img/btnTopBlack.png
new file mode 100644
index 0000000..7546179
Binary files /dev/null and b/public/img/btnTopBlack.png differ
diff --git a/public/img/btnTopSmallBlack.png b/public/img/btnTopSmallBlack.png
new file mode 100644
index 0000000..ebdcd69
Binary files /dev/null and b/public/img/btnTopSmallBlack.png differ
diff --git a/public/img/signalK_16x16.png b/public/img/signalK_16x16.png
new file mode 100644
index 0000000..3081078
Binary files /dev/null and b/public/img/signalK_16x16.png differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..0dce1bd
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+ SignalK RayRemote
+
+
+
+
+
+
+
+
Ray Remote
+
+
Loading Ray Remote...
+
+
+
+
+
Mute alarm press:
+
Next alarm press:
+
+
+
+
+
: TACK :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/rayremote.js b/public/js/rayremote.js
new file mode 100644
index 0000000..6701ec4
--- /dev/null
+++ b/public/js/rayremote.js
@@ -0,0 +1,433 @@
+/*
+ * Copyright 2019 Christian MOTELET
+ *
+ * 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.
+ */
+
+const commands = {
+ "auto": '{"action":"setState","value":"auto"}',
+ "wind": '{"action":"setState","value":"wind"}',
+ "route": '{"action":"setState","value":"route"}',
+ "standby": '{"action":"setState","value":"standby"}',
+ "+1": '{"action":"changeHeadingByKey","value":"+1"}',
+ "+10": '{"action":"changeHeadingByKey","value":"+10"}',
+ "-1": '{"action":"changeHeadingByKey","value":"-1"}',
+ "-10": '{"action":"changeHeadingByKey","value":"-10"}',
+ "tackToPort": '{"action":"tackTo","value":"port"}',
+ "tackToStarboard": '{"action":"tackTo","value":"starboard"}'
+}
+
+var notificationsArray = {};
+
+var touchEnd = function(event) {
+ event.currentTarget.onclick();
+ event.preventDefault(true);
+}
+
+var ws = null;
+var handlePilotStatusTimeout = null;
+var handleHeadindValueTimeout = null;
+var handleReceiveTimeout = null;
+var handleSilenceScreenTimeout = null;
+var handleConfirmTackTimeout = null;
+var handleCountDownCounterTimeout = null;
+var connected = false;
+var reconnect = true;
+const timeoutReconnect = 2000;
+const timeoutValue = 2000;
+const timeoutBlink = 500;
+//const noDataMessage = 'NO DATA';
+const noDataMessage = '-- -- -- --';
+var pilotStatusDiv = undefined;
+var headingValueDiv = undefined;
+var receiveIconDiv = undefined;
+var sendIconDiv = undefined;
+var errorIconDiv = undefined;
+var countDownCounterDiv = undefined;
+var powerOnIconDiv = undefined;
+var powerOffIconDiv = undefined;
+var bottomBarIconDiv = undefined;
+var notificationCounterDiv = undefined;
+var notificationCounterTextDiv = undefined;
+var silenceScreenDiv = undefined;
+var silenceScreenText = undefined;
+var tackScreenDiv = undefined;
+var skPathToAck = '';
+var tackConfirmed = false;
+var countDownValue = 0;
+
+var startUpRayRemote = function() {
+ pilotStatusDiv = document.getElementById('pilotStatus');
+ headingValueDiv = document.getElementById('headingValue');
+ receiveIconDiv = document.getElementById('receiveIcon');
+ sendIconDiv = document.getElementById('sendIcon');
+ errorIconDiv = document.getElementById('errorIcon');
+ powerOnIconDiv = document.getElementById('powerOnIcon');
+ powerOffIconDiv = document.getElementById('powerOffIcon');
+ bottomBarIconDiv = document.getElementById('bottomBarIcon');
+ notificationCounterDiv = document.getElementById('notificationCounter');
+ notificationCounterTextDiv = document.getElementById('notificationCounterText');
+ silenceScreenDiv = document.getElementById('silenceScreen');
+ silenceScreenTextDiv = document.getElementById('silenceScreenText');
+ tackScreenDiv = document.getElementById('tackScreen');
+ countDownCounterDiv = document.getElementById('countDownCounter');
+ setPilotStatus(noDataMessage);
+ setHeadindValue(noDataMessage);
+// demo(); return;
+ setTimeout(() => {
+ receiveIconDiv.style.visibility = 'hidden';
+ sendIconDiv.style.visibility = 'hidden';
+ errorIconDiv.style.visibility = 'hidden';
+ bottomBarIconDiv.style.visibility = 'hidden';
+ notificationCounterDiv.style.visibility = 'hidden';
+ countDownCounterDiv.innerHTML = '';
+ wsConnect();
+ }, 1500);
+}
+
+var demo = function () {
+ setHeadindValue(100);
+ setPilotStatus('WIND');
+ setNotificationMessage({"path":"notifications.autopilot.PilotWarningWindShift","value":{"state":"alarm","message":"Pilot Warning Wind Shift"}});
+ powerOffIconDiv.style.visibility = 'hidden';
+ powerOnIconDiv.style.visibility = 'visible';
+ countDownCounterDiv.innerHTML = '5';
+}
+
+var buildAndSendCommand = function(cmd) {
+ var cmdJson = commands[cmd];
+ if (((cmd === 'tackToPort')||(cmd === 'tackToStarboard')) && (tackConfirmed === false)) {
+ confirmTack(cmd);
+ return null;
+ }
+ if (typeof cmdJson !== 'undefined') {
+ if ((cmd === 'tackToPort')||(cmd === 'tackToStarboard')) {
+ countDownValue = 0;
+ updateCountDownCounter();
+ sendCommand(commands['auto']);
+ }
+ sendCommand(cmdJson);
+ } else {
+ alert('Unknown command !')
+ }
+ if (tackConfirmed) {
+ clearTimeout(handleConfirmTackTimeout);
+ tackScreenDiv.style.visibility = 'hidden';
+ tackScreenDiv.innerHTML = '';
+ tackConfirmed = false;
+ }
+}
+
+var sendCommand = function(cmdJson) {
+ errorIconDiv.style.visibility = 'hidden';
+ sendIconDiv.style.visibility = 'visible';
+ window.fetch('/plugins/raymarineautopilot/command', {
+ method: 'POST',
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: cmdJson,
+ }).then(function(response) {
+ setTimeout(() => {sendIconDiv.style.visibility = 'hidden';}, timeoutBlink);
+ if (response.status !== 200) {
+ errorIconDiv.style.visibility = 'visible';
+ if (response.status === 401) {
+ alert('You must be authenticated to send commands !')
+ } else {
+ errorIconDiv.style.visibility = 'visible';
+ alert('[' + response.status + ']' + response.text)
+ }
+ }
+ }, function(status) {
+ sendIconDiv.style.visibility = 'hidden';
+ errorIconDiv.style.visibility = 'visible';
+ alert(status.message)
+ }
+ );
+ reconnect = true;
+ wsConnect();
+}
+
+var notificationToValue = function (skPathToAck) {
+ var message = notificationsArray[skPathToAck];
+ if (typeof message === 'undefined') {
+ message = 'No current alarm...';
+ }
+ return message;
+}
+
+var sendSilence = function() {
+ if (silenceScreenDiv.style.visibility !== 'visible') {
+ silenceScreenDiv.style.visibility = 'visible';
+ autoHhideSilenceScreen();
+ if ((Object.keys(notificationsArray).length > 0) && (skPathToAck === '')) {
+ skPathToAck = Object.keys(notificationsArray)[0];
+ }
+ } else {
+ if (skPathToAck !== '') {
+ sendCommand('{"action":"silenceAlarm","value":{"signalkPath":"' + skPathToAck + '"}}');
+ }
+ countDownValue = 0;
+ updateCountDownCounter();
+ silenceScreenDiv.style.visibility = 'hidden';
+ }
+ silenceScreenTextDiv.innerHTML = notificationToValue(skPathToAck);
+}
+
+var notificationScroll = function() {
+ autoHhideSilenceScreen();
+ if (silenceScreenDiv.style.visibility !== 'visible') {
+ silenceScreenDiv.style.visibility = 'visible';
+ if ((Object.keys(notificationsArray).length > 0) && (skPathToAck === '')) {
+ skPathToAck = Object.keys(notificationsArray)[0];
+ }
+ } else {
+ skPathToAck = getNextNotification(skPathToAck);
+ }
+ silenceScreenTextDiv.innerHTML = notificationToValue(skPathToAck);
+}
+
+var autoHhideSilenceScreen = function() {
+ countDownValue = 5;
+ updateCountDownCounter();
+ clearTimeout(handleSilenceScreenTimeout);
+ handleSilenceScreenTimeout = setTimeout(() => {
+ silenceScreenDiv.style.visibility = 'hidden';
+ countDownValue = 0;
+ updateCountDownCounter();
+ }, 5000);
+}
+
+var getNextNotification = function(skPath) {
+ var notificationsKeys = Object.keys(notificationsArray);
+ var newSkPathToAck = '';
+ var index;
+ if (notificationsKeys.length > 0) {
+ if (typeof skPath !== 'undefined') {
+ index = notificationsKeys.indexOf(skPath) + 1;
+ } else {
+ index = 0;
+ }
+ if (notificationsKeys.length <= index) {
+ index = 0;
+ }
+ newSkPathToAck = notificationsKeys[index];
+ }
+ return newSkPathToAck;
+}
+
+var confirmTack = function(cmd) {
+ var message = 'Repeat same key
to confirm
tack to ';
+ tackConfirmed = true;
+ if (cmd === 'tackToPort') {
+ message += 'port';
+ } else if (cmd === 'tackToStarboard') {
+ message += 'starboard';
+ } else {
+ tackConfirmed = false;
+ return null;
+ }
+ countDownValue = 5;
+ updateCountDownCounter();
+ tackScreenDiv.innerHTML = '' + message + '
';
+ tackScreenDiv.style.visibility = 'visible';
+ clearTimeout(handleConfirmTackTimeout);
+ handleConfirmTackTimeout = setTimeout(() => {
+ tackScreenDiv.style.visibility = 'hidden';
+ tackScreenDiv.innerHTML = '';
+ tackConfirmed = false;
+ }, 5000);
+
+}
+
+var wsConnect = function() {
+ if (ws === null) {
+ try {
+ reconnect = true;
+ ws = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + "://" + window.location.host + "/signalk/v1/stream?subscribe=none");
+
+ ws.onopen = function() {
+ connected = true;
+ powerOffIconDiv.style.visibility = 'hidden';
+ powerOnIconDiv.style.visibility = 'visible';
+ errorIconDiv.style.visibility = 'hidden';
+ var subscriptionObject = {
+ "context": "vessels.self",
+ "subscribe": [
+ {
+ "path": "steering.autopilot.state",
+ "format": "delta",
+ "minPeriod": 900
+ },
+ {
+ "path": "navigation.headingMagnetic",
+ "format": "delta",
+ "minPeriod": 900
+ },
+ {
+ "path": "notifications.autopilot.*",
+ "format": "delta",
+ "minPeriod": 200
+ }
+ ]
+ };
+ var subscriptionMessage = JSON.stringify(subscriptionObject);
+ ws.send(subscriptionMessage);
+ handlePilotStatusTimeout = setTimeout(() => {setPilotStatus(noDataMessage)}, timeoutValue);
+ handleHeadindValueTimeout = setTimeout(() => {setHeadindValue(noDataMessage)}, timeoutValue);
+ }
+
+ ws.onclose = function() {
+ cleanOnClosed();
+ if (reconnect === true) {
+ setTimeout(() => {wsConnect()}, timeoutReconnect);
+ }
+ }
+
+ ws.onerror = function() {
+ console.log("ws error");
+ cleanOnClosed();
+ errorIconDiv.style.visibility = 'visible';
+ if (reconnect === true) {
+ setTimeout(() => {wsConnect()}, timeoutReconnect);
+ }
+ }
+
+ ws.onmessage = function(event) {
+ receiveIconDiv.style.visibility = 'visible';
+ clearTimeout(handleReceiveTimeout);
+ handleReceiveTimeout = setTimeout(() => {receiveIconDiv.style.visibility = 'hidden';}, timeoutBlink);
+ var jsonData = JSON.parse(event.data)
+ dispatchMessages(jsonData);
+ }
+
+ } catch (exception) {
+ console.error(exception);
+ cleanOnClosed();
+ errorIconDiv.style.visibility = 'visible';
+ setTimeout(() => {wsConnect()}, timeoutReconnect);
+ }
+ }
+}
+
+var dispatchMessages = function(jsonData) {
+ if (typeof jsonData.updates === 'object') {
+ jsonData.updates.forEach((update) => {
+ if (typeof update.values === 'object') {
+ update.values.forEach((value) => {
+ if (value.path === "steering.autopilot.state") {
+ clearTimeout(handlePilotStatusTimeout);
+ handlePilotStatusTimeout = setTimeout(() => {setPilotStatus(noDataMessage)}, timeoutValue);
+ setPilotStatus(value.value);
+ } else if (value.path === "navigation.headingMagnetic") {
+ clearTimeout(handleHeadindValueTimeout);
+ handleHeadindValueTimeout = setTimeout(() => {setHeadindValue(noDataMessage)}, timeoutValue);
+ setHeadindValue(Math.round(value.value * (180/Math.PI)));
+ } else if (value.path.startsWith("notifications.autopilot")) {
+ setNotificationMessage(value);
+ }
+ });
+ }
+ });
+ }
+}
+
+var setHeadindValue = function(value) {
+ if (value !== '') {
+ value = ((typeof value === 'undefined') || isNaN(value)) ? noDataMessage : 'Mag:' + value + '°';
+ }
+ headingValueDiv.innerHTML = value;
+}
+
+var setPilotStatus = function(value) {
+ if (typeof value === 'undefined') {
+ value = noDataMessage;
+ }
+ pilotStatusDiv.innerHTML = value;
+}
+
+var setNotificationMessage = function(value) {
+ if (typeof value.path !== 'undefined') {
+ value.path = value.path.replace('notifications.', '');
+ if (typeof value.value !== 'undefined') {
+ if (value.value.state === 'normal') {
+ delete notificationsArray[value.path]
+ } else {
+ notificationsArray[value.path] = value.value.message.replace('Pilot', '');
+ bottomBarIconDiv.style.visibility = 'visible';
+ bottomBarIconDiv.innerHTML = notificationsArray[value.path];
+ }
+ }
+ }
+ var alarmsCount = Object.keys(notificationsArray).length;
+ if (alarmsCount > 0) {
+ notificationCounterTextDiv.innerHTML = alarmsCount;
+ notificationCounterDiv.style.visibility = 'visible';
+ } else {
+ notificationCounterTextDiv.innerHTML = '';
+ notificationCounterDiv.style.visibility = 'hidden';
+ }
+}
+
+var displayHelp = function() {
+ bottomBarIconDiv.style.visibility = 'visible';
+ bottomBarIconDiv.innerHTML = ' Not yet implemented...'
+ setTimeout(() => {bottomBarIconDiv.style.visibility = 'hidden';}, 2000);
+}
+
+var wsOpenClose = function() {
+ if (connected === false) {
+ wsConnect();
+ } else {
+ reconnect = false;
+ if (ws !== null) {
+ ws.close();
+ }
+ cleanOnClosed();
+ }
+}
+
+var cleanOnClosed = function() {
+ ws = null;
+ connected = false;
+ receiveIconDiv.style.visibility = 'hidden';
+ sendIconDiv.style.visibility = 'hidden';
+ errorIconDiv.style.visibility = 'hidden';
+ bottomBarIconDiv.style.visibility = 'hidden';
+ notificationCounterDiv.style.visibility = 'hidden';
+ powerOffIconDiv.style.visibility = 'visible';
+ powerOnIconDiv.style.visibility = 'hidden';
+ notificationCounterDiv.style.visibility = 'hidden';
+ silenceScreenDiv.style.visibility = 'hidden';
+ notificationCounterTextDiv.innerHTML = '';
+ notificationsArray = {};
+ skPathToAck = '';
+ tackConfirmed = false;
+ clearTimeout(handleHeadindValueTimeout);
+ clearTimeout(handlePilotStatusTimeout);
+ setPilotStatus('');
+ setHeadindValue('');
+}
+
+var updateCountDownCounter = function() {
+ if (countDownValue > 0) {
+ clearTimeout(handleCountDownCounterTimeout);
+ countDownCounterDiv.innerHTML = countDownValue;
+ countDownValue -= 1;
+ handleCountDownCounterTimeout = setTimeout(() => {
+ updateCountDownCounter();
+ }, 1000);
+ } else {
+ clearTimeout(handleCountDownCounterTimeout);
+ countDownCounterDiv.innerHTML = '';
+ }
+}
diff --git a/public/js/scale2fit.js b/public/js/scale2fit.js
new file mode 100644
index 0000000..06ec48d
--- /dev/null
+++ b/public/js/scale2fit.js
@@ -0,0 +1,27 @@
+(function(global) {
+ /*
+ Simple functions to scale content to fit it's parent
+
+ Author: Liudmil Mitev
+ License: WTFPL
+ Demo: https://jsfiddle.net/oxzxyxqn/7/
+
+ */
+ function scaleAmountNeededToFit(el, margin = 0) {
+ const parentSize = {
+ width: el.parentElement.clientWidth - margin * 2,
+ height: el.parentElement.clientHeight - margin * 2
+ };
+
+ return Math.min(parentSize.width / el.clientWidth,
+ parentSize.height / el.clientHeight);
+ }
+
+ function fitToParent(element, margin) {
+ const scale = scaleAmountNeededToFit(element, margin);
+ element.style.transformOrigin = "0 0";
+ element.style.transform = `translate(${margin}px, ${margin}px) scale(${scale})`;
+ }
+
+ global.fitToParent = fitToParent;
+})(this);
\ No newline at end of file
diff --git a/small-rayremote.png b/small-rayremote.png
new file mode 100644
index 0000000..5e52c8f
Binary files /dev/null and b/small-rayremote.png differ