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

feat(control schemes): read servo values from port during edit #210

Merged
merged 1 commit into from
Aug 23, 2023
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 @@ -19,17 +19,39 @@
{{ 'controlScheme.servoBinding.calibrate' | transloco }}
</button>

<app-slider-control [min]="-motorLimits.maxServoDegreesRange / 2"
[max]="motorLimits.maxServoDegreesRange / 2"
[control]="form.controls.aposCenter"
[translocoTitle]="'controlScheme.servoBinding.outputCenterOffset'"
></app-slider-control>
<div class="readable-input-container">
<app-slider-control class="readable-input"
[min]="-motorLimits.maxServoDegreesRange / 2"
[max]="motorLimits.maxServoDegreesRange / 2"
[control]="form.controls.aposCenter"
[translocoTitle]="'controlScheme.servoBinding.outputCenterOffset'"
></app-slider-control>
<button mat-icon-button
[disabled]="!(canReadAposFromMotor$ | ngrxPush)"
[title]="'controlScheme.servoBinding.readServoCenterFromPort' | transloco"
(click)="readAposCenterFromMotor()"
>
<mat-icon [fontIcon]="'sync'"></mat-icon>
<span class="cdk-visually-hidden">{{ 'controlScheme.servoBinding.readServoCenterFromPort' | transloco }}</span>
</button>
</div>

<app-slider-control [min]="motorLimits.minServoDegreesRange"
[max]="motorLimits.maxServoDegreesRange"
[control]="form.controls.range"
[translocoTitle]="'controlScheme.servoBinding.outputAngleRange'"
></app-slider-control>
<div class="readable-input-container">
<app-slider-control class="readable-input"
[min]="motorLimits.minServoDegreesRange"
[max]="motorLimits.maxServoDegreesRange"
[control]="form.controls.range"
[translocoTitle]="'controlScheme.servoBinding.outputAngleRange'"
></app-slider-control>
<button mat-icon-button
[disabled]="!(canReadAposFromMotor$ | ngrxPush)"
[title]="'controlScheme.servoBinding.readServoRangeFromPort' | transloco"
(click)="readServoRangeFromMotor()"
>
<mat-icon [fontIcon]="'sync'"></mat-icon>
<span class="cdk-visually-hidden">{{ 'controlScheme.servoBinding.readServoRangeFromPort' | transloco }}</span>
</button>
</div>

<app-slider-control [min]="0"
[max]="motorLimits.maxSpeed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@
.calibrate-button {
width: 100%;
}

.readable-input-container {
display: flex;
flex-direction: row;
align-items: center;
}

.readable-input {
flex: 1;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { NgIf } from '@angular/common';
import { MOTOR_LIMITS } from '@nvsukhanov/rxpoweredup';
import { MOTOR_LIMITS, PortModeName } from '@nvsukhanov/rxpoweredup';
import { TranslocoModule } from '@ngneat/transloco';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Store } from '@ngrx/store';
import { Observable, combineLatest, map, of, startWith, switchMap } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, combineLatest, combineLatestWith, finalize, map, of, startWith, switchMap, take } from 'rxjs';
import { PushPipe } from '@ngrx/component';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { CONTROL_SCHEME_ACTIONS } from '@app/store';
import { ControlSchemeBindingType, ControllerInputType, SliderControlComponent, ToggleControlComponent } from '@app/shared';
import { concatLatestFrom } from '@ngrx/effects';
import { ATTACHED_IO_PORT_MODE_INFO_SELECTORS, ATTACHED_IO_PROPS_SELECTORS, CONTROL_SCHEME_ACTIONS, HubFacadeService } from '@app/store';
import { ControlSchemeBindingType, ControllerInputType, SliderControlComponent, ToggleControlComponent, getTranslationArcs } from '@app/shared';

