diff --git a/api/Main.run.xml b/api/Main.run.xml index 80f401202..3858f0c50 100644 --- a/api/Main.run.xml +++ b/api/Main.run.xml @@ -9,7 +9,7 @@ value="--server.port=7000"/> - diff --git a/api/objectbox-models/default.json b/api/objectbox-models/default.json index 90e6f8f4f..770a59bfe 100644 --- a/api/objectbox-models/default.json +++ b/api/objectbox-models/default.json @@ -4,25 +4,25 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:6181435429496929274", - "lastPropertyId": "3:2049888129534034209", + "id": "1:5776797672757045049", + "lastPropertyId": "3:3535084991721034915", "name": "AppPreferenceEntity", "properties": [ { - "id": "1:9082423197436790865", + "id": "1:3648553440984228217", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:4215084375954292184", + "id": "2:2124566032914222588", "name": "key", - "indexId": "1:7113897099230922797", + "indexId": "1:8731786258593188691", "type": 9, "flags": 2080 }, { - "id": "3:2049888129534034209", + "id": "3:3535084991721034915", "name": "value", "type": 9 } @@ -30,244 +30,244 @@ "relations": [] }, { - "id": "2:3529169340335800270", - "lastPropertyId": "46:5232874833281750836", + "id": "2:8175475990112523542", + "lastPropertyId": "46:5459924747698235846", "name": "DeepSkyObjectEntity", "properties": [ { - "id": "1:1377932098503485955", + "id": "1:8031305581542708360", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:1446097180539935074", + "id": "2:3867893197287947294", "name": "names", - "indexId": "2:242894646262700455", + "indexId": "2:2958314983157220291", "type": 9, "flags": 2048 }, { - "id": "3:3053736478989140092", + "id": "3:7468541319178669629", "name": "m", "type": 5 }, { - "id": "4:1548821562589357302", + "id": "4:5732595879098917995", "name": "ngc", "type": 5 }, { - "id": "5:719076718265527216", + "id": "5:1860395038402982982", "name": "ic", "type": 5 }, { - "id": "6:7638106542208916219", + "id": "6:3109655201388253241", "name": "c", "type": 5 }, { - "id": "7:2365221518425800996", + "id": "7:1662179932824845119", "name": "b", "type": 5 }, { - "id": "8:3594538813363643906", + "id": "8:7000406893438898968", "name": "sh2", "type": 5 }, { - "id": "9:8727472982290215506", + "id": "9:1719370003916529235", "name": "vdb", "type": 5 }, { - "id": "10:7177111477641113606", + "id": "10:7226050300403368829", "name": "rcw", "type": 5 }, { - "id": "11:7750990697794784930", + "id": "11:2796761565173218114", "name": "ldn", "type": 5 }, { - "id": "12:2240636949352847970", + "id": "12:5121100508576785587", "name": "lbn", "type": 5 }, { - "id": "13:3311853968242468079", + "id": "13:8957689397827832219", "name": "cr", "type": 5 }, { - "id": "14:3064799059795629685", + "id": "14:1172328729302351715", "name": "mel", "type": 5 }, { - "id": "15:6521394662377878035", + "id": "15:1401526219202387832", "name": "pgc", "type": 5 }, { - "id": "16:3374393653206518051", + "id": "16:6475927734166110648", "name": "ugc", "type": 5 }, { - "id": "17:1103055697476331323", + "id": "17:842742792555683985", "name": "arp", "type": 5 }, { - "id": "18:6037304017788880350", + "id": "18:8643705774808946926", "name": "vv", "type": 5 }, { - "id": "19:6842439638525042505", + "id": "19:4356003472370982693", "name": "dwb", "type": 5 }, { - "id": "20:8692595731890012533", + "id": "20:8572455064705455070", "name": "tr", "type": 5 }, { - "id": "21:498271057417144293", + "id": "21:9170275646445893648", "name": "st", "type": 5 }, { - "id": "22:5767663301524445085", + "id": "22:7276575557658538461", "name": "ru", "type": 5 }, { - "id": "23:6101438325885106272", + "id": "23:2307814510695419786", "name": "vdbha", "type": 5 }, { - "id": "24:7592722542751515798", + "id": "24:2856269200240902613", "name": "ced", "type": 9 }, { - "id": "25:7395418421723446673", + "id": "25:3314672844880851221", "name": "pk", "type": 9 }, { - "id": "26:7160271996394415028", + "id": "26:7432163545612298903", "name": "png", "type": 9 }, { - "id": "27:1458083588970681448", + "id": "27:1489643382466742996", "name": "snrg", "type": 9 }, { - "id": "28:207326814581126847", + "id": "28:8333110151046150691", "name": "aco", "type": 9 }, { - "id": "29:7981856666798707457", + "id": "29:4276677181346567572", "name": "hcg", "type": 9 }, { - "id": "30:2708588211679486333", + "id": "30:168183967826750394", "name": "eso", "type": 9 }, { - "id": "31:2193234144447602224", + "id": "31:6493813078798436855", "name": "vdbh", "type": 9 }, { - "id": "32:1201022568876930348", + "id": "32:5641121374609113175", "name": "mType", "type": 9 }, { - "id": "33:6351624634212117789", + "id": "33:1222111939442661769", "name": "magnitude", "type": 8 }, { - "id": "34:6199691868803693989", + "id": "34:6667166158761046508", "name": "rightAscension", "type": 8 }, { - "id": "35:4099664894377311928", + "id": "35:26785200891998700", "name": "declination", "type": 8 }, { - "id": "36:7265668369063542470", + "id": "36:7720778463420309276", "name": "type", - "indexId": "3:3825443249382383696", + "indexId": "3:1321834746003941962", "type": 9, "flags": 2048 }, { - "id": "37:4155224603465527448", + "id": "37:2355614584817792455", "name": "redshift", "type": 8 }, { - "id": "38:4710504809211347125", + "id": "38:254275912919339563", "name": "parallax", "type": 8 }, { - "id": "39:8402338778664249864", + "id": "39:7854380326459065140", "name": "radialVelocity", "type": 8 }, { - "id": "40:6425506964773028327", + "id": "40:5151697508813816321", "name": "distance", "type": 8 }, { - "id": "41:7446062089288424732", + "id": "41:5842067752025674668", "name": "majorAxis", "type": 8 }, { - "id": "42:7447395708529256292", + "id": "42:2123832343000978414", "name": "minorAxis", "type": 8 }, { - "id": "43:2460491531195153196", + "id": "43:3370884554215411183", "name": "orientation", "type": 8 }, { - "id": "44:3359147573850599584", + "id": "44:1037789170412638473", "name": "pmRA", "type": 8 }, { - "id": "45:475909710878501137", + "id": "45:6827986888724207687", "name": "pmDEC", "type": 8 }, { - "id": "46:5232874833281750836", + "id": "46:5459924747698235846", "name": "constellation", - "indexId": "4:2085894475146538115", + "indexId": "4:4295375806952668567", "type": 9, "flags": 2048 } @@ -275,40 +275,40 @@ "relations": [] }, { - "id": "3:3687818935518123181", - "lastPropertyId": "6:1378251454367415926", + "id": "3:6406648682378849653", + "lastPropertyId": "6:225447716551502857", "name": "LocationEntity", "properties": [ { - "id": "1:8900169280048248059", + "id": "1:6170724685856910241", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:638248452666932387", + "id": "2:8824361351900874153", "name": "name", - "indexId": "5:4644305272188441135", + "indexId": "5:2545530025364719306", "type": 9, "flags": 2048 }, { - "id": "3:7015124920765295920", + "id": "3:8455259564949334823", "name": "latitude", "type": 8 }, { - "id": "4:8034103937966611549", + "id": "4:6936939927193533470", "name": "longitude", "type": 8 }, { - "id": "5:4790845182865895556", + "id": "5:6738031836211430322", "name": "elevation", "type": 8 }, { - "id": "6:1378251454367415926", + "id": "6:225447716551502857", "name": "offsetInMinutes", "type": 5 } @@ -316,52 +316,52 @@ "relations": [] }, { - "id": "4:1312076498930645704", - "lastPropertyId": "8:4062514020634142444", + "id": "4:7673643265581253128", + "lastPropertyId": "8:1041584857719315728", "name": "SavedCameraImageEntity", "properties": [ { - "id": "1:1470698634581912117", + "id": "1:7321601813720959856", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:2309677218504319612", - "name": "name", - "indexId": "6:109477348146597818", + "id": "2:5340429251763737174", + "name": "camera", + "indexId": "6:1385493830568322932", "type": 9, "flags": 2048 }, { - "id": "3:9212877232261517779", + "id": "3:7102262212121260765", "name": "path", - "indexId": "7:2061226652511406733", + "indexId": "7:5690025095883604695", "type": 9, "flags": 2048 }, { - "id": "4:8693844248671937366", + "id": "4:6521121451096569379", "name": "width", "type": 5 }, { - "id": "5:2751523451270529294", + "id": "5:2012162393260240623", "name": "height", "type": 5 }, { - "id": "6:4199542614559446180", + "id": "6:3961502057014220590", "name": "mono", "type": 1 }, { - "id": "7:1117756668830926513", + "id": "7:7862701044975435904", "name": "exposure", "type": 6 }, { - "id": "8:4062514020634142444", + "id": "8:1041584857719315728", "name": "savedAt", "type": 6 } @@ -369,99 +369,99 @@ "relations": [] }, { - "id": "5:6334310940653380800", - "lastPropertyId": "17:7416203170732233434", + "id": "5:5995688963305056486", + "lastPropertyId": "17:6908146377183714567", "name": "StarEntity", "properties": [ { - "id": "1:4925725063049436712", + "id": "1:7354604127742279028", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:7763524045392374965", + "id": "2:1873896716010536262", "name": "hd", "type": 5 }, { - "id": "3:7137885472007646789", + "id": "3:9198588760457966090", "name": "hr", "type": 5 }, { - "id": "4:2423401273281489010", + "id": "4:5438882553390493848", "name": "hip", "type": 5 }, { - "id": "5:8635530276982094868", + "id": "5:5531494784901923123", "name": "names", - "indexId": "8:928559090882709704", + "indexId": "8:5884312162198391547", "type": 9, "flags": 2048 }, { - "id": "6:6413941903919652136", + "id": "6:5336144851495778126", "name": "magnitude", "type": 8 }, { - "id": "7:1595994345599261701", + "id": "7:5880904726185918158", "name": "rightAscension", "type": 8 }, { - "id": "8:6229451337927168818", + "id": "8:6467724268252731290", "name": "declination", "type": 8 }, { - "id": "9:1116787059923714414", + "id": "9:7669445162224982608", "name": "type", - "indexId": "9:3461547145283148694", + "indexId": "9:887994720086051545", "type": 9, "flags": 2048 }, { - "id": "10:8682805032169680688", + "id": "10:3026778065346219902", "name": "spType", "type": 9 }, { - "id": "11:3922786936477708641", + "id": "11:1550116386399954078", "name": "redshift", "type": 8 }, { - "id": "12:7993572756810235854", + "id": "12:1862248787241539626", "name": "parallax", "type": 8 }, { - "id": "13:4668432657909700755", + "id": "13:389720407666263596", "name": "radialVelocity", "type": 8 }, { - "id": "14:2769065628519268215", + "id": "14:6834542253157601152", "name": "distance", "type": 8 }, { - "id": "15:4712811064526553850", + "id": "15:8894469394456548881", "name": "pmRA", "type": 8 }, { - "id": "16:2755500362122136726", + "id": "16:4881622396230091832", "name": "pmDEC", "type": 8 }, { - "id": "17:7416203170732233434", + "id": "17:6908146377183714567", "name": "constellation", - "indexId": "10:5209202730553871543", + "indexId": "10:2763726675053391859", "type": 9, "flags": 2048 } @@ -469,8 +469,8 @@ "relations": [] } ], - "lastEntityId": "5:6334310940653380800", - "lastIndexId": "10:5209202730553871543", + "lastEntityId": "5:5995688963305056486", + "lastIndexId": "10:2763726675053391859", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/api/src/main/kotlin/nebulosa/api/components/loaders/IERSLoader.kt b/api/src/main/kotlin/nebulosa/api/components/loaders/IERSLoader.kt index 8f20a70bd..510265806 100644 --- a/api/src/main/kotlin/nebulosa/api/components/loaders/IERSLoader.kt +++ b/api/src/main/kotlin/nebulosa/api/components/loaders/IERSLoader.kt @@ -1,5 +1,6 @@ package nebulosa.api.components.loaders +import jakarta.annotation.PostConstruct import nebulosa.io.transferAndClose import nebulosa.log.loggerFor import nebulosa.time.IERS @@ -16,8 +17,13 @@ import kotlin.io.path.outputStream class IERSLoader( private val dataDirectory: Path, private val okHttpClient: OkHttpClient, - override val systemExecutorService: ExecutorService, -) : Loader() { + private val systemExecutorService: ExecutorService, +) : Runnable { + + @PostConstruct + private fun initialize() { + systemExecutorService.submit(this) + } override fun run() { val finals2000A = Path.of("$dataDirectory", "finals2000A.all") diff --git a/api/src/main/kotlin/nebulosa/api/components/loaders/Loader.kt b/api/src/main/kotlin/nebulosa/api/components/loaders/Loader.kt deleted file mode 100644 index a935ff171..000000000 --- a/api/src/main/kotlin/nebulosa/api/components/loaders/Loader.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.api.components.loaders - -import jakarta.annotation.PostConstruct -import java.util.concurrent.ExecutorService - -sealed class Loader : Runnable { - - abstract val systemExecutorService: ExecutorService - - @PostConstruct - private fun initialize() { - systemExecutorService.submit(this) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/controllers/CameraController.kt b/api/src/main/kotlin/nebulosa/api/controllers/CameraController.kt index bcf44c23f..60455ac67 100644 --- a/api/src/main/kotlin/nebulosa/api/controllers/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/controllers/CameraController.kt @@ -5,56 +5,66 @@ import jakarta.validation.constraints.NotBlank import nebulosa.api.data.requests.CameraStartCaptureRequest import nebulosa.api.data.responses.CameraResponse import nebulosa.api.services.CameraService +import nebulosa.api.services.EquipmentService import org.hibernate.validator.constraints.Range import org.springframework.web.bind.annotation.* @RestController class CameraController( + private val equipmentService: EquipmentService, private val cameraService: CameraService, ) { @GetMapping("attachedCameras") fun attachedCameras(): List { - return cameraService.attachedCameras() + return equipmentService.cameras().map(::CameraResponse) } @GetMapping("camera") fun camera(@RequestParam @Valid @NotBlank name: String): CameraResponse { - return cameraService[name] + val camera = requireNotNull(equipmentService.camera(name)) + return CameraResponse(camera) } @PostMapping("cameraConnect") fun connect(@RequestParam @Valid @NotBlank name: String) { - cameraService.connect(name) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.connect(camera) } @PostMapping("cameraDisconnect") fun disconnect(@RequestParam @Valid @NotBlank name: String) { - cameraService.disconnect(name) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.disconnect(camera) } @GetMapping("cameraIsCapturing") fun isCapturing(@RequestParam @Valid @NotBlank name: String): Boolean { - return cameraService.isCapturing(name) + val camera = requireNotNull(equipmentService.camera(name)) + return cameraService.isCapturing(camera) } @PostMapping("cameraSetpointTemperature") fun setpointTemperature(@RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @Range(min = -50, max = 50) temperature: Double) { - cameraService.setpointTemperature(name, temperature) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.setpointTemperature(camera, temperature) } @PostMapping("cameraCooler") fun cooler(@RequestParam @Valid @NotBlank name: String, @RequestParam value: Boolean) { - cameraService.cooler(name, value) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.cooler(camera, value) } @PostMapping("cameraStartCapture") fun startCapture(@RequestParam @Valid @NotBlank name: String, @RequestBody @Valid body: CameraStartCaptureRequest) { - cameraService.startCapture(name, body) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.startCapture(camera, body) } @PostMapping("cameraAbortCapture") fun abortCapture(@RequestParam @Valid @NotBlank name: String) { - cameraService.abortCapture(name) + val camera = requireNotNull(equipmentService.camera(name)) + cameraService.abortCapture(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/controllers/FilterWheelController.kt b/api/src/main/kotlin/nebulosa/api/controllers/FilterWheelController.kt index 047352faa..b12640df7 100644 --- a/api/src/main/kotlin/nebulosa/api/controllers/FilterWheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/controllers/FilterWheelController.kt @@ -4,6 +4,7 @@ import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.data.responses.FilterWheelResponse +import nebulosa.api.services.EquipmentService import nebulosa.api.services.FilterWheelService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -12,27 +13,30 @@ import org.springframework.web.bind.annotation.RestController @RestController class FilterWheelController( + private val equipmentService: EquipmentService, private val filterWheelService: FilterWheelService, ) { @GetMapping("attachedFilterWheels") fun attachedFilterWheels(): List { - return filterWheelService.attachedFilterWheels() + return equipmentService.filterWheels().map(::FilterWheelResponse) } @GetMapping("filterWheel") fun filterWheel(@RequestParam @Valid @NotBlank name: String): FilterWheelResponse { - return filterWheelService[name] + return FilterWheelResponse(requireNotNull(equipmentService.filterWheel(name))) } @PostMapping("filterWheelConnect") fun connect(@RequestParam @Valid @NotBlank name: String) { - filterWheelService.connect(name) + val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + filterWheelService.connect(filterWheel) } @PostMapping("filterWheelDisconnect") fun disconnect(@RequestParam @Valid @NotBlank name: String) { - filterWheelService.disconnect(name) + val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + filterWheelService.disconnect(filterWheel) } @PostMapping("filterWheelMoveTo") @@ -40,7 +44,8 @@ class FilterWheelController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero position: Int, ) { - filterWheelService.moveTo(name, position) + val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + filterWheelService.moveTo(filterWheel, position) } @PostMapping("filterWheelSyncNames") @@ -48,6 +53,7 @@ class FilterWheelController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero filterNames: String, ) { - filterWheelService.syncNames(name, filterNames.split(",")) + val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + filterWheelService.syncNames(filterWheel, filterNames.split(",")) } } diff --git a/api/src/main/kotlin/nebulosa/api/controllers/FocuserController.kt b/api/src/main/kotlin/nebulosa/api/controllers/FocuserController.kt index 2dc546697..29ec5ce20 100644 --- a/api/src/main/kotlin/nebulosa/api/controllers/FocuserController.kt +++ b/api/src/main/kotlin/nebulosa/api/controllers/FocuserController.kt @@ -4,6 +4,7 @@ import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.data.responses.FocuserResponse +import nebulosa.api.services.EquipmentService import nebulosa.api.services.FocuserService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -12,27 +13,30 @@ import org.springframework.web.bind.annotation.RestController @RestController class FocuserController( + private val equipmentService: EquipmentService, private val focuserService: FocuserService, ) { @GetMapping("attachedFocusers") fun attachedFocusers(): List { - return focuserService.attachedFocusers() + return equipmentService.focusers().map(::FocuserResponse) } @GetMapping("focuser") fun focuser(@RequestParam @Valid @NotBlank name: String): FocuserResponse { - return focuserService[name] + return FocuserResponse(requireNotNull(equipmentService.focuser(name))) } @PostMapping("focuserConnect") fun connect(@RequestParam @Valid @NotBlank name: String) { - focuserService.connect(name) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.connect(focuser) } @PostMapping("focuserDisconnect") fun disconnect(@RequestParam @Valid @NotBlank name: String) { - focuserService.disconnect(name) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.disconnect(focuser) } @PostMapping("focuserMoveIn") @@ -40,7 +44,8 @@ class FocuserController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero steps: Int, ) { - focuserService.moveIn(name, steps) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.moveIn(focuser, steps) } @PostMapping("focuserMoveOut") @@ -48,7 +53,8 @@ class FocuserController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero steps: Int, ) { - focuserService.moveOut(name, steps) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.moveOut(focuser, steps) } @PostMapping("focuserMoveTo") @@ -56,12 +62,14 @@ class FocuserController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero steps: Int, ) { - focuserService.moveTo(name, steps) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.moveTo(focuser, steps) } @PostMapping("focuserAbort") fun abort(@RequestParam @Valid @NotBlank name: String) { - focuserService.abort(name) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.abort(focuser) } @PostMapping("focuserSyncTo") @@ -69,6 +77,7 @@ class FocuserController( @RequestParam @Valid @NotBlank name: String, @RequestParam @Valid @PositiveOrZero steps: Int, ) { - focuserService.syncTo(name, steps) + val focuser = requireNotNull(equipmentService.focuser(name)) + focuserService.syncTo(focuser, steps) } } diff --git a/api/src/main/kotlin/nebulosa/api/controllers/FramingController.kt b/api/src/main/kotlin/nebulosa/api/controllers/FramingController.kt index ad71f98d0..f50735eed 100644 --- a/api/src/main/kotlin/nebulosa/api/controllers/FramingController.kt +++ b/api/src/main/kotlin/nebulosa/api/controllers/FramingController.kt @@ -10,7 +10,11 @@ import nebulosa.hips2fits.HipsSurvey import nebulosa.math.Angle import nebulosa.math.Angle.Companion.deg import org.hibernate.validator.constraints.Range -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.nio.file.Path @RestController class FramingController( @@ -31,10 +35,10 @@ class FramingController( @RequestParam(required = false, defaultValue = "1.0") @Valid @Positive @Max(90) fov: Double, @RequestParam(required = false, defaultValue = "0.0") rotation: Double, @RequestParam(required = false, defaultValue = "CDS_P_DSS2_COLOR") hipsSurvey: HipsSurveyType, - ): String { + ): Path { return imageService.frame( Angle.from(rightAscension, true), Angle.from(declination), width, height, fov.deg, rotation.deg, hipsSurvey, - ).toString() + ) } } diff --git a/api/src/main/kotlin/nebulosa/api/controllers/INDIController.kt b/api/src/main/kotlin/nebulosa/api/controllers/INDIController.kt index 8cae76d59..089a93d43 100644 --- a/api/src/main/kotlin/nebulosa/api/controllers/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/controllers/INDIController.kt @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotEmpty import nebulosa.api.data.requests.INDISendPropertyRequest import nebulosa.api.data.responses.INDIPropertyResponse +import nebulosa.api.services.EquipmentService import nebulosa.api.services.INDIService import nebulosa.api.services.WebSocketService import nebulosa.indi.device.DeviceMessageReceived @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.* @RestController class INDIController( + private val equipmentService: EquipmentService, private val indiService: INDIService, private val webSocketService: WebSocketService, private val eventBus: EventBus, @@ -40,7 +42,7 @@ class INDIController( @Subscribe(threadMode = ThreadMode.ASYNC) fun onDeviceMessageReceived(event: DeviceMessageReceived) { if (event.device == null) { - indiService.onMessageReceived(event.message) + indiService.addFirst(event.message) } webSocketService.sendINDIMessageReceived(event) @@ -48,7 +50,8 @@ class INDIController( @GetMapping("indiProperties") fun properties(@RequestParam @Valid @NotBlank name: String): List { - return indiService.properties(name) + val device = requireNotNull(equipmentService[name]) + return indiService.properties(device) } @PostMapping("sendIndiProperty") @@ -56,12 +59,15 @@ class INDIController( @RequestParam @Valid @NotBlank name: String, @RequestBody @Valid body: INDISendPropertyRequest, ) { - return indiService.sendProperty(name, body) + val device = requireNotNull(equipmentService[name]) + return indiService.sendProperty(device, body) } @GetMapping("indiLog") fun indiLog(@RequestParam(required = false) name: String?): List { - return indiService.indiLog(name) + if (name.isNullOrBlank()) return indiService + val device = equipmentService[name] ?: return emptyList() + return device.messages } @PostMapping("indiStartListening") diff --git a/api/src/main/kotlin/nebulosa/api/controllers/MountController.kt b/api/src/main/kotlin/nebulosa/api/controllers/MountController.kt new file mode 100644 index 000000000..4072afe5e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/controllers/MountController.kt @@ -0,0 +1,212 @@ +package nebulosa.api.controllers + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import nebulosa.api.data.responses.ComputedCoordinateResponse +import nebulosa.api.data.responses.MountResponse +import nebulosa.api.services.EquipmentService +import nebulosa.api.services.MountService +import nebulosa.indi.device.mount.TrackMode +import nebulosa.math.Angle +import nebulosa.math.Distance.Companion.m +import org.hibernate.validator.constraints.Range +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset + +@RestController +class MountController( + private val equipmentService: EquipmentService, + private val mountService: MountService, +) { + + @GetMapping("attachedMounts") + fun attachedMounts(): List { + return equipmentService.mounts().map(::MountResponse) + } + + @GetMapping("mount") + fun mount(@RequestParam @Valid @NotBlank name: String): MountResponse { + val mount = requireNotNull(equipmentService.mount(name)) + return MountResponse(mount) + } + + @PostMapping("mountConnect") + fun connect(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.connect(mount) + } + + @PostMapping("mountDisconnect") + fun disconnect(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.disconnect(mount) + } + + @PostMapping("mountTracking") + fun tracking( + @RequestParam @Valid @NotBlank name: String, + enable: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.tracking(mount, enable) + } + + @PostMapping("mountSync") + fun sync( + @RequestParam @Valid @NotBlank name: String, + @RequestParam @Valid @NotBlank rightAscension: String, + @RequestParam @Valid @NotBlank declination: String, + @RequestParam(required = false, defaultValue = "false") j2000: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.sync(mount, Angle.from(rightAscension, true), Angle.from(declination), j2000) + } + + @PostMapping("mountSlewTo") + fun slewTo( + @RequestParam @Valid @NotBlank name: String, + @RequestParam @Valid @NotBlank rightAscension: String, + @RequestParam @Valid @NotBlank declination: String, + @RequestParam(required = false, defaultValue = "false") j2000: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.slewTo(mount, Angle.from(rightAscension, true), Angle.from(declination), j2000) + } + + @PostMapping("mountGoTo") + fun goTo( + @RequestParam @Valid @NotBlank name: String, + @RequestParam @Valid @NotBlank rightAscension: String, + @RequestParam @Valid @NotBlank declination: String, + @RequestParam(required = false, defaultValue = "false") j2000: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.goTo(mount, Angle.from(rightAscension, true), Angle.from(declination), j2000) + } + + @PostMapping("mountHome") + fun home(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.home(mount) + } + + @PostMapping("mountAbort") + fun abort(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.abort(mount) + } + + @PostMapping("mountTrackMode") + fun trackMode( + @RequestParam @Valid @NotBlank name: String, + @RequestParam mode: TrackMode, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.trackMode(mount, mode) + } + + @PostMapping("mountSlewRate") + fun slewRate( + @RequestParam @Valid @NotBlank name: String, + @RequestParam @Valid @NotBlank rate: String, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) + } + + @PostMapping("mountMoveNorth") + fun moveNorth( + @RequestParam @Valid @NotBlank name: String, + @RequestParam enable: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.moveNorth(mount, enable) + } + + @PostMapping("mountMoveSouth") + fun moveSouth( + @RequestParam @Valid @NotBlank name: String, + @RequestParam enable: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.moveSouth(mount, enable) + } + + @PostMapping("mountMoveWest") + fun moveWest( + @RequestParam @Valid @NotBlank name: String, + @RequestParam enable: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.moveWest(mount, enable) + } + + @PostMapping("mountMoveEast") + fun moveEast( + @RequestParam @Valid @NotBlank name: String, + @RequestParam enable: Boolean, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.moveEast(mount, enable) + } + + @PostMapping("mountPark") + fun park(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.park(mount) + } + + @PostMapping("mountUnpark") + fun unpark(@RequestParam @Valid @NotBlank name: String) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.unpark(mount) + } + + @PostMapping("mountCoordinates") + fun coordinates( + @RequestParam @Valid @NotBlank name: String, + @RequestParam @Valid @NotBlank longitude: String, + @RequestParam @Valid @NotBlank latitude: String, + @RequestParam(required = false, defaultValue = "0.0") elevation: Double, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + mountService.coordinates(mount, Angle.from(longitude), Angle.from(latitude), elevation.m) + } + + @PostMapping("mountDateTime") + fun dateTime( + @RequestParam @Valid @NotBlank name: String, + @RequestParam date: LocalDate, + @RequestParam time: LocalTime, + @RequestParam @Valid @Range(min = -720, max = 720) offsetInMinutes: Int, + ) { + val mount = requireNotNull(equipmentService.mount(name)) + val dateTime = OffsetDateTime.of(date, time, ZoneOffset.ofTotalSeconds(offsetInMinutes * 60)) + mountService.dateTime(mount, dateTime) + } + + @PostMapping("mountComputeCoordinates") + fun computeCoordinates( + @RequestParam @Valid @NotBlank name: String, + @RequestParam(required = false) rightAscension: String?, + @RequestParam(required = false) declination: String?, + @RequestParam(required = false, defaultValue = "false") j2000: Boolean, + @RequestParam(required = false, defaultValue = "true") equatorial: Boolean, + @RequestParam(required = false, defaultValue = "true") horizontal: Boolean, + @RequestParam(required = false, defaultValue = "true") meridian: Boolean, + ): ComputedCoordinateResponse { + val mount = requireNotNull(equipmentService.mount(name)) + return mountService.computeCoordinates( + mount, + Angle.from(rightAscension, true, defaultValue = mount.rightAscension), + Angle.from(declination, defaultValue = mount.declination), + j2000, equatorial, horizontal, meridian, + ) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/data/entities/SavedCameraImageEntity.kt b/api/src/main/kotlin/nebulosa/api/data/entities/SavedCameraImageEntity.kt index 9d8f50102..da8d9722a 100644 --- a/api/src/main/kotlin/nebulosa/api/data/entities/SavedCameraImageEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/data/entities/SavedCameraImageEntity.kt @@ -7,7 +7,7 @@ import io.objectbox.annotation.Index @Entity data class SavedCameraImageEntity( @Id var id: Long = 0, - @Index var name: String = "", + @Index var camera: String = "", @Index var path: String = "", var width: Int = 0, var height: Int = 0, diff --git a/api/src/main/kotlin/nebulosa/api/data/responses/ComputedCoordinateResponse.kt b/api/src/main/kotlin/nebulosa/api/data/responses/ComputedCoordinateResponse.kt new file mode 100644 index 000000000..cf8de853d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/data/responses/ComputedCoordinateResponse.kt @@ -0,0 +1,14 @@ +package nebulosa.api.data.responses + +import nebulosa.nova.astrometry.Constellation + +data class ComputedCoordinateResponse( + val rightAscension: String, + val declination: String, + val azimuth: String, + val altitude: String, + val constellation: Constellation, + val lst: String, + val meridianAt: String, + val timeLeftToMeridianFlip: String, +) diff --git a/api/src/main/kotlin/nebulosa/api/data/responses/FocuserResponse.kt b/api/src/main/kotlin/nebulosa/api/data/responses/FocuserResponse.kt index 8d57a7370..e55fa2d2c 100644 --- a/api/src/main/kotlin/nebulosa/api/data/responses/FocuserResponse.kt +++ b/api/src/main/kotlin/nebulosa/api/data/responses/FocuserResponse.kt @@ -13,7 +13,7 @@ data class FocuserResponse( val canReverse: Boolean, val reverse: Boolean, val canSync: Boolean, - val hasBackslash: Boolean, + val hasBacklash: Boolean, val maxPosition: Int, val hasThermometer: Boolean, val temperature: Double, @@ -30,7 +30,7 @@ data class FocuserResponse( focuser.canReverse, focuser.reverse, focuser.canSync, - focuser.hasBackslash, + focuser.hasBacklash, focuser.maxPosition, focuser.hasThermometer, focuser.temperature, diff --git a/api/src/main/kotlin/nebulosa/api/data/responses/MountResponse.kt b/api/src/main/kotlin/nebulosa/api/data/responses/MountResponse.kt new file mode 100644 index 000000000..e3e90650e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/data/responses/MountResponse.kt @@ -0,0 +1,72 @@ +package nebulosa.api.data.responses + +import nebulosa.indi.device.mount.* +import nebulosa.math.AngleFormatter +import java.time.ZoneOffset + +data class MountResponse( + val name: String, + val connected: Boolean, + val slewing: Boolean, + val tracking: Boolean, + val canAbort: Boolean, + val canSync: Boolean, + val canGoTo: Boolean, + val canHome: Boolean, + val slewRates: List, + val slewRate: SlewRate?, + val mountType: MountType, + val trackModes: List, + val trackMode: TrackMode, + val pierSide: PierSide, + val guideRateWE: Double, + val guideRateNS: Double, + val rightAscension: String, + val declination: String, + val canPulseGuide: Boolean, + val pulseGuiding: Boolean, + val canPark: Boolean, + val parking: Boolean, + val parked: Boolean, + val hasGPS: Boolean, + val longitude: Double, + val latitude: Double, + val elevation: Double, + val dateTime: Long, + val offsetInMinutes: Int, + val computedCoordinates: ComputedCoordinateResponse?, +) { + + constructor(mount: Mount, computedCoordinates: ComputedCoordinateResponse? = null) : this( + mount.name, + mount.connected, + mount.slewing, + mount.tracking, + mount.canAbort, + mount.canSync, + mount.canGoTo, + mount.canHome, + mount.slewRates, + mount.slewRate, + mount.mountType, + mount.trackModes, + mount.trackMode, + mount.pierSide, + mount.guideRateWE, + mount.guideRateNS, + mount.rightAscension.format(AngleFormatter.HMS), + mount.declination.format(AngleFormatter.SIGNED_DMS), + mount.canPulseGuide, + mount.pulseGuiding, + mount.canPark, + mount.parking, + mount.parked, + mount.hasGPS, + mount.longitude.degrees, + mount.latitude.degrees, + mount.elevation.meters, + mount.dateTime.toLocalDateTime().toInstant(ZoneOffset.UTC).toEpochMilli(), + mount.dateTime.offset.totalSeconds / 60, + computedCoordinates, + ) +} diff --git a/api/src/main/kotlin/nebulosa/api/repositories/SavedCameraImageRepository.kt b/api/src/main/kotlin/nebulosa/api/repositories/SavedCameraImageRepository.kt index 36c01b41f..76993b153 100644 --- a/api/src/main/kotlin/nebulosa/api/repositories/SavedCameraImageRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/repositories/SavedCameraImageRepository.kt @@ -15,22 +15,22 @@ class SavedCameraImageRepository(boxStore: BoxStore) : BoxRepository { + fun withCamera(camera: String): List { return box.query() - .equal(SavedCameraImageEntity_.name, name, QueryBuilder.StringOrder.CASE_SENSITIVE) + .equal(SavedCameraImageEntity_.camera, camera, QueryBuilder.StringOrder.CASE_SENSITIVE) .build().use { it.find() } } - fun withNameLatest(name: String): SavedCameraImageEntity? { + fun withCameraLatest(camera: String): SavedCameraImageEntity? { return box.query() - .equal(SavedCameraImageEntity_.name, name, QueryBuilder.StringOrder.CASE_SENSITIVE) + .equal(SavedCameraImageEntity_.camera, camera, QueryBuilder.StringOrder.CASE_SENSITIVE) .orderDesc(SavedCameraImageEntity_.savedAt) .build().use { it.findFirst() } } - fun withNameAndPath(name: String, path: String): SavedCameraImageEntity? { + fun withCameraAndPath(camera: String, path: String): SavedCameraImageEntity? { return box.query() - .equal(SavedCameraImageEntity_.name, name, QueryBuilder.StringOrder.CASE_SENSITIVE) + .equal(SavedCameraImageEntity_.camera, camera, QueryBuilder.StringOrder.CASE_SENSITIVE) .and() .equal(SavedCameraImageEntity_.path, path, QueryBuilder.StringOrder.CASE_SENSITIVE) .build().use { it.findFirst() } diff --git a/api/src/main/kotlin/nebulosa/api/services/CameraExposureTask.kt b/api/src/main/kotlin/nebulosa/api/services/CameraExposureTask.kt index 18ac10f76..67c4e0dac 100644 --- a/api/src/main/kotlin/nebulosa/api/services/CameraExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/services/CameraExposureTask.kt @@ -6,9 +6,7 @@ import nebulosa.api.data.events.CameraCaptureFinished import nebulosa.api.data.requests.CameraStartCaptureRequest import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.common.concurrency.ThreadedJob -import nebulosa.fits.FITS_DEC_ANGLE_FORMATTER -import nebulosa.fits.FITS_RA_ANGLE_FORMATTER -import nebulosa.fits.FitsKeywords +import nebulosa.fits.imageHDU import nebulosa.fits.naxis import nebulosa.imaging.Image import nebulosa.indi.device.camera.* @@ -16,7 +14,6 @@ import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.mount.Mount import nebulosa.log.loggerFor import nom.tam.fits.Fits -import nom.tam.fits.ImageHDU import nom.tam.util.FitsOutputStream import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -183,21 +180,10 @@ data class CameraExposureTask( try { Fits(inputStream).use { fits -> - val hdu = fits.read().firstOrNull { it is ImageHDU } + val hdu = fits.imageHDU(0) if (hdu != null) { - val header = hdu.header.also { - val mount = mount ?: return@also - - val raStr = mount.rightAscensionJ2000.format(FITS_RA_ANGLE_FORMATTER) - val decStr = mount.declinationJ2000.format(FITS_DEC_ANGLE_FORMATTER) - - it.addValue(FitsKeywords.RA, raStr) - it.addValue(FitsKeywords.OBJCTRA, raStr) - it.addValue(FitsKeywords.DEC, decStr) - it.addValue(FitsKeywords.OBJCTDEC, decStr) - } - + val header = hdu.header path.parent.createDirectories() path.outputStream().use { fits.write(FitsOutputStream(it)) } diff --git a/api/src/main/kotlin/nebulosa/api/services/CameraService.kt b/api/src/main/kotlin/nebulosa/api/services/CameraService.kt index 2782504b7..364d1cbfb 100644 --- a/api/src/main/kotlin/nebulosa/api/services/CameraService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/CameraService.kt @@ -4,9 +4,9 @@ import jakarta.annotation.PostConstruct import nebulosa.api.data.entities.SavedCameraImageEntity import nebulosa.api.data.events.CameraCaptureFinished import nebulosa.api.data.requests.CameraStartCaptureRequest -import nebulosa.api.data.responses.CameraResponse import nebulosa.api.repositories.SavedCameraImageRepository import nebulosa.indi.device.PropertyChangedEvent +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraAttached import nebulosa.indi.device.camera.CameraDetached import nebulosa.indi.device.camera.CameraEvent @@ -24,7 +24,6 @@ import kotlin.io.path.isDirectory @Service class CameraService( - private val equipmentService: EquipmentService, private val savedCameraImageRepository: SavedCameraImageRepository, private val capturesDirectory: Path, private val cameraExecutorService: ExecutorService, @@ -32,7 +31,7 @@ class CameraService( private val eventBus: EventBus, ) { - private val runningTasks = Collections.synchronizedMap(HashMap(2)) + private val runningTasks = Collections.synchronizedMap(HashMap(2)) @PostConstruct private fun initialize() { @@ -57,59 +56,45 @@ class CameraService( } } - fun attachedCameras(): List { - return equipmentService.cameras().map(::CameraResponse) - } - - operator fun get(name: String): CameraResponse { - val camera = requireNotNull(equipmentService.camera(name)) - return CameraResponse(camera) - } - - fun connect(name: String) { - val camera = requireNotNull(equipmentService.camera(name)) + fun connect(camera: Camera) { camera.connect() } - fun disconnect(name: String) { - val camera = requireNotNull(equipmentService.camera(name)) + fun disconnect(camera: Camera) { camera.disconnect() } - fun isCapturing(name: String): Boolean { - return runningTasks.containsKey(name) + fun isCapturing(camera: Camera): Boolean { + return runningTasks.containsKey(camera) } - fun setpointTemperature(name: String, temperature: Double) { - val camera = requireNotNull(equipmentService.camera(name)) + fun setpointTemperature(camera: Camera, temperature: Double) { camera.temperature(temperature) } - fun cooler(name: String, enable: Boolean) { - val camera = requireNotNull(equipmentService.camera(name)) + fun cooler(camera: Camera, enable: Boolean) { camera.cooler(enable) } @Synchronized - fun startCapture(name: String, data: CameraStartCaptureRequest) { - if (isCapturing(name)) return + fun startCapture(camera: Camera, data: CameraStartCaptureRequest) { + if (isCapturing(camera)) return - val camera = requireNotNull(equipmentService.camera(name)) val savePath = data.savePath?.ifBlank { null }?.let(Path::of) ?.takeIf { it.exists() && it.isDirectory() } - ?: Path.of("$capturesDirectory", name).createDirectories() + ?: Path.of("$capturesDirectory", camera.name).createDirectories() val task = CameraExposureTask(camera, data, savePath) val future = CompletableFuture.runAsync(task, cameraExecutorService) - runningTasks[name] = task + runningTasks[camera] = task future.whenComplete { _, _ -> - runningTasks.remove(name) + runningTasks.remove(camera) } } - fun abortCapture(name: String) { - runningTasks[name]?.abort() + fun abortCapture(camera: Camera) { + runningTasks[camera]?.abort() } } diff --git a/api/src/main/kotlin/nebulosa/api/services/EquipmentService.kt b/api/src/main/kotlin/nebulosa/api/services/EquipmentService.kt index 72ea35d72..67552db03 100644 --- a/api/src/main/kotlin/nebulosa/api/services/EquipmentService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/EquipmentService.kt @@ -12,14 +12,17 @@ import nebulosa.indi.device.filterwheel.FilterWheelDetached import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserAttached import nebulosa.indi.device.focuser.FocuserDetached +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountAttached +import nebulosa.indi.device.mount.MountDetached import org.greenrobot.eventbus.EventBus import org.springframework.stereotype.Service -import java.util.* @Service class EquipmentService(private val eventBus: EventBus) : DeviceEventHandler { private val cameras = ArrayList(2) + private val mounts = ArrayList(2) private val focusers = ArrayList(2) private val filterWheels = ArrayList(2) @@ -28,6 +31,8 @@ class EquipmentService(private val eventBus: EventBus) : DeviceEventHandler { when (event) { is CameraAttached -> cameras.add(event.device) is CameraDetached -> cameras.remove(event.device) + is MountAttached -> mounts.add(event.device) + is MountDetached -> mounts.remove(event.device) is FocuserAttached -> focusers.add(event.device) is FocuserDetached -> focusers.remove(event.device) is FilterWheelAttached -> filterWheels.add(event.device) @@ -38,15 +43,23 @@ class EquipmentService(private val eventBus: EventBus) : DeviceEventHandler { } fun cameras(): List { - return Collections.unmodifiableList(cameras) + return cameras } fun camera(name: String): Camera? { return cameras.firstOrNull { it.name == name } } + fun mounts(): List { + return mounts + } + + fun mount(name: String): Mount? { + return mounts.firstOrNull { it.name == name } + } + fun focusers(): List { - return Collections.unmodifiableList(focusers) + return focusers } fun focuser(name: String): Focuser? { @@ -54,7 +67,7 @@ class EquipmentService(private val eventBus: EventBus) : DeviceEventHandler { } fun filterWheels(): List { - return Collections.unmodifiableList(filterWheels) + return filterWheels } fun filterWheel(name: String): FilterWheel? { @@ -62,8 +75,6 @@ class EquipmentService(private val eventBus: EventBus) : DeviceEventHandler { } operator fun get(name: String): Device? { - return camera(name) - ?: focuser(name) - ?: filterWheel(name) + return camera(name) ?: mount(name) ?: focuser(name) ?: filterWheel(name) } } diff --git a/api/src/main/kotlin/nebulosa/api/services/FilterWheelService.kt b/api/src/main/kotlin/nebulosa/api/services/FilterWheelService.kt index 4d960982f..286e1fb01 100644 --- a/api/src/main/kotlin/nebulosa/api/services/FilterWheelService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/FilterWheelService.kt @@ -1,8 +1,8 @@ package nebulosa.api.services import jakarta.annotation.PostConstruct -import nebulosa.api.data.responses.FilterWheelResponse import nebulosa.indi.device.PropertyChangedEvent +import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelAttached import nebulosa.indi.device.filterwheel.FilterWheelDetached import nebulosa.indi.device.filterwheel.FilterWheelEvent @@ -13,7 +13,6 @@ import org.springframework.stereotype.Service @Service class FilterWheelService( - private val equipmentService: EquipmentService, private val webSocketService: WebSocketService, private val eventBus: EventBus, ) { @@ -32,32 +31,19 @@ class FilterWheelService( } } - fun attachedFilterWheels(): List { - return equipmentService.filterWheels().map(::FilterWheelResponse) - } - - operator fun get(name: String): FilterWheelResponse { - val filterWheel = requireNotNull(equipmentService.filterWheel(name)) - return FilterWheelResponse(filterWheel) - } - - fun connect(name: String) { - val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + fun connect(filterWheel: FilterWheel) { filterWheel.connect() } - fun disconnect(name: String) { - val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + fun disconnect(filterWheel: FilterWheel) { filterWheel.disconnect() } - fun moveTo(name: String, steps: Int) { - val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + fun moveTo(filterWheel: FilterWheel, steps: Int) { filterWheel.moveTo(steps) } - fun syncNames(name: String, filterNames: List) { - val filterWheel = requireNotNull(equipmentService.filterWheel(name)) + fun syncNames(filterWheel: FilterWheel, filterNames: List) { filterWheel.syncNames(filterNames) } } diff --git a/api/src/main/kotlin/nebulosa/api/services/FocuserService.kt b/api/src/main/kotlin/nebulosa/api/services/FocuserService.kt index db4eaa64d..67446a549 100644 --- a/api/src/main/kotlin/nebulosa/api/services/FocuserService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/FocuserService.kt @@ -1,8 +1,8 @@ package nebulosa.api.services import jakarta.annotation.PostConstruct -import nebulosa.api.data.responses.FocuserResponse import nebulosa.indi.device.PropertyChangedEvent +import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserAttached import nebulosa.indi.device.focuser.FocuserDetached import nebulosa.indi.device.focuser.FocuserEvent @@ -13,7 +13,6 @@ import org.springframework.stereotype.Service @Service class FocuserService( - private val equipmentService: EquipmentService, private val webSocketService: WebSocketService, private val eventBus: EventBus, ) { @@ -32,47 +31,31 @@ class FocuserService( } } - fun attachedFocusers(): List { - return equipmentService.focusers().map(::FocuserResponse) - } - - operator fun get(name: String): FocuserResponse { - val focuser = requireNotNull(equipmentService.focuser(name)) - return FocuserResponse(focuser) - } - - fun connect(name: String) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun connect(focuser: Focuser) { focuser.connect() } - fun disconnect(name: String) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun disconnect(focuser: Focuser) { focuser.disconnect() } - fun moveIn(name: String, steps: Int) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun moveIn(focuser: Focuser, steps: Int) { focuser.moveFocusIn(steps) } - fun moveOut(name: String, steps: Int) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun moveOut(focuser: Focuser, steps: Int) { focuser.moveFocusOut(steps) } - fun moveTo(name: String, steps: Int) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun moveTo(focuser: Focuser, steps: Int) { focuser.moveFocusTo(steps) } - fun abort(name: String) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun abort(focuser: Focuser) { focuser.abortFocus() } - fun syncTo(name: String, steps: Int) { - val focuser = requireNotNull(equipmentService.focuser(name)) + fun syncTo(focuser: Focuser, steps: Int) { focuser.syncFocusTo(steps) } } diff --git a/api/src/main/kotlin/nebulosa/api/services/INDIService.kt b/api/src/main/kotlin/nebulosa/api/services/INDIService.kt index 46115d7e8..ced2b7763 100644 --- a/api/src/main/kotlin/nebulosa/api/services/INDIService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/INDIService.kt @@ -3,24 +3,18 @@ package nebulosa.api.services import nebulosa.api.data.enums.INDISendPropertyType import nebulosa.api.data.requests.INDISendPropertyRequest import nebulosa.api.data.responses.INDIPropertyResponse +import nebulosa.indi.device.Device import org.springframework.stereotype.Service import java.util.* @Service("indiService") -class INDIService( - private val equipmentService: EquipmentService, -) { +class INDIService : LinkedList() { - private val messageReceived = LinkedList() - - fun properties(name: String): List { - val device = equipmentService[name] ?: return emptyList() + fun properties(device: Device): List { return device.properties.values.map(::INDIPropertyResponse) } - fun sendProperty(name: String, vector: INDISendPropertyRequest) { - val device = equipmentService[name] ?: return - + fun sendProperty(device: Device, vector: INDISendPropertyRequest) { when (vector.type) { INDISendPropertyType.NUMBER -> { val elements = vector.items.map { it.name to "${it.value}".toDouble() } @@ -36,14 +30,4 @@ class INDIService( } } } - - fun indiLog(name: String?): List { - if (name.isNullOrBlank()) return messageReceived - val device = equipmentService[name] ?: return emptyList() - return device.messages - } - - internal fun onMessageReceived(message: String) { - messageReceived.addFirst(message) - } } diff --git a/api/src/main/kotlin/nebulosa/api/services/ImageService.kt b/api/src/main/kotlin/nebulosa/api/services/ImageService.kt index cd8c846d3..b36175f4d 100644 --- a/api/src/main/kotlin/nebulosa/api/services/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/ImageService.kt @@ -92,7 +92,7 @@ class ImageService( val savedImage = savedCameraImageRepository.withPath("$path") val info = ImageInfoResponse( - savedImage?.name ?: "", + savedImage?.camera ?: "", savedImage?.path ?: "", savedImage?.savedAt ?: 0L, transformedImage.width, @@ -125,11 +125,11 @@ class ImageService( } fun imagesOfCamera(name: String): List { - return savedCameraImageRepository.withName(name) + return savedCameraImageRepository.withCamera(name) } fun latestImageOfCamera(name: String): SavedCameraImageEntity { - return savedCameraImageRepository.withNameLatest(name)!! + return savedCameraImageRepository.withCameraLatest(name)!! } fun savedImageOfPath(path: Path): SavedCameraImageEntity { diff --git a/api/src/main/kotlin/nebulosa/api/services/MountService.kt b/api/src/main/kotlin/nebulosa/api/services/MountService.kt new file mode 100644 index 000000000..c2d665618 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/services/MountService.kt @@ -0,0 +1,204 @@ +package nebulosa.api.services + +import jakarta.annotation.PostConstruct +import nebulosa.api.data.responses.ComputedCoordinateResponse +import nebulosa.constants.PI +import nebulosa.constants.TAU +import nebulosa.indi.device.PropertyChangedEvent +import nebulosa.indi.device.mount.* +import nebulosa.math.Angle +import nebulosa.math.AngleFormatter +import nebulosa.math.Distance +import nebulosa.nova.astrometry.Constellation +import nebulosa.nova.position.GeographicPosition +import nebulosa.nova.position.Geoid +import nebulosa.nova.position.ICRF +import nebulosa.time.UTC +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +@Service +class MountService( + private val webSocketService: WebSocketService, + private val eventBus: EventBus, +) { + + @Volatile private var prevTime = 0L + private val site = HashMap(2) + + private var currentTime = UTC.now() + @Synchronized get() { + val curTime = System.currentTimeMillis() + + if (curTime - prevTime >= 60000L) { + field = UTC.now() + } + + return field + } + + @PostConstruct + private fun initialize() { + eventBus.register(this) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onMountEvent(event: MountEvent) { + when (event) { + is PropertyChangedEvent -> webSocketService.sendMountUpdated(event.device!!) + is MountAttached -> webSocketService.sendMountAttached(event.device) + is MountDetached -> webSocketService.sendMountDetached(event.device) + } + + if (event is MountGeographicCoordinateChanged) { + val site = Geoid.IERS2010.latLon(event.device.longitude, event.device.latitude, event.device.elevation) + this.site[event.device] = site + } + } + + fun connect(mount: Mount) { + mount.connect() + } + + fun disconnect(mount: Mount) { + mount.disconnect() + } + + fun tracking(mount: Mount, enable: Boolean) { + mount.tracking(enable) + } + + fun sync(mount: Mount, ra: Angle, dec: Angle, j2000: Boolean) { + if (j2000) mount.syncJ2000(ra, dec) + else mount.sync(ra, dec) + } + + fun slewTo(mount: Mount, ra: Angle, dec: Angle, j2000: Boolean) { + if (j2000) mount.slewToJ2000(ra, dec) + else mount.slewTo(ra, dec) + } + + fun goTo(mount: Mount, ra: Angle, dec: Angle, j2000: Boolean) { + if (j2000) mount.goToJ2000(ra, dec) + else mount.goTo(ra, dec) + } + + fun home(mount: Mount) { + mount.home() + } + + fun abort(mount: Mount) { + mount.abortMotion() + } + + fun trackMode(mount: Mount, mode: TrackMode) { + mount.trackMode(mode) + } + + fun slewRate(mount: Mount, rate: SlewRate) { + mount.slewRate(rate) + } + + fun moveNorth(mount: Mount, enable: Boolean) { + mount.moveNorth(enable) + } + + fun moveSouth(mount: Mount, enable: Boolean) { + mount.moveSouth(enable) + } + + fun moveWest(mount: Mount, enable: Boolean) { + mount.moveWest(enable) + } + + fun moveEast(mount: Mount, enable: Boolean) { + mount.moveEast(enable) + } + + fun park(mount: Mount) { + mount.park() + } + + fun unpark(mount: Mount) { + mount.unpark() + } + + private fun computeTimeLeftToMeridianFlip(rightAscension: Angle, lst: Angle): Angle { + val timeLeft = rightAscension - lst + return if (timeLeft.value < 0.0) timeLeft - SIDEREAL_TIME_DIFF * (timeLeft.normalized.value / TAU) + else timeLeft + SIDEREAL_TIME_DIFF * (1.0 - timeLeft.value / TAU) + } + + fun coordinates(mount: Mount, longitude: Angle, latitude: Angle, elevation: Distance) { + mount.coordinates(longitude, latitude, elevation) + } + + fun dateTime(mount: Mount, dateTime: OffsetDateTime) { + mount.dateTime(dateTime) + } + + @Suppress("NAME_SHADOWING") + fun computeCoordinates( + mount: Mount, + rightAscension: Angle = mount.rightAscension, declination: Angle = mount.declination, + j2000: Boolean, + equatorial: Boolean, horizontal: Boolean, meridian: Boolean, + ): ComputedCoordinateResponse { + val center = site[mount]!! + val time = currentTime + val epoch = if (j2000) null else time + + val icrf = ICRF.equatorial(rightAscension, declination, time = time, epoch = epoch, center = center) + val constellation = Constellation.find(icrf) + + var rightAscension = "" + var declination = "" + var azimuth = "" + var altitude = "" + + if (equatorial) { + val raDec = if (j2000) icrf.equatorialAtDate() else icrf.equatorialJ2000() + rightAscension = raDec.longitude.normalized.format(AngleFormatter.HMS) + declination = raDec.latitude.format(AngleFormatter.SIGNED_DMS) + } + + if (horizontal) { + val altAz = icrf.horizontal() + azimuth = altAz.longitude.normalized.format(AngleFormatter.SIGNED_DMS) + altitude = altAz.latitude.format(AngleFormatter.SIGNED_DMS) + } + + var meridianAt = "" + var timeLeftToMeridianFlip = "" + var lst = "" + + if (meridian) { + val lst = site[mount]!!.lstAt(currentTime).also { lst = it.format(LST_FORMAT) } + val timeLeftToMeridianFlip = computeTimeLeftToMeridianFlip(mount.rightAscension, lst) + .also { timeLeftToMeridianFlip = it.format(LST_FORMAT) } + meridianAt = LocalDateTime.now().plusSeconds((timeLeftToMeridianFlip.hours * 3600.0).toLong()).format(MERIDIAN_TIME_FORMAT) + } + + return ComputedCoordinateResponse( + rightAscension, declination, azimuth, altitude, + constellation, lst, meridianAt, timeLeftToMeridianFlip, + ) + } + + companion object { + + private const val SIDEREAL_TIME_DIFF = 0.06552777 * PI / 12.0 + + @JvmStatic private val MERIDIAN_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss") + @JvmStatic private val LST_FORMAT = AngleFormatter.Builder() + .hours() + .noSign() + .secondsDecimalPlaces(0) + .build() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/services/WebSocketService.kt b/api/src/main/kotlin/nebulosa/api/services/WebSocketService.kt index 5b71c6bf2..cff725257 100644 --- a/api/src/main/kotlin/nebulosa/api/services/WebSocketService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/WebSocketService.kt @@ -1,16 +1,14 @@ package nebulosa.api.services import nebulosa.api.data.entities.SavedCameraImageEntity -import nebulosa.api.data.responses.CameraResponse -import nebulosa.api.data.responses.FilterWheelResponse -import nebulosa.api.data.responses.FocuserResponse -import nebulosa.api.data.responses.INDIPropertyResponse +import nebulosa.api.data.responses.* import nebulosa.indi.device.DeviceMessageReceived import nebulosa.indi.device.DevicePropertyEvent import nebulosa.indi.device.PropertyVector 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.log.loggerFor import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Service @@ -66,6 +64,25 @@ class WebSocketService(private val simpleMessageTemplate: SimpMessagingTemplate) sendMessage(eventName, CameraResponse(camera)) } + // MOUNT + + fun sendMountUpdated(mount: Mount) { + sendMountEvent(MOUNT_UPDATED, mount) + } + + fun sendMountAttached(mount: Mount) { + sendMountEvent(MOUNT_ATTACHED, mount) + } + + fun sendMountDetached(mount: Mount) { + sendMountEvent(MOUNT_DETACHED, mount) + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMountEvent(eventName: String, mount: Mount) { + sendMessage(eventName, MountResponse(mount)) + } + // FOCUSER fun sendFocuserUpdated(focuser: Focuser) { @@ -131,6 +148,9 @@ class WebSocketService(private val simpleMessageTemplate: SimpMessagingTemplate) const val CAMERA_CAPTURE_FINISHED = "CAMERA_CAPTURE_FINISHED" const val CAMERA_ATTACHED = "CAMERA_ATTACHED" const val CAMERA_DETACHED = "CAMERA_DETACHED" + const val MOUNT_UPDATED = "MOUNT_UPDATED" + const val MOUNT_ATTACHED = "MOUNT_ATTACHED" + const val MOUNT_DETACHED = "MOUNT_DETACHED" const val FOCUSER_UPDATED = "FOCUSER_UPDATED" const val FOCUSER_ATTACHED = "FOCUSER_ATTACHED" const val FOCUSER_DETACHED = "FOCUSER_DETACHED" @@ -157,6 +177,13 @@ class WebSocketService(private val simpleMessageTemplate: SimpMessagingTemplate) CAMERA_DETACHED, ) + @JvmStatic + private val MOUNT_EVENT_NAMES = setOf( + MOUNT_UPDATED, + MOUNT_ATTACHED, + MOUNT_DETACHED, + ) + @JvmStatic private val FOCUSER_EVENT_NAMES = setOf( FOCUSER_UPDATED, @@ -173,7 +200,7 @@ class WebSocketService(private val simpleMessageTemplate: SimpMessagingTemplate) @JvmStatic private val ALL_EVENT_NAMES = listOf( - DEVICE_EVENT_NAMES, CAMERA_EVENT_NAMES, + DEVICE_EVENT_NAMES, CAMERA_EVENT_NAMES, MOUNT_EVENT_NAMES, FOCUSER_EVENT_NAMES, FILTER_WHEEL_EVENT_NAMES, ).flatten().toSet() @@ -182,6 +209,7 @@ class WebSocketService(private val simpleMessageTemplate: SimpMessagingTemplate) "ALL" -> ALL_EVENT_NAMES "DEVICE" -> DEVICE_EVENT_NAMES "CAMERA" -> CAMERA_EVENT_NAMES + "MOUNT" -> MOUNT_EVENT_NAMES "FOCUSER" -> FOCUSER_EVENT_NAMES "FILTER_WHEEL" -> FILTER_WHEEL_EVENT_NAMES else -> setOf(this) diff --git a/api/src/main/kotlin/nebulosa/api/services/ephemeris/CachedEphemerisProvider.kt b/api/src/main/kotlin/nebulosa/api/services/ephemeris/CachedEphemerisProvider.kt index 7dbc46cef..15cfffc41 100644 --- a/api/src/main/kotlin/nebulosa/api/services/ephemeris/CachedEphemerisProvider.kt +++ b/api/src/main/kotlin/nebulosa/api/services/ephemeris/CachedEphemerisProvider.kt @@ -67,7 +67,7 @@ abstract class CachedEphemerisProvider : EphemerisProvider { val elements = compute(key.first, key.second, startTime, endTime) val cachedElements = ephemeris.getOrPut(key) { HashMap(1441) } - elements.forEach { cachedElements[it.time] = it } + elements.forEach { cachedElements[it.dateTime] = it } return elements } diff --git a/desktop/app/.gitignore b/desktop/app/.gitignore new file mode 100644 index 000000000..063134a24 --- /dev/null +++ b/desktop/app/.gitignore @@ -0,0 +1 @@ +types.ts diff --git a/desktop/app/main.ts b/desktop/app/main.ts index c724579f0..f670a1e81 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -2,20 +2,27 @@ import { Client } from '@stomp/stompjs' import { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } from 'electron' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' -import { Camera, FilterWheel, Focuser, INDI_EVENT_TYPES, OpenWindow } from './types' +import { Camera, FilterWheel, Focuser, INDI_EVENT_TYPES, INTERNAL_EVENT_TYPES, Mount, OpenWindow } from './types' import { WebSocket } from 'ws' Object.assign(global, { WebSocket }) -let mainWindow: BrowserWindow | null = null +let homeWindow: BrowserWindow | null = null const secondaryWindows = new Map() let api: ChildProcessWithoutNullStreams | null = null let apiPort = 7000 let wsClient: Client +let selectedCamera: Camera +let selectedMount: Mount +let selectedFocuser: Focuser +let selectedFilterWheel: FilterWheel + const args = process.argv.slice(1) const serve = args.some(e => e === '--serve') +app.commandLine.appendSwitch('disable-http-cache') + function createMainWindow() { createWindow({ id: 'home', path: 'home' }) @@ -24,7 +31,7 @@ function createMainWindow() { onConnect: () => { for (const item of INDI_EVENT_TYPES) { if (item === 'ALL' || item === 'DEVICE' || item === 'CAMERA' || - item === 'FOCUSER') { + item === 'FOCUSER' || item === 'MOUNT') { continue } @@ -35,13 +42,7 @@ function createMainWindow() { console.log(item, message.body) } - for (const [_, window] of secondaryWindows) { - window.webContents.send(item, data) - } - - if (item.endsWith('ATTACHED') || item.endsWith('DETACHED')) { - mainWindow?.webContents.send(item, data) - } + sendToAllWindows(item, data) }) } }, @@ -59,8 +60,8 @@ function createWindow(data: OpenWindow) { } return window - } else if (data.id === 'home' && mainWindow) { - return mainWindow + } else if (data.id === 'home' && homeWindow) { + return homeWindow } const size = screen.getPrimaryDisplay().workAreaSize @@ -130,12 +131,12 @@ function createWindow(data: OpenWindow) { }) window.on('close', () => { - if (window === mainWindow) { + if (window === homeWindow) { for (const [_, value] of secondaryWindows) { value.close() } - mainWindow = null + homeWindow = null api?.kill('SIGHUP') } else { @@ -149,7 +150,7 @@ function createWindow(data: OpenWindow) { }) if (data.id === 'home') { - mainWindow = window + homeWindow = window } else { secondaryWindows.set(data.id, window) } @@ -204,7 +205,7 @@ try { }) app.on('activate', () => { - if (mainWindow === null) { + if (homeWindow === null) { startApp() } }) @@ -232,7 +233,7 @@ try { }) ipcMain.on('OPEN_FITS', async (event) => { - const value = await dialog.showOpenDialog(mainWindow!, { + const value = await dialog.showOpenDialog(homeWindow!, { filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }], properties: ['openFile'], }) @@ -241,7 +242,7 @@ try { }) ipcMain.on('SAVE_FITS_AS', async (event) => { - const value = await dialog.showSaveDialog(mainWindow!, { + const value = await dialog.showSaveDialog(homeWindow!, { filters: [ { name: 'FITS files', extensions: ['fits', 'fit'] }, { name: 'Image files', extensions: ['png', 'jpe?g'] }, @@ -253,7 +254,7 @@ try { }) ipcMain.on('OPEN_DIRECTORY', async (event) => { - const value = await dialog.showOpenDialog(mainWindow!, { + const value = await dialog.showOpenDialog(homeWindow!, { properties: ['openDirectory'], }) @@ -272,29 +273,56 @@ try { event.returnValue = false }) - ipcMain.on('CAMERA_CHANGED', (event, camera: Camera) => { - for (const [_, value] of secondaryWindows) { - if (value.webContents !== event.sender) { - value.webContents.send('CAMERA_CHANGED', camera) - } - } - }) - - ipcMain.on('FOCUSER_CHANGED', (event, focuser: Focuser) => { - for (const [_, value] of secondaryWindows) { - if (value.webContents !== event.sender) { - value.webContents.send('FOCUSER_CHANGED', focuser) + for (const item of INTERNAL_EVENT_TYPES) { + ipcMain.on(item, (event, data) => { + switch (item) { + case 'CAMERA_CHANGED': + selectedCamera = data + break + case 'MOUNT_CHANGED': + selectedMount = data + break + case 'FOCUSER_CHANGED': + selectedFocuser = data + break + case 'FILTER_WHEEL_CHANGED': + selectedFilterWheel = data + break } - } - }) - ipcMain.on('FILTER_WHEEL_CHANGED', (event, filterWheel: FilterWheel) => { - for (const [_, value] of secondaryWindows) { - if (value.webContents !== event.sender) { - value.webContents.send('FILTER_WHEEL_CHANGED', filterWheel) + switch (item) { + case 'SELECTED_CAMERA': + event.returnValue = selectedCamera + break + case 'SELECTED_MOUNT': + event.returnValue = selectedMount + break + case 'SELECTED_FOCUSER': + event.returnValue = selectedFocuser + break + case 'SELECTED_FILTER_WHEEL': + event.returnValue = selectedFilterWheel + break + default: + sendToAllWindows(item, data) + break } - } - }) + }) + } } catch (e) { console.error(e) } + +function sendToAllWindows(channel: string, data: any, home: boolean = true) { + for (const [_, value] of secondaryWindows) { + value.webContents.send(channel, data) + } + + if (home) { + homeWindow?.webContents?.send(channel, data) + } + + if (serve) { + console.log(channel, data) + } +} diff --git a/desktop/app/types.ts b/desktop/app/types.ts deleted file mode 120000 index 282c50c68..000000000 --- a/desktop/app/types.ts +++ /dev/null @@ -1 +0,0 @@ -../src/shared/types.ts \ No newline at end of file diff --git a/desktop/copyFiles.js b/desktop/copyFiles.js new file mode 100644 index 000000000..34480147a --- /dev/null +++ b/desktop/copyFiles.js @@ -0,0 +1,6 @@ +const fs = require('fs') +const { copyFiles } = require('./package.json') + +for (const file of copyFiles) { + fs.copyFile(file.from, file.to, () => null) +} diff --git a/desktop/filter-wheel.png b/desktop/filter-wheel.png index b00c9a0b2..44cff6aff 100644 Binary files a/desktop/filter-wheel.png and b/desktop/filter-wheel.png differ diff --git a/desktop/focuser.png b/desktop/focuser.png index 6fdcf16e7..06251b88a 100644 Binary files a/desktop/focuser.png and b/desktop/focuser.png differ diff --git a/desktop/image.png b/desktop/image.png index 0867c1442..82cd99f9b 100644 Binary files a/desktop/image.png and b/desktop/image.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 9997d92cd..94beb6199 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -11,22 +11,22 @@ "license": "MIT", "dependencies": { "@angular/animations": "16.1.3", - "@angular/cdk": "16.1.6", + "@angular/cdk": "16.1.7", "@angular/common": "16.1.3", "@angular/compiler": "16.1.3", "@angular/core": "16.1.3", "@angular/forms": "16.1.3", - "@angular/language-service": "16.1.7", + "@angular/language-service": "16.1.8", "@angular/platform-browser": "16.1.3", "@angular/platform-browser-dynamic": "16.1.3", "@angular/router": "16.1.3", - "chart.js": "4.3.2", + "chart.js": "4.3.3", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "16.1.0", + "primeng": "16.0.2", "rxjs": "7.8.1", "tslib": "2.6.1", "uuid": "9.0.0", @@ -34,13 +34,13 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "16.0.0", - "@angular-devkit/build-angular": "16.1.6", - "@angular/cli": "16.1.6", + "@angular-devkit/build-angular": "16.1.7", + "@angular/cli": "16.1.7", "@angular/compiler-cli": "16.1.3", "@types/leaflet": "1.9.3", - "@types/node": "20.4.5", + "@types/node": "20.4.7", "@types/uuid": "9.0.2", - "electron": "25.3.2", + "electron": "25.4.0", "electron-builder": "24.6.3", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", @@ -89,12 +89,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1601.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.6.tgz", - "integrity": "sha512-dY+/FNUNrOj+m4iG5/v8N0PfbDmjkjjoy/YkquRHS1yo7fGGDFNqji2552mbtjN6/LwyWDhOO7fxdqppadjnvA==", + "version": "0.1601.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.7.tgz", + "integrity": "sha512-uFa7/TTPYoYLqgiRi5HZJaOXterVe9A83Sd+e3NXMmvT6oTMyLv0/t0Luhjic6c4vFrNnF3ECkrlGs2qW4TslA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", + "@angular-devkit/core": "16.1.7", "rxjs": "7.8.1" }, "engines": { @@ -104,15 +104,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.6.tgz", - "integrity": "sha512-IEC1tApX8/Qa/RIVmbj0nYbOQ5WGcrkGNJ7D42q4DkIo74XKPzxDRruJE1RCjdZsj8lf4CCCZgSOPBsEI8Zbdw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.7.tgz", + "integrity": "sha512-zUkPH3QDpA//hhMjMm9pKcbhO2gZjy4EkWrPglvR6G6a0myZc6/rEYK3W8gQWP1jiYiha/PiGnxTb+XeN/74tQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1601.6", - "@angular-devkit/build-webpack": "0.1601.6", - "@angular-devkit/core": "16.1.6", + "@angular-devkit/architect": "0.1601.7", + "@angular-devkit/build-webpack": "0.1601.7", + "@angular-devkit/core": "16.1.7", "@babel/core": "7.22.5", "@babel/generator": "7.22.7", "@babel/helper-annotate-as-pure": "7.22.5", @@ -124,7 +124,7 @@ "@babel/runtime": "7.22.5", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.1.6", + "@ngtools/webpack": "16.1.7", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -328,12 +328,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1601.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.6.tgz", - "integrity": "sha512-Uz/GjnhgAqSDPxrO4HP/tHNGPPZU3tEShtAVKyAypBl20bh2Aw1L5D+lCZi/Uq3Sh2JTPD9/M0ei2u9CMLhLDw==", + "version": "0.1601.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.7.tgz", + "integrity": "sha512-BFqjL9mz0gtS10ucwmx7fFb1PprBzt6glN7ZEGhx58tterW68N9zZNFHR0AXmWoyVp/1B2oJWmqAQ52fakyshg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.6", + "@angular-devkit/architect": "0.1601.7", "rxjs": "7.8.1" }, "engines": { @@ -347,9 +347,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.6.tgz", - "integrity": "sha512-3OjtrPWvsqVkMBwqPeE65ccCIw56FooNpVVAJ0XwhVQv5mA81pmbCzU7JsR6U449ZT7O4cQblzZMQvWvx74HCg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.7.tgz", + "integrity": "sha512-AXc9/F57Nf/A26yGu+w7PhNYriTvwazPTQsVPW/SBcTcpBa/hAsBTbPl8o8ErRJneJIoYqy/EIuabf9iiU8bRA==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -373,12 +373,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.6.tgz", - "integrity": "sha512-KA8P78gaS76HMHGBOM8JHJXWLOxCIShYVB2Un/Cu6z3jVODvXq+ILZUc1Y0RsAce/vsl2wf8qpoh5Lku9KJHUQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.7.tgz", + "integrity": "sha512-FQ/svSrUyalHVxgudoXAJbwgdPZ1iaEc6My4s4ti2HFtw0vqPw7Dh9bkRiMN3/MGMAdvsUIBiz8oSELZhuJTJg==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", + "@angular-devkit/core": "16.1.7", "jsonc-parser": "3.2.0", "magic-string": "0.30.0", "ora": "5.4.1", @@ -405,9 +405,9 @@ } }, "node_modules/@angular/cdk": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.1.6.tgz", - "integrity": "sha512-ICwX3OyxmVotlhzlkvilvfZz32y9RXvUAaVtPsU1i20orgQBOMp+JGdP/vahLjTQRioUus834Wh6bu0KdHjCEg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.1.7.tgz", + "integrity": "sha512-KLiqzbilkGBtQcaNdqjN16XyNdQxEkN4Oqbg6coahWqwvEVEdhNwLrwOJcCHMH2vvMzCd4XHaOnAxQjVy5pkjQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -421,15 +421,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.6.tgz", - "integrity": "sha512-yXVgUKMXxlAHkhc6xk3ljR7TXpMLBykyu8do+ooSP08VKEQnWjTdVgrcOHd0n5w9YHXUQgBSmjDKxtQaBmvyZQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.7.tgz", + "integrity": "sha512-RNmRytkCFqmkGiiy6IrLEvKkipTEeZW1Un2aKbaPBM8qqfZCeUx8TfU+G4DI4w1jvJ8IwzkYQLAi4NPk1DPxmQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.6", - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", - "@schematics/angular": "16.1.6", + "@angular-devkit/architect": "0.1601.7", + "@angular-devkit/core": "16.1.7", + "@angular-devkit/schematics": "16.1.7", + "@schematics/angular": "16.1.7", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -549,9 +549,9 @@ } }, "node_modules/@angular/language-service": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.1.7.tgz", - "integrity": "sha512-BBqT8ETBu1JtNZQS5Vs8e/Ru5UQKuNf2W4TGsWJVHFKdsjaghryG4NZQPXaYERDjU3k/64dZjcFNgzhP96LlZA==", + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.1.8.tgz", + "integrity": "sha512-aP0M8NXt1VoVoRoK4te922X7UIrI0Wsi3XMudySCeOZwkMwuTO/cI9Bq/jF4di4pweAAlTA0HfewdKgyDa6ebA==", "engines": { "node": "^16.14.0 || >=18.10.0" } @@ -3414,9 +3414,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.6.tgz", - "integrity": "sha512-rDE1bV3+Ys/VyeD6l7JKtbs3+bTQAfWhi7meEuq5mkaJHOERu6Z40ce866faAIX2I1AVpsSv8rLlb7kB7t7kzw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.7.tgz", + "integrity": "sha512-VRFH8crY969hKLIkYJKUuWjNIRCssq4QkKlUK4LCOLMd/xKNzl4H4EOsASKAQbBz58y6IUyh6b4utqMZ+oSTxA==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -3613,13 +3613,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.6.tgz", - "integrity": "sha512-BxghkeLfnMgV0D4DZDcbfPpox/Orw1ismSVGoQMIV/Daj2pqfSK+n97NAu0r0EsQyR5agPxOX9khVft+otODhg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.7.tgz", + "integrity": "sha512-ynMh1o24ImTyHDsrwWzMwxIb5VUgk22ZD3SgcQ8I9ZSSPiFyCzNrbwz20+WqUfVtT0nsI+LWw9UaGPgwCLZnBQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", + "@angular-devkit/core": "16.1.7", + "@angular-devkit/schematics": "16.1.7", "jsonc-parser": "3.2.0" }, "engines": { @@ -3959,9 +3959,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz", - "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==", + "version": "20.4.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz", + "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==", "dev": true }, "node_modules/@types/plist": { @@ -5640,9 +5640,9 @@ "dev": true }, "node_modules/chart.js": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.2.tgz", - "integrity": "sha512-pvQNyFOY1QmbmIr8oDORL16/FFivfxj8V26VFpFilMo4cNvkV5WXLJetDio365pd9gKUHGdirUTbqJfw8tr+Dg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.3.tgz", + "integrity": "sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -6922,9 +6922,9 @@ } }, "node_modules/electron": { - "version": "25.3.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.2.tgz", - "integrity": "sha512-xiktJvXraaE/ARf2OVHFyTze1TksSbsbJgOaBtdIiBvUduez6ipATEPIec8Msz1n6eQ+xqYb6YF8tDuIZtJSPw==", + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.4.0.tgz", + "integrity": "sha512-VLTRxDhL4UvQbqM7pTNENnJo62cdAPZT92N+B7BZQ5Xfok1wuVPEewIjBot4K7U3EpLUuHn1veeLzho3ihiP+Q==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -12416,9 +12416,9 @@ "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" }, "node_modules/primeng": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-16.1.0.tgz", - "integrity": "sha512-qqYQ2xO6EmiBEqvlKHIWJPrC90HVVQGitnrGurpdT9f8/Mkz0YCCo4GwLElKyHZ52STd+cw2MtoXa1sJRVR30g==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-16.0.2.tgz", + "integrity": "sha512-gLFUSQ0fV5948yM1fMCv9oGaJ54AS8+HHSMOeR2lHWFiZzomxjXR0MST9yyAQ0NjrOlhke3BBpl+zYjISBeEJg==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/desktop/package.json b/desktop/package.json index 60cd9803b..022ac126a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -13,9 +13,10 @@ "scripts": { "postinstall": "electron-builder install-app-deps", "ng": "ng", - "start": "npm-run-all -p electron:serve ng:serve", + "copy:files": "node copyFiles.js", + "start": "npm run copy:files && npm-run-all -p electron:serve ng:serve", "ng:serve": "ng serve -c web --hmr", - "build": "npm run electron:serve-tsc && ng build --base-href ./", + "build": "npm run copy:files && npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", "web:build": "npm run build -- -c web-production", @@ -30,22 +31,22 @@ }, "dependencies": { "@angular/animations": "16.1.3", - "@angular/cdk": "16.1.6", + "@angular/cdk": "16.1.7", "@angular/common": "16.1.3", "@angular/compiler": "16.1.3", "@angular/core": "16.1.3", "@angular/forms": "16.1.3", - "@angular/language-service": "16.1.7", + "@angular/language-service": "16.1.8", "@angular/platform-browser": "16.1.3", "@angular/platform-browser-dynamic": "16.1.3", "@angular/router": "16.1.3", - "chart.js": "4.3.2", + "chart.js": "4.3.3", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "16.1.0", + "primeng": "16.0.2", "rxjs": "7.8.1", "tslib": "2.6.1", "uuid": "9.0.0", @@ -53,13 +54,13 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "16.0.0", - "@angular-devkit/build-angular": "16.1.6", - "@angular/cli": "16.1.6", + "@angular-devkit/build-angular": "16.1.7", + "@angular/cli": "16.1.7", "@angular/compiler-cli": "16.1.3", "@types/leaflet": "1.9.3", - "@types/node": "20.4.5", + "@types/node": "20.4.7", "@types/uuid": "9.0.2", - "electron": "25.3.2", + "electron": "25.4.0", "electron-builder": "24.6.3", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", @@ -74,5 +75,11 @@ }, "browserslist": [ "chrome 114" + ], + "copyFiles": [ + { + "from": "src/shared/types.ts", + "to": "app/types.ts" + } ] -} +} \ No newline at end of file diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index 49046d149..299532bda 100644 --- a/desktop/src/app/app-routing.module.ts +++ b/desktop/src/app/app-routing.module.ts @@ -10,6 +10,7 @@ import { FramingComponent } from './framing/framing.component' import { HomeComponent } from './home/home.component' import { ImageComponent } from './image/image.component' import { INDIComponent } from './indi/indi.component' +import { MountComponent } from './mount/mount.component' const routes: Routes = [ { @@ -33,6 +34,10 @@ const routes: Routes = [ path: 'filterWheel', component: FilterWheelComponent, }, + { + path: 'mount', + component: MountComponent, + }, { path: 'image', component: ImageComponent, diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index bddde78c2..fc235c0c3 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -43,6 +43,7 @@ import { HomeComponent } from './home/home.component' import { ImageComponent } from './image/image.component' import { INDIComponent } from './indi/indi.component' import { INDIPropertyComponent } from './indi/property/indi-property.component' +import { MountComponent } from './mount/mount.component' @NgModule({ declarations: [ @@ -59,6 +60,7 @@ import { INDIPropertyComponent } from './indi/property/indi-property.component' AboutComponent, FocuserComponent, FilterWheelComponent, + MountComponent, EnvPipe, WinPipe, EnumPipe, diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 697f1acbc..871532b91 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core' import { Title } from '@angular/platform-browser' import { ChartData, ChartOptions } from 'chart.js' import * as moment from 'moment' @@ -9,6 +9,7 @@ import { MoonComponent } from '../../shared/components/moon/moon.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { CONSTELLATIONS, Constellation, DeepSkyObject, EMPTY_BODY_POSITION, EMPTY_LOCATION, Location, MinorPlanet, SkyObjectType, Star, TypeWithAll } from '../../shared/types' +import { ElectronService } from '../../shared/services/electron.service' export interface PlanetItem { name: string @@ -31,7 +32,7 @@ export interface SearchFilter { templateUrl: './atlas.component.html', styleUrls: ['./atlas.component.scss'] }) -export class AtlasComponent implements OnInit, OnDestroy { +export class AtlasComponent implements AfterViewInit, OnDestroy { refreshing = false @@ -56,14 +57,26 @@ export class AtlasComponent implements OnInit, OnDestroy { { icon: 'mdi mdi-check', label: 'Go To', + command: async () => { + const mount = await this.electron.sendSync('SELECTED_MOUNT') + this.api.mountGoTo(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + }, }, { icon: 'mdi mdi-check', label: 'Slew To', + command: async () => { + const mount = await this.electron.sendSync('SELECTED_MOUNT') + this.api.mountSlewTo(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + }, }, { icon: 'mdi mdi-sync', label: 'Sync', + command: async () => { + const mount = await this.electron.sendSync('SELECTED_MOUNT') + this.api.mountSync(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + }, }, { icon: 'mdi mdi-image', @@ -428,6 +441,7 @@ export class AtlasComponent implements OnInit, OnDestroy { private title: Title, private api: ApiService, private browserWindow: BrowserWindowService, + private electron: ElectronService, ) { title.setTitle('Sky Atlas') @@ -436,7 +450,7 @@ export class AtlasComponent implements OnInit, OnDestroy { setInterval(() => this.refreshTab(), 60000) } - async ngOnInit() { + async ngAfterViewInit() { this.locations = await this.api.locations() } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 3ebd29947..d1a2ddb0a 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -40,7 +40,7 @@
@@ -55,7 +55,7 @@ + [allowEmpty]="false" (ngModelChange)="savePreference()" /> @@ -65,7 +65,8 @@
- +
@@ -73,19 +74,22 @@
Exposure Mode - +
+ [showButtons]="true" [step]="0" [min]="0" [max]="600" locale="en" styleClass="border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" />
+ [showButtons]="true" [step]="1.0" [min]="1" [max]="10000" locale="en" styleClass="border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" />
@@ -93,29 +97,33 @@
- +
- +
- +
- +
@@ -123,23 +131,23 @@
Subframe - +
-
+ [step]="1.0" [min]="1" [max]="4" styleClass="border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" />
+ [step]="1.0" [min]="1" [max]="4" styleClass="border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" />
@@ -147,21 +155,24 @@
- +
+ [step]="1.0" [min]="gainMin" [max]="gainMax" styleClass="border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" />
+ [step]="1.0" [min]="offsetMin" [max]="offsetMax" styleClass="border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" />
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 6586d40d7..c5c86aa60 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { Title } from '@angular/platform-browser' import { MenuItem } from 'primeng/api' import { ApiService } from '../../shared/services/api.service' @@ -12,7 +12,7 @@ import { AutoSubFolderMode, Camera, CameraStartCapture, ExposureMode, ExposureTi templateUrl: './camera.component.html', styleUrls: ['./camera.component.scss'] }) -export class CameraComponent implements OnInit, OnDestroy { +export class CameraComponent implements AfterViewInit, OnDestroy { cameras: Camera[] = [] camera?: Camera @@ -37,7 +37,7 @@ export class CameraComponent implements OnInit, OnDestroy { icon: 'mdi mdi-folder', label: 'Save path...', command: async () => { - const path = await this.electron.ipcRenderer.sendSync('OPEN_DIRECTORY') + const path = await this.electron.sendSync('OPEN_DIRECTORY') if (path) { this.savePath = path @@ -92,19 +92,19 @@ export class CameraComponent implements OnInit, OnDestroy { exposureMode: ExposureMode = 'SINGLE' exposureDelay = 0 exposureCount = 1 - x = 0.0 - minX = 0.0 - maxX = 0.0 - y = 0.0 - minY = 0.0 - maxY = 0.0 + x = 0 + minX = 0 + maxX = 0 + y = 0 + minY = 0 + maxY = 0 width = 1023 minWidth = 1023 maxWidth = 1023 height = 1280 minHeight = 1280 maxHeight = 1280 - subframe = false + subFrame = false binX = 1 binY = 1 frameType: FrameType = 'LIGHT' @@ -120,22 +120,35 @@ export class CameraComponent implements OnInit, OnDestroy { readonly exposureModeOptions: ExposureMode[] = ['SINGLE', 'FIXED', 'LOOP'] readonly frameTypeOptions: FrameType[] = ['LIGHT', 'DARK', 'FLAT', 'BIAS'] + readonly exposureTimeUnitOptions: MenuItem[] = [ { label: 'Minute (m)', - command: () => this.updateExposureUnit(ExposureTimeUnit.MINUTE) + command: () => { + this.updateExposureUnit(ExposureTimeUnit.MINUTE) + this.savePreference() + } }, { label: 'Second (s)', - command: () => this.updateExposureUnit(ExposureTimeUnit.SECOND) + command: () => { + this.updateExposureUnit(ExposureTimeUnit.SECOND) + this.savePreference() + } }, { label: 'Millisecond (ms)', - command: () => this.updateExposureUnit(ExposureTimeUnit.MILLISECOND) + command: () => { + this.updateExposureUnit(ExposureTimeUnit.MILLISECOND) + this.savePreference() + } }, { label: 'Microsecond (µs)', - command: () => this.updateExposureUnit(ExposureTimeUnit.MICROSECOND) + command: () => { + this.updateExposureUnit(ExposureTimeUnit.MICROSECOND) + this.savePreference() + } } ] @@ -175,13 +188,8 @@ export class CameraComponent implements OnInit, OnDestroy { }) } - async ngOnInit() { + async ngAfterViewInit() { this.cameras = await this.api.attachedCameras() - - if (this.cameras.length > 0) { - this.camera = this.cameras[0] - this.update() - } } @HostListener('window:unload') @@ -193,27 +201,29 @@ export class CameraComponent implements OnInit, OnDestroy { if (this.camera) { this.title.setTitle(`Camera ・ ${this.camera.name}`) - this.loadPreference() - const camera = await this.api.camera(this.camera.name) Object.assign(this.camera, camera) + + this.loadPreference() this.update() + this.savePreference() } else { this.title.setTitle(`Camera`) } - this.electron.ipcRenderer.send('CAMERA_CHANGED', this.camera) + this.electron.send('CAMERA_CHANGED', this.camera) } - async connect() { + connect() { if (this.connected) { - await this.api.cameraDisconnect(this.camera!) + this.api.cameraDisconnect(this.camera!) } else { - await this.api.cameraConnect(this.camera!) + this.api.cameraConnect(this.camera!) } } applySetpointTemperature() { + this.savePreference() this.api.cameraSetpointTemperature(this.camera!, this.setpointTemperature) } @@ -227,14 +237,15 @@ export class CameraComponent implements OnInit, OnDestroy { this.y = this.camera.minY this.width = this.camera.maxWidth this.height = this.camera.maxHeight + this.savePreference() } } async startCapture() { - const x = this.subframe ? this.x : this.camera!.minX - const y = this.subframe ? this.y : this.camera!.minY - const width = this.subframe ? this.width : this.camera!.maxWidth - const height = this.subframe ? this.height : this.camera!.maxHeight + const x = this.subFrame ? this.x : this.camera!.minX + const y = this.subFrame ? this.y : this.camera!.minY + const width = this.subFrame ? this.width : this.camera!.maxWidth + const height = this.subFrame ? this.height : this.camera!.maxHeight const exposureFactor = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) const exposure = Math.trunc(this.exposureTime * 60000000 / exposureFactor) const amount = this.exposureMode === 'LOOP' ? 2147483647 : @@ -288,36 +299,34 @@ export class CameraComponent implements OnInit, OnDestroy { } private async update() { - if (!this.camera) { - return + if (this.camera) { + this.connected = this.camera.connected + this.cooler = this.camera.cooler + this.hasCooler = this.camera.hasCooler + this.coolerPower = this.camera.coolerPower + this.dewHeater = this.camera.dewHeater + this.temperature = this.camera.temperature + this.canSetTemperature = this.camera.canSetTemperature + this.minX = this.camera.minX + this.maxX = this.camera.maxX + this.x = Math.max(this.minX, Math.min(this.x, this.maxX)) + this.minY = this.camera.minY + this.maxY = this.camera.maxY + this.y = Math.max(this.minY, Math.min(this.y, this.maxY)) + this.minWidth = this.camera.minWidth + this.maxWidth = this.camera.maxWidth + this.width = Math.max(this.minWidth, Math.min(this.width, this.maxWidth)) + this.minHeight = this.camera.minHeight + this.maxHeight = this.camera.maxHeight + this.height = Math.max(this.minHeight, Math.min(this.height, this.maxHeight)) + this.frameFormats = this.camera.frameFormats + this.gainMin = this.camera.gainMin + this.gainMax = this.camera.gainMax + this.offsetMin = this.camera.offsetMin + this.offsetMax = this.camera.offsetMax + + this.updateExposureUnit(this.exposureTimeUnit) } - - this.connected = this.camera.connected - this.cooler = this.camera.cooler - this.hasCooler = this.camera.hasCooler - this.coolerPower = this.camera.coolerPower - this.dewHeater = this.camera.dewHeater - this.temperature = this.camera.temperature - this.canSetTemperature = this.camera.canSetTemperature - this.minX = this.camera.minX - this.maxX = this.camera.maxX - this.x = Math.max(this.minX, Math.min(this.x, this.maxX)) - this.minY = this.camera.minY - this.maxY = this.camera.maxY - this.y = Math.max(this.minY, Math.min(this.y, this.maxY)) - this.minWidth = this.camera.minWidth - this.maxWidth = this.camera.maxWidth - this.width = Math.max(this.minWidth, Math.min(this.width, this.maxWidth)) - this.minHeight = this.camera.minHeight - this.maxHeight = this.camera.maxHeight - this.height = Math.max(this.minHeight, Math.min(this.height, this.maxHeight)) - this.frameFormats = this.camera.frameFormats - this.gainMin = this.camera.gainMin - this.gainMax = this.camera.gainMax - this.offsetMin = this.camera.offsetMin - this.offsetMax = this.camera.offsetMax - - this.updateExposureUnit(this.exposureTimeUnit) } private loadPreference() { @@ -325,14 +334,49 @@ export class CameraComponent implements OnInit, OnDestroy { this.autoSave = this.preference.get(`camera.${this.camera.name}.autoSave`, false) this.savePath = this.preference.get(`camera.${this.camera.name}.savePath`, '') this.autoSubFolderMode = this.preference.get(`camera.${this.camera.name}.autoSubFolderMode`, 'OFF') + + this.setpointTemperature = this.preference.get(`camera.${this.camera.name}.setpointTemperature`, 0) + this.exposureTime = this.preference.get(`camera.${this.camera.name}.exposureTime`, this.camera.exposureMin) + this.exposureTimeUnit = this.preference.get(`camera.${this.camera.name}.exposureTimeUnit`, ExposureTimeUnit.MICROSECOND) + this.exposureMode = this.preference.get(`camera.${this.camera.name}.exposureMode`, 'SINGLE') + this.exposureDelay = this.preference.get(`camera.${this.camera.name}.exposureDelay`, 0) + this.exposureCount = this.preference.get(`camera.${this.camera.name}.exposureCount`, 1) + this.x = this.preference.get(`camera.${this.camera.name}.x`, this.camera.minX) + this.y = this.preference.get(`camera.${this.camera.name}.y`, this.camera.minY) + this.width = this.preference.get(`camera.${this.camera.name}.width`, this.camera.maxWidth) + this.height = this.preference.get(`camera.${this.camera.name}.height`, this.camera.maxHeight) + this.subFrame = this.preference.get(`camera.${this.camera.name}.subFrame`, false) + this.binX = this.preference.get(`camera.${this.camera.name}.binX`, 1) + this.binY = this.preference.get(`camera.${this.camera.name}.binY`, 1) + this.frameType = this.preference.get(`camera.${this.camera.name}.frameType`, 'LIGHT') + this.gain = this.preference.get(`camera.${this.camera.name}.gain`, 0) + this.offset = this.preference.get(`camera.${this.camera.name}.offset`, 0) + this.frameFormat = this.preference.get(`camera.${this.camera.name}.frameFormat`, '') } } - private savePreference() { + savePreference() { if (this.camera) { this.preference.set(`camera.${this.camera.name}.autoSave`, this.autoSave) this.preference.set(`camera.${this.camera.name}.savePath`, this.savePath) this.preference.set(`camera.${this.camera.name}.autoSubFolderMode`, this.autoSubFolderMode) + this.preference.set(`camera.${this.camera.name}.setpointTemperature`, this.setpointTemperature) + this.preference.set(`camera.${this.camera.name}.exposureTime`, this.exposureTime) + this.preference.set(`camera.${this.camera.name}.exposureTimeUnit`, this.exposureTimeUnit) + this.preference.set(`camera.${this.camera.name}.exposureMode`, this.exposureMode) + this.preference.set(`camera.${this.camera.name}.exposureDelay`, this.exposureDelay) + this.preference.set(`camera.${this.camera.name}.exposureCount`, this.exposureCount) + this.preference.set(`camera.${this.camera.name}.x`, this.x) + this.preference.set(`camera.${this.camera.name}.y`, this.y) + this.preference.set(`camera.${this.camera.name}.width`, this.width) + this.preference.set(`camera.${this.camera.name}.height`, this.height) + this.preference.set(`camera.${this.camera.name}.subFrame`, this.subFrame) + this.preference.set(`camera.${this.camera.name}.binX`, this.binX) + this.preference.set(`camera.${this.camera.name}.binY`, this.binY) + this.preference.set(`camera.${this.camera.name}.frameType`, this.frameType) + this.preference.set(`camera.${this.camera.name}.gain`, this.gain) + this.preference.set(`camera.${this.camera.name}.offset`, this.offset) + this.preference.set(`camera.${this.camera.name}.frameFormat`, this.frameFormat) } } } diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 43c947c36..d6d122577 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -19,13 +19,13 @@
-
+
-
+
@@ -36,18 +36,18 @@
-
+
+ (ngModelChange)="filterToMoveChanged()" styleClass="border-0" emptyMessage="No filter found" scrollHeight="120px" />
-
+
@@ -64,7 +64,7 @@
-
+
Shutter
diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 6e813c7b7..e0beff8f5 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { Title } from '@angular/platform-browser' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' @@ -10,7 +10,7 @@ import { FilterWheel } from '../../shared/types' templateUrl: './filterwheel.component.html', styleUrls: ['./filterwheel.component.scss'] }) -export class FilterWheelComponent implements OnInit, OnDestroy { +export class FilterWheelComponent implements AfterViewInit, OnDestroy { filterWheels: FilterWheel[] = [] filterWheel?: FilterWheel @@ -48,7 +48,7 @@ export class FilterWheelComponent implements OnInit, OnDestroy { }) } - async ngOnInit() { + async ngAfterViewInit() { this.filterWheels = await this.api.attachedFilterWheels() } @@ -65,12 +65,15 @@ export class FilterWheelComponent implements OnInit, OnDestroy { const filterWheel = await this.api.filterWheel(this.filterWheel.name) Object.assign(this.filterWheel, filterWheel) + + this.loadPreference() this.update() + this.savePreference() } else { this.title.setTitle(`Filter Wheel`) } - this.electron.ipcRenderer.send('FILTER_WHEEL_CHANGED', this.filterWheel) + this.electron.send('FILTER_WHEEL_CHANGED', this.filterWheel) } async connect() { @@ -117,6 +120,8 @@ export class FilterWheelComponent implements OnInit, OnDestroy { this.filterToMove = this.filterToEdit this.api.filterWheelSyncNames(this.filterWheel!, this.filterNames) + + this.electron.send('FILTER_WHEEL_RENAMED', this.filterWheel) } } diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 109cd4d52..7fe430b8d 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -32,34 +32,27 @@
-
+
+ - +
-
- -
-
- -
-
+
-
-
- -
-
- + +
diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index 03ba1f28b..f6f78342a 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { Title } from '@angular/platform-browser' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' @@ -10,7 +10,7 @@ import { Camera, Focuser } from '../../shared/types' templateUrl: './focuser.component.html', styleUrls: ['./focuser.component.scss'] }) -export class FocuserComponent implements OnInit, OnDestroy { +export class FocuserComponent implements AfterViewInit, OnDestroy { focusers: Focuser[] = [] focuser?: Focuser @@ -26,7 +26,7 @@ export class FocuserComponent implements OnInit, OnDestroy { canReverse = false reverse = false canSync = false - hasBackslash = false + hasBacklash = false maxPosition = 0 stepsRelative = 0 @@ -61,7 +61,7 @@ export class FocuserComponent implements OnInit, OnDestroy { }) } - async ngOnInit() { + async ngAfterViewInit() { this.focusers = await this.api.attachedFocusers() } @@ -74,16 +74,17 @@ export class FocuserComponent implements OnInit, OnDestroy { if (this.focuser) { this.title.setTitle(`Focuser ・ ${this.focuser.name}`) - this.loadPreference() - const focuser = await this.api.focuser(this.focuser.name) Object.assign(this.focuser, focuser) + + this.loadPreference() this.update() + this.savePreference() } else { this.title.setTitle(`Focuser`) } - this.electron.ipcRenderer.send('FOCUSER_CHANGED', this.focuser) + this.electron.send('FOCUSER_CHANGED', this.focuser) } async connect() { @@ -137,7 +138,7 @@ export class FocuserComponent implements OnInit, OnDestroy { this.canReverse = this.focuser.canReverse this.reverse = this.focuser.reverse this.canSync = this.focuser.canSync - this.hasBackslash = this.focuser.hasBackslash + this.hasBacklash = this.focuser.hasBacklash this.maxPosition = this.focuser.maxPosition } diff --git a/desktop/src/app/framing/framing.component.ts b/desktop/src/app/framing/framing.component.ts index 73abfc7b0..df6435bb8 100644 --- a/desktop/src/app/framing/framing.component.ts +++ b/desktop/src/app/framing/framing.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { Title } from '@angular/platform-browser' import { ActivatedRoute } from '@angular/router' import hipsSurveys from '../../assets/data/hipsSurveys.json' @@ -22,7 +22,7 @@ export interface FramingParams { templateUrl: './framing.component.html', styleUrls: ['./framing.component.scss'], }) -export class FramingComponent implements OnInit, AfterViewInit, OnDestroy { +export class FramingComponent implements AfterViewInit, OnDestroy { rightAscension = '00h00m00s' declination = `+000°00'00"` @@ -56,8 +56,6 @@ export class FramingComponent implements OnInit, AfterViewInit, OnDestroy { }) } - async ngOnInit() { } - ngAfterViewInit() { this.route.queryParams.subscribe(e => { const params = JSON.parse(decodeURIComponent(e.params)) as FramingParams @@ -68,7 +66,7 @@ export class FramingComponent implements OnInit, AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { this.closeFrameImage() - this.electron.ipcRenderer.sendSync('CLOSE_WINDOW', this.frameId) + this.electron.sendSync('CLOSE_WINDOW', this.frameId) } private frameFromParams(params: FramingParams) { diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 7677b3ff4..f81ea32c2 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -28,7 +28,7 @@
- +
Mount
diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 5f9908873..28d5f2a81 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,24 +1,24 @@ -import { Component, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { MessageService } from 'primeng/api' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { Camera, FilterWheel, Focuser, HomeWindowType } from '../../shared/types' +import { Camera, Device, FilterWheel, Focuser, HomeWindowType, Mount } from '../../shared/types' @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], }) -export class HomeComponent implements OnInit, OnDestroy { +export class HomeComponent implements AfterViewInit, OnDestroy { host = '' port = 7624 connected = false cameras: Camera[] = [] - mounts: Camera[] = [] + mounts: Mount[] = [] focusers: Focuser[] = [] filterWheels: FilterWheel[] = [] domes: Camera[] = [] @@ -70,57 +70,106 @@ export class HomeComponent implements OnInit, OnDestroy { || this.hasFilterWheel || this.hasDome || this.hasRotator || this.hasSwitch } + private startListening( + type: 'CAMERA' | 'MOUNT' | 'FOCUSER' | 'FILTER_WHEEL', + onAdd: (device: T) => number, + onRemove: (device: T) => number, + ) { + this.api.indiStartListening(`${type}_ATTACHED`) + this.api.indiStartListening(`${type}_DETACHED`) + + this.electron.ipcRenderer.on(`${type}_ATTACHED`, (_, device: T) => { + this.ngZone.run(() => { + if (onAdd(device) === 1) { + this.electron.send(`${type}_CHANGED`, device) + } + }) + }) + + this.electron.ipcRenderer.on(`${type}_DETACHED`, (_, device: T) => { + this.ngZone.run(() => { + if (onRemove(device) === 0) { + this.electron.send(`${type}_CHANGED`, undefined) + } + }) + }) + } + constructor( private electron: ElectronService, private browserWindow: BrowserWindowService, private api: ApiService, private message: MessageService, private preference: PreferenceService, - ngZone: NgZone, + private ngZone: NgZone, ) { - this.api.indiStartListening('CAMERA_ATTACHED') - this.api.indiStartListening('CAMERA_DETACHED') - - electron.ipcRenderer.on('CAMERA_ATTACHED', (_, camera: Camera) => { - ngZone.run(() => this.cameras.push(camera)) - }) - - electron.ipcRenderer.on('CAMERA_DETACHED', (_, camera: Camera) => { - ngZone.run(() => this.cameras.splice(this.cameras.findIndex(e => e.name === camera.name), 1)) - }) - - this.api.indiStartListening('FOCUSER_ATTACHED') - this.api.indiStartListening('FOCUSER_DETACHED') - - electron.ipcRenderer.on('FOCUSER_ATTACHED', (_, focuser: Focuser) => { - ngZone.run(() => this.focusers.push(focuser)) - }) - - electron.ipcRenderer.on('FOCUSER_DETACHED', (_, focuser: Focuser) => { - ngZone.run(() => this.focusers.splice(this.focusers.findIndex(e => e.name === focuser.name), 1)) - }) - - this.api.indiStartListening('FILTER_WHEEL_ATTACHED') - this.api.indiStartListening('FILTER_WHEEL_DETACHED') - - electron.ipcRenderer.on('FILTER_WHEEL_ATTACHED', (_, filterWheel: FilterWheel) => { - ngZone.run(() => this.filterWheels.push(filterWheel)) - }) - - electron.ipcRenderer.on('FILTER_WHEEL_DETACHED', (_, filterWheel: FilterWheel) => { - ngZone.run(() => this.filterWheels.splice(this.filterWheels.findIndex(e => e.name === filterWheel.name), 1)) - }) - } - - async ngOnInit() { + this.startListening('CAMERA', + (device) => { + return this.cameras.push(device) + }, + (device) => { + this.cameras.splice(this.cameras.findIndex(e => e.name === device.name), 1) + return this.cameras.length + }, + ) + + this.startListening('MOUNT', + (device) => { + return this.mounts.push(device) + }, + (device) => { + this.mounts.splice(this.mounts.findIndex(e => e.name === device.name), 1) + return this.mounts.length + }, + ) + + this.startListening('FOCUSER', + (device) => { + return this.focusers.push(device) + }, + (device) => { + this.focusers.splice(this.focusers.findIndex(e => e.name === device.name), 1) + return this.focusers.length + }, + ) + + this.startListening('FILTER_WHEEL', + (device) => { + return this.filterWheels.push(device) + }, + (device) => { + this.filterWheels.splice(this.filterWheels.findIndex(e => e.name === device.name), 1) + return this.filterWheels.length + }, + ) + } + + async ngAfterViewInit() { this.updateConnection() this.host = this.preference.get('home.host', 'localhost') this.port = this.preference.get('home.port', 7624) this.cameras = await this.api.attachedCameras() + this.mounts = await this.api.attachedMounts() this.focusers = await this.api.attachedFocusers() this.filterWheels = await this.api.attachedFilterWheels() + + if (this.cameras.length > 0) { + this.electron.send('CAMERA_CHANGED', this.cameras[0]) + } + + if (this.mounts.length > 0) { + this.electron.send('MOUNT_CHANGED', this.mounts[0]) + } + + if (this.focusers.length > 0) { + this.electron.send('FOCUSER_CHANGED', this.focusers[0]) + } + + if (this.filterWheels.length > 0) { + this.electron.send('FILTER_WHEEL_CHANGED', this.filterWheels[0]) + } } @HostListener('window:unload') @@ -156,6 +205,9 @@ export class HomeComponent implements OnInit, OnDestroy { async open(type: HomeWindowType) { switch (type) { + case 'MOUNT': + this.browserWindow.openMount({ bringToFront: true }) + break case 'CAMERA': this.browserWindow.openCamera({ bringToFront: true }) break @@ -175,7 +227,7 @@ export class HomeComponent implements OnInit, OnDestroy { this.browserWindow.openINDI(undefined, { bringToFront: true }) break case 'IMAGE': - const path = await this.electron.ipcRenderer.sendSync('OPEN_FITS') + const path = await this.electron.sendSync('OPEN_FITS') if (path) this.browserWindow.openImage(path, undefined, 'PATH') break case 'ABOUT': diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index 5a4167056..18871938c 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -32,7 +32,7 @@ -
@@ -137,18 +137,33 @@ - -
+
-
- Shadow: {{ stretchShadowhHighlight[0] }} - Highlight: {{ stretchShadowhHighlight[1] }} +
+ + + + + + + +
-
Midtone: {{ stretchMidtone }}
+
+ + + + +
+
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index e59d7e21a..3318f2757 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { Title } from '@angular/platform-browser' import { ActivatedRoute } from '@angular/router' import createPanZoom, { PanZoom } from 'panzoom' @@ -10,7 +10,7 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { - Calibration, Camera, FITSHeaderItem, ImageAnnotation, ImageChannel, ImageSource, PlateSolverType, + Calibration, Camera, FITSHeaderItem, ImageAnnotation, ImageChannel, ImageInfo, ImageSource, PlateSolverType, SCNRProtectionMethod, SCNR_PROTECTION_METHODS, SavedCameraImage } from '../../shared/types' @@ -26,7 +26,7 @@ export interface ImageParams { templateUrl: './image.component.html', styleUrls: ['./image.component.scss'], }) -export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { +export class ImageComponent implements AfterViewInit, OnDestroy { @ViewChild('image') private readonly image!: ElementRef @@ -80,7 +80,7 @@ export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { private panZoom?: PanZoom private imageURL!: string - private imageInfo?: SavedCameraImage + private imageInfo?: ImageInfo private imageMouseX = 0 private imageMouseY = 0 private imageParams: ImageParams = {} @@ -117,7 +117,7 @@ export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { label: 'Save as...', icon: 'mdi mdi-content-save', command: async () => { - const path = await this.electron.ipcRenderer.sendSync('SAVE_FITS_AS') + const path = await this.electron.sendSync('SAVE_FITS_AS') if (path) this.api.saveImageAs(this.imageParams.path!, path) }, }, @@ -244,9 +244,7 @@ export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { electron.ipcRenderer.on('PARAMS_CHANGED', (_, data: ImageParams) => { this.loadImageFromParams(data) }) - } - ngOnInit() { this.solverPathOrUrl = this.preference.get('image.solver.pathOrUrl', '') this.solverRadius = this.preference.get('image.solver.radius', 4) this.solverDownsampleFactor = this.preference.get('image.solver.downsampleFactor', 1) @@ -284,8 +282,8 @@ export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { await this.loadImageFromPath(this.imageParams.path) } else if (this.imageParams.camera) { try { - this.imageInfo = await this.api.latestImageOfCamera(this.imageParams.camera) - await this.loadImageFromPath(this.imageInfo.path) + const savedImage = await this.api.latestImageOfCamera(this.imageParams.camera) + await this.loadImageFromPath(savedImage.path) } catch (e) { console.error(e) } @@ -310,6 +308,7 @@ export class ImageComponent implements OnInit, AfterViewInit, OnDestroy { this.imageInfo = info this.scnrMenuItem.disabled = info.mono + if (info.rightAscension) this.solverCenterRA = info.rightAscension if (info.declination) this.solverCenterDEC = info.declination diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 68f49b94c..1d2f0c373 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -15,7 +15,7 @@ export interface INDIParams { templateUrl: './indi.component.html', styleUrls: ['./indi.component.scss'], }) -export class INDIComponent implements OnInit, AfterViewInit, OnDestroy { +export class INDIComponent implements AfterViewInit, OnDestroy { devices: Device[] = [] properties: INDIProperty[] = [] @@ -64,8 +64,6 @@ export class INDIComponent implements OnInit, AfterViewInit, OnDestroy { }) } - ngOnInit() { } - async ngAfterViewInit() { this.route.queryParams.subscribe(e => { const params = JSON.parse(decodeURIComponent(e.params)) as INDIParams @@ -74,6 +72,7 @@ export class INDIComponent implements OnInit, AfterViewInit, OnDestroy { this.devices = [ ...await this.api.attachedCameras(), + ...await this.api.attachedMounts(), ...await this.api.attachedFocusers(), ...await this.api.attachedFilterWheels(), ] diff --git a/desktop/src/app/indi/property/indi-property.component.ts b/desktop/src/app/indi/property/indi-property.component.ts index acf9ac98f..e2133f1b9 100644 --- a/desktop/src/app/indi/property/indi-property.component.ts +++ b/desktop/src/app/indi/property/indi-property.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' +import { AfterContentInit, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core' import { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem } from '../../../shared/types' @Component({ @@ -6,7 +6,7 @@ import { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem templateUrl: './indi-property.component.html', styleUrls: ['./indi-property.component.scss'], }) -export class INDIPropertyComponent implements OnInit, AfterContentInit, OnDestroy { +export class INDIPropertyComponent implements AfterContentInit, OnDestroy { @Input() property!: INDIProperty @@ -17,8 +17,6 @@ export class INDIPropertyComponent implements OnInit, AfterContentInit, OnDestro @Output() readonly onSend = new EventEmitter() - ngOnInit() { } - ngAfterContentInit() { for (const item of this.property.items) { if (!item.valueToSend) { diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html new file mode 100644 index 000000000..f4d2e6ff6 --- /dev/null +++ b/desktop/src/app/mount/mount.component.html @@ -0,0 +1,189 @@ +
+
+
+ + + + +
+
+ + +
+
+ + {{ parking ? 'parking' : (parked ? 'parked' : (slewing ? 'slewing' : 'idle')) }} +
+
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + Target coordinates: +
+
+ +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Tracking + +
+
+ + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
\ No newline at end of file diff --git a/desktop/src/app/mount/mount.component.scss b/desktop/src/app/mount/mount.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts new file mode 100644 index 000000000..a94277ff2 --- /dev/null +++ b/desktop/src/app/mount/mount.component.ts @@ -0,0 +1,313 @@ +import { AfterViewInit, Component, NgZone, OnDestroy } from '@angular/core' +import { Title } from '@angular/platform-browser' +import { MenuItem } from 'primeng/api' +import { Subject, Subscription, debounceTime, interval } from 'rxjs' +import { ApiService } from '../../shared/services/api.service' +import { BrowserWindowService } from '../../shared/services/browser-window.service' +import { ElectronService } from '../../shared/services/electron.service' +import { PreferenceService } from '../../shared/services/preference.service' +import { Constellation, Mount, PierSide, SlewRate, TargetCoordinateType, TrackMode } from '../../shared/types' + +@Component({ + selector: 'app-mount', + templateUrl: './mount.component.html', + styleUrls: ['./mount.component.scss'], +}) +export class MountComponent implements AfterViewInit, OnDestroy { + + mounts: Mount[] = [] + mount?: Mount + connected = false + slewing = false + parking = false + parked = false + trackModes: TrackMode[] = ['SIDEREAL'] + trackMode: TrackMode = 'SIDEREAL' + slewRates: SlewRate[] = [] + slewRate?: SlewRate + tracking = false + canPark = false + canHome = false + + rightAscensionJ2000 = '00h00m00s' + declinationJ2000 = `00°00'00"` + rightAscension = '00h00m00s' + declination = `00°00'00"` + azimuth = `000°00'00"` + altitude = `+00°00'00"` + lst = '00:00:00' + constellation: Constellation = 'AND' + timeLeftToMeridianFlip = '00:00:00' + meridianAt = '00:00:00' + pierSide: PierSide = 'NEITHER' + targetCoordinateType: TargetCoordinateType = 'JNOW' + targetRightAscension = '00h00m00s' + targetDeclination = `00°00'00"` + + private readonly computeCoordinatePublisher = new Subject() + private computeCoordinateSubscription: Subscription[] = [] + private readonly moveToDirection = [false, false] + + readonly targetCoordinateOptions: MenuItem[] = [ + { + icon: 'mdi mdi-check', + label: 'Go To', + command: () => { + this.targetCoordinateOption = this.targetCoordinateOptions[0] + this.goTo() + }, + }, + { + icon: 'mdi mdi-check', + label: 'Slew To', + command: () => { + this.targetCoordinateOption = this.targetCoordinateOptions[1] + this.slewTo() + }, + }, + { + icon: 'mdi mdi-sync', + label: 'Sync', + command: () => { + this.targetCoordinateOption = this.targetCoordinateOptions[2] + this.sync() + }, + }, + { + icon: 'mdi mdi-target', + label: 'Locations', + items: [ + { + icon: 'mdi mdi-target', + label: 'Current location', + }, + { + icon: 'mdi mdi-target', + label: 'Current location (J2000)', + }, + { + icon: 'mdi mdi-target', + label: 'Zenith', + }, + { + icon: 'mdi mdi-target', + label: 'North celestial pole', + }, + { + icon: 'mdi mdi-target', + label: 'South celestial pole', + }, + { + icon: 'mdi mdi-target', + label: 'Galactic center', + }, + ], + }, + ] + + targetCoordinateOption = this.targetCoordinateOptions[0] + + constructor( + private title: Title, + private api: ApiService, + private browserWindow: BrowserWindowService, + private electron: ElectronService, + private preference: PreferenceService, + ngZone: NgZone, + ) { + title.setTitle('Mount') + + this.api.indiStartListening('MOUNT') + + electron.ipcRenderer.on('MOUNT_UPDATED', (_, mount: Mount) => { + if (mount.name === this.mount?.name) { + ngZone.run(() => { + Object.assign(this.mount!, mount) + this.update() + }) + } + }) + + this.computeCoordinateSubscription[0] = this.computeCoordinatePublisher + .pipe(debounceTime(1000)) + .subscribe(() => this.computeCoordinates()) + + this.computeCoordinateSubscription[1] = interval(5000) + .subscribe(() => this.computeCoordinatePublisher.next()) + } + + async ngAfterViewInit() { + this.mounts = await this.api.attachedMounts() + } + + ngOnDestroy() { + this.api.indiStopListening('MOUNT') + + this.computeCoordinateSubscription[0]?.unsubscribe() + this.computeCoordinateSubscription[1]?.unsubscribe() + } + + async mountChanged() { + if (this.mount) { + this.title.setTitle(`Mount ・ ${this.mount!.name}`) + + const mount = await this.api.mount(this.mount.name) + Object.assign(this.mount, mount) + + this.loadPreference() + this.update() + this.savePreference() + } else { + this.title.setTitle(`Mount`) + } + + this.electron.send('MOUNT_CHANGED', this.mount) + } + + connect() { + if (this.connected) { + this.api.mountDisconnect(this.mount!) + } else { + this.api.mountConnect(this.mount!) + } + } + + goTo() { + this.api.mountGoTo(this.mount!, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + } + + slewTo() { + this.api.mountSlewTo(this.mount!, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + } + + sync() { + this.api.mountSync(this.mount!, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + } + + targetCoordinateOptionClicked() { + if (this.targetCoordinateOption === this.targetCoordinateOptions[0]) { + this.goTo() + } else if (this.targetCoordinateOption === this.targetCoordinateOptions[1]) { + this.slewTo() + } else if (this.targetCoordinateOption === this.targetCoordinateOptions[2]) { + this.sync() + } + } + + moveTo(direction: string, pressed: boolean, event: MouseEvent) { + if (event.button === 0) { + if (this.moveToDirection[0] !== pressed) { + switch (direction[0]) { + case 'N': + this.api.mountMoveNorth(this.mount!, pressed) + break + case 'S': + this.api.mountMoveSouth(this.mount!, pressed) + break + case 'W': + this.api.mountMoveWest(this.mount!, pressed) + break + case 'E': + this.api.mountMoveEast(this.mount!, pressed) + break + } + + this.moveToDirection[0] = pressed + } + + if (this.moveToDirection[1] !== pressed) { + switch (direction[1]) { + case 'W': + this.api.mountMoveWest(this.mount!, pressed) + break + case 'E': + this.api.mountMoveEast(this.mount!, pressed) + break + default: + return + } + + this.moveToDirection[1] = pressed + } + } + } + + trackingToggled() { + if (this.connected) { + this.api.mountTracking(this.mount!, this.tracking) + } + } + + trackModeChanged() { + if (this.connected) { + this.api.mountTrackMode(this.mount!, this.trackMode) + } + } + + slewRateChanged() { + if (this.connected && this.slewRate) { + this.api.mountSlewRate(this.mount!, this.slewRate) + } + } + + park() { + if (this.connected) { + this.api.mountPark(this.mount!) + } + } + + unpark() { + if (this.connected) { + this.api.mountUnpark(this.mount!) + } + } + + home() { + if (this.connected) { + this.api.mountHome(this.mount!) + } + } + + private async update() { + if (this.mount) { + this.connected = this.mount.connected + this.slewing = this.mount.slewing + this.parking = this.mount.parking + this.parked = this.mount.parked + this.canPark = this.mount.canPark + this.canHome = this.mount.canHome + this.trackModes = this.mount.trackModes + this.trackMode = this.mount.trackMode + this.slewRates = this.mount.slewRates + this.slewRate = this.mount.slewRate + this.rightAscension = this.mount.rightAscension + this.declination = this.mount.declination + this.pierSide = this.mount.pierSide + this.tracking = this.mount.tracking + + this.computeCoordinatePublisher.next() + } + } + + private async computeCoordinates() { + if (this.mount && this.mount.connected) { + const computedCoordinates = await this.api.mountComputeCoordinates(this.mount!, false, '', '', true, true, true) + this.rightAscensionJ2000 = computedCoordinates.rightAscension + this.declinationJ2000 = computedCoordinates.declination + this.azimuth = computedCoordinates.azimuth + this.altitude = computedCoordinates.altitude + this.constellation = computedCoordinates.constellation + this.meridianAt = computedCoordinates.meridianAt + this.timeLeftToMeridianFlip = computedCoordinates.timeLeftToMeridianFlip + this.lst = computedCoordinates.lst + } + } + + private loadPreference() { + + } + + private savePreference() { + + } +} diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index a5633dc3a..eb4efcf60 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -1,12 +1,11 @@ import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' -import * as moment from 'moment' +import moment from 'moment' import { firstValueFrom } from 'rxjs' import { - BodyPosition, Calibration, Camera, CameraStartCapture, Constellation, DeepSkyObject, Device, - FilterWheel, Focuser, - HipsSurvey, INDIEventType, INDIProperty, INDISendProperty, ImageAnnotation, ImageChannel, Location, MinorPlanet, - PlateSolverType, SCNRProtectionMethod, SavedCameraImage, SkyObjectType, Star, Twilight + BodyPosition, Calibration, Camera, CameraStartCapture, ComputedCoordinates, Constellation, DeepSkyObject, Device, + FilterWheel, Focuser, HipsSurvey, INDIEventType, INDIProperty, INDISendProperty, ImageAnnotation, ImageChannel, ImageInfo, Location, MinorPlanet, + Mount, PlateSolverType, SCNRProtectionMethod, SavedCameraImage, SkyObjectType, SlewRate, Star, TrackMode, Twilight } from '../types' @Injectable({ providedIn: 'root' }) @@ -94,6 +93,85 @@ export class ApiService { return this.get(`latestImageOfCamera?name=${camera.name}`) } + attachedMounts() { + return this.get(`attachedMounts`) + } + + mount(name: string) { + return this.get(`mount?name=${name}`) + } + + mountConnect(mount: Mount) { + return this.post(`mountConnect?name=${mount.name}`) + } + + mountDisconnect(mount: Mount) { + return this.post(`mountDisconnect?name=${mount.name}`) + } + + mountTracking(mount: Mount, enable: boolean) { + return this.post(`mountTracking?name=${mount.name}&enable=${enable}`) + } + + mountSync(mount: Mount, rightAscension: string, declination: string, j2000: boolean) { + return this.post(`mountSync?name=${mount.name}&rightAscension=${rightAscension}&declination=${declination}&j2000=${j2000}`) + } + + mountSlewTo(mount: Mount, rightAscension: string, declination: string, j2000: boolean) { + return this.post(`mountSlewTo?name=${mount.name}&rightAscension=${rightAscension}&declination=${declination}&j2000=${j2000}`) + } + + mountGoTo(mount: Mount, rightAscension: string, declination: string, j2000: boolean) { + return this.post(`mountGoTo?name=${mount.name}&rightAscension=${rightAscension}&declination=${declination}&j2000=${j2000}`) + } + + mountPark(mount: Mount) { + return this.post(`mountPark?name=${mount.name}`) + } + + mountUnpark(mount: Mount) { + return this.post(`mountUnpark?name=${mount.name}`) + } + + mountHome(mount: Mount) { + return this.post(`mountHome?name=${mount.name}`) + } + + mountAbort(mount: Mount) { + return this.post(`mountAbort?name=${mount.name}`) + } + + mountTrackMode(mount: Mount, mode: TrackMode) { + return this.post(`mountTrackMode?name=${mount.name}&mode=${mode}`) + } + + mountSlewRate(mount: Mount, rate: SlewRate) { + return this.post(`mountSlewRate?name=${mount.name}&rate=${rate.name}`) + } + + mountMoveNorth(mount: Mount, enable: boolean) { + return this.post(`mountMoveNorth?name=${mount.name}&enable=${enable}`) + } + + mountMoveSouth(mount: Mount, enable: boolean) { + return this.post(`mountMoveSouth?name=${mount.name}&enable=${enable}`) + } + + mountMoveEast(mount: Mount, enable: boolean) { + return this.post(`mountMoveEast?name=${mount.name}&enable=${enable}`) + } + + mountMoveWest(mount: Mount, enable: boolean) { + return this.post(`mountMoveWest?name=${mount.name}&enable=${enable}`) + } + + mountComputeCoordinates(mount: Mount, j2000: boolean, rightAscension?: string, declination?: string, + equatorial: boolean = true, horizontal: boolean = true, meridian: boolean = false, + ) { + return this.post(`mountComputeCoordinates?name=${mount.name}&rightAscension=${rightAscension || ''}&declination=${declination || ''}` + + `&j2000=${j2000}&equatorial=${equatorial}&horizontal=${horizontal}&meridian=${meridian}`) + } + attachedFocusers() { return this.get(`attachedFocusers`) } @@ -177,7 +255,7 @@ export class ApiService { responseType: 'blob' })) - const info = JSON.parse(response.headers.get('X-Image-Info')!) as SavedCameraImage + const info = JSON.parse(response.headers.get('X-Image-Info')!) as ImageInfo return { info, blob: response.body! } } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 0001080a5..b1d011519 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -15,6 +15,14 @@ export class BrowserWindowService { await this.electron.ipcRenderer.invoke('OPEN_WINDOW', data) } + openMount(options: OpenWindowOptions = {}) { + this.openWindow({ + ...options, + id: 'mount', path: 'mount', icon: options.icon || 'mount', + width: options.width || 400, height: options.height || 440, + }) + } + openCamera(options: OpenWindowOptions = {}) { this.openWindow({ ...options, @@ -27,7 +35,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'focuser', path: 'focuser', icon: options.icon || 'focus', - width: options.width || 380, height: options.height || 276, + width: options.width || 360, height: options.height || 225, }) } @@ -35,7 +43,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'filterWheel', path: 'filterWheel', icon: options.icon || 'filter-wheel', - width: options.width || 340, height: options.height || 204, + width: options.width || 320, height: options.height || 182, }) } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 678a494f0..9ba42277b 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -52,4 +52,12 @@ export class ElectronService { get isElectron() { return !!(window && window.process && window.process.type) } + + send(channel: string, ...data: any[]) { + this.ipcRenderer.send(channel, ...data) + } + + sendSync(channel: string, ...data: any[]) { + return this.ipcRenderer.sendSync(channel, ...data) + } } diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index c77ee0c9d..6542db6d8 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -1,79 +1,124 @@ export interface Device { - name: string - connected: boolean -} - -export interface Thermometer { - hasThermometer: boolean - temperature: number -} - -export interface Camera extends Device, Thermometer { - exposuring: boolean - hasCoolerControl: boolean - coolerPower: number - cooler: boolean - hasDewHeater: boolean - dewHeater: boolean - frameFormats: string[] - canAbort: boolean - cfaOffsetX: number - cfaOffsetY: number - cfaType: CfaPattern - exposureMin: number - exposureMax: number - exposureState: PropertyState - exposure: number - hasCooler: boolean - canSetTemperature: boolean - canSubFrame: boolean - x: number - minX: number - maxX: number - y: number - minY: number - maxY: number - width: number - minWidth: number - maxWidth: number - height: number - minHeight: number - maxHeight: number - canBin: boolean - maxBinX: number - maxBinY: number - binX: number - binY: number - gain: number - gainMin: number - gainMax: number - offset: number - offsetMin: number - offsetMax: number - hasGuiderHead: boolean - pixelSizeX: number - pixelSizeY: number - canPulseGuide: boolean - pulseGuiding: boolean + readonly name: string + readonly connected: boolean +} + +export interface Thermometer extends Device { + readonly hasThermometer: boolean + readonly temperature: number +} + +export interface GuideOutput extends Device { + readonly canPulseGuide: boolean + readonly pulseGuiding: boolean +} + +export interface Camera extends GuideOutput, Thermometer { + readonly exposuring: boolean + readonly hasCoolerControl: boolean + readonly coolerPower: number + readonly cooler: boolean + readonly hasDewHeater: boolean + readonly dewHeater: boolean + readonly frameFormats: string[] + readonly canAbort: boolean + readonly cfaOffsetX: number + readonly cfaOffsetY: number + readonly cfaType: CfaPattern + readonly exposureMin: number + readonly exposureMax: number + readonly exposureState: PropertyState + readonly exposure: number + readonly hasCooler: boolean + readonly canSetTemperature: boolean + readonly canSubFrame: boolean + readonly x: number + readonly minX: number + readonly maxX: number + readonly y: number + readonly minY: number + readonly maxY: number + readonly width: number + readonly minWidth: number + readonly maxWidth: number + readonly height: number + readonly minHeight: number + readonly maxHeight: number + readonly canBin: boolean + readonly maxBinX: number + readonly maxBinY: number + readonly binX: number + readonly binY: number + readonly gain: number + readonly gainMin: number + readonly gainMax: number + readonly offset: number + readonly offsetMin: number + readonly offsetMax: number + readonly hasGuiderHead: boolean + readonly pixelSizeX: number + readonly pixelSizeY: number + readonly canPulseGuide: boolean + readonly pulseGuiding: boolean +} + +export interface Parkable { + readonly canPark: boolean + readonly parking: boolean + readonly parked: boolean +} + +export interface GPS extends Device { + readonly hasGPS: boolean + readonly longitude: number + readonly latitude: number + readonly elevation: number + readonly dateTime: number + readonly offsetInMinutes: number +} + +export interface Mount extends GPS, GuideOutput, Parkable { + readonly name: string + readonly connected: boolean + readonly slewing: boolean + readonly tracking: boolean + readonly canAbort: boolean + readonly canSync: boolean + readonly canGoTo: boolean + readonly canHome: boolean + readonly slewRates: SlewRate[] + readonly slewRate?: SlewRate + readonly trackModes: TrackMode[] + readonly trackMode: TrackMode + readonly pierSide: PierSide + readonly guideRateWE: number + readonly guideRateNS: number + readonly rightAscension: string + readonly declination: string +} + +export interface SlewRate { + readonly name: string + readonly label: string } export interface Focuser extends Device, Thermometer { - moving: boolean - position: number - canAbsoluteMove: boolean - canRelativeMove: boolean - canAbort: boolean - canReverse: boolean - reverse: boolean - canSync: boolean - hasBackslash: boolean - maxPosition: number + readonly moving: boolean + readonly position: number + readonly canAbsoluteMove: boolean + readonly canRelativeMove: boolean + readonly canAbort: boolean + readonly canReverse: boolean + readonly reverse: boolean + readonly canSync: boolean + readonly hasBacklash: boolean + readonly maxPosition: number } export interface FilterWheel extends Device { - count: number - position: number - moving: boolean + readonly count: number + readonly position: number + readonly moving: boolean } export interface CameraStartCapture { @@ -117,6 +162,9 @@ export interface SavedCameraImage { height: number mono: boolean savedAt: number +} + +export interface ImageInfo extends SavedCameraImage { stretchShadow: number stretchHighlight: number stretchMidtone: number @@ -340,6 +388,17 @@ export interface Calibration { radius: number } +export interface ComputedCoordinates { + rightAscension: string + declination: string + azimuth: string + altitude: string + constellation: Constellation + meridianAt: string + timeLeftToMeridianFlip: string + lst: string +} + export enum ExposureTimeUnit { MINUTE = 'm', SECOND = 's', @@ -519,17 +578,27 @@ export type PlateSolverType = 'ASTROMETRY_NET_LOCAL' | 'WATNEY' export const INDI_EVENT_TYPES = [ - 'ALL', 'DEVICE', 'CAMERA', 'FOCUSER', 'FILTER_WHEEL', + 'ALL', 'DEVICE', 'CAMERA', 'MOUNT', 'FOCUSER', 'FILTER_WHEEL', 'DEVICE_PROPERTY_CHANGED', 'DEVICE_PROPERTY_DELETED', 'DEVICE_MESSAGE_RECEIVED', 'CAMERA_IMAGE_SAVED', 'CAMERA_UPDATED', 'CAMERA_CAPTURE_FINISHED', 'CAMERA_ATTACHED', 'CAMERA_DETACHED', + 'MOUNT_UPDATED', 'MOUNT_ATTACHED', 'MOUNT_DETACHED', 'FOCUSER_UPDATED', 'FOCUSER_ATTACHED', 'FOCUSER_DETACHED', 'FILTER_WHEEL_UPDATED', 'FILTER_WHEEL_ATTACHED', 'FILTER_WHEEL_DETACHED', ] as const export type INDIEventType = (typeof INDI_EVENT_TYPES)[number] +export const INTERNAL_EVENT_TYPES = [ + 'SELECTED_CAMERA', 'SELECTED_FOCUSER', 'SELECTED_FILTER_WHEEL', + 'SELECTED_MOUNT', + 'CAMERA_CHANGED', 'FOCUSER_CHANGED', 'MOUNT_CHANGED', + 'FILTER_WHEEL_CHANGED', 'FILTER_WHEEL_RENAMED', +] as const + +export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] + export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' export const HIPS_SURVEY_TYPES = [ @@ -562,3 +631,9 @@ export const HIPS_SURVEY_TYPES = [ ] as const export type HipsSurveyType = (typeof HIPS_SURVEY_TYPES)[number] + +export type PierSide = 'EAST' | 'WEST' | 'NEITHER' + +export type TargetCoordinateType = 'J2000' | 'JNOW' + +export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' diff --git a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsElement.kt b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsElement.kt index 1754dcd8d..77ed4cda0 100644 --- a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsElement.kt +++ b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsElement.kt @@ -3,7 +3,7 @@ package nebulosa.horizons import java.time.LocalDateTime import java.time.ZoneOffset -data class HorizonsElement(val time: LocalDateTime) : HashMap(7), Comparable { +data class HorizonsElement(val dateTime: LocalDateTime) : HashMap(7), Comparable { fun asString(quantity: HorizonsQuantity, defaultValue: String = "", index: Int = 0): String { return if (quantity.numberOfColumns > 1) this[quantity]?.split(',')?.get(index) ?: defaultValue @@ -19,30 +19,30 @@ data class HorizonsElement(val time: LocalDateTime) : HashMap, dateTime: LocalDateTime): HorizonsElement? { val seconds = dateTime.toEpochSecond(ZoneOffset.UTC) - return ephemeris.find { it.time.toEpochSecond(ZoneOffset.UTC) >= seconds } + return ephemeris.find { it.dateTime.toEpochSecond(ZoneOffset.UTC) >= seconds } } } } diff --git a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsEphemeris.kt b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsEphemeris.kt index 31cc830ef..a73a01105 100644 --- a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsEphemeris.kt +++ b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsEphemeris.kt @@ -12,10 +12,10 @@ data class HorizonsEphemeris(private val elements: MutableList) ClosedRange, List by elements { override val start - get() = elements.first().time + get() = elements.first().dateTime override val endInclusive - get() = elements.last().time + get() = elements.last().dateTime override fun isEmpty() = elements.isEmpty() diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt index afff28afe..b60e6ded4 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt @@ -21,7 +21,7 @@ internal open class FocuserDevice( override var canReverse = false override var reverse = false override var canSync = false - override var hasBackslash = false + override var hasBacklash = false override var maxPosition = 0 override var hasThermometer = false @@ -188,7 +188,7 @@ internal open class FocuserDevice( return "Focuser(name=$name, moving=$moving, position=$position," + " canAbsoluteMove=$canAbsoluteMove, canRelativeMove=$canRelativeMove," + " canAbort=$canAbort, canReverse=$canReverse, reverse=$reverse," + - " canSync=$canSync, hasBackslash=$hasBackslash," + + " canSync=$canSync, hasBacklash=$hasBacklash," + " maxPosition=$maxPosition, hasThermometer=$hasThermometer," + " temperature=$temperature)" } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt index 52da93688..684a2b49d 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt @@ -24,7 +24,7 @@ internal class GPSDevice( override var longitude = Angle.ZERO override var latitude = Angle.ZERO override var elevation = Distance.ZERO - override var time = OffsetDateTime.MIN!! + override var dateTime = OffsetDateTime.MIN!! override fun handleMessage(message: INDIProtocol) { when (message) { @@ -45,7 +45,7 @@ internal class GPSDevice( val utcTime = GPS.extractTime(message["UTC"]!!.value) ?: return val utcOffset = message["OFFSET"]!!.value.toDoubleOrNull() ?: 0.0 - time = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 60.0).toInt())) + dateTime = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 60.0).toInt())) handler.fireOnEventReceived(GPSTimeChanged(this)) } @@ -61,6 +61,6 @@ internal class GPSDevice( override fun toString(): String { return "GPS(hasGPS=$hasGPS, longitude=$longitude, latitude=$latitude," + - " elevation=$elevation, time=$time)" + " elevation=$elevation, dateTime=$dateTime)" } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt index 9c24063c6..f1e1cc666 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt @@ -17,11 +17,7 @@ import nebulosa.math.Angle.Companion.deg import nebulosa.math.Angle.Companion.hours import nebulosa.math.Distance import nebulosa.math.Distance.Companion.m -import nebulosa.nova.astrometry.Constellation -import nebulosa.nova.position.GeographicPosition -import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF -import nebulosa.time.UTC import java.time.OffsetDateTime import java.time.ZoneOffset @@ -50,11 +46,6 @@ internal open class MountDevice( override var guideRateNS = 0.0 override var rightAscension = Angle.ZERO override var declination = Angle.ZERO - override var rightAscensionJ2000 = Angle.ZERO - override var declinationJ2000 = Angle.ZERO - override var azimuth = Angle.ZERO - override var altitude = Angle.ZERO - override var constellation = Constellation.PSC override var canPulseGuide = false override var pulseGuiding = false @@ -63,9 +54,7 @@ internal open class MountDevice( override var longitude = Angle.ZERO override var latitude = Angle.ZERO override var elevation = Distance.ZERO - override var time = OffsetDateTime.now()!! - - @Volatile private var centerPosition: GeographicPosition? = null + override var dateTime = OffsetDateTime.now()!! override fun handleMessage(message: INDIProtocol) { when (message) { @@ -188,8 +177,6 @@ internal open class MountDevice( longitude = message["LONG"]!!.value.deg elevation = message["ELEV"]!!.value.m - centerPosition = Geoid.IERS2010.latLon(longitude, latitude, elevation) - handler.fireOnEventReceived(MountGeographicCoordinateChanged(this)) } } @@ -200,7 +187,7 @@ internal open class MountDevice( val utcTime = GPS.extractTime(message["UTC"]!!.value) ?: return val utcOffset = message["OFFSET"]!!.value.toDoubleOrNull() ?: 0.0 - time = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 3600.0).toInt())) + dateTime = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 3600.0).toInt())) handler.fireOnEventReceived(MountTimeChanged(this)) } @@ -264,7 +251,7 @@ internal open class MountDevice( } } - override fun trackingMode(mode: TrackMode) { + override fun trackMode(mode: TrackMode) { sendNewSwitch("TELESCOPE_TRACK_MODE", "TRACK_$mode" to true) } @@ -322,35 +309,12 @@ internal open class MountDevice( sendNewNumber("GEOGRAPHIC_COORD", "LAT" to latitude.degrees, "LONG" to longitude.degrees, "ELEV" to elevation.meters) } - override fun time(time: OffsetDateTime) { - val offsetHours = time.offset.totalSeconds / 3600.0 + override fun dateTime(dateTime: OffsetDateTime) { + val offsetHours = dateTime.offset.totalSeconds / 3600.0 val offsetMinutes = (offsetHours - offsetHours.toInt()) * 60.0 % 60.0 val offset = "%02d:%02d".format(offsetHours.toInt(), offsetMinutes.toInt()) - sendNewText("TIME_UTC", "UTC" to GPS.formatTime(time.toLocalDateTime()), "OFFSET" to offset) - } - - override fun computeCoordinates(j2000: Boolean, horizontal: Boolean, epoch: UTC) { - val center = centerPosition ?: return - - val icrf = ICRF.equatorial(rightAscension, declination, time = epoch, epoch = epoch, center = center) - constellation = Constellation.find(icrf) - - if (j2000) { - val raDec = icrf.equatorialJ2000() - rightAscensionJ2000 = raDec.longitude.normalized - declinationJ2000 = raDec.latitude - - handler.fireOnEventReceived(MountEquatorialJ2000CoordinatesChanged(this)) - } - - if (horizontal) { - val altAz = icrf.horizontal() - azimuth = altAz.longitude.normalized - altitude = altAz.latitude - - handler.fireOnEventReceived(MountHorizontalCoordinatesChanged(this)) - } + sendNewText("TIME_UTC", "UTC" to GPS.formatTime(dateTime.toLocalDateTime()), "OFFSET" to offset) } override fun close() { diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt index a9fb59e02..12d4b4787 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt @@ -21,7 +21,7 @@ interface Focuser : Device, Thermometer { val canSync: Boolean - val hasBackslash: Boolean + val hasBacklash: Boolean val maxPosition: Int diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/gps/GPS.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/gps/GPS.kt index d190169d0..1f7bd73a5 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/gps/GPS.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/gps/GPS.kt @@ -17,7 +17,7 @@ interface GPS : Device { val elevation: Distance - val time: OffsetDateTime + val dateTime: OffsetDateTime companion object { @@ -33,8 +33,8 @@ interface GPS : Device { } @JvmStatic - fun formatTime(time: LocalDateTime): String { - return time.format(UTC_TIME_FORMAT_1) + fun formatTime(dateTime: LocalDateTime): String { + return dateTime.format(UTC_TIME_FORMAT_1) } @JvmStatic val DRIVERS = setOf( diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt index 5eb13b8c0..1561eb03d 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt @@ -5,8 +5,6 @@ import nebulosa.indi.device.gps.GPS import nebulosa.indi.device.guide.GuideOutput import nebulosa.math.Angle import nebulosa.math.Distance -import nebulosa.nova.astrometry.Constellation -import nebulosa.time.UTC import java.time.OffsetDateTime interface Mount : GuideOutput, GPS, Parkable { @@ -43,16 +41,6 @@ interface Mount : GuideOutput, GPS, Parkable { val declination: Angle - val rightAscensionJ2000: Angle - - val declinationJ2000: Angle - - val azimuth: Angle - - val altitude: Angle - - val constellation: Constellation - fun tracking(enable: Boolean) fun sync(ra: Angle, dec: Angle) @@ -71,7 +59,7 @@ interface Mount : GuideOutput, GPS, Parkable { fun abortMotion() - fun trackingMode(mode: TrackMode) + fun trackMode(mode: TrackMode) fun slewRate(rate: SlewRate) @@ -85,9 +73,7 @@ interface Mount : GuideOutput, GPS, Parkable { fun coordinates(longitude: Angle, latitude: Angle, elevation: Distance) - fun time(time: OffsetDateTime) - - fun computeCoordinates(j2000: Boolean = true, horizontal: Boolean = true, epoch: UTC = UTC.now()) + fun dateTime(dateTime: OffsetDateTime) companion object { diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/Constellation.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/Constellation.kt index e319de1ae..459c9f9cf 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/Constellation.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/Constellation.kt @@ -458,14 +458,14 @@ enum class Constellation( @JvmStatic private val RA: DoubleArray @JvmStatic private val DEC: DoubleArray @JvmStatic private val RA_TO_INDEX: ByteArray - @JvmStatic private val ENTRIES = values() @JvmStatic private val EPOCH = TT(TimeBesselianEpoch.B1875) init { - val s = bufferedResource("CONSTELLATIONS.dat")!! - RA = s.readDoubleArrayLe(235) - DEC = s.readDoubleArrayLe(199) - RA_TO_INDEX = s.readByteArray(202 * 236) + with(bufferedResource("CONSTELLATIONS.dat")!!) { + RA = readDoubleArrayLe(235) + DEC = readDoubleArrayLe(199) + RA_TO_INDEX = readByteArray(202 * 236) + } } @JvmStatic @@ -474,7 +474,7 @@ enum class Constellation( val i = RA.binarySearch(ra.normalized.hours).let { if (it < 0) -it - 1 else it } val j = DEC.binarySearch(dec.degrees).let { if (it < 0) -it - 1 else it } val k = RA_TO_INDEX[i * 202 + j].toInt() and 0xFF - return ENTRIES[k] + return Constellation.entries[k] } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index d057ceb33..21a70dd00 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ buildCache { dependencyResolutionManagement { versionCatalogs { create("libs") { - library("okio", "com.squareup.okio:okio:3.4.0") + library("okio", "com.squareup.okio:okio:3.5.0") library("okhttp", "com.squareup.okhttp3:okhttp:5.0.0-alpha.11") library("okhttp-logging", "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11") library("fits", "gov.nasa.gsfc.heasarc:nom-tam-fits:1.18.0")