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 + +![GUI help](./GUI-help.png "GUI help") + 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 @@ [![Greenkeeper badge](https://badges.greenkeeper.io/sbender9/signalk-raymarine-autopilot.svg)](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
+
+
+
88
+
+
8
+
+
+
+
+
+
+
 Loading Ray Remote...
+
+
+
+
+
 Mute alarm press:
+
 Next alarm press:
+
+
+
+
+
+
+
+
+
: TACK :
+
+
+
+
+
+
+
+
-1
+
+
+
-10
+
+
+
+10
+
+
+
+1
+
+
+
AUTO
+
+
+
WIND
+
+
+
TRACK
+
+
+
STANDBY
+
+ +
+
+
+
+
+ + 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