/// <summary> /// Internal Motor Rotation Handler /// Exclusive handler which controls all motor commands /// Responds to a flood of incoming motor commands by /// only processing the newest command. /// </summary> /// <param name="setMotorPower"></param> /// <returns></returns> public virtual IEnumerator<ITask> InternalMotorRotationHandler(SetMotorRotation setMotorPower) { try { #region Check for a backlog of motor commands // Take a snapshot of the number of pending commands at the time // we entered this routine. // This will prevent a livelock which can occur if we try to // process the queue until it is empty, but the inbound queue // is growing faster than we are pulling off the queue. int pendingCommands = _internalMotorRotationPort.ItemCount; // If newer commands have been issued, // respond fault to the older command and // move to the newer one. SetMotorRotation newerRotationRequest; Fault faultNewerCommand = Fault.FromException(new InvalidOperationException("A newer motor command was received before this command was processed.")); while (pendingCommands > 0) { if (_internalMotorRotationPort.Test(out newerRotationRequest)) { setMotorPower.ResponsePort.Post(faultNewerCommand); setMotorPower = newerRotationRequest; } pendingCommands--; } #endregion // Get a snapshot of our state. MotorState currentState = _state.Clone(); Fault faultResponse = null; int motorPower; long targetEncoderDegrees = 0; long targetRotationDegrees; CalculatePowerAndTargetDegrees(setMotorPower.Body, currentState.ReversePolarity, out motorPower, out targetRotationDegrees); #region Cancel any prior encoder target // If a prior encoder target was active, // signal that it is cancelled by a new Motor command. if (_targetEncoderPending) { lock (_targetEncoderReachedPort) { if (_targetEncoderPending) { LogVerbose(LogGroups.Console, string.Format("Cancel prior target! New TgtDegrees: {0}", targetRotationDegrees)); _targetEncoderReachedPort.Post(false); _targetEncoderPending = false; _state.TargetEncoderDegrees = 0; _state.CurrentEncoderTimeStamp = DateTime.Now; } } } #endregion _state.TargetPower = setMotorPower.Body.TargetPower; if (_state.TargetPower == 0) SendNotification<AllStop>(_subMgrPort, new AllStop(setMotorPower.Body.StopState)); // If rotating for a specific number of rotations, // set up the extended transaction and get the most recent encoder reading. if (targetRotationDegrees != 0) { #region If encoder reading is old, update the encoder now. if (DateTime.Now.Subtract(currentState.CurrentEncoderTimeStamp).TotalMilliseconds > 45) { LegoGetOutputState getEncoder = new LegoGetOutputState(currentState.MotorPort); getEncoder.TryCount = 2; yield return Arbiter.Choice(_legoBrickPort.SendNxtCommand(getEncoder), delegate(LegoResponse legoResponse) { LegoResponseGetOutputState encoderResponse = legoResponse as LegoResponseGetOutputState; if (encoderResponse == null) encoderResponse = new LegoResponseGetOutputState(legoResponse.CommandData); if (encoderResponse.Success) { int reversePolaritySign = (currentState.ReversePolarity) ? -1 : 1; _state.CurrentPower = ((double)(encoderResponse.PowerSetPoint * reversePolaritySign)) / 100.0; _state.CurrentEncoderTimeStamp = encoderResponse.TimeStamp; _state.CurrentEncoderDegrees = encoderResponse.EncoderCount * reversePolaritySign; _state.ResetableEncoderDegrees = encoderResponse.ResettableCount * reversePolaritySign; currentState = _state.Clone(); } else { LogError(LogGroups.Console, "Failed to set motor: " + legoResponse.ErrorCode.ToString()); faultResponse = Fault.FromException(new InvalidOperationException(legoResponse.ErrorCode.ToString())); } }, delegate(Fault fault) { LogError(LogGroups.Console, fault.ToString()); faultResponse = fault; }); if (faultResponse != null) { setMotorPower.ResponsePort.Post(faultResponse); yield break; } } #endregion // Calcuate a new target encoder which is based on the current encoder position. targetEncoderDegrees = targetRotationDegrees + currentState.CurrentEncoderDegrees; // A target of zero diables the PD control. Make sure our result isn't 0. if (targetEncoderDegrees == 0) targetEncoderDegrees++; LogVerbose(LogGroups.Console, string.Format("*** Target Degrees {0} Encoder {1}", targetRotationDegrees, targetEncoderDegrees)); _state.TargetEncoderDegrees = targetEncoderDegrees; _state.TargetStopState = setMotorPower.Body.StopState; lock (_targetEncoderReachedPort) { _targetEncoderPending = true; } } else { _state.TargetEncoderDegrees = 0; } LogVerbose(LogGroups.Console, string.Format("{0} SetMotorPower: {1} Encoder: {2} AdjPwr: {3} TgtRot: {4} TgtEnc {5}", DateTime.Now.ToString("HH:mm:ss.fffffff"), motorPower, currentState.CurrentEncoderDegrees, motorPower, targetRotationDegrees, targetEncoderDegrees)); #region Set up the LEGO Motor Command // If we aren't polling, let the motor regulate it's own distance. int motorRotationLimit = 0; if (targetRotationDegrees != 0 && (currentState.PollingFrequencyMs <= 0 || currentState.PollingFrequencyMs > 100)) motorRotationLimit = (int)targetRotationDegrees; LegoRegulationMode regulationMode = LegoRegulationMode.Individual; LegoOutputMode outputMode = LegoOutputMode.MotorOn | LegoOutputMode.Regulated; RunState powerAdjustment = (setMotorPower.Body.RampUp && targetRotationDegrees != 0) ? RunState.RampUp : RunState.Constant; // Are we stopping? if (setMotorPower.Body.TargetPower == 0.0) { motorRotationLimit = 0; switch (setMotorPower.Body.StopState) { case MotorStopState.Brake: outputMode = LegoOutputMode.MotorOn | LegoOutputMode.Regulated | LegoOutputMode.Brake; powerAdjustment = RunState.Constant; break; case MotorStopState.Coast: powerAdjustment = RunState.Idle; regulationMode = LegoRegulationMode.Idle; break; } } // Set up the LEGO motor command LegoSetOutputState motorCmd = new LegoSetOutputState( currentState.MotorPort, motorPower, outputMode, regulationMode, 0, /* Turn ratio */ powerAdjustment, motorRotationLimit); #endregion #region Send the LEGO Motor Command bool abort = false; LogVerbose(LogGroups.Console, motorCmd.ToString()); yield return Arbiter.Choice(_legoBrickPort.SendNxtCommand(motorCmd), delegate(LegoResponse response) { if (!response.Success) { LogError(LogGroups.Console, "Invalid SetMotorOutput " + response.ErrorCode.ToString()); abort = true; setMotorPower.ResponsePort.Post( Fault.FromException( new InvalidOperationException(response.ErrorCode.ToString()))); } }, delegate(Fault fault) { abort = true; LogError(LogGroups.Console, "Fault in SetMotorOutput " + fault.ToString()); setMotorPower.ResponsePort.Post(fault); }); #endregion if (abort) yield break; // Send a notification that the power has been set. SendNotification<SetMotorRotation>(_subMgrPort, setMotorPower); // Wait for the target encoder to either be cancelled or reach its target. if (_targetEncoderPending) { LogVerbose(LogGroups.Console, "Waiting for motor to reach target..."); // Wait for the target encoder to either be cancelled or reached. Activate(Arbiter.Receive<bool>(false, _targetEncoderReachedPort, delegate(bool finished) { RespondToMotorPowerRequest(setMotorPower.ResponsePort, finished); })); } else { RespondToMotorPowerRequest(setMotorPower.ResponsePort, true); } yield break; } finally { Activate(Arbiter.ReceiveWithIterator<SetMotorRotation>(false, _internalMotorRotationPort, InternalMotorRotationHandler)); } }
/// <summary> /// Handle periodic sensor readings from the pxbrick /// </summary> /// <param name="update"></param> private IEnumerator<ITask> NotificationHandler(brick.LegoSensorUpdate update) { LegoResponseGetOutputState outputState = new LegoResponseGetOutputState(update.Body.CommandData); if (outputState.Success && _state.CurrentEncoderTimeStamp < outputState.TimeStamp) { LogVerbose(LogGroups.Console, outputState.ToString()); int reversePolaritySign = (_state.ReversePolarity) ? -1 : 1; _rpmCalcPort.Post(outputState); LogVerbose(LogGroups.Console, string.Format("{0} Encoder: RPM {1} AvgPoll {2}", outputState.TimeStamp.ToString("HH:mm:ss.fffffff"), _state.CurrentMotorRpm, _state.AvgEncoderPollingRateMs, outputState.RegulationMode )); long newEncoderDegrees = outputState.EncoderCount * reversePolaritySign; long newResettableEncoderDegrees = outputState.ResettableCount * reversePolaritySign; bool encodersChanged = (_state.ResetableEncoderDegrees != newResettableEncoderDegrees); // Update the current encoder readings as soon as possible. _state.CurrentPower = ((double)(outputState.PowerSetPoint * reversePolaritySign)) / 100.0; _state.CurrentEncoderTimeStamp = outputState.TimeStamp; _state.CurrentEncoderDegrees = newEncoderDegrees; _state.ResetableEncoderDegrees = newResettableEncoderDegrees; if (encodersChanged) { // Prepare the encoder notification pxencoder.UpdateTickCount tickNotification = new pxencoder.UpdateTickCount( new pxencoder.UpdateTickCountRequest(outputState.TimeStamp, (int)newResettableEncoderDegrees)); SendNotification<pxencoder.UpdateTickCount>(_subMgrPort, tickNotification); } // get a snapshot of the current state. MotorState currentState = _state.Clone(); #region Simple PD Control if (currentState.TargetEncoderDegrees != 0) { double direction = Math.Sign(currentState.TargetPower); double pwrtarget = currentState.TargetPower * direction; double enctarget = currentState.TargetEncoderDegrees * direction; double encoder = outputState.EncoderCount * direction; double rpm = (double)currentState.CurrentMotorRpm * direction; double msUntilNext = currentState.AvgEncoderPollingRateMs; // negative remaining means we have passed the target. double remaining = enctarget - encoder; int goodEnough = (msUntilNext < 100) ? 5 : (msUntilNext < 300) ? 20 : 45; bool targetReached = (remaining <= goodEnough); LogVerbose(LogGroups.Console, string.Format("UpdateState: TargetPower {5} CurrentEncoder {0} {1} Target {2} done? {3} RPM {4} AvgMs {6}", currentState.CurrentEncoderDegrees, currentState.CurrentEncoderTimeStamp, currentState.TargetEncoderDegrees, targetReached, currentState.CurrentMotorRpm, currentState.TargetPower, currentState.AvgEncoderPollingRateMs)); // If a prior encoder target was active, signal that it is cancelled. if (_targetEncoderPending && targetReached) { lock (_targetEncoderReachedPort) { if (_targetEncoderPending) { LogVerbose(LogGroups.Console, "Signal target is complete!"); _targetEncoderReachedPort.Post(true); _targetEncoderPending = false; } } } // How soon do we want to start slowing down? // This is a factor of how fast and for how long the motor has been building up inertia. double forwardLooking = (pwrtarget < 0.5) ? ((pwrtarget * 150.0) + 20) : ((pwrtarget * 200.0) + 20.0); double degreesAtNextNotification = forwardLooking; if (rpm != 0.0 && msUntilNext != 0.0) // The distance in degrees we will travel at this rpm by the next encoder reading. degreesAtNextNotification = (rpm * 360.0) * msUntilNext / 60000.0; // We are not up-to-speed, wait a little longer before stopping if (rpm < pwrtarget) { if (rpm > 0) forwardLooking *= 0.7; if (rpm < 0) LogVerbose(LogGroups.Console, string.Format("Traveling in the wrong direction: Pwr {0} Enc {1} Tgt {2} RPM {3} Direction {4} FwdLook: {5} Remain: {6}", pwrtarget, encoder, enctarget, rpm, direction, forwardLooking, remaining)); else LogVerbose(LogGroups.Console, string.Format("Not up to speed: Pwr {0} Enc {1} Tgt {2} RPM {3} Direction {4} FwdLook: {5} Remain: {6}", pwrtarget, encoder, enctarget, rpm, direction, forwardLooking, remaining)); } else { LogVerbose(LogGroups.Console, string.Format("Targeting: Pwr {0} Enc {1} Tgt {2} RPM {3} Direction {4} FwdLook: {5} Remain: {6}", pwrtarget, encoder, enctarget, rpm, direction, forwardLooking, remaining)); } if (remaining < Math.Max(degreesAtNextNotification, forwardLooking) || remaining < forwardLooking || rpm <= 0) { LegoOutputMode outputMode = LegoOutputMode.MotorOn | LegoOutputMode.Regulated; LegoRegulationMode regulationMode = LegoRegulationMode.Individual; RunState powerAdjustment = RunState.Constant; long tachoLimit = outputState.EncoderLimit; int powerSetPoint = outputState.PowerSetPoint * (int)direction; if (Math.Abs(remaining) < 5 && rpm == 0) { // If we are completely stopped, // set the brake/coast and disengage active monitoring // in the notification handler _state.TargetEncoderDegrees = 0; powerSetPoint = 0; if (currentState.TargetStopState == MotorStopState.Brake) { outputMode |= LegoOutputMode.Brake; } else { regulationMode = LegoRegulationMode.Idle; powerAdjustment = RunState.Idle; } } else if ( remaining >= 0 && ( (remaining < 100 && powerSetPoint > 0 && rpm > 30) || (remaining < goodEnough && rpm < 10))) { powerSetPoint = 0; if (remaining < (pwrtarget / 2.0)) outputMode |= LegoOutputMode.Brake; } else if (forwardLooking < degreesAtNextNotification && (rpm >= pwrtarget) /* and up-to-speed */) { // we need to stop before the next encoder reading powerSetPoint = 0; outputMode |= LegoOutputMode.Brake; // The ms until we reach the point where forwardlooking rotations are remaining. double msUntilWeShouldStop = (degreesAtNextNotification - forwardLooking) / ((rpm * 360.0) / 60000.0); yield return Timeout(msUntilWeShouldStop); } else if (remaining < 0) { // Overshot, reverse direction powerSetPoint = CalculateMaxMotorPower((long)-remaining, -currentState.TargetPower, rpm, degreesAtNextNotification) / 2; } else { powerSetPoint = CalculateMaxMotorPower((long)remaining, currentState.TargetPower, rpm, degreesAtNextNotification); } if (degreesAtNextNotification < remaining) powerAdjustment = RunState.RampDown; if ((powerSetPoint != outputState.PowerSetPoint) || (outputState.Mode != outputMode) || (rpm < pwrtarget)) { LogVerbose(LogGroups.Console, string.Format("Target: {0} Encoder: {1} New Power: {2}", currentState.TargetEncoderDegrees, newEncoderDegrees, powerSetPoint)); #region Send an interim LEGO Motor command LegoSetOutputState setOutputState = new LegoSetOutputState(currentState.MotorPort, powerSetPoint, outputMode, regulationMode, 0, /* turn ratio */ powerAdjustment, tachoLimit); setOutputState.RequireResponse = (rpm == 0.0); LogVerbose(LogGroups.Console, setOutputState.ToString()); yield return Arbiter.Choice(_legoBrickPort.SendNxtCommand(setOutputState), delegate(LegoResponse ok) { if (ok.Success) LogVerbose(LogGroups.Console, "Moto Power was set"); else LogError(LogGroups.Console, "Moto Power Failed: " + ok.ErrorCode.ToString()); }, delegate(Fault fail) { LogError(LogGroups.Console, "Moto Power Failed: " + fail.ToString()); }); #endregion } } else { LogVerbose(LogGroups.Console, string.Format("Remaining: {0} Current Power: {1} Forward Looking: {2}", remaining, currentState.TargetPower, forwardLooking)); } } #endregion } yield break; }
/// <summary> /// Process the most recent Drive Power command /// When complete, self activate for the next internal command /// </summary> /// <param name="driveDistance"></param> /// <returns></returns> public virtual IEnumerator<ITask> InternalDrivePowerHandler(DriveDistance driveDistance) { try { #region Check for a backlog of drive commands // Take a snapshot of the number of pending commands at the time // we entered this routine. // This will prevent a livelock which can occur if we try to // process the queue until it is empty, but the inbound queue // is growing at the same rate as we are pulling off the queue. int pendingCommands = _internalDrivePowerPort.ItemCount; // If newer commands have been issued, // respond success to the older command and // move to the newer one. DriveDistance newerUpdate; while (pendingCommands > 0) { if (_internalDrivePowerPort.Test(out newerUpdate)) { driveDistance = newerUpdate; } pendingCommands--; } #endregion // Get a snapshot of our state. DriveState currentState = _state.Clone(); #region Cancel any prior encoder target // If a prior encoder target was active, // signal that it is cancelled by a new Motor command. if (_targetEncoderPending[LEFT] || _targetEncoderPending[RIGHT]) { lock (_targetEncoderPending) { LogVerbose(LogGroups.Console, "Cancel prior target!"); if (_targetEncoderPending[LEFT]) { _targetEncoderReachedPort[LEFT].Post(false); _targetEncoderPending[LEFT] = false; } if (_targetEncoderPending[RIGHT]) { _targetEncoderReachedPort[RIGHT].Post(false); _targetEncoderPending[RIGHT] = false; } _state.RuntimeStatistics.LeftEncoderTarget = 0; _state.RuntimeStatistics.RightEncoderTarget = 0; } } #endregion // Calculate the motor power and target degrees for both motors. int leftMotorPower, rightMotorPower; long leftTargetEncoderDegrees, rightTargetEncoderDegrees; CalculatePowerAndTargetDegrees(driveDistance.Body, currentState, out leftMotorPower, out rightMotorPower, out leftTargetEncoderDegrees, out rightTargetEncoderDegrees); _state.RuntimeStatistics.LeftPowerCurrent = ((double)leftMotorPower) / 100.0; ; _state.RuntimeStatistics.LeftPowerTarget = driveDistance.Body.LeftPower; _state.RuntimeStatistics.RightPowerCurrent = ((double)rightMotorPower) / 100.0; _state.RuntimeStatistics.RightPowerTarget = driveDistance.Body.RightPower; LegoOutputMode leftOutputMode = (leftMotorPower == 0 || leftTargetEncoderDegrees != 0) ? LegoOutputMode.PowerBrake : LegoOutputMode.PowerRegulated; LegoOutputMode rightOutputMode = (rightMotorPower == 0 || rightTargetEncoderDegrees != 0) ? LegoOutputMode.PowerBrake : LegoOutputMode.PowerRegulated; if (leftTargetEncoderDegrees != 0) { // Calcuate a new target encoder which is based on the current encoder position. leftTargetEncoderDegrees += currentState.RuntimeStatistics.LeftEncoderCurrent; // A target of zero diables the PD control. Make sure our result isn't 0. if (leftTargetEncoderDegrees == 0) leftTargetEncoderDegrees++; _state.RuntimeStatistics.TargetStopState = driveDistance.Body.StopState; _targetEncoderPending[LEFT] = true; } _state.RuntimeStatistics.LeftEncoderTarget = leftTargetEncoderDegrees; if (rightTargetEncoderDegrees != 0) { // Calcuate a new target encoder which is based on the current encoder position. rightTargetEncoderDegrees += currentState.RuntimeStatistics.RightEncoderCurrent; // A target of zero diables the PD control. Make sure our result isn't 0. if (rightTargetEncoderDegrees == 0) rightTargetEncoderDegrees++; _state.RuntimeStatistics.TargetStopState = driveDistance.Body.StopState; _targetEncoderPending[RIGHT] = true; } _state.RuntimeStatistics.RightEncoderTarget = rightTargetEncoderDegrees; _state.TimeStamp = DateTime.Now; LegoRegulationMode syncMode = LegoRegulationMode.Individual; // Send the left motor command to the brick LegoSetOutputState motorCmd = new LegoSetOutputState( currentState.LeftWheel.MotorPort, leftMotorPower, leftOutputMode, syncMode, 0, // turnratio RunState.Constant, driveDistance.Body.LeftStopAtRotationDegrees); motorCmd.RequireResponse = false; LogVerbose(LogGroups.Console, motorCmd.ToString()); Fault fault = null; PortSet<LegoResponse, Fault> leftResponse = _legoBrickPort.SendNxtCommand(motorCmd); // Send the right motor comand to the brick motorCmd.MotorPort = currentState.RightWheel.MotorPort; motorCmd.PowerSetPoint = rightMotorPower; motorCmd.Mode = rightOutputMode; motorCmd.TurnRatio = 0; motorCmd.EncoderLimit = driveDistance.Body.RightStopAtRotationDegrees; LogVerbose(LogGroups.Console, motorCmd.ToString()); PortSet<LegoResponse, Fault> rightResponse = _legoBrickPort.SendNxtCommand(motorCmd); yield return Arbiter.Choice(leftResponse, delegate(LegoResponse response) { if (!response.Success) fault = Fault.FromException( new InvalidOperationException(response.ErrorCode.ToString())); }, delegate(Fault f) { fault = f; }); yield return Arbiter.Choice(rightResponse, delegate(LegoResponse response) { if (!response.Success) fault = Fault.FromException( new InvalidOperationException(response.ErrorCode.ToString())); }, delegate(Fault f) { fault = f; }); if (leftTargetEncoderDegrees != 0 && rightTargetEncoderDegrees != 0) { // Wait for the target encoders to either be cancelled or reach its target. Activate(Arbiter.MultiplePortReceive<bool>(false, _targetEncoderReachedPort, delegate(bool[] finished) { HandleDriveResponse(driveDistance, finished[LEFT] && finished[RIGHT], null); })); } else if (leftTargetEncoderDegrees != 0 || rightTargetEncoderDegrees != 0) { int ix = (leftTargetEncoderDegrees != 0) ? LEFT : RIGHT; // Wait for the target encoder to either be cancelled or reach its target. Activate(Arbiter.Receive<bool>(false, _targetEncoderReachedPort[ix], delegate(bool finished) { HandleDriveResponse(driveDistance, finished, null); })); } else { bool success = (fault == null); HandleDriveResponse(driveDistance, success, fault); } } finally { // Wait one time for the next InternalDrivePower command Activate(Arbiter.ReceiveWithIterator(false, _internalDrivePowerPort, InternalDrivePowerHandler)); } yield break; }