import { IBindingsDetailsEditComponent } from '../i-bindings-details-edit-component';
import { CalibrationResult, CalibrationResultType, ServoCalibrationDialogComponent } from '../servo-calibration-dialog';
Expand Down Expand Up @@ -38,7 +39,7 @@ import { getInputTypesForOperationMode } from '../wait-for-controller-input-dial
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BindingServoEditComponent implements IBindingsDetailsEditComponent<ServoBindingForm> {
export class BindingServoEditComponent implements IBindingsDetailsEditComponent<ServoBindingForm>, OnDestroy {
public readonly motorLimits = MOTOR_LIMITS;

public readonly acceptableInputTypes = getInputTypesForOperationMode(ControlSchemeBindingType.Servo);
Expand All @@ -47,10 +48,17 @@ export class BindingServoEditComponent implements IBindingsDetailsEditComponent<

private _canCalibrate$: Observable<boolean> = of(false);

private readAposCenterSubscription?: Subscription;

private readServoRangeSubscription?: Subscription;

private readonly isQueryingPort = new BehaviorSubject<boolean>(false);

constructor(
private readonly cd: ChangeDetectorRef,
private readonly store: Store,
private readonly matDialog: MatDialog
private readonly matDialog: MatDialog,
private readonly hubFacadeService: HubFacadeService
) {
}

Expand All @@ -64,7 +72,83 @@ export class BindingServoEditComponent implements IBindingsDetailsEditComponent<
}

public get canCalibrate$(): Observable<boolean> {
return this._canCalibrate$;
return this._canCalibrate$.pipe(
combineLatestWith(this.isQueryingPort),
map(([ canCalibrate, isQueryingPort ]) => canCalibrate && !isQueryingPort)
);
}

public get canReadAposFromMotor$(): Observable<boolean> {
if (!this._form
|| !this._form.value
|| this._form.value.hubId === undefined
|| this._form.value.portId === undefined
) {
return of(false);
}
const hubId = this._form.value.hubId;
const portId = this._form.value.portId;
return this.store.select(ATTACHED_IO_PROPS_SELECTORS.selectById({ hubId, portId })).pipe(
concatLatestFrom(() => this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortHastInputModeForPortModeName({
hubId,
portId,
portModeName: PortModeName.absolutePosition
}))),
combineLatestWith(this.isQueryingPort),
map(([ [ props, hasAbsolutePositionMode ], isQueringPort ]) => !isQueringPort && hasAbsolutePositionMode && !!props),
);
}

public readAposCenterFromMotor(): void {
if (!this._form
|| !this._form.value
|| this._form.value.hubId === undefined
|| this._form.value.portId === undefined
|| this.isQueryingPort.value
) {
return;
}
this.readAposCenterSubscription?.unsubscribe();
this.isQueryingPort.next(true);
this.readAposCenterSubscription = this.hubFacadeService.getMotorAbsolutePosition(
this._form.value.hubId,
this._form.value.portId
).pipe(
take(1),
finalize(() => this.isQueryingPort.next(false))
).subscribe((apos) => {
this._form?.controls.aposCenter.setValue(apos);
});
}

public readServoRangeFromMotor(): void {
if (!this._form
|| !this._form.value
|| this._form.value.hubId === undefined
|| this._form.value.portId === undefined
|| this._form.value.aposCenter === undefined
|| this.isQueryingPort.value
) {
return;
}
const aposCenter = this._form.value.aposCenter;
this.readServoRangeSubscription?.unsubscribe();
this.isQueryingPort.next(true);
this.readServoRangeSubscription = this.hubFacadeService.getMotorAbsolutePosition(
this._form.value.hubId,
this._form.value.portId
).pipe(
take(1),
finalize(() => this.isQueryingPort.next(false))
).subscribe((apos) => {
const { cw, ccw } = getTranslationArcs(aposCenter, apos);
this._form?.controls.range.setValue(Math.min(Math.abs(cw), Math.abs(ccw)));
});
}

public ngOnDestroy(): void {
this.readAposCenterSubscription?.unsubscribe();
this.readServoRangeSubscription?.unsubscribe();
}

