Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: proper servo center position calculation for servo ranges > 360deg #539

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class ServoBindingEditComponent implements IBindingsDetailsEditComponent<
).subscribe(([position, offset]) => {
// we temporarily store the result to use it later in the range read request
this.centerReadPositionResult = position;
const absolutePosition = transformRelativeDegToAbsoluteDeg(position - offset);
const absolutePosition = transformRelativeDegToAbsoluteDeg(position + offset);
if (this._form && this._form.controls.aposCenter.value !== absolutePosition) {
this._form.controls.aposCenter.setValue(absolutePosition);
this._form.controls.aposCenter.markAsDirty();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MotorServoEndState } from 'rxpoweredup';
import { Injectable } from '@angular/core';
import { ControlSchemeBindingType, getTranslationArcs } from '@app/shared-misc';
import { ControlSchemeBindingType, getTranslationArcs, transformRelativeDegToAbsoluteDeg } from '@app/shared-misc';
import {
AttachedIoPropsModel,
ControlSchemeServoBinding,
Expand All @@ -17,8 +17,6 @@ import { BindingInputExtractionResult } from '../i-binding-task-input-extractor'

@Injectable()
export class ServoBindingTaskPayloadBuilderService implements ITaskPayloadBuilder<ControlSchemeBindingType.Servo> {
private readonly snappingThreshold = 10;

public buildPayload(
binding: ControlSchemeServoBinding,
currentInput: BindingInputExtractionResult<ControlSchemeBindingType.Servo>,
Expand All @@ -29,26 +27,37 @@ export class ServoBindingTaskPayloadBuilderService implements ITaskPayloadBuilde
const cwInput = currentInput[ServoBindingInputAction.Cw];
const ccwInput = currentInput[ServoBindingInputAction.Ccw];

// If this is not the first task and there is no input - we do nothing
if (!cwInput && !ccwInput && !!previousTaskPayload) {
// If this is not the first task and there is no input - we do nothing
return null;
}

// without necessary data we can't do anything
if (!ioProps?.startupMotorPositionData || ioProps.motorEncoderOffset === null) {
// TODO: log an error
return null;
}

const startingAbsolutePosition = transformRelativeDegToAbsoluteDeg(ioProps.startupMotorPositionData.position + ioProps.motorEncoderOffset);

const translationPaths = getTranslationArcs(
ioProps?.motorEncoderOffset ?? 0,
this.getArcCenter(binding, ioProps)
startingAbsolutePosition,
this.getAposCenter(binding, ioProps)
);
const resultingCenter = translationPaths.cw < translationPaths.ccw ? translationPaths.cw : -translationPaths.ccw;

const resultingCenterPosition = translationPaths.cw < translationPaths.ccw
? ioProps.startupMotorPositionData.position + translationPaths.cw
: ioProps.startupMotorPositionData.position - translationPaths.ccw;

if (!cwInput && !ccwInput) {
// If there were no inputs and no previous task, we should center the servo
return this.composeResult(
resultingCenter,
resultingCenterPosition,
binding.speed,
binding.power,
binding.useAccelerationProfile,
binding.useDecelerationProfile,
Date.now()
0
);
}

Expand All @@ -57,26 +66,22 @@ export class ServoBindingTaskPayloadBuilderService implements ITaskPayloadBuilde
const cwValue = extractDirectionAwareInputValue(cwInput?.value ?? 0, cwInputDirection);
const ccwValue = extractDirectionAwareInputValue(ccwInput?.value ?? 0, ccwInputDirection);

const servoNonClampedInputValue = Math.abs(cwValue) - Math.abs(ccwValue);
const cumulativeInputValue = Math.abs(cwValue) - Math.abs(ccwValue);

// TODO: create a function to clamp the value
const servoInputValue = Math.max(-1, Math.min(1, servoNonClampedInputValue));
const clampedCumulativeInputValue = Math.max(-1, Math.min(1, cumulativeInputValue));

// Math.max will never return 0 here because there is at least one input.
// Null coalescing operator is used to avoid errors in case if any of the inputs being null
const inputTimestamp = Math.max(cwInput?.timestamp ?? 0, ccwInput?.timestamp ?? 0);

const arcSize = this.getArcSize(binding, ioProps);
const arcPosition = servoInputValue * arcSize / 2;
const servoRange = this.getServoRange(binding, ioProps);
const rangePosition = clampedCumulativeInputValue * servoRange / 2;

const targetAngle = arcPosition + resultingCenter;
const minAngle = resultingCenter - arcSize / 2;
const maxAngle = resultingCenter + arcSize / 2;

const snappedAngle = this.snapAngle(targetAngle, resultingCenter, minAngle, maxAngle);
const targetAngle = rangePosition + resultingCenterPosition;

return this.composeResult(
snappedAngle,
targetAngle,
binding.speed,
binding.power,
binding.useAccelerationProfile,
Expand Down Expand Up @@ -117,26 +122,15 @@ export class ServoBindingTaskPayloadBuilderService implements ITaskPayloadBuilde
power,
endState: MotorServoEndState.hold,
useAccelerationProfile,
useDecelerationProfile,
useDecelerationProfile
},
inputTimestamp
};
}

private snapAngle(
targetAngle: number,
arcCenter: number,
maxAngle: number,
minAngle: number
): number {
const snappedToZeroAngle = Math.abs(targetAngle - arcCenter) < this.snappingThreshold ? arcCenter : targetAngle;
const snappedToMaxAngle = Math.abs(snappedToZeroAngle - maxAngle) < this.snappingThreshold ? maxAngle : snappedToZeroAngle;
return Math.abs(snappedToMaxAngle - minAngle) < this.snappingThreshold ? minAngle : snappedToMaxAngle;
}

private getArcSize(
private getServoRange(
binding: ControlSchemeServoBinding,
ioProps: Omit<AttachedIoPropsModel, 'hubId' | 'portId'> | null,
ioProps: Omit<AttachedIoPropsModel, 'hubId' | 'portId'> | null
): number {
if (binding.calibrateOnStart) {
const range = ioProps?.startupServoCalibrationData?.range;
Expand All @@ -148,9 +142,9 @@ export class ServoBindingTaskPayloadBuilderService implements ITaskPayloadBuilde
return binding.range;
}

private getArcCenter(
private getAposCenter(
binding: ControlSchemeServoBinding,
ioProps: Omit<AttachedIoPropsModel, 'hubId' | 'portId'> | null,
ioProps: Omit<AttachedIoPropsModel, 'hubId' | 'portId'> | null
): number {
if (binding.calibrateOnStart) {
const center = ioProps?.startupServoCalibrationData?.aposCenter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ export class MotorPositionAdjustmentComponent implements OnChanges, OnDestroy {
this._canGoToZero$ = of(false);
return;
}
this._canExecuteStep$ = this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectIoCanOperateOutputPortModeName({
this._canExecuteStep$ = this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectIsIoSupportInputMode({
hubId: this.hubId,
portId: this.portId,
portModeName: PortModeName.position
}));
this._canGoToZero$ = this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectIoCanOperateOutputPortModeName({
this._canGoToZero$ = this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectIsIoSupportInputMode({
hubId: this.hubId,
portId: this.portId,
portModeName: PortModeName.absolutePosition
Expand Down
1 change: 1 addition & 0 deletions modules/store/src/lib/actions/attached-io-props.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const ATTACHED_IO_PROPS_ACTIONS = createActionGroup({
events: {
'motor encoder offset received': props<{ hubId: string; portId: number; offset: number }>(),
'startup servo calibration data received': props<{ hubId: string; portId: number; range: number; aposCenter: number }>(),
'startup motor position received': props<{ hubId: string; portId: number; position: number }>(),
'compensate pitch': props<{ hubId: string; portId: number; currentPitch: number }>(),
'compensate yaw': props<{ hubId: string; portId: number; currentYaw: number }>(),
'compensate roll': props<{ hubId: string; portId: number; currentRoll: number }>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const COMPOSE_TASKS_EFFECT = createEffect((
tasks
);
const shouldUpdateQueue = nextPendingTask !== pendingTask;
const isNextTaskMoreRecent = (nextPendingTask?.inputTimestamp ?? 0) > (currentTask?.inputTimestamp ?? 0);
const isNextTaskMoreRecent = (nextPendingTask?.inputTimestamp ?? -Infinity) > (currentTask?.inputTimestamp ?? -Infinity);
return {
hubId,
portId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { EMPTY, Observable, first, switchMap, tap } from 'rxjs';
import { Store } from '@ngrx/store';
import { PortModeName, ValueTransformers } from 'rxpoweredup';

import { ControlSchemeModel } from '../../../models';
import { HubStorageService } from '../../../hub-storage.service';
import { attachedIosIdFn } from '../../../reducers';
import { ATTACHED_IO_PROPS_ACTIONS } from '../../../actions';
import { ATTACHED_IO_PORT_MODE_INFO_SELECTORS } from '../../../selectors';

export function createPreRunMotorPositionQueryTasks(
scheme: ControlSchemeModel,
hubStorage: HubStorageService,
store: Store
): Array<Observable<unknown>> {
const uniqueIosMap = new Map<string, { hubId: string; portId: number }>();
scheme.bindings.forEach((binding) => {
uniqueIosMap.set(attachedIosIdFn(binding), { hubId: binding.hubId, portId: binding.portId });
});
const uniqueIos = Array.from(uniqueIosMap.values());

return uniqueIos.map(({ hubId, portId }) => {
return store.select(
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName({ hubId, portId, portModeName: PortModeName.position })
).pipe(
first(),
switchMap((portModeData) => {
if (portModeData === null) {
return EMPTY;
}
return hubStorage.get(hubId).ports.getPortValue(portId, portModeData.modeId, ValueTransformers.position);
}),
tap((position) => {
store.dispatch(ATTACHED_IO_PROPS_ACTIONS.startupMotorPositionReceived({ hubId, portId, position }));
}),
first()
);
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import { catchError, filter, forkJoin, map, of, switchMap, timeout } from 'rxjs';
import { catchError, concatWith, filter, forkJoin, last, map, of, switchMap, tap, timeout } from 'rxjs';
import { inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { APP_CONFIG, IAppConfig } from '@app/shared-misc';
Expand All @@ -14,6 +14,7 @@ import { createPreRunSetDecelerationProfileTasks } from './create-pre-run-set-de
import { createWidgetReadTasks } from './create-widget-read-tasks';
import { HubServoCalibrationFacadeService } from '../../../hub-facades';
import { IWidgetsReadTasksFactory, WIDGET_READ_TASKS_FACTORY } from './i-widgets-read-tasks-factory';
import { createPreRunMotorPositionQueryTasks } from './create-pre-run-motor-position-query-tasks';

export const PRE_RUN_SCHEME_EFFECT = createEffect((
actions: Actions = inject(Actions),
Expand All @@ -32,17 +33,21 @@ export const PRE_RUN_SCHEME_EFFECT = createEffect((
const combinedTasks = [
...createPreRunSetAccelerationProfileTasks(scheme, hubStorage),
...createPreRunSetDecelerationProfileTasks(scheme, hubStorage),
...createWidgetReadTasks(scheme, store, widgetReadTaskFactory)
...createWidgetReadTasks(scheme, store, widgetReadTaskFactory),
...createPreRunMotorPositionQueryTasks(scheme, hubStorage, store)
];
// TODO: move to Bindings module
const calibrationServoTasks = createPreRunServoCalibrationTasks(scheme, hubCalibrationFacade, store, appConfig);
if (calibrationServoTasks.length > 0) {
combinedTasks.push(forkJoin(calibrationServoTasks));
}
if (combinedTasks.length === 0) {
if (combinedTasks.length + calibrationServoTasks.length === 0) {
return of(CONTROL_SCHEME_ACTIONS.schemeStarted({ name: scheme.name }));
}

return forkJoin(combinedTasks).pipe(
// We have to start calibration tasks after all other tasks are done to avoid race conditions with position queries.
// Also, somehow running calibration tasks in parallel causes issues with receiving port values.
// For now, we run them sequentially. TODO: investigate and fix the root cause.
calibrationServoTasks.length > 0 ? concatWith(...calibrationServoTasks) : tap(() => void 0),
last(),
timeout(appConfig.schemeStartStopTimeoutMs),
map(() => CONTROL_SCHEME_ACTIONS.schemeStarted({ name: scheme.name })),
catchError((e) => of(CONTROL_SCHEME_ACTIONS.schemeStartFailed({ reason: e })))
Expand Down
3 changes: 3 additions & 0 deletions modules/store/src/lib/models/attached-io-props.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ export type AttachedIoPropsModel = {
range: number;
aposCenter: number;
} | null;
startupMotorPositionData: {
position: number;
} | null;
runtimeTiltCompensation: TiltData | null;
};
20 changes: 20 additions & 0 deletions modules/store/src/lib/reducers/attached-io-props.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
portId: data.portId,
motorEncoderOffset: data.offset,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
runtimeTiltCompensation: state.entities[hubAttachedIoPropsIdFn(data)]?.runtimeTiltCompensation ?? null,
},
state
Expand All @@ -41,6 +42,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
hubId: data.hubId,
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
startupServoCalibrationData: {
aposCenter: data.aposCenter,
range: data.range,
Expand All @@ -57,6 +59,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
runtimeTiltCompensation: {
yaw: currentCompensation.yaw,
pitch: currentCompensation.pitch + data.currentPitch,
Expand All @@ -73,6 +76,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
runtimeTiltCompensation: {
yaw: currentCompensation.yaw,
pitch: 0,
Expand All @@ -89,6 +93,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
runtimeTiltCompensation: {
yaw: currentCompensation.yaw + data.currentYaw,
pitch: currentCompensation.pitch,
Expand All @@ -105,6 +110,7 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupMotorPositionData ?? null,
runtimeTiltCompensation: {
yaw: 0,
pitch: currentCompensation.pitch,
Expand All @@ -114,6 +120,20 @@ export const ATTACHED_IO_PROPS_FEATURE = createFeature({
state
);
}),
on(ATTACHED_IO_PROPS_ACTIONS.startupMotorPositionReceived, (state, data): AttacheIoPropsState => {
return ATTACHED_IO_PROPS_ENTITY_ADAPTER.upsertOne({
hubId: data.hubId,
portId: data.portId,
motorEncoderOffset: state.entities[hubAttachedIoPropsIdFn(data)]?.motorEncoderOffset ?? 0,
startupServoCalibrationData: state.entities[hubAttachedIoPropsIdFn(data)]?.startupServoCalibrationData ?? null,
startupMotorPositionData: {
position: data.position,
},
runtimeTiltCompensation: state.entities[hubAttachedIoPropsIdFn(data)]?.runtimeTiltCompensation ?? null,
},
state
);
}),
on(HUBS_ACTIONS.forgetHub, HUBS_ACTIONS.disconnected, (state, { hubId }): AttacheIoPropsState =>
ATTACHED_IO_PROPS_ENTITY_ADAPTER.removeMany((io) => io.hubId === hubId, state)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const ATTACHED_IO_PORT_MODE_INFO_SELECTORS = {
return null;
}
),
selectIoCanOperateOutputPortModeName: (
selectIsIoSupportInputMode: (
{ hubId, portId, portModeName }: { hubId: string; portId: number; portModeName: PortModeName }
) => createSelector(
HUB_RUNTIME_DATA_SELECTORS.selectIsHubConnected(hubId),
Expand Down
Loading