diff --git a/locales/en/messages.json b/locales/en/messages.json index ec0a89a6..faf48bf9 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -2537,6 +2537,40 @@ "message": "I understand the risks, the propellers are removed - enable motor control and arming, and disable Runaway Takeoff Prevention." }, + "motorOutputReorderDialogClose": { + "message": "Cancel" + }, + "motorOutputReorderDialogAgree": { + "message": "Start" + }, + "motorsRemapDialogTitle": { + "message": "Reorder motors" + }, + "motorOutputReorderDialogOpen": { + "message": "Reorder motors" + }, + "motorOutputReorderDialogSelectSpinningMotor": { + "message": "Click on the spinning motor..." + }, + "motorOutputReorderDialogRemapIsDone": { + "message": "Ready! Check the motors spinning order by clicking on the image." + }, + "motorsRemapDialogUnderstandRisks": { + "message": "I understand the risks,
the propellers are removed." + }, + "motorsRemapDialogRiskNotice": { + "message": "Safety notice
Remove all propellers to prevent injury!
The motors will spin up!" + }, + "motorsRemapDialogExplanations": { + "message": "Information notice
Motors will spin up one by one and you will be able to select which motor is spinning. The battery should be plugged in, correct ESC protocol should be selected. This utility can only re-arrange currently active motors. More complex re-mapping requires the CLI Resource command. Refer to this Wiki page." + }, + "motorsRemapDialogSave": { + "message": "Save" + }, + "motorsRemapDialogStartOver": { + "message": "Start over" + }, + "sensorsInfo": { "message": "Keep in mind that using fast update periods and rendering multiple graphs at the same time is resource heavy and will burn your battery quicker if you use a laptop.
We recommend to only render graphs for sensors you are interested in while using reasonable update periods." }, diff --git a/src/Components/MotorOutputReordering/Body.html b/src/Components/MotorOutputReordering/Body.html new file mode 100644 index 00000000..eeeed91b --- /dev/null +++ b/src/Components/MotorOutputReordering/Body.html @@ -0,0 +1,30 @@ +
+

+
+ +
+

+
+
+ + +
+
+
+
+

+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
diff --git a/src/Components/MotorOutputReordering/MotorOutputReorderingCanvas.js b/src/Components/MotorOutputReordering/MotorOutputReorderingCanvas.js new file mode 100644 index 00000000..24c1cd3a --- /dev/null +++ b/src/Components/MotorOutputReordering/MotorOutputReorderingCanvas.js @@ -0,0 +1,269 @@ +'use strict'; + +class MotorOutputReorderCanvas +{ + constructor(canvas, droneConfiguration, motorClickCallback, spinMotorCallback) + { + this._spinMotorCallback = spinMotorCallback; + this._canvas = canvas; + this._motorClickCallback = motorClickCallback; + this._width = this._canvas.width(); + this._height = this._canvas.height(); + this._screenSize = Math.min(this._width, this._height); + + this._config = new MotorOutputReorderConfig(this._screenSize); + + // no component resize allowing yet + this._canvas.prop({ + width: this._width, + height: this._height, + }); + + this._droneConfiguration = droneConfiguration; + + this._ctx = this._canvas[0].getContext("2d"); + this._ctx.translate(this._width / 2, this._height / 2); + + this._canvas.mousemove((event) => + { + this._onMouseMove(event); + }); + this._canvas.mouseleave(() => + { + this._onMouseLeave(); + }); + this._canvas.mousedown(() => + { + this._onMouseDown(); + }); + this._canvas.mouseup(() => + { + this._onMouseUp(event); + }); + this._canvas.click(() => + { + this._onMouseClick(); + }); + + this.startOver(); + } + + pause() + { + this._keepDrawing = false; + } + + startOver() + { + this.readyMotors = []; //motors that already being selected for remapping by user + this.remappingReady = false; + this._motorIndexToSpinOnMouseDown = -1; + this._keepDrawing = true; + this._mouse = {x : 0, y: 0}; + window.requestAnimationFrame(() => + { + this._drawOnce(); + }); + } + + _drawOnce() + { + this._ctx.clearRect(-this._width / 2, -this._height / 2, this._width, this._height); + + this._drawFrame(); + this._drawDirectionArrow(); + this._markMotors(); + this._drawMotors(); + + if (this._keepDrawing) { + window.requestAnimationFrame(() => + { + this._drawOnce(); + }); + } + } + + _onMouseDown() + { + if (this.remappingReady) { + this._motorIndexToSpinOnMouseDown = this._getMouseHoverMotorIndex(); + + if (this._spinMotorCallback) { + this._spinMotorCallback(this._motorIndexToSpinOnMouseDown); + } + } + } + + _onMouseUp() + { + if (-1 !== this._motorIndexToSpinOnMouseDown) { + this._motorIndexToSpinOnMouseDown = -1; + + if (this._spinMotorCallback) { + this._spinMotorCallback(this._motorIndexToSpinOnMouseDown); + } + } + } + + _onMouseClick() + { + const motorIndex = this._getMouseHoverMotorIndex(); + + if (this._motorClickCallback && -1 !== motorIndex && !this.readyMotors.includes(motorIndex)) { + this._motorClickCallback(motorIndex); + } + } + + _onMouseMove(event) + { + const boundingRect = this._canvas[0].getBoundingClientRect(); + this._mouse.x = event.clientX - boundingRect.left - this._width / 2; + this._mouse.y = event.clientY - boundingRect.top - this._height / 2; + } + + _onMouseLeave() + { + this._mouse.x = Number.MIN_SAFE_INTEGER; + this._mouse.y = Number.MIN_SAFE_INTEGER; + + if (-1 !== this._motorIndexToSpinOnMouseDown) { + this._motorIndexToSpinOnMouseDown = -1; + + if (this._spinMotorCallback) { + this._spinMotorCallback(this._motorIndexToSpinOnMouseDown); + } + } + } + + _markMotors() + { + const motors = this._config[this._droneConfiguration].Motors; + const mouseHoverMotorIndex = this._getMouseHoverMotorIndex(); + + if (-1 === this._motorIndexToSpinOnMouseDown) { + for (let i = 0; i < this.readyMotors.length; i++) { + const motorIndex = this.readyMotors[i]; + this._ctx.beginPath(); + this._ctx.arc(motors[motorIndex].x, motors[motorIndex].y, this._config[this._droneConfiguration].PropRadius, 0, 2 * Math.PI); + this._ctx.closePath(); + this._ctx.fillStyle = this._config.MotorReadyColor; + this._ctx.fill(); + } + + if (-1 !== mouseHoverMotorIndex && !this.readyMotors.includes(mouseHoverMotorIndex)) { + this._ctx.beginPath(); + this._ctx.arc(motors[mouseHoverMotorIndex].x, motors[mouseHoverMotorIndex].y, this._config[this._droneConfiguration].PropRadius, 0, 2 * Math.PI); + this._ctx.closePath(); + this._ctx.fillStyle = this._config.MotorMouseHoverColor; + this._ctx.fill(); + } + } else { + const spinningMotor = this._motorIndexToSpinOnMouseDown; + + for (let i = 0; i < motors.length; i++) { + this._ctx.fillStyle = this._config.MotorReadyColor; + if (spinningMotor === i) { + this._ctx.fillStyle = this._config.MotorSpinningColor; + } else if (mouseHoverMotorIndex === i) { + this._ctx.fillStyle = this._config.MotorMouseHoverColor; + } + + this._ctx.beginPath(); + this._ctx.arc(motors[i].x, motors[i].y, this._config[this._droneConfiguration].PropRadius, 0, 2 * Math.PI); + this._ctx.closePath(); + this._ctx.fill(); + } + } + } + + _getMouseHoverMotorIndex() + { + const x = this._mouse.x; + const y = this._mouse.y; + + let result = -1; + let currentDist = Number.MAX_SAFE_INTEGER; + const motors = this._config[this._droneConfiguration].Motors; + + for (let i = 0; i < motors.length; i++) { + const dist = Math.sqrt((x - motors[i].x) * (x - motors[i].x) + (y - motors[i].y) * (y - motors[i].y)); + + if (dist < this._config[this._droneConfiguration].PropRadius && dist < currentDist) { + currentDist = dist; + result = i; + } + } + + return result; + } + + _drawMotors() + { + + this._ctx.lineWidth = this._config.PropEdgeLineWidth; + this._ctx.strokeStyle = this._config.PropEdgeColor; + const motors = this._config[this._droneConfiguration].Motors; + + for (let i = 0; i < motors.length; i++) { + this._ctx.beginPath(); + this._ctx.arc(motors[i].x, motors[i].y, this._config[this._droneConfiguration].PropRadius, 0, 2 * Math.PI); + this._ctx.stroke(); + } + } + + _drawDirectionArrow() + { + this._ctx.beginPath(); + this._ctx.moveTo(this._config.DirectionArrowPoints[0].x, this._config.DirectionArrowPoints[0].y); + + for (let i = 1; i < this._config.DirectionArrowPoints.length; i++) { + this._ctx.lineTo(this._config.DirectionArrowPoints[i].x, this._config.DirectionArrowPoints[i].y); + } + + this._ctx.closePath(); + this._ctx.fillStyle = this._config.ArrowColor; + this._ctx.fill(); + } + + _drawFrame() + { + this._ctx.beginPath(); + this._ctx.lineWidth = this._config[this._droneConfiguration].ArmWidth; + this._ctx.lineCap = "round"; + this._ctx.strokeStyle = this._config.FrameColor; + const motors = this._config[this._droneConfiguration].Motors; + + switch(this._droneConfiguration) { + case "Quad X": + case "Quad +": + this._ctx.moveTo(motors[0].x, motors[0].y); + this._ctx.lineTo(motors[3].x, motors[3].y); + this._ctx.moveTo(motors[1].x, motors[1].y); + this._ctx.lineTo(motors[2].x, motors[2].y); + break; + case "Quad X 1234": + this._ctx.moveTo(motors[0].x, motors[0].y); + this._ctx.lineTo(motors[2].x, motors[2].y); + this._ctx.moveTo(motors[3].x, motors[3].y); + this._ctx.lineTo(motors[1].x, motors[1].y); + break; + case "Tricopter": + this._ctx.moveTo(motors[1].x, motors[1].y); + this._ctx.lineTo(motors[2].x, motors[2].y); + this._ctx.moveTo(motors[0].x, motors[0].y); + this._ctx.lineTo(motors[0].x, motors[2].y); + break; + case "Hex +": + case "Hex X": + this._ctx.moveTo(motors[0].x, motors[0].y); + this._ctx.lineTo(motors[3].x, motors[3].y); + this._ctx.moveTo(motors[1].x, motors[1].y); + this._ctx.lineTo(motors[2].x, motors[2].y); + this._ctx.moveTo(motors[4].x, motors[4].y); + this._ctx.lineTo(motors[5].x, motors[5].y); + break; + } + + this._ctx.stroke(); + } +} diff --git a/src/Components/MotorOutputReordering/MotorOutputReorderingComponent.js b/src/Components/MotorOutputReordering/MotorOutputReorderingComponent.js new file mode 100644 index 00000000..2f22573c --- /dev/null +++ b/src/Components/MotorOutputReordering/MotorOutputReorderingComponent.js @@ -0,0 +1,258 @@ +'use strict'; + +class MotorOutputReorderComponent +{ + constructor(contentDiv, onLoadedCallback, droneConfiguration, motorStopValue, motorSpinValue) + { + this._contentDiv = contentDiv; + this._onLoadedCallback = onLoadedCallback; + this._droneConfiguration = droneConfiguration; + this._motorStopValue = motorStopValue; + this._motorSpinValue = motorSpinValue; + this._config = new MotorOutputReorderConfig(100); + + this._currentJerkingTimeout = -1; + this._currentJerkingMotor = -1; + + this._currentSpinningMotor = -1; + + this._contentDiv.load("./Components/MotorOutputReordering/Body.html", () => + { + this._setupdialog(); + }); + } + + _readDom() + { + this._domAgreeSafetyCheckBox = $('#motorsEnableTestMode-dialogMotorOutputReorder'); + this._domAgreeButton = $('#dialogMotorOutputReorderAgreeButton'); + this._domStartOverButton = $('#motorsRemapDialogStartOver'); + this._domSaveButton = $('#motorsRemapDialogSave'); + this._domMainContentBlock = $('#dialogMotorOutputReorderMainContent'); + this._domWarningContentBlock = $('#dialogMotorOutputReorderWarning'); + this._domActionHintBlock = $('#motorOutputReorderActionHint'); + this._domCanvas = $('#motorOutputReorderCanvas'); + } + + _setupdialog() + { + i18n.localizePage(); + this._readDom(); + + this._resetGui(); + + this._domAgreeSafetyCheckBox.change(() => + { + const enabled = this._domAgreeSafetyCheckBox.is(':checked'); + this._domAgreeButton.toggle(enabled); + }); + + this._domAgreeButton.click(() => + { + this._onAgreeButtonClicked(); + }); + this._domStartOverButton.click(() => + { + this._startOver(); + }); + this._domSaveButton.click(() => + { + this._save(); + }); + + this._onLoadedCallback(); + } + + close() + { + this._stopAnyMotorJerking(); + this._stopMotor(); + this._stopUserInteraction(); + this._resetGui(); + } + + _resetGui() + { + this._domMainContentBlock.hide(); + this._domWarningContentBlock.show(); + this._domAgreeButton.hide(); + + this._domAgreeSafetyCheckBox.prop('checked', false); + this._domAgreeSafetyCheckBox.change(); + this._showSaveStartOverButtons(false); + } + + _save() + { + function save_to_eeprom() + { + MSP.send_message(MSPCodes.MSP_EEPROM_WRITE, false, false, reboot); + } + + function reboot() + { + GUI.log(i18n.getMessage('configurationEepromSaved')); + + GUI.tab_switch_cleanup(function() + { + MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false); + reinitialiseConnection(self); + }); + } + + FC.MOTOR_OUTPUT_ORDER = Array.from(this._newMotorOutputReorder); + + MSP.send_message(MSPCodes.MSP2_SET_MOTOR_OUTPUT_REORDERING, mspHelper.crunch(MSPCodes.MSP2_SET_MOTOR_OUTPUT_REORDERING)); + + save_to_eeprom(); + } + + _calculateNewMotorOutputReorder() + { + this._newMotorOutputReorder = []; + + for (let i = 0; i < this.motorOutputReorderCanvas.readyMotors.length; i++) { + this._newMotorOutputReorder.push(this._remapMotorIndex(i)); + } + } + + _remapMotorIndex(motorIndex) + { + return FC.MOTOR_OUTPUT_ORDER[this.motorOutputReorderCanvas.readyMotors.indexOf(motorIndex)]; + } + + _startOver() + { + this._showSaveStartOverButtons(false); + this._startUserInteraction(); + } + + _showSaveStartOverButtons(show) + { + if (show) { + this._domStartOverButton.show(); + this._domSaveButton.show(); + } else { + this._domStartOverButton.hide(); + this._domSaveButton.hide(); + } + } + + _onAgreeButtonClicked() + { + this._domActionHintBlock.text(i18n.getMessage("motorOutputReorderDialogSelectSpinningMotor")); + this._domWarningContentBlock.hide(); + this._domMainContentBlock.show(); + this._startUserInteraction(); + } + + _stopUserInteraction() + { + if (this.motorOutputReorderCanvas) { + this.motorOutputReorderCanvas.pause(); + } + } + + _startUserInteraction() + { + if (this.motorOutputReorderCanvas) { + this.motorOutputReorderCanvas.startOver(); + } else { + this.motorOutputReorderCanvas = new MotorOutputReorderCanvas(this._domCanvas, + this._droneConfiguration, + (motorIndex) => + { // motor click callback + this._onMotorClick(motorIndex); + }, + (motorIndex) => + { // motor spin callback + let indexToSpin = -1; + + if (-1 !== motorIndex) { + indexToSpin = this.motorOutputReorderCanvas.readyMotors.indexOf(motorIndex); + } + + this._spinMotor(indexToSpin); + }, + ); + } + + this._startMotorJerking(0); + } + + _stopAnyMotorJerking() + { + if (-1 !== this._currentJerkingTimeout) { + clearTimeout(this._currentJerkingTimeout); + this._currentJerkingTimeout = -1; + this._spinMotor(-1); + } + + this._currentJerkingMotor = -1; + } + + _startMotorJerking(motorIndex) + { + this._stopAnyMotorJerking(); + this._currentJerkingMotor = motorIndex; + this._motorStartTimeout(motorIndex); + } + + _motorStartTimeout(motorIndex) + { + this._spinMotor(motorIndex); + this._currentJerkingTimeout = setTimeout(() => + { + this._motorStopTimeout(motorIndex); + }, 250); + } + + _motorStopTimeout(motorIndex) + { + this._spinMotor(-1); + this._currentJerkingTimeout = setTimeout(() => + { + this._motorStartTimeout(motorIndex); + }, 500); + } + + + _spinMotor(motorIndex) + { + this._currentSpinningMotor = motorIndex; + const buffer = []; + + for (let i = 0; i < this._config[this._droneConfiguration].Motors.length; i++) { + if (i === motorIndex) { + buffer.push16(this._motorSpinValue); + } else { + buffer.push16(this._motorStopValue); + } + } + + MSP.send_message(MSPCodes.MSP_SET_MOTOR, buffer); + } + + _stopMotor() + { + if (-1 !== this._currentSpinningMotor) { + this._spinMotor(-1); + } + } + + _onMotorClick(motorIndex) + { + this.motorOutputReorderCanvas.readyMotors.push(motorIndex); + this._currentJerkingMotor ++; + + if (this._currentJerkingMotor < this._config[this._droneConfiguration].Motors.length) { + this._startMotorJerking(this._currentJerkingMotor); + } else { + this._stopAnyMotorJerking(); + this._domActionHintBlock.text(i18n.getMessage("motorOutputReorderDialogRemapIsDone")); + this._calculateNewMotorOutputReorder(); + this.motorOutputReorderCanvas.remappingReady = true; + this._showSaveStartOverButtons(true); + } + } +} diff --git a/src/Components/MotorOutputReordering/MotorOutputReorderingConfig.js b/src/Components/MotorOutputReordering/MotorOutputReorderingConfig.js new file mode 100644 index 00000000..d83330ad --- /dev/null +++ b/src/Components/MotorOutputReordering/MotorOutputReorderingConfig.js @@ -0,0 +1,140 @@ +'use strict'; + +function MotorOutputReorderConfig(screenSize) +{ + this.FrameColor = 'rgb(186, 186, 186)'; + this.PropEdgeColor = 'rgb(255, 187, 0)'; + this.PropEdgeLineWidth = 3; + this.MotorNumberTextFont = `${screenSize * 0.1}px 'Open Sans', 'Segoe UI', Tahoma, sans-serif`; + this.MotorNumberTextColor = 'rgb(0, 0, 0)'; + this.MotorMouseHoverColor = 'rgba(255, 187, 0, 0.4)'; + this.MotorSpinningColor = 'rgba(255, 0, 0, 0.4)'; + this.MotorReadyColor = 'rgba(0,128,0,0.4)'; + + this.ArrowColor = 'rgb(182,67,67)'; + this.DirectionArrowPoints = [ + {x: -0.03 * screenSize, y: 0.11 * screenSize}, + {x: -0.03 * screenSize, y: -0.01 * screenSize}, + {x: -0.07 * screenSize, y: -0.01 * screenSize}, + {x: 0.0 * screenSize, y: -0.13 * screenSize}, + {x: 0.07 * screenSize, y: -0.01 * screenSize}, + {x: 0.03 * screenSize, y: -0.01 * screenSize}, + {x: 0.03 * screenSize, y: 0.11 * screenSize}, + ]; + + //=========================================== + let frameRaduis = 0.28 * screenSize; + this["Quad X"] = + { + PropRadius: 0.2 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: + [ + {x: frameRaduis, y: frameRaduis}, + {x: frameRaduis, y: -frameRaduis}, + {x: -frameRaduis, y: frameRaduis}, + {x: -frameRaduis, y: -frameRaduis}, + ], + }; + + //=========================================== + frameRaduis = 0.28 * screenSize; + this["Quad X 1234"] = + { + PropRadius: 0.2 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: + [ + {x: -frameRaduis, y: -frameRaduis}, + {x: frameRaduis, y: -frameRaduis}, + {x: frameRaduis, y: frameRaduis}, + {x: -frameRaduis, y: frameRaduis}, + ], + }; + + //=========================================== + frameRaduis = 0.32 * screenSize; + this["Quad +"] = + { + PropRadius: 0.15 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: + [ + {x: 0, y: frameRaduis}, + {x: frameRaduis, y: 0 }, + {x: -frameRaduis, y: 0 }, + {x: 0, y: -frameRaduis}, + ], + }; + + //=========================================== + frameRaduis = 0.30 * screenSize; + this["Tricopter"] = + { + PropRadius: 0.15 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: + [ + {x: 0, y: frameRaduis}, + {x: frameRaduis, y: -frameRaduis}, + {x: -frameRaduis, y: -frameRaduis}, + ], + }; + + //=========================================== + frameRaduis = 0.35 * screenSize; + this["Hex +"] = + { + PropRadius: 0.14 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: [], + }; + let dAngle = Math.PI / 3; + let angle = 0; + + angle = dAngle * 1; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + angle = dAngle * 2; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + angle = -dAngle * 1; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + angle = -dAngle * 2; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + angle = dAngle * 3; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + angle = dAngle * 0; + this["Hex +"].Motors.push({x: Math.sin(angle) * frameRaduis, y: Math.cos(angle) * frameRaduis}); + + //=========================================== + frameRaduis = 0.35 * screenSize; + this["Hex X"] = + { + PropRadius: 0.14 * screenSize, + ArmWidth: 0.1 * screenSize, + Motors: [], + }; + dAngle = Math.PI / 3; + + angle = dAngle * 1; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); + + angle = -dAngle * 1; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); + + angle = dAngle * 2; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); + + angle = -dAngle * 2; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); + + angle = dAngle * 0; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); + + angle = dAngle * 3; + this["Hex X"].Motors.push({x: Math.cos(angle) * frameRaduis, y: Math.sin(angle) * frameRaduis}); +} diff --git a/src/Components/MotorOutputReordering/Styles.css b/src/Components/MotorOutputReordering/Styles.css new file mode 100644 index 00000000..7afacb0c --- /dev/null +++ b/src/Components/MotorOutputReordering/Styles.css @@ -0,0 +1,72 @@ +.motorOutputReorderComponent { + display:flex; + height:100%; + flex-flow:column; +} + +.motorOutputReorderComponentHeader { + border-bottom: 1px solid var(--accent); + margin-bottom: 10px; + padding-bottom: 12px; +} + +#dialogMotorOutputReorderMainContent { + display:flex; + height:100%; + flex-flow:column; +} + +#dialogMotorOutputReorderWarning { + display:flex; + height:100%; + flex-flow:column; +} + +#motorOutputReorderCanvas { + width:100%; + flex-grow: 1; +} + +#motorOutputReorderActionPanel { + height: 46px; +} + +#dialogMotorOutputReorderSave { + margin-right: 0px; +} + +.motorsRemapToggleParentContainer { + display: flex; + margin-bottom: 1.5em; + margin-top: 1.5em; +} + +#motorOutputReorderActionHint { + margin-top: 1.0em; + display: inline-block; +} + +.motorsRemapToggleNarrow { + margin-right: 12px; + display: flex; + align-items: center; +} + +.motorsRemapToggleWide { + flex: 1; +} + +.motorsRemapDialogRiskNoticeText { + font-size: 1.2em; +} + +.motorsRemapDialogRExplanationText { + font-size: 1.0em; + margin-bottom: 1.5em; + margin-top: 1.5em; +} + +#motorOutputReorderSaveStartOverButtonsPanel { + position: absolute; + bottom: 4px; +} \ No newline at end of file diff --git a/src/css/tabs/motors.css b/src/css/tabs/motors.css index fa4ab8eb..e707f2d0 100644 --- a/src/css/tabs/motors.css +++ b/src/css/tabs/motors.css @@ -18,6 +18,27 @@ float: left; } +.tab-motors #dialogMotorOutputReorder-closebtn { + margin-right: 0px; + margin-bottom: 0px; +} + +.tab-motors #dialogMotorOutputReorder { + width: 400px; + height:440px +;} + +.tab-motors #dialogMotorOutputReorderContentWrapper { + display: flex; + flex-flow: column; + width: 100%; + height: 100%; +} + +.tab-motors #dialogMotorOutputReorderContent { + flex-grow: 1; +} + .tab-motors .mixerPreview img { width: 120px; height: 120px; @@ -424,4 +445,15 @@ .tab-motors .motor_testing .telemetry li { font-size: 6px; } + .tab-motors #dialogMotorOutputReorder { + position: fixed; + width: calc(100% - 2em); + bottom: 0; + top: 56px; + border-radius: unset; + border: none; + overflow: auto; + margin: 0; + height: auto; + } } diff --git a/src/js/fc.js b/src/js/fc.js index 210a3e9e..9ce73051 100644 --- a/src/js/fc.js +++ b/src/js/fc.js @@ -41,6 +41,7 @@ const FC = { MOTOR_3D_CONFIG: null, MOTOR_CONFIG: null, MOTOR_DATA: null, + MOTOR_OUTPUT_ORDER: null, MOTOR_TELEMETRY_DATA: null, MULTIPLE_MSP: null, PID: null, @@ -605,6 +606,8 @@ const FC = { vtxtable_powerlevel_label: 0, }; + this.MOTOR_OUTPUT_ORDER = []; + this.MULTIPLE_MSP = { msp_commands: [], }; diff --git a/src/js/msp/MSPCodes.js b/src/js/msp/MSPCodes.js index 3f1c929b..e5bbf54b 100644 --- a/src/js/msp/MSPCodes.js +++ b/src/js/msp/MSPCodes.js @@ -179,4 +179,6 @@ var MSPCodes = { // MSPv2 Betaflight specific MSP2_BETAFLIGHT_BIND: 0x3000, + MSP2_MOTOR_OUTPUT_REORDERING: 0x3001, + MSP2_SET_MOTOR_OUTPUT_REORDERING: 0x3002, }; diff --git a/src/js/msp/MSPHelper.js b/src/js/msp/MSPHelper.js index 355ec307..1f10bc19 100644 --- a/src/js/msp/MSPHelper.js +++ b/src/js/msp/MSPHelper.js @@ -147,6 +147,13 @@ MspHelper.prototype.process_data = function(dataHandler) { FC.MOTOR_DATA[i] = data.readU16(); } break; + case MSPCodes.MSP2_MOTOR_OUTPUT_REORDERING: + FC.MOTOR_OUTPUT_ORDER = []; + const arraySize = data.read8(); + for (let i = 0; i < arraySize; i++) { + FC.MOTOR_OUTPUT_ORDER[i] = data.readU8(); + } + break; case MSPCodes.MSP_MOTOR_TELEMETRY: var telemMotorCount = data.readU8(); for (let i = 0; i < telemMotorCount; i++) { @@ -1549,6 +1556,9 @@ MspHelper.prototype.process_data = function(dataHandler) { case MSPCodes.MSP_SET_RTC: console.log('Real time clock set'); break; + case MSPCodes.MSP2_SET_MOTOR_OUTPUT_REORDERING: + console.log('Motor output reordering set'); + break; case MSPCodes.MSP_MULTIPLE_MSP: @@ -2249,6 +2259,15 @@ MspHelper.prototype.crunch = function(code) { break; + case MSPCodes.MSP2_SET_MOTOR_OUTPUT_REORDERING: + + buffer.push8(FC.MOTOR_OUTPUT_ORDER.length); + for (let i = 0; i < FC.MOTOR_OUTPUT_ORDER.length; i++) { + buffer.push8(FC.MOTOR_OUTPUT_ORDER[i]); + } + + break; + default: return false; } diff --git a/src/js/tabs/motors.js b/src/js/tabs/motors.js index e55b1611..d79e807a 100644 --- a/src/js/tabs/motors.js +++ b/src/js/tabs/motors.js @@ -57,7 +57,11 @@ TABS.motors.initialize = function (callback) { } function load_esc_protocol() { - MSP.send_message(MSPCodes.MSP_ADVANCED_CONFIG, false, false, load_motor_data); + MSP.send_message(MSPCodes.MSP_ADVANCED_CONFIG, false, false, load_motor_output_reordering); + } + + function load_motor_output_reordering() { + MSP.send_message(MSPCodes.MSP2_MOTOR_OUTPUT_REORDERING, false, false, load_motor_data); } function load_motor_data() { @@ -222,6 +226,13 @@ TABS.motors.initialize = function (callback) { } $('.mixerPreview img').attr('src', './resources/motor_order/' + mixerList[mixer - 1].image + reverse + '.svg'); + + const motorOutputReorderConfig = new MotorOutputReorderConfig(100); + const domMotorOutputReorderDialogOpen = $('#motorOutputReorderDialogOpen'); + + const isMotorReorderingAvailable = (mixerList[mixer - 1].name in motorOutputReorderConfig) + && (FC.MOTOR_OUTPUT_ORDER) && (FC.MOTOR_OUTPUT_ORDER.length > 0); + domMotorOutputReorderDialogOpen.toggle(isMotorReorderingAvailable); } function process_html() { @@ -726,7 +737,50 @@ TABS.motors.initialize = function (callback) { // enable Status and Motor data pulling GUI.interval_add('motor_and_status_pull', get_status, 50, true); - GUI.content_ready(callback); + let zeroThrottleValue = rangeMin; + + if (self.feature3DEnabled) { + zeroThrottleValue = neutral3d; + } + + setup_motor_output_reordering_dialog(content_ready, zeroThrottleValue); + + function content_ready() { + GUI.content_ready(callback); + } + + GUI.content_ready(callback); + } + + function setup_motor_output_reordering_dialog(callbackFunction, zeroThrottleValue) + { + const domDialogMotorOutputReorder = $('#dialogMotorOutputReorder'); + + const motorOutputReorderComponent = new MotorOutputReorderComponent($('#dialogMotorOutputReorderContent'), + callbackFunction, mixerList[FC.MIXER_CONFIG.mixer - 1].name, + zeroThrottleValue, zeroThrottleValue + 200); + + $('#dialogMotorOutputReorder-closebtn').click(closeDialog); + + function closeDialog() + { + domDialogMotorOutputReorder[0].close(); + motorOutputReorderComponent.close(); + $(document).off("keydown", onDocumentKeyPress); + } + + function onDocumentKeyPress(event) + { + if (27 === event.which) { + closeDialog(); + } + } + + $('#motorOutputReorderDialogOpen').click(function() + { + $(document).on("keydown", onDocumentKeyPress); + domDialogMotorOutputReorder[0].showModal(); + }); } }; diff --git a/src/main.html b/src/main.html index 682cf83d..5c4fe13b 100644 --- a/src/main.html +++ b/src/main.html @@ -38,6 +38,7 @@ + @@ -135,6 +136,9 @@ + + + diff --git a/src/tabs/motors.html b/src/tabs/motors.html index 8e622737..db97ccbc 100644 --- a/src/tabs/motors.html +++ b/src/tabs/motors.html @@ -99,8 +99,8 @@ -
+ @@ -190,4 +190,14 @@
+ + +
+
+
+
+ +
+
+