public calibrate(): void {
Expand All @@ -76,13 +160,15 @@ export class BindingServoEditComponent implements IBindingsDetailsEditComponent<
) {
return;
}
this.isQueryingPort.next(true);
this.matDialog.open(ServoCalibrationDialogComponent, {
data: {
hubId: this._form.value.hubId,
portId: this._form.value.portId,
power: this._form.value.power
}
}).afterClosed().subscribe((result: CalibrationResult | null) => {
this.isQueryingPort.next(false);
if (!result) { // cancelled
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export class ServoCalibrationService {
const cancel$ = new Subject<void>();
zip(
this.store.select(
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName(hubId, portId, PortModeName.position)
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName({ hubId, portId, portModeName: PortModeName.position })
),
this.store.select(
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName(hubId, portId, PortModeName.absolutePosition)
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName({ hubId, portId, portModeName: PortModeName.absolutePosition })
)
).pipe(
take(1),
Expand Down
2 changes: 1 addition & 1 deletion src/app/store/effects/hub-attached-ios-state.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class HubAttachedIosStateEffects {
io: AttachedIoModel,
portModeName: PortModeName
): Observable<number | null> {
return this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectModeIdForInputModeName(io, portModeName)).pipe(
return this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectModeIdForIoAndPortModeName(io, portModeName)).pipe(
filter((modeId) => modeId !== null),
take(1)
);
Expand Down
33 changes: 33 additions & 0 deletions src/app/store/hub-facade.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { Observable, last, switchMap, take, throwError } from 'rxjs';
import { Store } from '@ngrx/store';
import { PortModeName } from '@nvsukhanov/rxpoweredup';

import { ATTACHED_IO_PORT_MODE_INFO_SELECTORS } from './selectors';
import { HubStorageService } from './hub-storage.service';

@Injectable()
export class HubFacadeService {
constructor(
private readonly hubStorage: HubStorageService,
private readonly store: Store
) {
}

public getMotorAbsolutePosition(
hubId: string,
portId: number
): Observable<number> {
return this.store.select(ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName({
hubId,
portId,
portModeName: PortModeName.absolutePosition
})).pipe(
take(1),
switchMap((modeInfo) => modeInfo !== null
? this.hubStorage.get(hubId).motors.getAbsolutePosition(portId, modeInfo.modeId)
: throwError(() => new Error('Required absolute position mode not found'))),
last()
);
}
}
1 change: 1 addition & 0 deletions src/app/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './reducers';
export * from './models';
export * from './effects';
export * from './i-state';
export * from './hub-facade.service';
2 changes: 2 additions & 0 deletions src/app/store/provide-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { bluetoothAvailabilityCheckFactory } from './bluetooth-availability-chec
import { HubStorageService } from './hub-storage.service';
import { RoutesBuilderService } from '../routing';
import { HUB_STATS_ACTIONS } from './actions';
import { HubFacadeService } from './hub-facade.service';

const STORAGE_VERSION = '14';

Expand Down Expand Up @@ -121,6 +122,7 @@ export function provideApplicationStore(): EnvironmentProviders {
multi: true
},
HubStorageService,
HubFacadeService,
ControllerProfileFactoryService,
provideRouterStore()
]);
Expand Down
23 changes: 17 additions & 6 deletions src/app/store/selectors/attached-io-port-mode-info.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createSelector } from '@ngrx/store';
import { PortModeName } from '@nvsukhanov/rxpoweredup';
import { IOType, PortModeName } from '@nvsukhanov/rxpoweredup';

import { ATTACHED_IO_MODES_SELECTORS } from './attached-io-modes.selectors';
import { AttachedIoModel } from '../models';
import { ATTACHED_IO_SELECTORS } from './attached-ios.selectors';
import { ATTACHED_IO_PORT_MODE_INFO_ENTITY_ADAPTER, ATTACHED_IO_PORT_MODE_INFO_FEATURE, attachedIoModesIdFn, attachedIoPortModeInfoIdFn } from '../reducers';

Expand All @@ -15,16 +14,21 @@ export const ATTACHED_IO_PORT_MODE_INFO_SELECTORS = {
ATTACHED_IO_PORT_MODE_INFO_FEATURE.selectAttachedIoPortModeInfoState,
ATTACHED_IO_PORT_MODE_INFO_ENTITY_ADAPTER.getSelectors().selectEntities
),
selectModeIdForInputModeName: (io: AttachedIoModel, portModeName: PortModeName) => createSelector(
ATTACHED_IO_MODES_SELECTORS.selectIoPortModes(io),
selectModeIdForIoAndPortModeName: (
{ hardwareRevision, softwareRevision, ioType }: { hardwareRevision: string; softwareRevision: string; ioType: IOType },
portModeName: PortModeName
) => createSelector(
ATTACHED_IO_MODES_SELECTORS.selectIoPortModes({ hardwareRevision, softwareRevision, ioType }),
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectEntities,
(ioPortModes, portModeInfo): number | null => {
return ioPortModes?.portInputModes.find((modeId) => {
return portModeInfo[attachedIoPortModeInfoIdFn({ ...io, modeId })]?.name === portModeName;
return portModeInfo[attachedIoPortModeInfoIdFn({ hardwareRevision, softwareRevision, ioType, modeId })]?.name === portModeName;
}) ?? null;
}
),
selectHubPortInputModeForPortModeName: (hubId: string, portId: number, portModeName: PortModeName) => createSelector(
selectHubPortInputModeForPortModeName: (
{ hubId, portId, portModeName }: { hubId: string; portId: number; portModeName: PortModeName }
) => createSelector(
ATTACHED_IO_SELECTORS.selectIoAtPort({ hubId, portId }),
ATTACHED_IO_MODES_SELECTORS.selectEntities,
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectEntities,
Expand All @@ -42,4 +46,11 @@ export const ATTACHED_IO_PORT_MODE_INFO_SELECTORS = {
return null;
}
),
selectHubPortHastInputModeForPortModeName: (
{ hubId, portId, portModeName }: { hubId: string; portId: number; portModeName: PortModeName }
) => createSelector(
ATTACHED_IO_PORT_MODE_INFO_SELECTORS.selectHubPortInputModeForPortModeName({ hubId, portId, portModeName }),
(modeInfo) => modeInfo !== null
),

} as const;
2 changes: 2 additions & 0 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"calibrationIsInProgress": "Servo calibration is in progress",
"calibrationError": "An error occurred during servo calibration",
"calibrationCancel": "Cancel",
"readServoCenterFromPort": "Read servo center from port",
"readServoRangeFromPort": "Read servo range from port",
"taskSummary": "Servo: { angle }°"
},
"stepperBinding": {
Expand Down