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

ASCOM Rotator #420

Merged
merged 5 commits into from
May 20, 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
5 changes: 3 additions & 2 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import nebulosa.indi.device.camera.Camera
import nebulosa.indi.device.filterwheel.FilterWheel
import nebulosa.indi.device.focuser.Focuser
import nebulosa.indi.device.mount.Mount
import nebulosa.indi.device.rotator.Rotator
import org.hibernate.validator.constraints.Range
import org.springframework.web.bind.annotation.*

Expand Down Expand Up @@ -39,8 +40,8 @@ class CameraController(
@PutMapping("{camera}/snoop")
fun snoop(
camera: Camera,
mount: Mount?, wheel: FilterWheel?, focuser: Focuser?,
) = cameraService.snoop(camera, mount, wheel, focuser)
mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, rotator: Rotator?
) = cameraService.snoop(camera, mount, wheel, focuser, rotator)

@PutMapping("{camera}/cooler")
fun cooler(
Expand Down
83 changes: 52 additions & 31 deletions desktop/src/app/camera/camera.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi
import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
import { Camera, CameraDialogInput, CameraDialogMode, CameraPreference, CameraStartCapture, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureMode, ExposureTimeUnit, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { Device } from '../../shared/types/device.types'
import { Focuser } from '../../shared/types/focuser.types'
import { Equipment } from '../../shared/types/home.types'
import { Mount } from '../../shared/types/mount.types'
import { Rotator } from '../../shared/types/rotator.types'
import { FilterWheel } from '../../shared/types/wheel.types'
import { AppComponent } from '../app.component'

Expand Down Expand Up @@ -103,6 +105,11 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
label: 'Focuser',
menu: [],
},
{
icon: 'mdi mdi-rotate-right',
label: 'Rotator',
menu: [],
},
]
},
]
Expand Down Expand Up @@ -215,6 +222,14 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
}
})

electron.on('ROTATOR.UPDATED', event => {
if (event.device.id === this.equipment.rotator?.id) {
ngZone.run(() => {
Object.assign(this.equipment.rotator!, event.device)
})
}
})

electron.on('CALIBRATION.CHANGED', () => {
ngZone.run(() => this.loadCalibrationGroups())
})
Expand Down Expand Up @@ -289,51 +304,50 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
}

