Instrument Neutral Distributed Interface INDI 2.2.5
ioptronHC8406.cpp
Go to the documentation of this file.
1/*
2 ioptronHC8406 INDI driver
3
4 Copyright (C) 2017 Nacho Mas. Base on GotoNova driver by Jasem Mutlaq
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19*/
20
21/*
22HC8406 CMD hardware TEST
23V1.10 March 21, 2011
24UPGRADE INFO ON: http://www.ioptron.com/Articles.asp?ID=268
25
26INFO
27----
28# -> repeat last command
29:GG# +08:00:00 UTC OffSet
30:Gg# -003*18:03# longitud
31:Gt# +41*06:56# latitude
32:GL# 7:02:47.0# local time
33:GS# 20:12: 3.3# Sidereal Time
34:GR# 2:12:57.4# RA
35:GD# +90* 0: 0# DEC
36:GA# +41* 6:55# ALT
37:GZ# 0* 0: 0# AZ
38:GC# 03:12:09# Calendar day
39:pS# East# pier side
40:FirmWareDate# :20110506#
41:V# V1.00#
42
43COMMANDS
44--------
45:CM# Coordinates matched. #
46:CMR# Coordinates matched. #
47
48This only works if the mount is not stopped (tracking)
49:RT0# --> Lunar
50:RT1# --> solar
51:RT2# --> sidereal
52:RT9# --> zero but not work!!
53
54!!!There isn't a command to start/stop tracking !!! You have to do manually
55
56This speeds only are taken into account for protocol buttons, not for the HC Buttons
57:RG# --> Select guide speed for :Mn#,:Ms# ....
58:RG0,1,2 -->preselect guide speed 0.25x, 0.5x, 1.0x (HC shows it)
59:RC# --> Select center speed for :Mn#,:Ms# .... (Not Works)
60:RC0,1,2 -->preselect guide speed (HC doesn't shows it)
61:Mn# :Ms# :Me# :Mw# (move until :Q# at guiding or center speed :RG# (works)or :RC#(not work, use :RC0/1/2 instead))
62:MnXXX# :MsXXX# :MeXXX# :MwXXX# (move XXX ms at guiding speed no mather what :RCx#,:RGX# or :RSX# was issue)
63
64Firmware update (HC v1.10 -> v1.12, also mainboard firmware)
65------------------------------------------------------------------
66HC8406 CMD hardware TEST
67V1.12 2011-08-12
68UPGRADE INFO ON: http://www.ioptron.com/Articles.asp?ID=268
69
70:RT9# -> stop tracking
71:RT0# -> start tracking at sidera speed. Formerly only preselect sidereal speed but
72 not start the tracking itself
73
74:Me#
75:Mw#
76:Mexxx#
77:Mwxxx# ->Al this commands are broken. Only RA axes, the equivalent :Ms#,:Mn#... work.
78
79Summarizing:
80
81old firmware v1.10: is not possible to start/stop tracking. Guide/move commands OK
82new firmware v1.12: is possible to start/stop tracking. Guide/move commands NOT OK
83
84*/
85
86/* SOCAT sniffer
87socat -v PTY,link=/tmp/serial,wait-slave,raw /dev/ttyUSB0,raw
88*/
89
90
91
92#include "ioptronHC8406.h"
93
94#include "indicom.h"
95#include "lx200driver.h"
96
97#include <libnova/transform.h>
98
99#include <cmath>
100#include <cstring>
101#include <termios.h>
102#include <unistd.h>
103
104/* Simulation Parameters */
105#define SLEWRATE 1 /* slew rate, degrees/s */
106#define SIDRATE 0.004178 /* sidereal rate, degrees/s */
107#define ioptronHC8406_TIMEOUT 1 /* timeout */
108#define ioptronHC8406_CALDATE_RESULT " # " /* result of calendar date */
109
110
120
122{
124
125 // Sync Type
126 IUFillSwitch(&SyncCMRS[USE_REGULAR_SYNC], "USE_REGULAR_SYNC", ":CM#", ISS_ON);
127 IUFillSwitch(&SyncCMRS[USE_CMR_SYNC], "USE_CMR_SYNC", ":CMR#", ISS_OFF);
128 IUFillSwitchVector(&SyncCMRSP, SyncCMRS, 2, getDeviceName(), "SYNC_MODE", "Sync", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0,
129 IPS_IDLE);
130
131 // Cursor move Guiding/Center
132 IUFillSwitch(&CursorMoveSpeedS[USE_GUIDE_SPEED], "USE_GUIDE_SPEED", "Guide Speed", ISS_ON);
133 IUFillSwitch(&CursorMoveSpeedS[USE_CENTERING_SPEED], "USE_CENTERING_SPEED", "Centering Speed", ISS_OFF);
134 IUFillSwitchVector(&CursorMoveSpeedSP, CursorMoveSpeedS, 2, getDeviceName(),
135 "CURSOR_MOVE_MODE", "Cursor Move Speed", MOTION_TAB, IP_RO, ISR_1OFMANY, 0, IPS_IDLE);
136
137 // Guide Rate
138 IUFillSwitch(&GuideRateS[0], "0.25x", "", ISS_OFF);
139 IUFillSwitch(&GuideRateS[1], "0.50x", "", ISS_ON);
140 IUFillSwitch(&GuideRateS[2], "1.0x", "", ISS_OFF);
141 IUFillSwitchVector(&GuideRateSP, GuideRateS, 3, getDeviceName(),
142 "GUIDE_RATE", "Guide Speed", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
143
144 // Center Rate
145 IUFillSwitch(&CenterRateS[0], "12x", "", ISS_OFF);
146 IUFillSwitch(&CenterRateS[1], "64x", "", ISS_ON);
147 IUFillSwitch(&CenterRateS[2], "600x", "", ISS_OFF);
148 IUFillSwitch(&CenterRateS[3], "1200x", "", ISS_OFF);
149 IUFillSwitchVector(&CenterRateSP, CenterRateS, 4, getDeviceName(),
150 "CENTER_RATE", "Center Speed", MOTION_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
151
152 TrackModeSP = 3;
153
154 return true;
155}
156
158{
160
161 if (isConnected())
162 {
163 defineProperty(&SyncCMRSP);
164 defineProperty(&GuideRateSP);
165 defineProperty(&CenterRateSP);
166 defineProperty(&CursorMoveSpeedSP);
167 ioptronHC8406Init();
168 }
169 else
170 {
171 deleteProperty(SyncCMRSP.name);
172 deleteProperty(GuideRateSP.name);
173 deleteProperty(CenterRateSP.name);
174 deleteProperty(CursorMoveSpeedSP.name);
175 }
176
177 return true;
178}
179
181{
182 return (const char *)"iOptron HC8406";
183}
184
186{
187 const timespec timeout = {0, 50000000L};
188
189 if (isSimulation())
190 return true;
191
192 char initCMD[] = ":V#";
193 int errcode = 0;
194 char errmsg[MAXRBUF];
195 char response[8];
196 int nbytes_read = 0;
197 int nbytes_written = 0;
198
199 LOG_DEBUG("Initializing iOptron using :V# CMD...");
200
201 for (int i = 0; i < 2; i++)
202 {
203 if ((errcode = tty_write(PortFD, initCMD, 3, &nbytes_written)) != TTY_OK)
204 {
205 tty_error_msg(errcode, errmsg, MAXRBUF);
206 LOGF_ERROR("%s", errmsg);
207 nanosleep(&timeout, nullptr);
208 continue;
209 }
210
211 if ((errcode = tty_read_section(PortFD, response, '#', 3, &nbytes_read)))
212 {
213 tty_error_msg(errcode, errmsg, MAXRBUF);
214 LOGF_ERROR("%s", errmsg);
215 nanosleep(&timeout, nullptr);
216 continue;
217 }
218
219 if (nbytes_read > 0)
220 {
221 response[nbytes_read] = '\0';
222 LOGF_DEBUG("RES (%s)", response);
223
224 if (!strcmp(response, "V1.00#"))
225 return true;
226 }
227
228 nanosleep(&timeout, nullptr);
229 }
230
231 return false;
232}
233
234bool ioptronHC8406::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
235{
236 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
237 {
238
239 // Sync type
240 if (!strcmp(name, SyncCMRSP.name))
241 {
242 IUResetSwitch(&SyncCMRSP);
243 IUUpdateSwitch(&SyncCMRSP, states, names, n);
244 IUFindOnSwitchIndex(&SyncCMRSP);
245 SyncCMRSP.s = IPS_OK;
246 IDSetSwitch(&SyncCMRSP, nullptr);
247 return true;
248 }
249
250 // Cursor move type
251 if (!strcmp(name, CursorMoveSpeedSP.name))
252 {
253 int currentSwitch = IUFindOnSwitchIndex(&CursorMoveSpeedSP);
254 IUUpdateSwitch(&CursorMoveSpeedSP, states, names, n);
255 if (setioptronHC8406CursorMoveSpeed(IUFindOnSwitchIndex(&CursorMoveSpeedSP)) == TTY_OK)
256 CursorMoveSpeedSP.s = IPS_OK;
257 else
258 {
259 IUResetSwitch(&CursorMoveSpeedSP);
260 CursorMoveSpeedS[currentSwitch].s = ISS_ON;
261 CursorMoveSpeedSP.s = IPS_ALERT;
262 }
263 return true;
264 }
265
266 // Guide Rate
267 if (!strcmp(GuideRateSP.name, name))
268 {
269 int currentSwitch = IUFindOnSwitchIndex(&GuideRateSP);
270 IUUpdateSwitch(&GuideRateSP, states, names, n);
271 if (setioptronHC8406GuideRate(IUFindOnSwitchIndex(&GuideRateSP)) == TTY_OK)
272 {
273 GuideRateSP.s = IPS_OK;
274 //Shows guide speed selected
275 CursorMoveSpeedS[USE_GUIDE_SPEED].s = ISS_ON;
276 CursorMoveSpeedS[USE_CENTERING_SPEED].s = ISS_OFF;
277 CursorMoveSpeedSP.s = IPS_OK;
278 IDSetSwitch(&CursorMoveSpeedSP, nullptr);
279 }
280 else
281 {
282 IUResetSwitch(&GuideRateSP);
283 GuideRateS[currentSwitch].s = ISS_ON;
284 GuideRateSP.s = IPS_ALERT;
285 }
286
287 IDSetSwitch(&GuideRateSP, nullptr);
288 return true;
289 }
290
291 // Center Rate
292 if (!strcmp(CenterRateSP.name, name))
293 {
294 int currentSwitch = IUFindOnSwitchIndex(&CenterRateSP);
295 IUUpdateSwitch(&CenterRateSP, states, names, n);
296 if (setioptronHC8406CenterRate(IUFindOnSwitchIndex(&CenterRateSP)) == TTY_OK)
297 {
298 CenterRateSP.s = IPS_OK;
299 //Shows centering speed selected
300 CursorMoveSpeedS[USE_GUIDE_SPEED].s = ISS_OFF;
301 CursorMoveSpeedS[USE_CENTERING_SPEED].s = ISS_ON;
302 CursorMoveSpeedSP.s = IPS_OK;
303 IDSetSwitch(&CursorMoveSpeedSP, nullptr);
304 }
305 else
306 {
307 IUResetSwitch(&CenterRateSP);
308 CenterRateS[currentSwitch].s = ISS_ON;
309 CenterRateSP.s = IPS_ALERT;
310 }
311
312 IDSetSwitch(&CenterRateSP, nullptr);
313 return true;
314 }
315
316 // // Slew Rate
317 // if (!strcmp(SlewRateSP.name, name))
318 // {
319 // int currentSwitch = IUFindOnSwitchIndex(&SlewRateSP);
320 // IUUpdateSwitch(&SlewRateSP, states, names, n);
321 // if (setioptronHC8406SlewRate(IUFindOnSwitchIndex(&SlewRateSP)) == TTY_OK)
322 // SlewRateSP.s = IPS_OK;
323 // else
324 // {
325 // IUResetSwitch(&SlewRateSP);
326 // SlewRateS[currentSwitch].s = ISS_ON;
327 // SlewRateSP.s = IPS_ALERT;
328 // }
329
330 // IDSetSwitch(&SlewRateSP, nullptr);
331 // return true;
332 // }
333
334 }
335
336 return LX200Generic::ISNewSwitch(dev, name, states, names, n);
337}
338
340{
341 /* HC8406 doesn't have :SE# or :SE? command, thus we check if the slew is
342 completed comparing targetRA/DEC with actual RA/DEC */
343
344 float tolerance = 1 / 3600.; // 5 arcsec
345
346 if (fabs(currentRA - targetRA) <= tolerance && fabs(currentDEC - targetDEC) <= tolerance)
347 return true;
348
349 return false;
350}
351
358
359void ioptronHC8406::ioptronHC8406Init()
360{
361 //This mount doesn't report anything so we send some CMD
362 //just to get synchronized with the GUI at start time
363 LOG_WARN("Sending init CMDs. Unpark, Stop tracking");
364 UnPark();
366 SetTrackEnabled(false);
367 setioptronHC8406GuideRate(1);
368}
369
370bool ioptronHC8406::Goto(double r, double d)
371{
372 const timespec timeout = {0, 100000000L};
373
374 targetRA = r;
375 targetDEC = d;
376 char RAStr[64], DecStr[64];
377
378 fs_sexa(RAStr, targetRA, 2, 3600);
379 fs_sexa(DecStr, targetDEC, 2, 3600);
380 LOGF_DEBUG("<GOTO RA/DEC> %s/%s", RAStr, DecStr);
381
382 // If moving, let's stop it first.
383 if (EqNP.getState() == IPS_BUSY)
384 {
385 if (!isSimulation() && abortSlew(PortFD) < 0)
386 {
388 LOG_ERROR("Abort slew failed.");
389 Telescope::AbortSP.apply();
390 return false;
391 }
392
395 LOG_ERROR("Slew aborted.");
396 Telescope::AbortSP.apply();
397 EqNP.apply();
398
400 {
408 }
409
410 // sleep for 100 mseconds
411 nanosleep(&timeout, nullptr);
412 }
413
414 if (!isSimulation())
415 {
417 {
419 LOG_ERROR("Error setting RA/DEC");
420 EqNP.apply();
421 return false;
422 }
423
424 if (slewioptronHC8406() == 0)
425 {
427 LOGF_ERROR("Error Slewing to JNow RA %s - DEC %s\n", RAStr, DecStr);
428 EqNP.apply();
429 slewError(1);
430 return false;
431 }
432 }
433
435 LOGF_DEBUG("Slewing to RA: %s - DEC: %s", RAStr, DecStr);
436 return true;
437}
438
439bool ioptronHC8406::Sync(double ra, double dec)
440{
441 char syncString[256];
442 int syncType = IUFindOnSwitchIndex(&SyncCMRSP);
443
444 if (!isSimulation())
445 {
446 if (setObjectRA(PortFD, ra) < 0 || setObjectDEC(PortFD, dec) < 0)
447 {
449 LOG_ERROR("Error setting RA/DEC. Unable to Sync.");
450 EqNP.apply();
451 return false;
452 }
453
454 bool syncOK = true;
455
456 switch (syncType)
457 {
458 case USE_REGULAR_SYNC:
459 if (::Sync(PortFD, syncString) < 0)
460 syncOK = false;
461 break;
462
463 case USE_CMR_SYNC:
464 if (ioptronHC8406SyncCMR(syncString) < 0)
465 syncOK = false;
466 break;
467
468 default:
469 break;
470 }
471
472 if (syncOK == false)
473 {
475 LOG_ERROR("Synchronization failed.");
476 EqNP.apply();
477 return false;
478 }
479
480 }
481
482 currentRA = ra;
483 currentDEC = dec;
484
485 LOGF_DEBUG("%s Synchronization successful %s", (syncType == USE_REGULAR_SYNC ? "CM" : "CMR"), syncString);
486 LOG_INFO("Synchronization successful.");
488
489 return true;
490}
491
492int ioptronHC8406::ioptronHC8406SyncCMR(char *matchedObject)
493{
494 const timespec timeout = {0, 10000000L};
495 int error_type;
496 int nbytes_write = 0;
497 int nbytes_read = 0;
498
499 LOGF_DEBUG("CMD <%s>", ":CMR#");
500
501 if ((error_type = tty_write_string(PortFD, ":CMR#", &nbytes_write)) != TTY_OK)
502 return error_type;
503
504 if ((error_type = tty_read_section(PortFD, matchedObject, '#', 3, &nbytes_read)) != TTY_OK)
505 return error_type;
506
507 matchedObject[nbytes_read - 1] = '\0';
508
509 LOGF_DEBUG("RES <%s>", matchedObject);
510
511 /* Sleep 10ms before flushing. This solves some issues with LX200 compatible devices. */
512 nanosleep(&timeout, nullptr);
513
514 tcflush(PortFD, TCIFLUSH);
515
516 return 0;
517}
518
519
520int ioptronHC8406::slewioptronHC8406()
521{
522 DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__);
523 char slewNum[2];
524 int error_type;
525 int nbytes_write = 0, nbytes_read = 0;
526
527 DEBUGF(DBG_SCOPE, "CMD <%s>", ":MS#");
528
529 if ((error_type = tty_write_string(PortFD, ":MS#", &nbytes_write)) != TTY_OK)
530 return error_type;
531
532 error_type = tty_read(PortFD, slewNum, 1, 3, &nbytes_read);
533
534 if (nbytes_read < 1)
535 {
536 DEBUGF(DBG_SCOPE, "RES ERROR <%d>", error_type);
537 return error_type;
538 }
539
540 /* We don't need to read the string message, just return corresponding error code */
541 tcflush(PortFD, TCIFLUSH);
542
543 DEBUGF(DBG_SCOPE, "RES <%c>", slewNum[0]);
544
545 return slewNum[0];
546}
547
548
549
550bool ioptronHC8406::updateTime(ln_date *utc, double utc_offset)
551{
552 struct ln_zonedate ltm;
553
554 if (isSimulation())
555 return true;
556
557 ln_date_to_zonedate(utc, &ltm, utc_offset * 3600.0);
558
559 JD = ln_get_julian_day(utc);
560
561 LOGF_DEBUG("New JD is %.2f", JD);
562
563 // Set Local Time
564 if (setLocalTime(PortFD, ltm.hours, ltm.minutes, ltm.seconds) < 0)
565 {
566 LOG_ERROR("Error setting local time.");
567 return false;
568 }
569
570 if (setCalenderDate(PortFD, ltm.days, ltm.months, ltm.years) < 0)
571 {
572 LOG_ERROR("Error setting local date.");
573 return false;
574 }
575
576 if (setioptronHC8406UTCOffset(utc_offset) < 0)
577 {
578 LOG_ERROR("Error setting UTC Offset.");
579 return false;
580 }
581
582 return true;
583}
584
585int ioptronHC8406::setCalenderDate(int fd, int dd, int mm, int yy)
586{
587 char read_buffer[16];
588 char response[67];
589 char good_result[] = ioptronHC8406_CALDATE_RESULT;
590 int error_type;
591 int nbytes_write = 0, nbytes_read = 0;
592 yy = yy % 100;
593
594 snprintf(read_buffer, sizeof(read_buffer), ":SC %02d:%02d:%02d#", mm, dd, yy);
595
596 DEBUGF(DBG_SCOPE, "CMD <%s>", read_buffer);
597
598 tcflush(fd, TCIFLUSH);
599 /* Sleep 100ms before flushing. This solves some issues with LX200 compatible devices. */
600 //usleep(10);
601 if ((error_type = tty_write_string(fd, read_buffer, &nbytes_write)) != TTY_OK)
602 return error_type;
603
604 error_type = tty_read(fd, response, sizeof(response), ioptronHC8406_TIMEOUT, &nbytes_read);
605
606 tcflush(fd, TCIFLUSH);
607
608 if (nbytes_read < 1)
609 {
610 LOG_ERROR("Unable to read response");
611 return error_type;
612 }
613
614 response[nbytes_read] = '\0';
615
616 DEBUGF(DBG_SCOPE, "RES <%s>", response);
617
618 if (strncmp(response, good_result, strlen(good_result)) == 0)
619 {
620 return 0;
621 }
622
623
624 tcflush(fd, TCIFLUSH);
625
626 LOGF_DEBUG("Set date failed! Response: <%s>", response);
627
628 return -1;
629}
630
631bool ioptronHC8406::updateLocation(double latitude, double longitude, double elevation)
632{
633 INDI_UNUSED(elevation);
634
635 if (isSimulation())
636 return true;
637
638 double final_longitude;
639
640 if (longitude > 180)
641 final_longitude = longitude - 360.0;
642 else
643 final_longitude = longitude;
644
645 if (!isSimulation() && setioptronHC8406Longitude(final_longitude) < 0)
646 {
647 LOG_ERROR("Error setting site longitude coordinates");
648 return false;
649 }
650
651 if (!isSimulation() && setioptronHC8406Latitude(latitude) < 0)
652 {
653 LOG_ERROR("Error setting site latitude coordinates");
654 return false;
655 }
656
657 char l[32], L[32];
658 fs_sexa(l, latitude, 3, 3600);
659 fs_sexa(L, longitude, 4, 3600);
660
661 IDMessage(getDeviceName(), "Site location updated to Lat %.32s - Long %.32s", l, L);
662
663 return true;
664}
665
666int ioptronHC8406::setioptronHC8406Longitude(double Long)
667{
668 int d, m, s;
669 char sign;
670 char temp_string[32];
671
672 if (Long > 0)
673 sign = '+';
674 else
675 sign = '-';
676
677 Long = 360 - Long;
678
679 getSexComponents(Long, &d, &m, &s);
680
681 snprintf(temp_string, sizeof(temp_string), ":Sg %c%03d*%02d:%02d#", sign, abs(d), m, s);
682
683 return (setioptronHC8406StandardProcedure(PortFD, temp_string));
684}
685
686int ioptronHC8406::setioptronHC8406Latitude(double Lat)
687{
688 int d, m, s;
689 char sign;
690 char temp_string[32];
691
692 if (Lat > 0)
693 sign = '+';
694 else
695 sign = '-';
696
697 getSexComponents(Lat, &d, &m, &s);
698
699 snprintf(temp_string, sizeof(temp_string), ":St %c%02d*%02d:%02d#", sign, abs(d), m, s);
700
701 return (setioptronHC8406StandardProcedure(PortFD, temp_string));
702}
703
704int ioptronHC8406::setioptronHC8406UTCOffset(double hours)
705{
706 char temp_string[16];
707 char sign;
708 int h = 0, m = 0, s = 0;
709
710 if (hours > 0)
711 sign = '+';
712 else
713 sign = '-';
714
715 getSexComponents(hours, &h, &m, &s);
716
717 snprintf(temp_string, sizeof(temp_string), ":SG %c%02d#", sign, abs(h));
718
719 return (setioptronHC8406StandardProcedure(PortFD, temp_string));
720}
721
722int ioptronHC8406::setioptronHC8406StandardProcedure(int fd, const char *data)
723{
724 const timespec timeout = {0, 10000000L};
725 char bool_return[2];
726 int error_type = 0;
727 int nbytes_write = 0, nbytes_read = 0;
728
729 DEBUGF(DBG_SCOPE, "CMD <%s>", data);
730
731 if ((error_type = tty_write_string(fd, data, &nbytes_write)) != TTY_OK)
732 return error_type;
733
734 error_type = tty_read(fd, bool_return, 1, 5, &nbytes_read);
735
736 // JM: Hack from Jon in the INDI forums to fix longitude/latitude settings failure
737
738 nanosleep(&timeout, nullptr);
739 tcflush(fd, TCIFLUSH);
740 nanosleep(&timeout, nullptr);
741
742
743
744 if (nbytes_read < 1)
745 return error_type;
746
747 DEBUGF(DBG_SCOPE, "RES <%c>", bool_return[0]);
748
749 if (bool_return[0] == '0')
750 {
751 DEBUGF(DBG_SCOPE, "CMD <%s> failed.", data);
752 return -1;
753 }
754
755 DEBUGF(DBG_SCOPE, "CMD <%s> successful.", data);
756
757 return 0;
758}
759
761{
762 if (enabled)
763 {
764 LOG_WARN("<SetTrackEnabled> START TRACKING AT SIDEREAL SPEED (:RT2#)");
765 return setioptronHC8406TrackMode(0);
766 }
767 else
768 {
769 LOG_WARN("<SetTrackEnabled> STOP TRACKING (:RT9#)");
770 return setioptronHC8406TrackMode(3);
771 }
772}
773
775{
776 return (setioptronHC8406TrackMode(mode));
777}
778
779int ioptronHC8406::setioptronHC8406TrackMode(int mode)
780{
781
782 char cmd[8];
783 int mmode = 0;
784 int error_type = 0;
785 int nbytes_write = 0 ;
786
787 DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__);
788
789 if (mode == 0 )
790 {
791 mmode = 2;
792 }
793 else if (mode == 1)
794 {
795 mmode = 1;
796 }
797 else if (mode == 2)
798 {
799 mmode = 0;
800 }
801 else if (mode == 3)
802 {
803 mmode = 9;
804 }
805 snprintf(cmd, 8, ":RT%d#", mmode);
806
807 DEBUGF(DBG_SCOPE, "CMD <%s>", cmd);
808
809 //None return value so just write cmd and exit without reading the response
810 if ((error_type = tty_write_string(PortFD, cmd, &nbytes_write)) != TTY_OK)
811 return error_type;
812
813 return 1;
814}
815
817{
818 DEBUGF(DBG_SCOPE, "<%s>", __FUNCTION__);
819 int error_type;
820 int nbytes_write = 0;
821
822 if ((error_type = tty_write_string(PortFD, ":KA#", &nbytes_write)) != TTY_OK)
823 return error_type;
824 tcflush(PortFD, TCIFLUSH);
825 DEBUG(DBG_SCOPE, "CMD <:KA#>");
826
828 LOG_INFO("Parking is in progress...");
829
830 return true;
831}
832
834{
835 SetParked(false);
836 return true;
837}
838
840{
841 const timespec timeout = {1, 0L};
842
843 //return true; //for debug
844
845 if (!isConnected())
846 return false;
847
848 if (isSimulation())
849 {
850 mountSim();
851 return true;
852 }
853
854 switch (TrackState)
855 {
856 case SCOPE_IDLE:
857 LOG_WARN("<ReadScopeStatus> IDLE");
858 break;
859 case SCOPE_SLEWING:
860 LOG_WARN("<ReadScopeStatus> SLEWING");
861 break;
862 case SCOPE_TRACKING:
863 LOG_WARN("<ReadScopeStatus> TRACKING");
864 break;
865 case SCOPE_PARKING:
866 LOG_WARN("<ReadScopeStatus> PARKING");
867 break;
868 case SCOPE_PARKED:
869 LOG_WARN("<ReadScopeStatus> PARKED");
870 break;
871 default:
872 LOG_WARN("<ReadScopeStatus> UNDEFINED");
873 break;
874 }
875
877 {
878 // Check if LX200 is done slewing
879 if (isSlewComplete())
880 {
881 nanosleep(&timeout, nullptr); //Wait until :MS# finish
882 if (CoordSP.isSwitchOn("SYNC") || CoordSP.isSwitchOn("SLEW"))
883 {
885 LOG_WARN("Slew is complete. IDLE");
886 SetTrackEnabled(false);
887 }
888 else
889 {
891 LOG_WARN("Slew is complete. TRACKING");
892 SetTrackEnabled(true);
893 }
894 }
895 }
896 else if (TrackState == SCOPE_PARKING)
897 {
898 // isSlewComplete() not work because is base on actual RA/DEC vs target RA/DEC. DO ALWAYS
899 if (true || isSlewComplete())
900 {
901 SetParked(true);
903 }
904 }
905
907 {
909 LOG_ERROR("Error reading RA/DEC.");
910 EqNP.apply();
911 return false;
912 }
913
915
916 //sendScopeTime();
917 //syncSideOfPier();
918
919 return true;
920}
921
922void ioptronHC8406::mountSim()
923{
924 static struct timeval ltv;
925 struct timeval tv;
926 double dt, da, dx;
927 int nlocked;
928
929 /* update elapsed time since last poll, don't presume exactly POLLMS */
930 gettimeofday(&tv, nullptr);
931
932 if (ltv.tv_sec == 0 && ltv.tv_usec == 0)
933 ltv = tv;
934
935 dt = tv.tv_sec - ltv.tv_sec + (tv.tv_usec - ltv.tv_usec) / 1e6;
936 ltv = tv;
937 da = SLEWRATE * dt;
938
939 /* Process per current state. We check the state of EQUATORIAL_COORDS and act accordingly */
940 switch (TrackState)
941 {
942 case SCOPE_TRACKING:
943 /* RA moves at sidereal, Dec stands still */
944 currentRA += (SIDRATE * dt / 15.);
945 break;
946
947 case SCOPE_SLEWING:
948 case SCOPE_PARKING:
949 /* slewing - nail it when both within one pulse @ SLEWRATE */
950 nlocked = 0;
951
952 dx = targetRA - currentRA;
953
954 if (fabs(dx) <= da)
955 {
957 nlocked++;
958 }
959 else if (dx > 0)
960 currentRA += da / 15.;
961 else
962 currentRA -= da / 15.;
963
964 dx = targetDEC - currentDEC;
965 if (fabs(dx) <= da)
966 {
968 nlocked++;
969 }
970 else if (dx > 0)
971 currentDEC += da;
972 else
973 currentDEC -= da;
974
975 if (nlocked == 2)
976 {
979 else
980 SetParked(true);
981 }
982
983 break;
984
985 default:
986 break;
987 }
988
990}
991
992
993
994int ioptronHC8406::setioptronHC8406GuideRate(int rate)
995{
996 return setMoveRate(rate, USE_GUIDE_SPEED);
997}
998
999int ioptronHC8406::setioptronHC8406CenterRate(int rate)
1000{
1001 return setMoveRate(rate, USE_CENTERING_SPEED);
1002}
1003
1004int ioptronHC8406::setioptronHC8406SlewRate(int rate)
1005{
1006 return setMoveRate(rate, USE_SLEW_SPEED);
1007}
1008
1009int ioptronHC8406::setioptronHC8406CursorMoveSpeed(int type)
1010{
1011 return setMoveRate(-1, type);
1012}
1013
1014int ioptronHC8406::setMoveRate(int rate, int move_type)
1015{
1016 char cmd[16];
1017 int errcode = 0;
1018 char errmsg[MAXRBUF];
1019 int nbytes_written = 0;
1020
1021 if (isSimulation())
1022 {
1023 return 0;
1024 }
1025
1026 if (rate >= 0)
1027 {
1028 switch (move_type)
1029 {
1030 case USE_GUIDE_SPEED:
1031 snprintf(cmd, 16, ":RG%0d#", rate);
1032 break;
1033 case USE_CENTERING_SPEED:
1034 snprintf(cmd, 16, ":RC%0d#", rate);
1035 break;
1036 case USE_SLEW_SPEED:
1037 snprintf(cmd, 16, ":RS%0d#", rate); //NOT WORK!!
1038 break;
1039
1040 default:
1041 break;
1042 }
1043 }
1044 else
1045 {
1046 switch (move_type)
1047 {
1048 case USE_GUIDE_SPEED:
1049 snprintf(cmd, 16, ":RG#");
1050 break;
1051 case USE_CENTERING_SPEED:
1052 snprintf(cmd, 16, ":RC#"); //NOT WORK!!
1053 break;
1054 case USE_SLEW_SPEED:
1055 snprintf(cmd, 16, ":RS#"); //NOT WORK!!
1056 break;
1057 default:
1058 break;
1059 }
1060 }
1061
1062 LOGF_DEBUG("CMD (%s)", cmd);
1063
1064
1065 tcflush(PortFD, TCIFLUSH);
1066
1067 if ((errcode = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK)
1068 {
1069 tty_error_msg(errcode, errmsg, MAXRBUF);
1070 LOGF_ERROR("%s", errmsg);
1071 return errcode;
1072 }
1073
1074 return 0;
1075}
1076
1077
1078void ioptronHC8406::syncSideOfPier()
1079{
1080 const char *cmd = ":pS#";
1081 // Response
1082 char response[16] = { 0 };
1083 int rc = 0, nbytes_read = 0, nbytes_written = 0;
1084
1085 LOGF_DEBUG("CMD: <%s>", cmd);
1086
1087 tcflush(PortFD, TCIOFLUSH);
1088
1089 if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK)
1090 {
1091 char errmsg[256];
1092 tty_error_msg(rc, errmsg, 256);
1093 LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc);
1094 return;
1095 }
1096
1097 // Read Side
1098 if ((rc = tty_read_section(PortFD, response, '#', 3, &nbytes_read)) != TTY_OK)
1099 {
1100 char errmsg[256];
1101 tty_error_msg(rc, errmsg, 256);
1102 LOGF_ERROR("Error reading from device %s (%d)", errmsg, rc);
1103 return;
1104 }
1105
1106 response[nbytes_read - 1] = '\0';
1107
1108 tcflush(PortFD, TCIOFLUSH);
1109
1110 LOGF_DEBUG("RES: <%s>", response);
1111
1112 if (!strcmp(response, "East"))
1114 else
1116
1117}
1118
1120{
1122
1123 IUSaveConfigSwitch(fp, &SyncCMRSP);
1124
1125 return true;
1126}
1127
1128int ioptronHC8406::getCommandString(int fd, char *data, const char *cmd)
1129{
1130 char *term;
1131 int error_type;
1132 int nbytes_write = 0, nbytes_read = 0;
1133
1134
1135 if ((error_type = tty_write_string(fd, cmd, &nbytes_write)) != TTY_OK)
1136 return error_type;
1137
1138 error_type = tty_read_section(fd, data, '#', ioptronHC8406_TIMEOUT, &nbytes_read);
1139 tcflush(fd, TCIFLUSH);
1140
1141 if (error_type != TTY_OK)
1142 return error_type;
1143
1144 term = strchr(data, '#');
1145 if (term)
1146 *term = '\0';
1147
1148
1149
1150 return 0;
1151
1152}
1153
1155{
1156 char cdate[32] = {0};
1157 double ctime;
1158 int h, m, s;
1159 int utc_h, utc_m, utc_s;
1160 double lx200_utc_offset = 0;
1161 char utc_offset_res[32] = {0};
1162 int day, month, year, result;
1163 struct tm ltm;
1164 struct tm utm;
1165
1166 memset(&ltm, 0, sizeof(ltm));
1167 memset(&utm, 0, sizeof(utm));
1168
1169 time_t time_epoch;
1170
1171 if (isSimulation())
1172 {
1173 snprintf(cdate, 32, "%d-%02d-%02dT%02d:%02d:%02d", 1979, 6, 25, 3, 30, 30);
1174 IDLog("Telescope ISO date and time: %s\n", cdate);
1175 TimeTP[UTC].setText(cdate);
1176 TimeTP[OFFSET].setText("3");
1178 TimeTP.apply();
1179 return true;
1180 }
1181
1182 //getCommandSexa(PortFD, &lx200_utc_offset, ":GG#");
1183 //tcflush(PortFD, TCIOFLUSH);
1184 getCommandString(PortFD, utc_offset_res, ":GG#");
1185
1186 f_scansexa(utc_offset_res, &lx200_utc_offset);
1187 result = sscanf(utc_offset_res, "%d%*c%d%*c%d", &utc_h, &utc_m, &utc_s);
1188 if (result != 3)
1189 {
1190 LOG_ERROR("Error reading UTC offset from Telescope.");
1191 return false;
1192 }
1193 LOGF_DEBUG("<VAL> UTC offset: %d:%d:%d --->%g", utc_h, utc_m, utc_s, lx200_utc_offset);
1194 // LX200 TimeT Offset is defined at the number of hours added to LOCAL TIME to get TimeT. This is contrary to the normal definition.
1195 LOGF_DEBUG("<VAL> UTC offset str: %s", utc_offset_res);
1196 TimeTP[OFFSET].setText(utc_offset_res);
1197 //IUSaveText(&TimeT[1], lx200_utc_offset);
1198
1199 getLocalTime24(PortFD, &ctime);
1200 getSexComponents(ctime, &h, &m, &s);
1201
1202 getCalendarDate(PortFD, cdate);
1203 result = sscanf(cdate, "%d%*c%d%*c%d", &year, &month, &day);
1204 if (result != 3)
1205 {
1206 LOG_ERROR("Error reading date from Telescope.");
1207 return false;
1208 }
1209
1210 // Let's fill in the local time
1211 ltm.tm_sec = s;
1212 ltm.tm_min = m;
1213 ltm.tm_hour = h;
1214 ltm.tm_mday = day;
1215 ltm.tm_mon = month - 1;
1216 ltm.tm_year = year - 1900;
1217 ltm.tm_isdst = 0;
1218
1219 // Get time epoch
1220 time_epoch = mktime(&ltm);
1221
1222 // Convert to TimeT
1223 //time_epoch -= (int)(atof(TimeT[1].text) * 3600.0);
1224 time_epoch -= (int)(lx200_utc_offset * 3600.0);
1225
1226 // Get UTC (we're using localtime_r, but since we shifted time_epoch above by UTCOffset, we should be getting the real UTC time
1227 localtime_r(&time_epoch, &utm);
1228
1229 /* Format it into ISO 8601 */
1230 strftime(cdate, 32, "%Y-%m-%dT%H:%M:%S", &utm);
1231 TimeTP[UTC].setText(cdate);
1232
1233 LOGF_DEBUG("Mount controller Local Time: %02d:%02d:%02d", h, m, s);
1234 LOGF_DEBUG("Mount controller UTC Time: %s", TimeTP[UTC].getText());
1235
1236 // Let's send everything to the client
1238 TimeTP.apply();
1239 return true;
1240}
1241
1242int ioptronHC8406::SendPulseCmd(int8_t direction, uint32_t duration_msec)
1243{
1244 const timespec timeout = {1, 0L};
1245 int rc = 0, nbytes_written = 0;
1246 char cmd[20];
1247 uint32_t duration_left = 0, duration_now = duration_msec;
1248 if (duration_msec > 999)
1249 {
1250 duration_now = 999; //limited to 999
1251 duration_left = duration_msec - duration_now; //pending ms
1252 }
1253 else
1254 {
1255 duration_left = 0;
1256 LOGF_DEBUG("Pulse %d <999 Sent only one", duration_msec);
1257 }
1258
1259 switch (direction)
1260 {
1261 case LX200_NORTH:
1262 sprintf(cmd, ":Mn%03d#", duration_now);
1263 break;
1264 case LX200_SOUTH:
1265 sprintf(cmd, ":Ms%03d#", duration_now);
1266 break;
1267 case LX200_EAST:
1268 sprintf(cmd, ":Me%03d#", duration_now);
1269 break;
1270 case LX200_WEST:
1271 sprintf(cmd, ":Mw%03d#", duration_now);
1272 break;
1273 default:
1274 return 1;
1275 }
1276 LOGF_DEBUG("CMD <%s>", cmd);
1277
1278 if ((rc = tty_write(PortFD, cmd, strlen(cmd), &nbytes_written)) != TTY_OK)
1279 {
1280 char errmsg[256];
1281 tty_error_msg(rc, errmsg, 256);
1282 LOGF_ERROR("Error writing to device %s (%d)", errmsg, rc);
1283 return 1;
1284 }
1285 tcflush(PortFD, TCIFLUSH);
1286
1287 if (duration_left != 0)
1288 {
1289 LOGF_DEBUG("pulse guide. Pulse >999. ms left:%d", duration_left);
1290 nanosleep(&timeout, nullptr); //wait until the previous one has fineshed
1291 return SendPulseCmd(direction, duration_left);
1292 }
1293 return 0;
1294}
1295
bool isConnected() const
INDI::PropertyText getText(const char *name) const
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
bool isSimulation() const
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool isSwitchOn(const std::string &name) const
TelescopeStatus TrackState
void SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount)
SetTelescopeCapability sets the Telescope capabilities. All capabilities must be initialized.
virtual void SetParked(bool isparked)
SetParked Change the mount parking status. The data park file (stored in ~/.indi/ParkData....
void NewRaDec(double ra, double dec)
The child class calls this function when it has updates.
void setPierSide(TelescopePierSide side)
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual void slewError(int slewCode)
virtual bool sendScopeLocation()
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
void setLX200Capability(uint32_t cap)
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual bool SetTrackEnabled(bool enabled) override
SetTrackEnabled Engages or disengages mount tracking. If there are no tracking modes available,...
virtual bool sendScopeTime() override
virtual bool updateLocation(double latitude, double longitude, double elevation) override
Update telescope location settings.
virtual const char * getDefaultName() override
virtual bool Park() override
Park the telescope to its home position.
virtual bool SetTrackMode(uint8_t mode) override
SetTrackMode Set active tracking mode. Do not change track state.
virtual bool checkConnection() override
virtual int SendPulseCmd(int8_t direction, uint32_t duration_msec) override
virtual bool Goto(double, double) override
Move the scope to the supplied RA and DEC coordinates.
virtual bool UnPark() override
Unpark the telescope if already parked.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool Sync(double ra, double dec) override
Set the telescope current RA and DEC coordinates to the supplied RA and DEC coordinates.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool isSlewComplete() override
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual void getBasicData() override
virtual bool updateTime(ln_date *utc, double utc_offset) override
Update telescope time, date, and UTC offset.
virtual bool ReadScopeStatus() override
Read telescope status.
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * MOTION_TAB
MOTION_TAB Where all the motion control properties of the device are located.
INDI::PropertySwitch AbortSP
INDI::PropertySwitch MovementNSSP
INDI::PropertyNumber EqNP
INDI::PropertySwitch TrackModeSP
INDI::PropertyText TimeTP
INDI::PropertySwitch MovementWESP
INDI::PropertySwitch CoordSP
#define setLocalTime(fd, x, y, z)
#define SLEWRATE
Definition ieqpro.cpp:35
double ra
double dec
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
int tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:653
int f_scansexa(const char *str0, double *dp)
convert sexagesimal string str AxBxC to double. x can be anything non-numeric. Any missing A,...
Definition indicom.c:194
int tty_write(int fd, const char *buf, int nbytes, int *nbytes_written)
Writes a buffer to fd.
Definition indicom.c:418
void IDLog(const char *fmt,...)
Definition indicom.c:305
void getSexComponents(double value, int *d, int *m, int *s)
Definition indicom.c:243
int tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read)
read buffer from terminal
Definition indicom.c:548
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
Definition indicom.c:473
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
int fs_sexa(char *out, double a, int w, int fracbase)
Converts a sexagesimal number to a string. sprint the variable a in sexagesimal format into out[].
Definition indicom.c:130
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
void IUSaveConfigSwitch(FILE *fp, const ISwitchVectorProperty *svp)
Add a switch vector property value to the configuration file.
Definition indidevapi.c:33
int IUFindOnSwitchIndex(const ISwitchVectorProperty *svp)
Returns the index of first ON switch it finds in the vector switch property.
Definition indidevapi.c:337
void IUResetSwitch(ISwitchVectorProperty *svp)
Reset all switches in a switch vector property to OFF.
Definition indidevapi.c:366
void IUFillSwitch(ISwitch *sp, const char *name, const char *label, ISState s)
Assign attributes for a switch property. The switch's auxiliary elements will be set to NULL.
Definition indidevapi.c:376
void IUFillSwitchVector(ISwitchVectorProperty *svp, ISwitch *sp, int nsp, const char *dev, const char *name, const char *label, const char *group, IPerm p, ISRule r, double timeout, IPState s)
Assign attributes for a switch vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:453
#define INDI_UNUSED(x)
Definition indidevapi.h:132
int IUUpdateSwitch(ISwitchVectorProperty *svp, ISState *states, char *names[], int n)
Update all switches in a switch vector property.
void IDSetSwitch(const ISwitchVectorProperty *svp, const char *fmt,...)
#define MAXRBUF
Definition indidriver.c:52
void IDMessage(const char *dev, const char *fmt,...)
Definition indidriver.c:983
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#define DEBUG(priority, msg)
Macro to print log messages. Example of usage of the Logger: DEBUG(DBG_DEBUG, "hello " << "world");.
Definition indilogger.h:56
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define DEBUGF(priority, msg,...)
Definition indilogger.h:57
#define getDeviceName
int fd
#define SIDRATE
#define ioptronHC8406_TIMEOUT
#define ioptronHC8406_CALDATE_RESULT
int setObjectRA(int fd, double ra, bool addSpace)
int setObjectDEC(int fd, double dec, bool addSpace)
int checkLX200EquatorialFormat(int fd)
int abortSlew(int fd)
int setCalenderDate(int fd, int dd, int mm, int yy, bool addSpace)
int getCalendarDate(int fd, char *date)
int getCommandString(int fd, char *data, const char *cmd)
#define getLX200DEC(fd, x)
#define getLX200RA(fd, x)
@ LX200_WEST
Definition lx200driver.h:42
@ LX200_SOUTH
Definition lx200driver.h:44
@ LX200_NORTH
Definition lx200driver.h:41
@ LX200_EAST
Definition lx200driver.h:43
#define getLocalTime24(fd, x)
uint16_t type
Definition pwc-ioctl.h:0
uint8_t cmd[4]
Definition pwc-ioctl.h:2
char name[MAXINDINAME]
Definition indiapi.h:369