Skip to content

Commit

Permalink
feat(control schemes): read servo values from port during edit
Browse files Browse the repository at this point in the history
  • Loading branch information
nvsukhanov committed Aug 23, 2023
1 parent 1784872 commit 0067e5d
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 27 deletions.
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

0 comments on commit 0067e5d

Please sign in to comment.