private async loadEquipment() {
const mounts = await this.api.mounts()
this.equipment.mount = mounts.find(e => e.name === this.equipment.mount?.name)

const buildStartTooltip = () => {
this.startTooltip =
`<b>MOUNT</b>: ${this.equipment.mount?.name ?? 'None'}
<b>FILTER WHEEL</b>: ${this.equipment.wheel?.name ?? 'None'}
<b>FOCUSER</b>: ${this.equipment.focuser?.name ?? 'None'}`
<b>FOCUSER</b>: ${this.equipment.focuser?.name ?? 'None'}
<b>ROTATOR</b>: ${this.equipment.rotator?.name ?? 'None'}`
}

const makeMountItem = (mount?: Mount) => {
const makeItem = (checked: boolean, command: () => void, device?: Device) => {
return <ExtendedMenuItem>{
icon: mount ? 'mdi mdi-connection' : 'mdi mdi-close',
label: mount?.name ?? 'None',
checked: this.equipment.mount?.name === mount?.name,
icon: device ? 'mdi mdi-connection' : 'mdi mdi-close',
label: device?.name ?? 'None',
checked,
command: async (event: SlideMenuItemCommandEvent) => {
this.equipment.mount = mount
command()
buildStartTooltip()
this.preference.equipmentForDevice(this.camera).set(this.equipment)
event.parent?.menu?.forEach(item => item.checked = item === event.item)
},
}
}

// MOUNT

const mounts = await this.api.mounts()
this.equipment.mount = mounts.find(e => e.name === this.equipment.mount?.name)

const makeMountItem = (mount?: Mount) => {
return makeItem(this.equipment.mount?.name === mount?.name, () => this.equipment.mount = mount, mount)
}

this.cameraModel[1].menu![0].menu!.push(makeMountItem())

for (const mount of mounts) {
this.cameraModel[1].menu![0].menu!.push(makeMountItem(mount))
}

// FILTER WHEEL

const wheels = await this.api.wheels()
this.equipment.wheel = wheels.find(e => e.name === this.equipment.wheel?.name)

const makeWheelItem = (wheel?: FilterWheel) => {
return <ExtendedMenuItem>{
icon: wheel ? 'mdi mdi-connection' : 'mdi mdi-close',
label: wheel?.name ?? 'None',
checked: this.equipment.wheel?.name === wheel?.name,
command: async (event: SlideMenuItemCommandEvent) => {
this.equipment.wheel = wheel
buildStartTooltip()
this.preference.equipmentForDevice(this.camera).set(this.equipment)
event.parent?.menu?.forEach(item => item.checked = item === event.item)
},
}
return makeItem(this.equipment.wheel?.name === wheel?.name, () => this.equipment.wheel = wheel, wheel)
}

this.cameraModel[1].menu![1].menu!.push(makeWheelItem())
Expand All @@ -342,21 +356,13 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
this.cameraModel[1].menu![1].menu!.push(makeWheelItem(wheel))
}

// FOCUSER

const focusers = await this.api.focusers()
this.equipment.focuser = focusers.find(e => e.name === this.equipment.focuser?.name)

const makeFocuserItem = (focuser?: Focuser) => {
return <ExtendedMenuItem>{
icon: focuser ? 'mdi mdi-connection' : 'mdi mdi-close',
label: focuser?.name ?? 'None',
checked: this.equipment.focuser?.name === focuser?.name,
command: async (event: SlideMenuItemCommandEvent) => {
this.equipment.focuser = focuser
buildStartTooltip()
this.preference.equipmentForDevice(this.camera).set(this.equipment)
event.parent?.menu?.forEach(item => item.checked = item === event.item)
},
}
return makeItem(this.equipment.focuser?.name === focuser?.name, () => this.equipment.focuser = focuser, focuser)
}

this.cameraModel[1].menu![2].menu!.push(makeFocuserItem())
Expand All @@ -365,6 +371,21 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
this.cameraModel[1].menu![2].menu!.push(makeFocuserItem(focuser))
}

// ROTATOR

const rotators = await this.api.rotators()
this.equipment.rotator = rotators.find(e => e.name === this.equipment.rotator?.name)

const makeRotatorItem = (rotator?: Rotator) => {
return makeItem(this.equipment.rotator?.name === rotator?.name, () => this.equipment.rotator = rotator, rotator)
}

this.cameraModel[1].menu![3].menu!.push(makeRotatorItem())

for (const rotator of rotators) {
this.cameraModel[1].menu![3].menu!.push(makeRotatorItem(rotator))
}

buildStartTooltip()
}

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/app/rotator/rotator.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class RotatorComponent implements AfterViewInit, OnDestroy {
}

private update() {
if (!this.rotator.id) {
if (this.rotator.id) {
this.moving = this.rotator.moving
this.reversed = this.rotator.reversed
}
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/shared/services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export class ApiService {

// TODO: Rotator
cameraSnoop(camera: Camera, equipment: Equipment) {
const { mount, wheel, focuser } = equipment
const query = this.http.query({ mount: mount?.name, wheel: wheel?.name, focuser: focuser?.name })
const { mount, wheel, focuser, rotator } = equipment
const query = this.http.query({ mount: mount?.name, wheel: wheel?.name, focuser: focuser?.name, rotator: rotator?.name })
return this.http.put<void>(`cameras/${camera.id}/snoop?${query}`)
}

Expand Down
2 changes: 2 additions & 0 deletions desktop/src/shared/types/home.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Camera } from './camera.types'
import { Focuser } from './focuser.types'
import { Mount } from './mount.types'
import { Rotator } from './rotator.types'
import { FilterWheel } from './wheel.types'

export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' |
Expand Down Expand Up @@ -45,4 +46,5 @@ export interface Equipment {
mount?: Mount
focuser?: Focuser
wheel?: FilterWheel
rotator?: Rotator
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package nebulosa.alpaca.api

import retrofit2.Call
import retrofit2.http.*

interface AlpacaRotatorService : AlpacaDeviceService {

@GET("api/v1/rotator/{id}/connected")
override fun isConnected(@Path("id") id: Int): Call<BoolResponse>

@FormUrlEncoded
@PUT("api/v1/rotator/{id}/connected")
override fun connect(@Path("id") id: Int, @Field("Connected") connected: Boolean): Call<NoneResponse>

@GET("api/v1/rotator/{id}/canreverse")
fun canReverse(@Path("id") id: Int): Call<BoolResponse>

@GET("api/v1/rotator/{id}/ismoving")
fun isMoving(@Path("id") id: Int): Call<BoolResponse>

@GET("api/v1/rotator/{id}/reverse")
fun isReversed(@Path("id") id: Int): Call<BoolResponse>

@GET("api/v1/rotator/{id}/position")
fun position(@Path("id") id: Int): Call<DoubleResponse>

@GET("api/v1/rotator/{id}/stepsize")
fun stepSize(@Path("id") id: Int): Call<DoubleResponse>

@FormUrlEncoded
@PUT("api/v1/rotator/{id}/reverse")
fun reverse(@Path("id") id: Int, @Field("Reverse") reverse: Boolean): Call<NoneResponse>

@PUT("api/v1/rotator/{id}/halt")
fun halt(@Path("id") id: Int): Call<NoneResponse>

@FormUrlEncoded
@PUT("api/v1/rotator/{id}/move")
fun move(@Path("id") id: Int, @Field("Position") position: Double): Call<NoneResponse>

@FormUrlEncoded
@PUT("api/v1/rotator/{id}/moveabsolute")
fun moveTo(@Path("id") id: Int, @Field("Position") position: Double): Call<NoneResponse>

@FormUrlEncoded
@PUT("api/v1/rotator/{id}/sync")
fun sync(@Path("id") id: Int, @Field("Position") position: Double): Call<NoneResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ class AlpacaService(
val filterWheel by lazy { retrofit.create<AlpacaFilterWheelService>() }

val focuser by lazy { retrofit.create<AlpacaFocuserService>() }

val rotator by lazy { retrofit.create<AlpacaRotatorService>() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import nebulosa.alpaca.api.DeviceType
import nebulosa.alpaca.indi.device.cameras.ASCOMCamera
import nebulosa.alpaca.indi.device.focusers.ASCOMFocuser
import nebulosa.alpaca.indi.device.mounts.ASCOMMount
import nebulosa.alpaca.indi.device.rotators.ASCOMRotator
import nebulosa.alpaca.indi.device.wheels.ASCOMFilterWheel
import nebulosa.indi.device.AbstractINDIDeviceProvider
import nebulosa.indi.protocol.INDIProtocol
Expand All @@ -21,7 +22,7 @@ data class AlpacaClient(

override val id = UUID.randomUUID().toString()

override fun sendMessageToServer(message: INDIProtocol) {}
override fun sendMessageToServer(message: INDIProtocol) = Unit

fun discovery() {
val response = service.management.configuredDevices().execute()
Expand Down Expand Up @@ -59,7 +60,13 @@ data class AlpacaClient(
}
}
}
DeviceType.ROTATOR -> Unit
DeviceType.ROTATOR -> {
with(ASCOMRotator(device, service.rotator, this)) {
if (registerRotator(this)) {
initialize()
}
}
}
DeviceType.DOME -> Unit
DeviceType.SWITCH -> Unit
DeviceType.COVER_CALIBRATOR -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import nebulosa.alpaca.indi.client.AlpacaClient
import nebulosa.common.Resettable
import nebulosa.common.time.Stopwatch
import nebulosa.indi.device.*
import nebulosa.log.debug
import nebulosa.log.loggerFor
import retrofit2.Call
import retrofit2.HttpException
Expand Down Expand Up @@ -84,23 +85,29 @@ abstract class ASCOMDevice : Device, Resettable {

protected fun <T : AlpacaResponse<*>> Call<T>.doRequest(): T? {
try {
val response = execute().body()
val request = request()
val response = execute()
val body = response.body()

return if (response == null) {
LOG.warn("response has no body. device={}, url={}", name, request().url)
return if (body == null) {
LOG.debug { "response has no body. device=%s, request=%s %s, response=%s".format(name, request.method, request.url, response) }
null
} else if (response.errorNumber != 0) {
val message = response.errorMessage
} else if (body.errorNumber != 0) {
val message = body.errorMessage

if (message.isNotEmpty()) {
addMessageAndFireEvent("[%s]: %s".format(LocalDateTime.now(), message))
}

// LOG.warn("unsuccessful response. device={}, code={}, message={}", name, response.errorNumber, response.errorMessage)
LOG.debug {
"unsuccessful response. device=%s, request=%s %s, errorNumber=%s, message=%s".format(
name, request.method, request.url, body.errorNumber, body.errorMessage
)
}

null
} else {
response
body
}
} catch (e: HttpException) {
LOG.error("unexpected response. device=$name", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,9 @@ data class ASCOMMount(
service.utcDate(device.number, dateTime.toInstant()).doRequest()
}

override fun snoop(devices: Iterable<Device?>) {}
override fun snoop(devices: Iterable<Device?>) = Unit

override fun handleMessage(message: INDIProtocol) {}
override fun handleMessage(message: INDIProtocol) = Unit

override fun onConnected() {
processCapabilities()
Expand All @@ -237,7 +237,7 @@ data class ASCOMMount(
equatorialSystem = service.equatorialSystem(device.number).doRequest()?.value ?: equatorialSystem
}

override fun onDisconnected() {}
override fun onDisconnected() = Unit

override fun reset() {
super.reset()
Expand Down
Loading