From 27c80d8ae2d3c46294cff65eb78d64cc56c6bc4c Mon Sep 17 00:00:00 2001 From: prkrtg Date: Tue, 7 Apr 2026 11:24:29 -1000 Subject: [PATCH 01/37] ETC GUI Update (#403) --- pygui/ETC/.gitignore | 8 + pygui/ETC/CSV/QE-LBNL-CCD-blue.csv | 742 --------------- pygui/ETC/CSV/QE-LBNL-CCD-red.csv | 742 --------------- .../CSV/throughput-NGPS-spectrograph-G.csv | 754 --------------- .../CSV/throughput-NGPS-spectrograph-I.csv | 754 --------------- .../CSV/throughput-NGPS-spectrograph-R.csv | 754 --------------- .../CSV/throughput-NGPS-spectrograph-U.csv | 754 --------------- .../throughput-spectrograph-20260206-G.csv | 55 ++ .../throughput-spectrograph-20260206-I.csv | 48 + .../throughput-spectrograph-20260206-R.csv | 69 ++ .../throughput-spectrograph-20260206-U.csv | 63 ++ pygui/ETC/ETC_arguments.py | 4 +- pygui/ETC/ETC_config.py | 28 +- pygui/ETC/ETC_import.py | 11 +- pygui/ETC/ETC_main.py | 52 +- pygui/ETC/requirements.txt | 5 - pygui/ETC/test.py | 21 + pygui/calib/thrufocus | 30 +- pygui/control_tab.py | 69 +- pygui/etc_popup.py | 858 ++++++++++++------ pygui/layout_service.py | 6 + pygui/ngps_gui.py | 32 +- 22 files changed, 1014 insertions(+), 4845 deletions(-) create mode 100644 pygui/ETC/.gitignore delete mode 100644 pygui/ETC/CSV/QE-LBNL-CCD-blue.csv delete mode 100644 pygui/ETC/CSV/QE-LBNL-CCD-red.csv delete mode 100644 pygui/ETC/CSV/throughput-NGPS-spectrograph-G.csv delete mode 100644 pygui/ETC/CSV/throughput-NGPS-spectrograph-I.csv delete mode 100644 pygui/ETC/CSV/throughput-NGPS-spectrograph-R.csv delete mode 100644 pygui/ETC/CSV/throughput-NGPS-spectrograph-U.csv create mode 100644 pygui/ETC/CSV/throughput-spectrograph-20260206-G.csv create mode 100644 pygui/ETC/CSV/throughput-spectrograph-20260206-I.csv create mode 100644 pygui/ETC/CSV/throughput-spectrograph-20260206-R.csv create mode 100644 pygui/ETC/CSV/throughput-spectrograph-20260206-U.csv delete mode 100644 pygui/ETC/requirements.txt create mode 100644 pygui/ETC/test.py diff --git a/pygui/ETC/.gitignore b/pygui/ETC/.gitignore new file mode 100644 index 00000000..3c4ae657 --- /dev/null +++ b/pygui/ETC/.gitignore @@ -0,0 +1,8 @@ +.* +!.gitignore +*.png +__pycache__/ +sources/current_calspec_all/* +old/ +skybrightness/ +scratch/ diff --git a/pygui/ETC/CSV/QE-LBNL-CCD-blue.csv b/pygui/ETC/CSV/QE-LBNL-CCD-blue.csv deleted file mode 100644 index f9c21516..00000000 --- a/pygui/ETC/CSV/QE-LBNL-CCD-blue.csv +++ /dev/null @@ -1,742 +0,0 @@ -Wavelength (nm),Blue QE -310,0.434667605 -311,0.437895211 -312,0.441300214 -313,0.444793043 -314,0.448285872 -315,0.4517787 -316,0.455271529 -317,0.458764358 -318,0.462195307 -319,0.465543252 -320,0.468891196 -321,0.472239141 -322,0.475587086 -323,0.47893503 -324,0.482282975 -325,0.485733862 -326,0.489204564 -327,0.492675266 -328,0.496145968 -329,0.49961667 -330,0.503087373 -331,0.506558075 -332,0.510028777 -333,0.513499479 -334,0.517349601 -335,0.521281485 -336,0.52521337 -337,0.529145254 -338,0.533077139 -339,0.537009024 -340,0.540981195 -341,0.545085105 -342,0.549189014 -343,0.553292924 -344,0.557396834 -345,0.561500744 -346,0.566262491 -347,0.571131914 -348,0.576001338 -349,0.580870762 -350,0.58647176 -351,0.592389029 -352,0.598306299 -353,0.604223568 -354,0.60993593 -355,0.615289677 -356,0.620643424 -357,0.625997171 -358,0.632875662 -359,0.64033949 -360,0.647803318 -361,0.655267146 -362,0.661268005 -363,0.667120972 -364,0.672973938 -365,0.678826904 -366,0.687462172 -367,0.696338066 -368,0.70364005 -369,0.709323755 -370,0.715007459 -371,0.720639571 -372,0.72606374 -373,0.731487909 -374,0.738413445 -375,0.746348009 -376,0.752071301 -377,0.756805112 -378,0.761538923 -379,0.76619169 -380,0.770562417 -381,0.774933143 -382,0.77930387 -383,0.783638421 -384,0.787920657 -385,0.792202894 -386,0.796356619 -387,0.799516977 -388,0.802677334 -389,0.805837692 -390,0.808976114 -391,0.812063388 -392,0.815150662 -393,0.818237936 -394,0.821178399 -395,0.823795654 -396,0.826412908 -397,0.829030163 -398,0.831647417 -399,0.834063289 -400,0.836430197 -401,0.838797105 -402,0.841164014 -403,0.843530922 -404,0.845897831 -405,0.847930118 -406,0.849842403 -407,0.851754687 -408,0.853666972 -409,0.855579256 -410,0.857491541 -411,0.859403825 -412,0.861149946 -413,0.862797372 -414,0.864444798 -415,0.866092224 -416,0.86773965 -417,0.869387076 -418,0.871034502 -419,0.872681928 -420,0.873820611 -421,0.87495856 -422,0.87609651 -423,0.877234459 -424,0.878372408 -425,0.879510358 -426,0.880638328 -427,0.881734108 -428,0.882829889 -429,0.88392567 -430,0.88502145 -431,0.885946847 -432,0.886735816 -433,0.887524786 -434,0.888313756 -435,0.889102726 -436,0.889891695 -437,0.890680665 -438,0.891469635 -439,0.892180711 -440,0.892866255 -441,0.893551799 -442,0.894237343 -443,0.894922887 -444,0.895608431 -445,0.896293975 -446,0.896810093 -447,0.897232749 -448,0.897655405 -449,0.89807806 -450,0.898500716 -451,0.898923372 -452,0.899346027 -453,0.89967378 -454,0.899834583 -455,0.899995387 -456,0.90015619 -457,0.900316993 -458,0.900477797 -459,0.9006386 -460,0.900799403 -461,0.900914826 -462,0.90095832 -463,0.901001815 -464,0.90104531 -465,0.901088804 -466,0.901132299 -467,0.901109856 -468,0.901019293 -469,0.900928729 -470,0.900838166 -471,0.900747603 -472,0.90065704 -473,0.900566477 -474,0.900475914 -475,0.900385351 -476,0.900214973 -477,0.900044285 -478,0.899873597 -479,0.899702909 -480,0.899532221 -481,0.899361533 -482,0.899190845 -483,0.899020157 -484,0.898849469 -485,0.898271518 -486,0.897685366 -487,0.897099214 -488,0.896513062 -489,0.895926909 -490,0.895340757 -491,0.894754605 -492,0.894168452 -493,0.8935823 -494,0.893069753 -495,0.892576653 -496,0.892083553 -497,0.891590453 -498,0.891097352 -499,0.890604252 -500,0.890111152 -501,0.889618052 -502,0.889124952 -503,0.888631852 -504,0.887914819 -505,0.88717516 -506,0.886435501 -507,0.885695841 -508,0.884956182 -509,0.884216523 -510,0.883476863 -511,0.882737204 -512,0.881997545 -513,0.881257885 -514,0.880518226 -515,0.87991839 -516,0.879350976 -517,0.878783562 -518,0.878216147 -519,0.877648733 -520,0.877081319 -521,0.876513905 -522,0.87594649 -523,0.875379076 -524,0.874811662 -525,0.874244247 -526,0.873676833 -527,0.873106945 -528,0.872523555 -529,0.871940166 -530,0.871356776 -531,0.870773387 -532,0.870189997 -533,0.869606608 -534,0.869023219 -535,0.868439829 -536,0.86785644 -537,0.86727305 -538,0.866689661 -539,0.866106271 -540,0.865435498 -541,0.864748665 -542,0.864061833 -543,0.863375 -544,0.862688167 -545,0.862001335 -546,0.861314502 -547,0.860627669 -548,0.859940837 -549,0.859254004 -550,0.858567171 -551,0.857880339 -552,0.857193506 -553,0.856506673 -554,0.855801918 -555,0.855051225 -556,0.854300532 -557,0.853549839 -558,0.852799146 -559,0.852048453 -560,0.85129776 -561,0.850547067 -562,0.849796374 -563,0.849045681 -564,0.848294988 -565,0.847544295 -566,0.846800927 -567,0.846061265 -568,0.845321602 -569,0.844581939 -570,0.843842277 -571,0.843102614 -572,0.842362952 -573,0.841623289 -574,0.840883626 -575,0.840143964 -576, -577, -578, -579, -580, -581, -582, -583, -584, -585, -586, -587, -588, -589, -590, -591, -592, -593, -594, -595, -596, -597, -598, -599, -600, -601, -602, -603, -604, -605, -606, -607, -608, -609, -610, -611, -612, -613, -614, -615, -616, -617, -618, -619, -620, -621, -622, -623, -624, -625, -626, -627, -628, -629, -630, -631, -632, -633, -634, -635, -636, -637, -638, -639, -640, -641, -642, -643, -644, -645, -646, -647, -648, -649, -650, -651, -652, -653, -654, -655, -656, -657, -658, -659, -660, -661, -662, -663, -664, -665, -666, -667, -668, -669, -670, -671, -672, -673, -674, -675, -676, -677, -678, -679, -680, -681, -682, -683, -684, -685, -686, -687, -688, -689, -690, -691, -692, -693, -694, -695, -696, -697, -698, -699, -700, -701, -702, -703, -704, -705, -706, -707, -708, -709, -710, -711, -712, -713, -714, -715, -716, -717, -718, -719, -720, -721, -722, -723, -724, -725, -726, -727, -728, -729, -730, -731, -732, -733, -734, -735, -736, -737, -738, -739, -740, -741, -742, -743, -744, -745, -746, -747, -748, -749, -750, -751, -752, -753, -754, -755, -756, -757, -758, -759, -760, -761, -762, -763, -764, -765, -766, -767, -768, -769, -770, -771, -772, -773, -774, -775, -776, -777, -778, -779, -780, -781, -782, -783, -784, -785, -786, -787, -788, -789, -790, -791, -792, -793, -794, -795, -796, -797, -798, -799, -800, -801, -802, -803, -804, -805, -806, -807, -808, -809, -810, -811, -812, -813, -814, -815, -816, -817, -818, -819, -820, -821, -822, -823, -824, -825, -826, -827, -828, -829, -830, -831, -832, -833, -834, -835, -836, -837, -838, -839, -840, -841, -842, -843, -844, -845, -846, -847, -848, -849, -850, -851, -852, -853, -854, -855, -856, -857, -858, -859, -860, -861, -862, -863, -864, -865, -866, -867, -868, -869, -870, -871, -872, -873, -874, -875, -876, -877, -878, -879, -880, -881, -882, -883, -884, -885, -886, -887, -888, -889, -890, -891, -892, -893, -894, -895, -896, -897, -898, -899, -900, -901, -902, -903, -904, -905, -906, -907, -908, -909, -910, -911, -912, -913, -914, -915, -916, -917, -918, -919, -920, -921, -922, -923, -924, -925, -926, -927, -928, -929, -930, -931, -932, -933, -934, -935, -936, -937, -938, -939, -940, -941, -942, -943, -944, -945, -946, -947, -948, -949, -950, -951, -952, -953, -954, -955, -956, -957, -958, -959, -960, -961, -962, -963, -964, -965, -966, -967, -968, -969, -970, -971, -972, -973, -974, -975, -976, -977, -978, -979, -980, -981, -982, -983, -984, -985, -986, -987, -988, -989, -990, -991, -992, -993, -994, -995, -996, -997, -998, -999, -1000, -1001, -1002, -1003, -1004, -1005, -1006, -1007, -1008, -1009, -1010, -1011, -1012, -1013, -1014, -1015, -1016, -1017, -1018, -1019, -1020, -1021, -1022, -1023, -1024, -1025, -1026, -1027, -1028, -1029, -1030, -1031, -1032, -1033, -1034, -1035, -1036, -1037, -1038, -1039, -1040, -1041, -1042, -1043, -1044, -1045, -1046, -1047, -1048, -1049, -1050, \ No newline at end of file diff --git a/pygui/ETC/CSV/QE-LBNL-CCD-red.csv b/pygui/ETC/CSV/QE-LBNL-CCD-red.csv deleted file mode 100644 index 2ab7a9d6..00000000 --- a/pygui/ETC/CSV/QE-LBNL-CCD-red.csv +++ /dev/null @@ -1,742 +0,0 @@ -Wavelength (nm),Red QE -310, -311, -312, -313, -314, -315, -316, -317, -318, -319, -320, -321, -322, -323, -324, -325, -326, -327, -328, -329, -330, -331, -332, -333, -334, -335, -336, -337, -338, -339, -340, -341,0.0834 -342,0.0876 -343,0.0917 -344,0.0959 -345,0.1 -346,0.1042 -347,0.1084 -348,0.1125 -349,0.1167 -350,0.1208 -351,0.125 -352,0.1292 -353,0.1333 -354,0.1375 -355,0.1416 -356,0.1458 -357,0.15 -358,0.1541 -359,0.1583 -360,0.1624 -361,0.1727 -362,0.1832 -363,0.1936 -364,0.2041 -365,0.2146 -366,0.225 -367,0.2355 -368,0.246 -369,0.2564 -370,0.2669 -371,0.2774 -372,0.2878 -373,0.2983 -374,0.3088 -375,0.3192 -376,0.3297 -377,0.3401 -378,0.3506 -379,0.3611 -380,0.371 -381,0.3769 -382,0.3828 -383,0.3887 -384,0.3946 -385,0.4005 -386,0.4064 -387,0.4123 -388,0.4182 -389,0.4241 -390,0.43 -391,0.4359 -392,0.4418 -393,0.4477 -394,0.4536 -395,0.4595 -396,0.4654 -397,0.4713 -398,0.4772 -399,0.4831 -400,0.489 -401,0.4949 -402,0.5007 -403,0.5055 -404,0.5101 -405,0.5147 -406,0.5194 -407,0.524 -408,0.5287 -409,0.5333 -410,0.538 -411,0.5426 -412,0.5473 -413,0.5519 -414,0.5565 -415,0.5612 -416,0.5658 -417,0.5705 -418,0.5751 -419,0.5798 -420,0.5844 -421,0.5869 -422,0.5892 -423,0.5915 -424,0.5939 -425,0.5962 -426,0.5985 -427,0.6008 -428,0.6031 -429,0.6055 -430,0.6078 -431,0.6101 -432,0.6124 -433,0.6148 -434,0.6171 -435,0.6194 -436,0.6217 -437,0.624 -438,0.6264 -439,0.6287 -440,0.631 -441,0.6333 -442,0.6355 -443,0.6375 -444,0.6394 -445,0.6414 -446,0.6433 -447,0.6453 -448,0.6472 -449,0.6492 -450,0.6511 -451,0.6531 -452,0.655 -453,0.657 -454,0.6589 -455,0.6609 -456,0.6628 -457,0.6648 -458,0.6667 -459,0.6687 -460,0.6706 -461,0.6719 -462,0.6733 -463,0.6746 -464,0.676 -465,0.6774 -466,0.6787 -467,0.6801 -468,0.6815 -469,0.6828 -470,0.6842 -471,0.6855 -472,0.6869 -473,0.6883 -474,0.6896 -475,0.691 -476,0.6924 -477,0.6937 -478,0.6951 -479,0.6965 -480,0.6978 -481,0.6992 -482,0.7005 -483,0.7024 -484,0.7042 -485,0.706 -486,0.7078 -487,0.7096 -488,0.7115 -489,0.7133 -490,0.7151 -491,0.7169 -492,0.7188 -493,0.7206 -494,0.7224 -495,0.7242 -496,0.7261 -497,0.7279 -498,0.7297 -499,0.7315 -500,0.7331 -501,0.7341 -502,0.7352 -503,0.7363 -504,0.7374 -505,0.7385 -506,0.7396 -507,0.7407 -508,0.7418 -509,0.7429 -510,0.744 -511,0.7451 -512,0.7462 -513,0.7473 -514,0.7484 -515,0.7495 -516,0.7506 -517,0.7517 -518,0.7528 -519,0.7539 -520,0.755 -521,0.7567 -522,0.7584 -523,0.7602 -524,0.7619 -525,0.7636 -526,0.7654 -527,0.7671 -528,0.7689 -529,0.7706 -530,0.7723 -531,0.7741 -532,0.7758 -533,0.7776 -534,0.7793 -535,0.7811 -536,0.7828 -537,0.7845 -538,0.7863 -539,0.788 -540,0.7896 -541,0.7906 -542,0.7916 -543,0.7925 -544,0.7935 -545,0.7945 -546,0.7954 -547,0.7964 -548,0.7974 -549,0.7984 -550,0.7993 -551,0.8003 -552,0.8013 -553,0.8022 -554,0.8032 -555,0.8042 -556,0.8051 -557,0.8061 -558,0.8071 -559,0.808 -560,0.809 -561,0.8103 -562,0.812 -563,0.8136 -564,0.8152 -565,0.8168 -566,0.8184 -567,0.8201 -568,0.8217 -569,0.8233 -570,0.8249 -571,0.8265 -572,0.8281 -573,0.8298 -574,0.8314 -575,0.833 -576,0.8346 -577,0.8362 -578,0.8378 -579,0.8395 -580,0.8411 -581,0.8422 -582,0.8432 -583,0.8442 -584,0.8453 -585,0.8463 -586,0.8473 -587,0.8483 -588,0.8493 -589,0.8504 -590,0.8514 -591,0.8524 -592,0.8534 -593,0.8544 -594,0.8555 -595,0.8565 -596,0.8575 -597,0.8585 -598,0.8596 -599,0.8606 -600,0.8616 -601,0.8625 -602,0.8635 -603,0.8644 -604,0.8654 -605,0.8664 -606,0.8673 -607,0.8683 -608,0.8692 -609,0.8702 -610,0.8712 -611,0.8721 -612,0.8731 -613,0.8741 -614,0.875 -615,0.876 -616,0.8769 -617,0.8779 -618,0.8789 -619,0.8798 -620,0.8808 -621,0.8818 -622,0.8828 -623,0.8838 -624,0.8848 -625,0.8858 -626,0.8868 -627,0.8878 -628,0.8888 -629,0.8898 -630,0.8908 -631,0.8918 -632,0.8928 -633,0.8939 -634,0.8949 -635,0.8959 -636,0.8969 -637,0.8979 -638,0.8989 -639,0.8999 -640,0.9009 -641,0.9016 -642,0.9022 -643,0.9029 -644,0.9035 -645,0.9041 -646,0.9048 -647,0.9054 -648,0.906 -649,0.9067 -650,0.9073 -651,0.908 -652,0.9086 -653,0.9092 -654,0.9099 -655,0.9105 -656,0.9111 -657,0.9118 -658,0.9124 -659,0.913 -660,0.9137 -661,0.9144 -662,0.9152 -663,0.9159 -664,0.9167 -665,0.9174 -666,0.9182 -667,0.9189 -668,0.9197 -669,0.9204 -670,0.9212 -671,0.922 -672,0.9227 -673,0.9235 -674,0.9242 -675,0.925 -676,0.9257 -677,0.9265 -678,0.9272 -679,0.928 -680,0.9285 -681,0.9287 -682,0.9289 -683,0.9291 -684,0.9292 -685,0.9294 -686,0.9296 -687,0.9298 -688,0.93 -689,0.9301 -690,0.9303 -691,0.9305 -692,0.9307 -693,0.9309 -694,0.931 -695,0.9312 -696,0.9314 -697,0.9316 -698,0.9318 -699,0.9319 -700,0.9321 -701,0.9326 -702,0.9332 -703,0.9338 -704,0.9343 -705,0.9349 -706,0.9355 -707,0.936 -708,0.9366 -709,0.9372 -710,0.9377 -711,0.9383 -712,0.9389 -713,0.9394 -714,0.94 -715,0.9406 -716,0.9411 -717,0.9417 -718,0.9423 -719,0.9428 -720,0.9433 -721,0.9437 -722,0.944 -723,0.9443 -724,0.9446 -725,0.945 -726,0.9453 -727,0.9456 -728,0.946 -729,0.9463 -730,0.9466 -731,0.947 -732,0.9473 -733,0.9476 -734,0.948 -735,0.9483 -736,0.9486 -737,0.949 -738,0.9493 -739,0.9496 -740,0.95 -741,0.9501 -742,0.9502 -743,0.9503 -744,0.9504 -745,0.9505 -746,0.9506 -747,0.9507 -748,0.9508 -749,0.9509 -750,0.951 -751,0.9511 -752,0.9512 -753,0.9513 -754,0.9514 -755,0.9515 -756,0.9516 -757,0.9517 -758,0.9518 -759,0.9519 -760,0.952 -761,0.9522 -762,0.9525 -763,0.9528 -764,0.9531 -765,0.9534 -766,0.9536 -767,0.9539 -768,0.9542 -769,0.9545 -770,0.9548 -771,0.9551 -772,0.9553 -773,0.9556 -774,0.9559 -775,0.9562 -776,0.9565 -777,0.9568 -778,0.9571 -779,0.9573 -780,0.9576 -781,0.9575 -782,0.9573 -783,0.9571 -784,0.9569 -785,0.9568 -786,0.9566 -787,0.9564 -788,0.9562 -789,0.956 -790,0.9558 -791,0.9556 -792,0.9554 -793,0.9552 -794,0.9551 -795,0.9549 -796,0.9547 -797,0.9545 -798,0.9543 -799,0.9541 -800,0.9539 -801,0.9538 -802,0.9538 -803,0.9537 -804,0.9536 -805,0.9535 -806,0.9534 -807,0.9533 -808,0.9532 -809,0.9531 -810,0.953 -811,0.9529 -812,0.9528 -813,0.9527 -814,0.9526 -815,0.9525 -816,0.9524 -817,0.9523 -818,0.9522 -819,0.9521 -820,0.952 -821,0.952 -822,0.952 -823,0.952 -824,0.952 -825,0.952 -826,0.952 -827,0.952 -828,0.952 -829,0.952 -830,0.952 -831,0.952 -832,0.952 -833,0.952 -834,0.952 -835,0.952 -836,0.952 -837,0.952 -838,0.952 -839,0.952 -840,0.952 -841,0.9519 -842,0.9516 -843,0.9514 -844,0.9512 -845,0.951 -846,0.9507 -847,0.9505 -848,0.9503 -849,0.9501 -850,0.9498 -851,0.9496 -852,0.9494 -853,0.9492 -854,0.949 -855,0.9487 -856,0.9485 -857,0.9483 -858,0.9481 -859,0.9478 -860,0.9476 -861,0.9475 -862,0.9475 -863,0.9474 -864,0.9473 -865,0.9472 -866,0.9472 -867,0.9471 -868,0.947 -869,0.9469 -870,0.9469 -871,0.9468 -872,0.9467 -873,0.9467 -874,0.9466 -875,0.9465 -876,0.9464 -877,0.9464 -878,0.9463 -879,0.9462 -880,0.9462 -881,0.9457 -882,0.9451 -883,0.9445 -884,0.9439 -885,0.9433 -886,0.9427 -887,0.9421 -888,0.9415 -889,0.9409 -890,0.9403 -891,0.9397 -892,0.9391 -893,0.9385 -894,0.9379 -895,0.9373 -896,0.9367 -897,0.9361 -898,0.9355 -899,0.9349 -900,0.9342 -901,0.9328 -902,0.9315 -903,0.9302 -904,0.9289 -905,0.9275 -906,0.9262 -907,0.9249 -908,0.9236 -909,0.9223 -910,0.9209 -911,0.9196 -912,0.9183 -913,0.917 -914,0.9157 -915,0.9143 -916,0.913 -917,0.9117 -918,0.9104 -919,0.9087 -920,0.9069 -921,0.9051 -922,0.9033 -923,0.9015 -924,0.8997 -925,0.8979 -926,0.8962 -927,0.8944 -928,0.8926 -929,0.8908 -930,0.889 -931,0.8872 -932,0.8854 -933,0.8836 -934,0.8818 -935,0.88 -936,0.8782 -937,0.8764 -938,0.8746 -939,0.8728 -940,0.8706 -941,0.8675 -942,0.8644 -943,0.8613 -944,0.8583 -945,0.8552 -946,0.8521 -947,0.849 -948,0.8459 -949,0.8428 -950,0.8397 -951,0.8366 -952,0.8336 -953,0.8305 -954,0.8274 -955,0.8243 -956,0.8212 -957,0.8181 -958,0.815 -959,0.8119 -960,0.8088 -961,0.8032 -962,0.7965 -963,0.7898 -964,0.7831 -965,0.7763 -966,0.7696 -967,0.7629 -968,0.7562 -969,0.7494 -970,0.7427 -971,0.736 -972,0.7293 -973,0.7225 -974,0.7158 -975,0.7091 -976,0.7024 -977,0.6956 -978,0.6889 -979,0.6822 -980,0.6755 -981,0.6674 -982,0.6584 -983,0.6494 -984,0.6404 -985,0.6314 -986,0.6224 -987,0.6134 -988,0.6044 -989,0.5954 -990,0.5864 -991,0.5774 -992,0.5685 -993,0.5595 -994,0.5505 -995,0.5415 -996,0.5325 -997,0.5235 -998,0.5145 -999,0.5055 -1000,0.4964 -1001,0.4858 -1002,0.4753 -1003,0.4648 -1004,0.4543 -1005,0.4438 -1006,0.4333 -1007,0.4228 -1008,0.4123 -1009,0.4018 -1010,0.3913 -1011,0.3807 -1012,0.3702 -1013,0.3597 -1014,0.3492 -1015,0.3387 -1016,0.3282 -1017,0.3177 -1018,0.3072 -1019,0.2967 -1020,0.2861 -1021,0.2763 -1022,0.2669 -1023,0.2576 -1024,0.2483 -1025,0.2389 -1026,0.2296 -1027,0.2203 -1028,0.211 -1029,0.2016 -1030,0.1923 -1031,0.183 -1032,0.1737 -1033,0.1643 -1034,0.155 -1035,0.1457 -1036,0.1364 -1037,0.127 -1038,0.1177 -1039,0.1084 -1040,0.0997 -1041,0.097 -1042,0.0943 -1043,0.0916 -1044,0.0889 -1045,0.0862 -1046,0.0835 -1047,0.0808 -1048,0.0781 -1049,0.0754 -1050,0.0727 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-NGPS-spectrograph-G.csv b/pygui/ETC/CSV/throughput-NGPS-spectrograph-G.csv deleted file mode 100644 index 1f6c80ed..00000000 --- a/pygui/ETC/CSV/throughput-NGPS-spectrograph-G.csv +++ /dev/null @@ -1,754 +0,0 @@ -# Based on MODELS of NGPS spectrograph optics: dichroics, collimator, gratings, camera", -# Does NOT include losses from slit and slicer, -Wavelength (nm),G -300, -301, -302, -303, -304, -305, -306, -307, -308, -309, -310,0 -311,0 -312,0 -313,0 -314,0 -315,0 -316,0 -317,0 -318,0 -319,0 -320,0 -321,0 -322,0 -323,0 -324,0 -325,0 -326,0 -327,0 -328,0 -329,0 -330,0 -331,0 -332,0 -333,0 -334,0 -335,0 -336,0 -337,0 -338,0 -339,0 -340,0 -341,0 -342,0 -343,0 -344,0 -345,0 -346,0 -347,0 -348,0 -349,0 -350,0 -351,0 -352,0 -353,0 -354,0 -355,0 -356,0 -357,0 -358,0 -359,0 -360,0 -361,0 -362,0 -363,0 -364,0 -365,0 -366,0 -367,0 -368,0 -369,0 -370,0 -371,0 -372,0 -373,0 -374,0 -375,0 -376,0 -377,0 -378,0 -379,0 -380,0 -381,0 -382,0 -383,0 -384,0 -385,0 -386,0 -387,0 -388,0 -389,0 -390,0 -391,0 -392,0 -393,0 -394,0 -395,0 -396,0 -397,0 -398,0 -399,0 -400,0 -401,0 -402,0 -403,0 -404,0 -405,0 -406,0 -407,0 -408,0 -409,0 -410,0 -411,0 -412,0 -413,0 -414,0 -415,0 -416,0 -417,0.027976472 -418,0.056740609 -419,0.109199322 -420,0.174949179 -421,0.219993355 -422,0.238836142 -423,0.250608037 -424,0.266397095 -425,0.28734428 -426,0.309678353 -427,0.328064244 -428,0.338901546 -429,0.342373982 -430,0.341643664 -431,0.340671522 -432,0.343262181 -433,0.355151612 -434,0.392595286 -435,0.511252471 -436,0.674340149 -437,0.70594771 -438,0.690929653 -439,0.692279977 -440,0.703894335 -441,0.714000388 -442,0.717988517 -443,0.71861268 -444,0.720467254 -445,0.725806996 -446,0.733925951 -447,0.742357523 -448,0.74854023 -449,0.751205252 -450,0.750839164 -451,0.749842831 -452,0.749061678 -453,0.749508371 -454,0.751766531 -455,0.755940297 -456,0.761687646 -457,0.768316146 -458,0.774942404 -459,0.780701445 -460,0.7849609 -461,0.787466404 -462,0.788365295 -463,0.788115525 -464,0.787328757 -465,0.786615016 -466,0.78647151 -467,0.787222272 -468,0.788996286 -469,0.791728854 -470,0.795193885 -471,0.799045834 -472,0.802816526 -473,0.806151186 -474,0.808857357 -475,0.810830562 -476,0.812124511 -477,0.812917009 -478,0.813454076 -479,0.813989756 -480,0.814792812 -481,0.816161373 -482,0.818082916 -483,0.820492171 -484,0.823302603 -485,0.826355358 -486,0.829437812 -487,0.832315756 -488,0.834768985 -489,0.836624747 -490,0.837784581 -491,0.838238837 -492,0.838064542 -493,0.837408658 -494,0.836459339 -495,0.835411738 -496,0.834430124 -497,0.833599035 -498,0.832845097 -499,0.832123461 -500,0.832667051 -501,0.83431499 -502,0.835879928 -503,0.83728082 -504,0.838484629 -505,0.839390193 -506,0.839884521 -507,0.83987871 -508,0.839328558 -509,0.838244476 -510,0.836694098 -511,0.834793811 -512,0.832692229 -513,0.830549128 -514,0.828506691 -515,0.826632921 -516,0.824964023 -517,0.823444915 -518,0.821192059 -519,0.815654942 -520,0.798109292 -521,0.793223628 -522,0.813294307 -523,0.821870242 -524,0.825841262 -525,0.828131992 -526,0.829534968 -527,0.830278912 -528,0.830430823 -529,0.83001405 -530,0.829048464 -531,0.827563009 -532,0.825593263 -533,0.823156284 -534,0.819936172 -535,0.815733956 -536,0.813597669 -537,0.809618146 -538,0.804819835 -539,0.798931341 -540,0.791725075 -541,0.783739588 -542,0.777112902 -543,0.774311524 -544,0.775126323 -545,0.777331342 -546,0.779288889 -547,0.780432306 -548,0.780717467 -549,0.780232163 -550,0.77905228 -551,0.777225803 -552,0.774765877 -553,0.771654542 -554,0.767840453 -555,0.76323056 -556,0.757606364 -557,0.750587714 -558,0.742037654 -559,0.731417517 -560,0.717929135 -561,0.700424398 -562,0.677344521 -563,0.64681759 -564,0.607201126 -565,0.55844546 -566,0.504138562 -567,0.452712661 -568,0.415379925 -569,0.397592669 -570,0.394531354 -571,0.398729638 -572,0.404605887 -573,0.40896374 -574,0.410386809 -575,0.408522629 -576,0.400086125 -577,0.393213276 -578,0.3844371 -579,0.374382339 -580,0.363543925 -581,0.352099993 -582,0.339694738 -583,0.325148564 -584,0.306079115 -585,0.278483456 -586,0.236846101 -587,0.176920454 -588,0.104294187 -589,0.041322877 -590,0.009916995 -591,0 -592,0 -593,0 -594,0 -595,0 -596,0 -597,0 -598,0 -599,0 -600,0 -601,0 -602,0 -603,0 -604,0 -605,0 -606,0 -607,0 -608,0 -609,0 -610,0 -611,0 -612,0 -613,0 -614,0 -615,0 -616,0 -617,0 -618,0 -619,0 -620,0 -621,0 -622,0 -623,0 -624,0 -625,0 -626,0 -627,0 -628,0 -629,0 -630,0 -631,0 -632,0 -633,0 -634,0 -635,0 -636,0 -637,0 -638,0 -639,0 -640,0 -641,0 -642,0 -643,0 -644,0 -645,0 -646,0 -647,0 -648,0 -649,0 -650,0 -651,0 -652,0 -653,0 -654,0 -655,0 -656,0 -657,0 -658,0 -659,0 -660,0 -661,0 -662,0 -663,0 -664,0 -665,0 -666,0 -667,0 -668,0 -669,0 -670,0 -671,0 -672,0 -673,0 -674,0 -675,0 -676,0 -677,0 -678,0 -679,0 -680,0 -681,0 -682,0 -683,0 -684,0 -685,0 -686,0 -687,0 -688,0 -689,0 -690,0 -691,0 -692,0 -693,0 -694,0 -695,0 -696,0 -697,0 -698,0 -699,0 -700,0 -701,0 -702,0 -703,0 -704,0 -705,0 -706,0 -707,0 -708,0 -709,0 -710,0 -711,0 -712,0 -713,0 -714,0 -715,0 -716,0 -717,0 -718,0 -719,0 -720,0 -721,0 -722,0 -723,0 -724,0 -725,0 -726,0 -727,0 -728,0 -729,0 -730,0 -731,0 -732,0 -733,0 -734,0 -735,0 -736,0 -737,0 -738,0 -739,0 -740,0 -741,0 -742,0 -743,0 -744,0 -745,0 -746,0 -747,0 -748,0 -749,0 -750,0 -751,0 -752,0 -753,0 -754,0 -755,0 -756,0 -757,0 -758,0 -759,0 -760,0 -761,0 -762,0 -763,0 -764,0 -765,0 -766,0 -767,0 -768,0 -769,0 -770,0 -771,0 -772,0 -773,0 -774,0 -775,0 -776,0 -777,0 -778,0 -779,0 -780,0 -781,0 -782,0 -783,0 -784,0 -785,0 -786,0 -787,0 -788,0 -789,0 -790,0 -791,0 -792,0 -793,0 -794,0 -795,0 -796,0 -797,0 -798,0 -799,0 -800,0 -801,0 -802,0 -803,0 -804,0 -805,0 -806,0 -807,0 -808,0 -809,0 -810,0 -811,0 -812,0 -813,0 -814,0 -815,0 -816,0 -817,0 -818,0 -819,0 -820,0 -821,0 -822,0 -823,0 -824,0 -825,0 -826,0 -827,0 -828,0 -829,0 -830,0 -831,0 -832,0 -833,0 -834,0 -835,0 -836,0 -837,0 -838,0 -839,0 -840,0 -841,0 -842,0 -843,0 -844,0 -845,0 -846,0 -847,0 -848,0 -849,0 -850,0 -851,0 -852,0 -853,0 -854,0 -855,0 -856,0 -857,0 -858,0 -859,0 -860,0 -861,0 -862,0 -863,0 -864,0 -865,0 -866,0 -867,0 -868,0 -869,0 -870,0 -871,0 -872,0 -873,0 -874,0 -875,0 -876,0 -877,0 -878,0 -879,0 -880,0 -881,0 -882,0 -883,0 -884,0 -885,0 -886,0 -887,0 -888,0 -889,0 -890,0 -891,0 -892,0 -893,0 -894,0 -895,0 -896,0 -897,0 -898,0 -899,0 -900,0 -901,0 -902,0 -903,0 -904,0 -905,0 -906,0 -907,0 -908,0 -909,0 -910,0 -911,0 -912,0 -913,0 -914,0 -915,0 -916,0 -917,0 -918,0 -919,0 -920,0 -921,0 -922,0 -923,0 -924,0 -925,0 -926,0 -927,0 -928,0 -929,0 -930,0 -931,0 -932,0 -933,0 -934,0 -935,0 -936,0 -937,0 -938,0 -939,0 -940,0 -941,0 -942,0 -943,0 -944,0 -945,0 -946,0 -947,0 -948,0 -949,0 -950,0 -951,0 -952,0 -953,0 -954,0 -955,0 -956,0 -957,0 -958,0 -959,0 -960,0 -961,0 -962,0 -963,0 -964,0 -965,0 -966,0 -967,0 -968,0 -969,0 -970,0 -971,0 -972,0 -973,0 -974,0 -975,0 -976,0 -977,0 -978,0 -979,0 -980,0 -981,0 -982,0 -983,0 -984,0 -985,0 -986,0 -987,0 -988,0 -989,0 -990,0 -991,0 -992,0 -993,0 -994,0 -995,0 -996,0 -997,0 -998,0 -999,0 -1000,0 -1001,0 -1002,0 -1003,0 -1004,0 -1005,0 -1006,0 -1007,0 -1008,0 -1009,0 -1010,0 -1011,0 -1012,0 -1013,0 -1014,0 -1015,0 -1016,0 -1017,0 -1018,0 -1019,0 -1020,0 -1021,0 -1022,0 -1023,0 -1024,0 -1025,0 -1026,0 -1027,0 -1028,0 -1029,0 -1030,0 -1031,0 -1032,0 -1033,0 -1034,0 -1035,0 -1036,0 -1037,0 -1038,0 -1039,0 -1040,0 -1041,0 -1042,0 -1043,0 -1044,0 -1045,0 -1046,0 -1047,0 -1048,0 -1049,0 -1050,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-NGPS-spectrograph-I.csv b/pygui/ETC/CSV/throughput-NGPS-spectrograph-I.csv deleted file mode 100644 index ee68eb7f..00000000 --- a/pygui/ETC/CSV/throughput-NGPS-spectrograph-I.csv +++ /dev/null @@ -1,754 +0,0 @@ -# Based on MODELS of NGPS spectrograph optics: dichroics, collimator, gratings, camera", -# Does NOT include losses from slit and slicer, -Wavelength (nm),I -300, -301, -302, -303, -304, -305, -306, -307, -308, -309, -310,0 -311,0 -312,0 -313,0 -314,0 -315,0 -316,0 -317,0 -318,0 -319,0 -320,0 -321,0 -322,0 -323,0 -324,0 -325,0 -326,0 -327,0 -328,0 -329,0 -330,0 -331,0 -332,0 -333,0 -334,0 -335,0 -336,0 -337,0 -338,0 -339,0 -340,0 -341,0 -342,0 -343,0 -344,0 -345,0 -346,0 -347,0 -348,0 -349,0 -350,0 -351,0 -352,0 -353,0 -354,0 -355,0 -356,0 -357,0 -358,0 -359,0 -360,0 -361,0 -362,0 -363,0 -364,0 -365,0 -366,0 -367,0 -368,0 -369,0 -370,0 -371,0 -372,0 -373,0 -374,0 -375,0 -376,0 -377,0 -378,0 -379,0 -380,0 -381,0 -382,0 -383,0 -384,0 -385,0 -386,0 -387,0 -388,0 -389,0 -390,0 -391,0 -392,0 -393,0 -394,0 -395,0 -396,0 -397,0 -398,0 -399,0 -400,0 -401,0 -402,0 -403,0 -404,0 -405,0 -406,0 -407,0 -408,0 -409,0 -410,0 -411,0 -412,0 -413,0 -414,0 -415,0 -416,0 -417,0 -418,0 -419,0 -420,0 -421,0 -422,0 -423,0 -424,0 -425,0 -426,0 -427,0 -428,0 -429,0 -430,0 -431,0 -432,0 -433,0 -434,0 -435,0 -436,0 -437,0 -438,0 -439,0 -440,0 -441,0 -442,0 -443,0 -444,0 -445,0 -446,0 -447,0 -448,0 -449,0 -450,0 -451,0 -452,0 -453,0 -454,0 -455,0 -456,0 -457,0 -458,0 -459,0 -460,0 -461,0 -462,0 -463,0 -464,0 -465,0 -466,0 -467,0 -468,0 -469,0 -470,0 -471,0 -472,0 -473,0 -474,0 -475,0 -476,0 -477,0 -478,0 -479,0 -480,0 -481,0 -482,0 -483,0 -484,0 -485,0 -486,0 -487,0 -488,0 -489,0 -490,0 -491,0 -492,0 -493,0 -494,0 -495,0 -496,0 -497,0 -498,0 -499,0 -500,0 -501,0 -502,0 -503,0 -504,0 -505,0 -506,0 -507,0 -508,0 -509,0 -510,0 -511,0 -512,0 -513,0 -514,0 -515,0 -516,0 -517,0 -518,0 -519,0 -520,0 -521,0 -522,0 -523,0 -524,0 -525,0 -526,0 -527,0 -528,0 -529,0 -530,0 -531,0 -532,0 -533,0 -534,0 -535,0 -536,0 -537,0 -538,0 -539,0 -540,0 -541,0 -542,0 -543,0 -544,0 -545,0 -546,0 -547,0 -548,0 -549,0 -550,0 -551,0 -552,0 -553,0 -554,0 -555,0 -556,0 -557,0 -558,0 -559,0 -560,0 -561,0 -562,0 -563,0 -564,0 -565,0 -566,0 -567,0 -568,0 -569,0 -570,0 -571,0 -572,0 -573,0 -574,0 -575,0 -576,0 -577,0 -578,0 -579,0 -580,0 -581,0 -582,0 -583,0 -584,0 -585,0 -586,0 -587,0 -588,0 -589,0 -590,0 -591,0 -592,0 -593,0 -594,0 -595,0 -596,0 -597,0 -598,0 -599,0 -600,0 -601,0 -602,0 -603,0 -604,0 -605,0 -606,0 -607,0 -608,0 -609,0 -610,0 -611,0 -612,0 -613,0 -614,0 -615,0 -616,0 -617,0 -618,0 -619,0 -620,0 -621,0 -622,0 -623,0 -624,0 -625,0 -626,0 -627,0 -628,0 -629,0 -630,0 -631,0 -632,0 -633,0 -634,0 -635,0 -636,0 -637,0 -638,0 -639,0 -640,0 -641,0 -642,0 -643,0 -644,0 -645,0 -646,0 -647,0 -648,0 -649,0 -650,0 -651,0 -652,0 -653,0 -654,0 -655,0 -656,0 -657,0 -658,0 -659,0 -660,0 -661,0 -662,0 -663,0 -664,0 -665,0 -666,0 -667,0 -668,0 -669,0 -670,0 -671,0 -672,0 -673,0 -674,0 -675,0 -676,0 -677,0 -678,0 -679,0 -680,0 -681,0 -682,0 -683,0 -684,0 -685,0 -686,0 -687,0 -688,0 -689,0 -690,0 -691,0 -692,0 -693,0 -694,0 -695,0 -696,0 -697,0 -698,0 -699,0 -700,0 -701,0 -702,0 -703,0 -704,0 -705,0 -706,0 -707,0 -708,0 -709,0 -710,0 -711,0 -712,0 -713,0 -714,0 -715,0 -716,0 -717,0 -718,0 -719,0 -720,0 -721,0 -722,0 -723,0 -724,0 -725,0 -726,0 -727,0 -728,0 -729,0 -730,0 -731,0 -732,0 -733,0 -734,0 -735,0 -736,0 -737,0 -738,0 -739,0 -740,0 -741,0 -742,0 -743,0 -744,0 -745,0 -746,0 -747,0 -748,0 -749,0 -750,0 -751,0 -752,0 -753,0 -754,0 -755,0 -756,0.050468073 -757,0.063506348 -758,0.080619711 -759,0.102907314 -760,0.130455731 -761,0.166013312 -762,0.209331663 -763,0.252800325 -764,0.284709087 -765,0.309308345 -766,0.325384596 -767,0.332523327 -768,0.332834849 -769,0.329210739 -770,0.320418218 -771,0.316097595 -772,0.312964879 -773,0.311471324 -774,0.311742896 -775,0.313707332 -776,0.317172745 -777,0.321877617 -778,0.327521957 -779,0.333795694 -780,0.34040778 -781,0.347119143 -782,0.35377905 -783,0.360365287 -784,0.367027829 -785,0.374142437 -786,0.382385448 -787,0.392848796 -788,0.407211111 -789,0.427944222 -790,0.458379747 -791,0.502047342 -792,0.560076121 -793,0.626399995 -794,0.685902196 -795,0.723606565 -796,0.736979411 -797,0.734499182 -798,0.725983857 -799,0.717606279 -800,0.71206589 -801,0.709937753 -802,0.71105047 -803,0.714709291 -804,0.72005544 -805,0.726192785 -806,0.732277506 -807,0.737601416 -808,0.741664795 -809,0.744221439 -810,0.745278971 -811,0.745055995 -812,0.743908807 -813,0.742249766 -814,0.740477415 -815,0.738927219 -816,0.737844114 -817,0.737374885 -818,0.737572109 -819,0.738407282 -820,0.739786949 -821,0.741572555 -822,0.743599864 -823,0.745699355 -824,0.747713947 -825,0.749514243 -826,0.751009309 -827,0.752152414 -828,0.752941119 -829,0.753412297 -830,0.75363455 -831,0.753697693 -832,0.753702005 -833,0.753748649 -834,0.75393093 -835,0.754327721 -836,0.754998605 -837,0.75598049 -838,0.757286043 -839,0.758903119 -840,0.760796129 -841,0.762907681 -842,0.765162736 -843,0.767473382 -844,0.769744539 -845,0.771881325 -846,0.773795631 -847,0.77541325 -848,0.776679123 -849,0.777561691 -850,0.778054391 -851,0.77817545 -852,0.777965425 -853,0.77748277 -854,0.776799891 -855,0.775996663 -856,0.775154903 -857,0.774354579 -858,0.773669217 -859,0.773163431 -860,0.772890076 -861,0.772888878 -862,0.773185326 -863,0.773789949 -864,0.774698244 -865,0.775891002 -866,0.777334964 -867,0.778984509 -868,0.780783607 -869,0.782668473 -870,0.78457058 -871,0.786419783 -872,0.788148199 -873,0.789693166 -874,0.791000856 -875,0.792028271 -876,0.792743796 -877,0.793133639 -878,0.793196812 -879,0.792945892 -880,0.792405664 -881,0.791611306 -882,0.79060546 -883,0.789436191 -884,0.7881533 -885,0.786807 -886,0.785444761 -887,0.784109853 -888,0.782840034 -889,0.781666371 -890,0.780674592 -891,0.779824575 -892,0.779118704 -893,0.778558221 -894,0.778137417 -895,0.777844606 -896,0.777663239 -897,0.777572925 -898,0.777550113 -899,0.777569904 -900,0.777606758 -901,0.777635906 -902,0.77763439 -903,0.77758227 -904,0.777463283 -905,0.777265391 -906,0.776981609 -907,0.776609963 -908,0.776153663 -909,0.775620309 -910,0.775021938 -911,0.774374022 -912,0.773694749 -913,0.773003871 -914,0.77232161 -915,0.771668295 -916,0.771062742 -917,0.770522084 -918,0.770060614 -919,0.769689464 -920,0.769416403 -921,0.769245155 -922,0.769175748 -923,0.769204232 -924,0.769322945 -925,0.769520916 -926,0.769783502 -927,0.770094014 -928,0.770433263 -929,0.770780796 -930,0.77111505 -931,0.771414445 -932,0.771657786 -933,0.771825739 -934,0.771900904 -935,0.771869197 -936,0.771719973 -937,0.771446893 -938,0.771048086 -939,0.770526039 -940,0.769887497 -941,0.769142708 -942,0.768304281 -943,0.76738648 -944,0.766403921 -945,0.765370681 -946,0.764299727 -947,0.76320285 -948,0.762090321 -949,0.760971684 -950,0.75985595 -951,0.758752044 -952,0.757669395 -953,0.756618286 -954,0.755610002 -955,0.754657271 -956,0.753774532 -957,0.752977148 -958,0.752280922 -959,0.75169983 -960,0.751244173 -961,0.750917828 -962,0.750715965 -963,0.750624129 -964,0.75061877 -965,0.750669068 -966,0.750740148 -967,0.750795461 -968,0.750799827 -969,0.750721316 -970,0.750532068 -971,0.750209024 -972,0.749734171 -973,0.749094514 -974,0.748281648 -975,0.747291968 -976,0.74612584 -977,0.744787904 -978,0.743286242 -979,0.74163223 -980,0.739840228 -981,0.7379268 -982,0.735910427 -983,0.733810939 -984,0.731649147 -985,0.729446257 -986,0.727223394 -987,0.725001525 -988,0.722800371 -989,0.720638472 -990,0.71853209 -991,0.716495383 -992,0.714539679 -993,0.712674271 -994,0.710905902 -995,0.709239459 -996,0.707677484 -997,0.706220467 -998,0.70486665 -999,0.703612459 -1000,0.702452133 -1001,0.701366411 -1002,0.700352472 -1003,0.699400549 -1004,0.698499843 -1005,0.697638661 -1006,0.696804571 -1007,0.695984776 -1008,0.695166249 -1009,0.694336068 -1010,0.693481723 -1011,0.692591184 -1012,0.691653497 -1013,0.690658424 -1014,0.6895974 -1015,0.688463271 -1016,0.687249631 -1017,0.685951933 -1018,0.684566891 -1019,0.683092572 -1020,0.681528201 -1021,0.679874093 -1022,0.678131211 -1023,0.676301217 -1024,0.674386255 -1025,0.672388711 -1026,0.670311202 -1027,0.668156072 -1028,0.665925645 -1029,0.663621744 -1030,0.661245722 -1031,0.658798019 -1032,0.656278286 -1033,0.653685021 -1034,0.651015324 -1035,0.648264978 -1036,0.64542821 -1037,0.642497332 -1038,0.639463039 -1039,0.636314026 -1040,0.633037197 -1041,0 -1042,0 -1043,0 -1044,0 -1045,0 -1046,0 -1047,0 -1048,0 -1049,0 -1050,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-NGPS-spectrograph-R.csv b/pygui/ETC/CSV/throughput-NGPS-spectrograph-R.csv deleted file mode 100644 index 5bf4e577..00000000 --- a/pygui/ETC/CSV/throughput-NGPS-spectrograph-R.csv +++ /dev/null @@ -1,754 +0,0 @@ -# Based on MODELS of NGPS spectrograph optics: dichroics, collimator, gratings, camera", -# Does NOT include losses from slit and slicer, -Wavelength (nm),R -300, -301, -302, -303, -304, -305, -306, -307, -308, -309, -310,0 -311,0 -312,0 -313,0 -314,0 -315,0 -316,0 -317,0 -318,0 -319,0 -320,0 -321,0 -322,0 -323,0 -324,0 -325,0 -326,0 -327,0 -328,0 -329,0 -330,0 -331,0 -332,0 -333,0 -334,0 -335,0 -336,0 -337,0 -338,0 -339,0 -340,0 -341,0 -342,0 -343,0 -344,0 -345,0 -346,0 -347,0 -348,0 -349,0 -350,0 -351,0 -352,0 -353,0 -354,0 -355,0 -356,0 -357,0 -358,0 -359,0 -360,0 -361,0 -362,0 -363,0 -364,0 -365,0 -366,0 -367,0 -368,0 -369,0 -370,0 -371,0 -372,0 -373,0 -374,0 -375,0 -376,0 -377,0 -378,0 -379,0 -380,0 -381,0 -382,0 -383,0 -384,0 -385,0 -386,0 -387,0 -388,0 -389,0 -390,0 -391,0 -392,0 -393,0 -394,0 -395,0 -396,0 -397,0 -398,0 -399,0 -400,0 -401,0 -402,0 -403,0 -404,0 -405,0 -406,0 -407,0 -408,0 -409,0 -410,0 -411,0 -412,0 -413,0 -414,0 -415,0 -416,0 -417,0 -418,0 -419,0 -420,0 -421,0 -422,0 -423,0 -424,0 -425,0 -426,0 -427,0 -428,0 -429,0 -430,0 -431,0 -432,0 -433,0 -434,0 -435,0 -436,0 -437,0 -438,0 -439,0 -440,0 -441,0 -442,0 -443,0 -444,0 -445,0 -446,0 -447,0 -448,0 -449,0 -450,0 -451,0 -452,0 -453,0 -454,0 -455,0 -456,0 -457,0 -458,0 -459,0 -460,0 -461,0 -462,0 -463,0 -464,0 -465,0 -466,0 -467,0 -468,0 -469,0 -470,0 -471,0 -472,0 -473,0 -474,0 -475,0 -476,0 -477,0 -478,0 -479,0 -480,0 -481,0 -482,0 -483,0 -484,0 -485,0 -486,0 -487,0 -488,0 -489,0 -490,0 -491,0 -492,0 -493,0 -494,0 -495,0 -496,0 -497,0 -498,0 -499,0 -500,0 -501,0 -502,0 -503,0 -504,0 -505,0 -506,0 -507,0 -508,0 -509,0 -510,0 -511,0 -512,0 -513,0 -514,0 -515,0 -516,0 -517,0 -518,0 -519,0 -520,0 -521,0 -522,0 -523,0 -524,0 -525,0 -526,0 -527,0 -528,0 -529,0 -530,0 -531,0 -532,0 -533,0 -534,0 -535,0 -536,0 -537,0 -538,0 -539,0 -540,0 -541,0 -542,0 -543,0 -544,0 -545,0 -546,0 -547,0 -548,0 -549,0 -550,0 -551,0 -552,0 -553,0 -554,0 -555,0 -556,0 -557,0 -558,0 -559,0 -560,0 -561,0.054869555 -562,0.073187651 -563,0.098435761 -564,0.131863236 -565,0.173852273 -566,0.221425687 -567,0.267096803 -568,0.300623795 -569,0.316747367 -570,0.319530849 -571,0.315607769 -572,0.310034069 -573,0.305760216 -574,0.304130661 -575,0.305510932 -576,0.307042336 -577,0.314036862 -578,0.322876628 -579,0.333030337 -580,0.344072164 -581,0.355777383 -582,0.368445431 -583,0.383273044 -584,0.402734175 -585,0.431035891 -586,0.474067804 -587,0.536552853 -588,0.61314072 -589,0.68050864 -590,0.71519604 -591,0.719024016 -592,0.70913566 -593,0.698517438 -594,0.692445334 -595,0.691803108 -596,0.69567663 -597,0.702517622 -598,0.710621321 -599,0.718392677 -600,0.724508202 -601,0.72700992 -602,0.711753369 -603,0.730718447 -604,0.730496691 -605,0.729530113 -606,0.728791641 -607,0.728734201 -608,0.729530954 -609,0.731131759 -610,0.733322983 -611,0.735805767 -612,0.738244828 -613,0.740347553 -614,0.741916821 -615,0.74287275 -616,0.743253109 -617,0.74319318 -618,0.742893205 -619,0.74258211 -620,0.742484892 -621,0.742796602 -622,0.743665643 -623,0.74517904 -624,0.747353576 -625,0.750139955 -626,0.753424584 -627,0.757039862 -628,0.760780776 -629,0.764427585 -630,0.767771111 -631,0.770638079 -632,0.772910966 -633,0.774539897 -634,0.775543676 -635,0.776000488 -636,0.77603269 -637,0.775787236 -638,0.775417118 -639,0.775065223 -640,0.774852757 -641,0.774871534 -642,0.775180076 -643,0.775806002 -644,0.776737091 -645,0.777932616 -646,0.779327722 -647,0.780839001 -648,0.782372626 -649,0.783834184 -650,0.785138395 -651,0.786217928 -652,0.787032128 -653,0.787569466 -654,0.787848464 -655,0.787914588 -656,0.787834176 -657,0.787686408 -658,0.787555034 -659,0.787520183 -660,0.787651914 -661,0.788005337 -662,0.788617415 -663,0.789505126 -664,0.790664928 -665,0.792072809 -666,0.793685014 -667,0.795442933 -668,0.797292396 -669,0.799207955 -670,0.801137668 -671,0.802964633 -672,0.804587292 -673,0.805941963 -674,0.806988588 -675,0.807706546 -676,0.808095727 -677,0.80817518 -678,0.807983078 -679,0.807572813 -680,0.807008286 -681,0.8063587 -682,0.805692948 -683,0.805074751 -684,0.804558802 -685,0.804187342 -686,0.803988546 -687,0.803975302 -688,0.804144894 -689,0.804479685 -690,0.804947691 -691,0.805502945 -692,0.806086513 -693,0.80664397 -694,0.807079461 -695,0.807292381 -696,0.807137643 -697,0.806354698 -698,0.80434343 -699,0.799639085 -700,0.792309043 -701,0.793136046 -702,0.797025548 -703,0.798230671 -704,0.797974924 -705,0.797150282 -706,0.796150109 -707,0.795149648 -708,0.794226881 -709,0.793416565 -710,0.792736954 -711,0.792202471 -712,0.791826176 -713,0.791618125 -714,0.791581729 -715,0.791710813 -716,0.791986999 -717,0.792377507 -718,0.792832264 -719,0.793272365 -720,0.793460969 -721,0.79321909 -722,0.793616232 -723,0.792893193 -724,0.7911783 -725,0.787919523 -726,0.78221161 -727,0.772979537 -728,0.760606353 -729,0.750682924 -730,0.750498096 -731,0.757164494 -732,0.763817379 -733,0.768011472 -734,0.769927978 -735,0.770215741 -736,0.769407072 -737,0.767871948 -738,0.765863895 -739,0.763560371 -740,0.761088841 -741,0.758541275 -742,0.755982761 -743,0.75345572 -744,0.750982148 -745,0.748564415 -746,0.746184561 -747,0.743803368 -748,0.741357375 -749,0.738755187 -750,0.73587164 -751,0.732539742 -752,0.728537858 -753,0.723574151 -754,0.717264713 -755,0.709105495 -756,0.698440553 -757,0.684434218 -758,0.66606639 -759,0.642185231 -760,0.611643923 -761,0.573431768 -762,0.527087543 -763,0.480747677 -764,0.446707545 -765,0.420346128 -766,0.402816408 -767,0.394424317 -768,0.39287245 -769,0.395111864 -770,0.394014137 -771,0.397566062 -772,0.399743087 -773,0.400141639 -774,0.398691881 -775,0.3955222 -776,0.390874428 -777,0.385054574 -778,0.378396694 -779,0.371235059 -780,0.363875717 -781,0.356565335 -782,0.349457292 -783,0.34257617 -784,0.335779274 -785,0.328710511 -786,0.32073557 -787,0.310842142 -788,0.297490805 -789,0.278438306 -790,0.250695534 -791,0.211159876 -792,0.158967825 -793,0.099797214 -794,0.047364617 -795,0 -796,0 -797,0 -798,0 -799,0 -800,0 -801,0 -802,0 -803,0 -804,0 -805,0 -806,0 -807,0 -808,0 -809,0 -810,0 -811,0 -812,0 -813,0 -814,0 -815,0 -816,0 -817,0 -818,0 -819,0 -820,0 -821,0 -822,0 -823,0 -824,0 -825,0 -826,0 -827,0 -828,0 -829,0 -830,0 -831,0 -832,0 -833,0 -834,0 -835,0 -836,0 -837,0 -838,0 -839,0 -840,0 -841,0 -842,0 -843,0 -844,0 -845,0 -846,0 -847,0 -848,0 -849,0 -850,0 -851,0 -852,0 -853,0 -854,0 -855,0 -856,0 -857,0 -858,0 -859,0 -860,0 -861,0 -862,0 -863,0 -864,0 -865,0 -866,0 -867,0 -868,0 -869,0 -870,0 -871,0 -872,0 -873,0 -874,0 -875,0 -876,0 -877,0 -878,0 -879,0 -880,0 -881,0 -882,0 -883,0 -884,0 -885,0 -886,0 -887,0 -888,0 -889,0 -890,0 -891,0 -892,0 -893,0 -894,0 -895,0 -896,0 -897,0 -898,0 -899,0 -900,0 -901,0 -902,0 -903,0 -904,0 -905,0 -906,0 -907,0 -908,0 -909,0 -910,0 -911,0 -912,0 -913,0 -914,0 -915,0 -916,0 -917,0 -918,0 -919,0 -920,0 -921,0 -922,0 -923,0 -924,0 -925,0 -926,0 -927,0 -928,0 -929,0 -930,0 -931,0 -932,0 -933,0 -934,0 -935,0 -936,0 -937,0 -938,0 -939,0 -940,0 -941,0 -942,0 -943,0 -944,0 -945,0 -946,0 -947,0 -948,0 -949,0 -950,0 -951,0 -952,0 -953,0 -954,0 -955,0 -956,0 -957,0 -958,0 -959,0 -960,0 -961,0 -962,0 -963,0 -964,0 -965,0 -966,0 -967,0 -968,0 -969,0 -970,0 -971,0 -972,0 -973,0 -974,0 -975,0 -976,0 -977,0 -978,0 -979,0 -980,0 -981,0 -982,0 -983,0 -984,0 -985,0 -986,0 -987,0 -988,0 -989,0 -990,0 -991,0 -992,0 -993,0 -994,0 -995,0 -996,0 -997,0 -998,0 -999,0 -1000,0 -1001,0 -1002,0 -1003,0 -1004,0 -1005,0 -1006,0 -1007,0 -1008,0 -1009,0 -1010,0 -1011,0 -1012,0 -1013,0 -1014,0 -1015,0 -1016,0 -1017,0 -1018,0 -1019,0 -1020,0 -1021,0 -1022,0 -1023,0 -1024,0 -1025,0 -1026,0 -1027,0 -1028,0 -1029,0 -1030,0 -1031,0 -1032,0 -1033,0 -1034,0 -1035,0 -1036,0 -1037,0 -1038,0 -1039,0 -1040,0 -1041,0 -1042,0 -1043,0 -1044,0 -1045,0 -1046,0 -1047,0 -1048,0 -1049,0 -1050,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-NGPS-spectrograph-U.csv b/pygui/ETC/CSV/throughput-NGPS-spectrograph-U.csv deleted file mode 100644 index fc87e4c2..00000000 --- a/pygui/ETC/CSV/throughput-NGPS-spectrograph-U.csv +++ /dev/null @@ -1,754 +0,0 @@ -# Based on MODELS of NGPS spectrograph optics: dichroics, collimator, gratings, camera", -# Does NOT include losses from slit and slicer, -Wavelength (nm),U -300, -301, -302, -303, -304, -305, -306, -307, -308, -309, -310,0.519132624 -311,0.529638678 -312,0.539757368 -313,0.549466376 -314,0.558712167 -315,0.567419935 -316,0.575612098 -317,0.583811295 -318,0.592839741 -319,0.602231286 -320,0.611114796 -321,0.619363874 -322,0.627110846 -323,0.634465454 -324,0.641492763 -325,0.648230905 -326,0.654711579 -327,0.66094198 -328,0.66693042 -329,0.672685827 -330,0.678211755 -331,0.683491661 -332,0.688504974 -333,0.693205057 -334,0.697513038 -335,0.701386898 -336,0.705163703 -337,0.70952668 -338,0.714139265 -339,0.718295394 -340,0.721780499 -341,0.724287466 -342,0.72513601 -343,0.726145413 -344,0.723913879 -345,0.7219103 -346,0.730090978 -347,0.742928761 -348,0.75144413 -349,0.757009696 -350,0.761762958 -351,0.763770174 -352,0.765417864 -353,0.766542635 -354,0.767312304 -355,0.766508884 -356,0.765023305 -357,0.768449513 -358,0.772784199 -359,0.775390574 -360,0.777040345 -361,0.778210443 -362,0.779092837 -363,0.779771095 -364,0.780283424 -365,0.780643838 -366,0.780828128 -367,0.780686819 -368,0.78076059 -369,0.781002041 -370,0.780980592 -371,0.780834818 -372,0.780607932 -373,0.780316508 -374,0.779970065 -375,0.779574004 -376,0.779144785 -377,0.778646362 -378,0.778027765 -379,0.776868601 -380,0.773717602 -381,0.775480711 -382,0.77585449 -383,0.775577351 -384,0.775147852 -385,0.774658249 -386,0.774126646 -387,0.773543862 -388,0.772866535 -389,0.771862981 -390,0.770024743 -391,0.761488358 -392,0.767304742 -393,0.769462095 -394,0.769586582 -395,0.769446712 -396,0.769295239 -397,0.769164157 -398,0.767937612 -399,0.769002003 -400,0.768964016 -401,0.768907847 -402,0.768820988 -403,0.768695668 -404,0.768522324 -405,0.768272665 -406,0.767914856 -407,0.767477945 -408,0.766942804 -409,0.766282494 -410,0.765915259 -411,0.765588417 -412,0.764867879 -413,0.763529396 -414,0.761132231 -415,0.756756767 -416,0.748392482 -417,0.731615124 -418,0.697735009 -419,0.63717369 -420,0.562296797 -421,0.511538898 -422,0.490444968 -423,0.477253089 -424,0.459664382 -425,0.436464108 -426,0.411825836 -427,0.391477697 -428,0.379125103 -429,0.374368888 -430,0.373712665 -431,0.372945749 -432,0.368156999 -433,0.353606529 -434,0.310996376 -435,0.193756074 -436,0.029671637 -437,0 -438,0 -439,0 -440,0 -441,0 -442,0 -443,0 -444,0 -445,0 -446,0 -447,0 -448,0 -449,0 -450,0 -451,0 -452,0 -453,0 -454,0 -455,0 -456,0 -457,0 -458,0 -459,0 -460,0 -461,0 -462,0 -463,0 -464,0 -465,0 -466,0 -467,0 -468,0 -469,0 -470,0 -471,0 -472,0 -473,0 -474,0 -475,0 -476,0 -477,0 -478,0 -479,0 -480,0 -481,0 -482,0 -483,0 -484,0 -485,0 -486,0 -487,0 -488,0 -489,0 -490,0 -491,0 -492,0 -493,0 -494,0 -495,0 -496,0 -497,0 -498,0 -499,0 -500,0 -501,0 -502,0 -503,0 -504,0 -505,0 -506,0 -507,0 -508,0 -509,0 -510,0 -511,0 -512,0 -513,0 -514,0 -515,0 -516,0 -517,0 -518,0 -519,0 -520,0 -521,0 -522,0 -523,0 -524,0 -525,0 -526,0 -527,0 -528,0 -529,0 -530,0 -531,0 -532,0 -533,0 -534,0 -535,0 -536,0 -537,0 -538,0 -539,0 -540,0 -541,0 -542,0 -543,0 -544,0 -545,0 -546,0 -547,0 -548,0 -549,0 -550,0 -551,0 -552,0 -553,0 -554,0 -555,0 -556,0 -557,0 -558,0 -559,0 -560,0 -561,0 -562,0 -563,0 -564,0 -565,0 -566,0 -567,0 -568,0 -569,0 -570,0 -571,0 -572,0 -573,0 -574,0 -575,0 -576,0 -577,0 -578,0 -579,0 -580,0 -581,0 -582,0 -583,0 -584,0 -585,0 -586,0 -587,0 -588,0 -589,0 -590,0 -591,0 -592,0 -593,0 -594,0 -595,0 -596,0 -597,0 -598,0 -599,0 -600,0 -601,0 -602,0 -603,0 -604,0 -605,0 -606,0 -607,0 -608,0 -609,0 -610,0 -611,0 -612,0 -613,0 -614,0 -615,0 -616,0 -617,0 -618,0 -619,0 -620,0 -621,0 -622,0 -623,0 -624,0 -625,0 -626,0 -627,0 -628,0 -629,0 -630,0 -631,0 -632,0 -633,0 -634,0 -635,0 -636,0 -637,0 -638,0 -639,0 -640,0 -641,0 -642,0 -643,0 -644,0 -645,0 -646,0 -647,0 -648,0 -649,0 -650,0 -651,0 -652,0 -653,0 -654,0 -655,0 -656,0 -657,0 -658,0 -659,0 -660,0 -661,0 -662,0 -663,0 -664,0 -665,0 -666,0 -667,0 -668,0 -669,0 -670,0 -671,0 -672,0 -673,0 -674,0 -675,0 -676,0 -677,0 -678,0 -679,0 -680,0 -681,0 -682,0 -683,0 -684,0 -685,0 -686,0 -687,0 -688,0 -689,0 -690,0 -691,0 -692,0 -693,0 -694,0 -695,0 -696,0 -697,0 -698,0 -699,0 -700,0 -701,0 -702,0 -703,0 -704,0 -705,0 -706,0 -707,0 -708,0 -709,0 -710,0 -711,0 -712,0 -713,0 -714,0 -715,0 -716,0 -717,0 -718,0 -719,0 -720,0 -721,0 -722,0 -723,0 -724,0 -725,0 -726,0 -727,0 -728,0 -729,0 -730,0 -731,0 -732,0 -733,0 -734,0 -735,0 -736,0 -737,0 -738,0 -739,0 -740,0 -741,0 -742,0 -743,0 -744,0 -745,0 -746,0 -747,0 -748,0 -749,0 -750,0 -751,0 -752,0 -753,0 -754,0 -755,0 -756,0 -757,0 -758,0 -759,0 -760,0 -761,0 -762,0 -763,0 -764,0 -765,0 -766,0 -767,0 -768,0 -769,0 -770,0 -771,0 -772,0 -773,0 -774,0 -775,0 -776,0 -777,0 -778,0 -779,0 -780,0 -781,0 -782,0 -783,0 -784,0 -785,0 -786,0 -787,0 -788,0 -789,0 -790,0 -791,0 -792,0 -793,0 -794,0 -795,0 -796,0 -797,0 -798,0 -799,0 -800,0 -801,0 -802,0 -803,0 -804,0 -805,0 -806,0 -807,0 -808,0 -809,0 -810,0 -811,0 -812,0 -813,0 -814,0 -815,0 -816,0 -817,0 -818,0 -819,0 -820,0 -821,0 -822,0 -823,0 -824,0 -825,0 -826,0 -827,0 -828,0 -829,0 -830,0 -831,0 -832,0 -833,0 -834,0 -835,0 -836,0 -837,0 -838,0 -839,0 -840,0 -841,0 -842,0 -843,0 -844,0 -845,0 -846,0 -847,0 -848,0 -849,0 -850,0 -851,0 -852,0 -853,0 -854,0 -855,0 -856,0 -857,0 -858,0 -859,0 -860,0 -861,0 -862,0 -863,0 -864,0 -865,0 -866,0 -867,0 -868,0 -869,0 -870,0 -871,0 -872,0 -873,0 -874,0 -875,0 -876,0 -877,0 -878,0 -879,0 -880,0 -881,0 -882,0 -883,0 -884,0 -885,0 -886,0 -887,0 -888,0 -889,0 -890,0 -891,0 -892,0 -893,0 -894,0 -895,0 -896,0 -897,0 -898,0 -899,0 -900,0 -901,0 -902,0 -903,0 -904,0 -905,0 -906,0 -907,0 -908,0 -909,0 -910,0 -911,0 -912,0 -913,0 -914,0 -915,0 -916,0 -917,0 -918,0 -919,0 -920,0 -921,0 -922,0 -923,0 -924,0 -925,0 -926,0 -927,0 -928,0 -929,0 -930,0 -931,0 -932,0 -933,0 -934,0 -935,0 -936,0 -937,0 -938,0 -939,0 -940,0 -941,0 -942,0 -943,0 -944,0 -945,0 -946,0 -947,0 -948,0 -949,0 -950,0 -951,0 -952,0 -953,0 -954,0 -955,0 -956,0 -957,0 -958,0 -959,0 -960,0 -961,0 -962,0 -963,0 -964,0 -965,0 -966,0 -967,0 -968,0 -969,0 -970,0 -971,0 -972,0 -973,0 -974,0 -975,0 -976,0 -977,0 -978,0 -979,0 -980,0 -981,0 -982,0 -983,0 -984,0 -985,0 -986,0 -987,0 -988,0 -989,0 -990,0 -991,0 -992,0 -993,0 -994,0 -995,0 -996,0 -997,0 -998,0 -999,0 -1000,0 -1001,0 -1002,0 -1003,0 -1004,0 -1005,0 -1006,0 -1007,0 -1008,0 -1009,0 -1010,0 -1011,0 -1012,0 -1013,0 -1014,0 -1015,0 -1016,0 -1017,0 -1018,0 -1019,0 -1020,0 -1021,0 -1022,0 -1023,0 -1024,0 -1025,0 -1026,0 -1027,0 -1028,0 -1029,0 -1030,0 -1031,0 -1032,0 -1033,0 -1034,0 -1035,0 -1036,0 -1037,0 -1038,0 -1039,0 -1040,0 -1041,0 -1042,0 -1043,0 -1044,0 -1045,0 -1046,0 -1047,0 -1048,0 -1049,0 -1050,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-spectrograph-20260206-G.csv b/pygui/ETC/CSV/throughput-spectrograph-20260206-G.csv new file mode 100644 index 00000000..dfdd073f --- /dev/null +++ b/pygui/ETC/CSV/throughput-spectrograph-20260206-G.csv @@ -0,0 +1,55 @@ +x, y +300,0 +413.63617,0 +417.02702,0.01853 +418.75926,0.0455 +421.10332,0.0873 +423.41945,0.12623 +426.10031,0.13749 +429.00119,0.13959 +431.20986,0.16348 +432.96579,0.19615 +433.84164,0.23671 +435.17445,0.28517 +436.55465,0.32293 +437.59467,0.35809 +442.12201,0.372 +445.69395,0.37904 +449.7584,0.40021 +453.03755,0.41357 +456.88451,0.43651 +461.61833,0.45174 +466.91066,0.46323 +474.14594,0.47773 +482.29176,0.50179 +489.13016,0.51187 +493.03129,0.52199 +501.19403,0.5466 +506.33996,0.55401 +512.43198,0.55773 +521.14901,0.5644 +528.00264,0.57386 +535.36655,0.57918 +543.74593,0.57192 +551.29346,0.58064 +557.17477,0.58704 +561.14698,0.57084 +563.53505,0.53981 +565.22328,0.49957 +566.92505,0.44182 +568.67336,0.37819 +569.74808,0.33999 +571.50655,0.32166 +575.06749,0.3245 +578.31278,0.34226 +580.89294,0.3404 +582.84774,0.32348 +584.74583,0.29047 +586.68794,0.2294 +588.23231,0.15912 +589.42126,0.10012 +591.94388,0.02933 +594.91246,0.00922 +596.35613,0 +598.46409,0 +1200,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-spectrograph-20260206-I.csv b/pygui/ETC/CSV/throughput-spectrograph-20260206-I.csv new file mode 100644 index 00000000..b311c2eb --- /dev/null +++ b/pygui/ETC/CSV/throughput-spectrograph-20260206-I.csv @@ -0,0 +1,48 @@ +x, y +300,0 +743.96007,0 +748.50343,0.00141 +750.82587,0.01305 +753.60862,0.03459 +758.27158,0.09927 +761.85676,0.13199 +766.03993,0.2039 +768.93811,0.24398 +774.26582,0.2909 +777.34479,0.30952 +779.92451,0.30405 +782.5821,0.33607 +785.22996,0.38649 +788.77342,0.50852 +789.58001,0.54617 +792.99553,0.5967 +797.34558,0.62497 +806.28767,0.64793 +812.27178,0.64454 +815.42724,0.6342 +826.72653,0.64952 +841.53313,0.68651 +858.91666,0.70297 +869.15069,0.70823 +886.77481,0.71827 +892.09556,0.71167 +895.48743,0.66583 +904.96354,0.6761 +907.11215,0.64376 +918.8801,0.64672 +927.0448,0.61031 +929.84981,0.53621 +933.83967,0.43849 +951.18148,0.4718 +970.09337,0.45913 +973.49776,0.4145 +984.4925,0.39414 +996.50661,0.29391 +1004.6268,0.23916 +1016.5074,0.14171 +1030.11106,0.04339 +1034.97844,0.02443 +1039.40638,0.03266 +1042.31568,0 +1045.57405,0 +1200,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-spectrograph-20260206-R.csv b/pygui/ETC/CSV/throughput-spectrograph-20260206-R.csv new file mode 100644 index 00000000..05b5d06e --- /dev/null +++ b/pygui/ETC/CSV/throughput-spectrograph-20260206-R.csv @@ -0,0 +1,69 @@ +x, y +300,0 +556.02526,0 +561.20948,0 +562.12304,0.02507 +564.05189,0.04369 +566.55386,0.09023 +569.94621,0.17965 +571.56584,0.20072 +573.04547,0.20827 +577.28994,0.1915 +580.24214,0.19651 +582.28078,0.21651 +584.21768,0.23783 +586.8385,0.29531 +587.87293,0.34286 +589.82394,0.42604 +590.92585,0.47077 +592.90002,0.51224 +594.80973,0.50602 +596.87859,0.50647 +601.05356,0.5262 +605.30509,0.55527 +610.22743,0.56811 +618.19262,0.58154 +624.32767,0.59446 +628.412,0.59126 +636.32481,0.62104 +641.42946,0.63488 +644.98801,0.6385 +648.11648,0.63213 +653.17983,0.63857 +656.15519,0.63176 +658.7881,0.65231 +664.89092,0.66015 +675.14756,0.67384 +676.51034,0.66647 +682.50237,0.68003 +689.30521,0.65158 +694.4703,0.68296 +700.5147,0.68293 +705.10465,0.69213 +709.55057,0.69851 +714.81739,0.69881 +718.523,0.65766 +727.89126,0.69024 +730.21696,0.71233 +737.54457,0.74129 +743.37241,0.74827 +751.01831,0.73037 +754.64032,0.68514 +757.07983,0.61401 +759.12451,0.57322 +762.92681,0.36098 +765.66044,0.38994 +768.82718,0.38693 +773.17137,0.34869 +776.97569,0.33466 +780.59165,0.32934 +783.22254,0.30362 +785.52104,0.24661 +788.00588,0.16181 +790.27719,0.08514 +791.34788,0.06108 +793.25356,0.04401 +795.86128,0.03774 +796.53915,0 +798.45491,0 +1200,0 \ No newline at end of file diff --git a/pygui/ETC/CSV/throughput-spectrograph-20260206-U.csv b/pygui/ETC/CSV/throughput-spectrograph-20260206-U.csv new file mode 100644 index 00000000..5dfd939d --- /dev/null +++ b/pygui/ETC/CSV/throughput-spectrograph-20260206-U.csv @@ -0,0 +1,63 @@ +x, y +300.01047,0 +304.64481,0 +304.91886,0.04319 +305.53241,0.0641 +306.37973,0.07051 +307.58309,0.09834 +309.55464,0.09967 +310.61703,0.11366 +312.8504,0.19044 +313.78187,0.1888 +314.19258,0.21649 +315.40242,0.22051 +316.81365,0.24149 +320.08136,0.24344 +321.36672,0.27661 +322.31833,0.27868 +324.5402,0.31346 +332.59689,0.36195 +333.79018,0.36106 +337.06364,0.38122 +339.93215,0.41083 +341.55341,0.41059 +348.01546,0.44959 +352.02043,0.4681 +355.19319,0.47412 +362.03214,0.49747 +366.42696,0.5114 +367.5735,0.50272 +370.17227,0.50645 +374.86487,0.5427 +376.64438,0.5088 +378.59436,0.55817 +383.85376,0.55221 +385.30096,0.56396 +386.88698,0.59627 +392.22047,0.62934 +394.27546,0.59929 +397.19575,0.63202 +399.03928,0.62837 +403.86279,0.62908 +409.94002,0.61076 +415.10232,0.61122 +417.04798,0.58946 +418.32543,0.55438 +420.18765,0.49129 +421.77439,0.42416 +422.99789,0.39016 +424.57816,0.36587 +427.89622,0.37487 +430.2605,0.36293 +431.62426,0.32918 +432.96716,0.2737 +433.25847,0.24229 +434.86895,0.15484 +435.62636,0.1126 +436.09461,0.08549 +436.84698,0.0532 +437.82161,0.02395 +439.98809,0.00629 +443.33636,0 +459.94102,0 +1200,0 \ No newline at end of file diff --git a/pygui/ETC/ETC_arguments.py b/pygui/ETC/ETC_arguments.py index 0b6e2387..f775ef8c 100644 --- a/pygui/ETC/ETC_arguments.py +++ b/pygui/ETC/ETC_arguments.py @@ -71,8 +71,8 @@ def slitfloat(value): # require slit in slit_w_range help = 'Only use flux from the center slit, not side slices' parser.add_argument('-noslicer', action='store_true', help=help) -help = 'Assume astronomer only uses 2 brightest pixels in center slice for SNR' -parser.add_argument('-fastSNR', action='store_true', help=help) +help = 'Assume astronomer computes SNR by summing 2N brightest pixels of profile in center slice (symmetric profile, N on 1 side of peak)' +parser.add_argument('-fastSNR', type=int, default=None, help=help) help = 'Plot SNR vs. wavelength for the solution' parser.add_argument('-plotSNR', action='store_true', help=help) diff --git a/pygui/ETC/ETC_config.py b/pygui/ETC/ETC_config.py index 3c3e15a1..c1035f6f 100644 --- a/pygui/ETC/ETC_config.py +++ b/pygui/ETC/ETC_config.py @@ -41,10 +41,12 @@ chanConfig=QTable([channels], names=['channel']) chanConfig.add_index('channel') # Allows us to specify rows by channel -chanConfig['channelRange']=[[310.,436.], [417.,590.], [561.,794.], [756.,1040.]] * u.nm +# chanConfig['channelRange']=[[305.1,443.5], [415.7,593.1], [561.9,793.4], [751.2,1040.5]] * u.nm +chanConfig['channelRange']=[[3051.,4435.], [4157.,5931.], [5619.,7934.], [7512.,10405.]] * u.AA # Width of detector (px) in the dispersion direction -chanConfig['Npix_dispers']=(4096, 4096, 4096, 4096) +chanConfig['Npix_dispers']=(4114, 4114, 4114, 4114) +chanConfig['dLambda'] = [ (cr[1]-cr[0])/npd for (cr,npd) in zip(chanConfig['channelRange'], chanConfig['Npix_dispers']) ] chanConfig['platescale']=(0.191, 0.191, 0.191, 0.191)*u.arcsec/u.pix @@ -54,24 +56,20 @@ #LSFFile={} # Wait for data -#1/2 of FWHM requirement --> sigma -chanConfig['LSFsigma']=(1.0, 1.4, 1.85, 2.25)*u.AA /2/2.35 +#FWHM ~ sigma*2.35 +#chanConfig['LSFsigma_px']=[2.585, 2.585, 2.585, 2.585] # R, I widths measured by Matt Matuszewski, 2025 +chanConfig['LSFsigma_px']=[1.23, 1.14, 1.25, 1.25] # Measured by C. Fremling, 2026 +chanConfig['LSFsigma'] = chanConfig['LSFsigma_px'] * chanConfig['dLambda'] # Colors for plotting chanConfig['channelColor']=('blue','green','red','magenta') +# Spectrograph Throughput here includes CCD QE chanConfig['throughputFile_spectrograph']=( - 'throughput-NGPS-spectrograph-U.csv', - 'throughput-NGPS-spectrograph-G.csv', - 'throughput-NGPS-spectrograph-R.csv', - 'throughput-NGPS-spectrograph-I.csv' -) - -chanConfig['QEFile']=( - 'QE-LBNL-CCD-blue.csv', - 'QE-LBNL-CCD-red.csv', - 'QE-LBNL-CCD-red.csv', - 'QE-LBNL-CCD-red.csv' + 'throughput-spectrograph-20260206-U.csv', + 'throughput-spectrograph-20260206-G.csv', + 'throughput-spectrograph-20260206-R.csv', + 'throughput-spectrograph-20260206-I.csv' ) # Make standalone dicts from the columns in the data table diff --git a/pygui/ETC/ETC_import.py b/pygui/ETC/ETC_import.py index 1d1bd0ee..2199a044 100644 --- a/pygui/ETC/ETC_import.py +++ b/pygui/ETC/ETC_import.py @@ -95,7 +95,7 @@ def seeingLambda(w ,FWHM ,pivot=500.*u.nm): '''Seeing law scaled to wavelength''' assert u.get_physical_type(w) == 'length', "w must have units of length" #pivot = 500.*u.nm - return FWHM*(w/pivot)**0.2 + return (FWHM*(w/pivot)**0.2).to('arcsec') # Force units to simplify def makeSource(args): ''' Load the source model, mix with astrophysics, normalize. @@ -567,7 +567,7 @@ def applySlit(slitw, source_at_slit, sky_at_slit, throughput_slicerOptics, args else: Npix_spatial = args.extended - if args.fastSNR: Npix_spatial = 2 # overrides extended source size + if args.fastSNR: Npix_spatial = 2*args.fastSNR # overrides extended source size sharpness = { k : { s: 1./array([Npix_spatial]*len(binCenters[k])) if Npix_spatial is not None # 1/sharpness = [N, N, N...] @@ -598,9 +598,10 @@ def applySlit(slitw, source_at_slit, sky_at_slit, throughput_slicerOptics, args if POINTSOURCE: spec = source_at_slit[k] * throughput_slicer[s] # This applies slit loss and optics - # scale signal down to 2 center pixels + # scale signal down to 2N center pixels if args.fastSNR: - spec *= SpectralElement(Empirical1D, points=binCenters[k], lookup_table=2*profile_slit[k][s][0]) + spec *= 2*profile_slit[k][s][0:args.fastSNR].sum() + #spec *= SpectralElement(Empirical1D, points=binCenters[k], lookup_table=2*profile_slit[k][s][0]) else: # extended source is normalized to mag/arcsec^2, so multiplying by arcsec^2/px gives signal in 1 pixel @@ -633,7 +634,7 @@ def applySlit_extended(slitw, source_at_slit, sky_at_slit, throughput_slicerOpti if args.noslicer or args.fastSNR: slicer_paths = ['center'] else: slicer_paths = ['center','side'] - if args.fastSNR: Npix_spatial = 2 + if args.fastSNR: Npix_spatial = 2*args.fastSNR elif args.extended != None: Npix_spatial = args.extended else: Npix_spatial = None diff --git a/pygui/ETC/ETC_main.py b/pygui/ETC/ETC_main.py index 44998138..c4b33bca 100755 --- a/pygui/ETC/ETC_main.py +++ b/pygui/ETC/ETC_main.py @@ -32,12 +32,11 @@ # Load telescope throughput throughput_telescope = LoadCSVSpec(throughputFile_telescope) -# Load throughputs and detector QE for all spectrograph channels +# Load throughputs for all spectrograph channels throughput_spectrograph = { k : LoadCSVSpec(throughputFile_spectrograph[k]) for k in channels } -QE = { k : LoadCSVSpec(QEFile[k]) for k in channels } # Combine spectra with all throughputs except for slit/slicer -TP = { k : throughput_spectrograph[k]*QE[k]*throughput_telescope for k in channels } +TP = { k : throughput_spectrograph[k]*throughput_telescope for k in channels } # Load throughput for slicer optics (either side of slit) throughput_slicerOptics = LoadCSVSpec(throughputFile_slicer) @@ -301,7 +300,8 @@ def SNRfunc_W(slitw_arcsec, SNRgoal=0): print( ' '.join(['%s=%s' % (k.upper(),v.round(3)) for (k,v) in result.items()]) ) if args.plotSNR: - result['plotSNR'] = computeSNR(t, slitw_result ,args, SSSfocalplane, allChans=True) + result['plotSNR'] = computeSNR(res_exptime, res_slitw ,args, SSSfocalplane, allChans=True) + result['SSSfocalplane'] = SSSfocalplane # return extra functions and data for plotting if ETCextras: return result, efffunc, SNRfunc @@ -371,17 +371,51 @@ def runETC(row ,check=False, skyspec=None): result_plot = main(args ,quiet=True ,plotSNR=True) SNR1 = result_plot['plotSNR'] - fig, ax = plt.subplots(figsize=(15,4)) + SSSfocalplane = result_plot['SSSfocalplane'] # signal, background, sharpness + # Access SIGNAL ct/s as e.g. SSSfocalplane(args.slit)[0]['R']['center'] + # breakpoint() binCenters = makeBinCenters(args.binspect) + + fig, axes = plt.subplots(3, 1, figsize=(15,9), sharex=True) + title = f'EXPTIME={result["exptime"].round(3)} ; SLIT={result["slitwidth"].round(3)} ; ' + title += f'BINSPECT={args.binspect} ; BINSPAT={args.binspat}' + # plt.suptitle('EXPTIME = '+str(result['exptime'].round(3))) + plt.suptitle(title) + + # SNR + ax = axes[0] for k in channels: ax.plot(binCenters[k], SNR1[k] ,color=channelColor[k] ,label=k) - plt.ylabel('SNR / wavelength bin') - plt.legend() + ax.set_ylabel('SNR / wavelength bin') + ax.legend() + ax.axvspan(args.wrange[0], args.wrange[1], alpha=0.2, color='grey') # shade user range + + # Counts + ax = axes[1] + for k in channels: + y = SSSfocalplane(args.slit)[0][k]['center']*args.ETCfixed # total counts in center slit + ax.plot(binCenters[k], y ,color=channelColor[k] ,label=k) + + ax.set_ylabel('Center signal e-') + ax.legend() ax.axvspan(args.wrange[0], args.wrange[1], alpha=0.2, color='grey') # shade user range - plt.title('EXPTIME = '+str(result['exptime'].round(3))) - plt.savefig('plotSNR.png') + + # Background + ax = axes[2] + for k in channels: + y = SSSfocalplane(args.slit)[1][k]['center']*args.ETCfixed*args.binspat # total counts per background pixel + ax.plot(binCenters[k], y ,color=channelColor[k] ,label=k) + + ax.set_ylabel('Center background e-') + ax.legend() + ax.axvspan(args.wrange[0], args.wrange[1], alpha=0.2, color='grey') # shade user range + + # Save + plt.tight_layout() + plt.show() + #plt.savefig('plotSNR.png') print('Wrote', 'plotSNR.png') if args.plotslit: diff --git a/pygui/ETC/requirements.txt b/pygui/ETC/requirements.txt deleted file mode 100644 index 7d8b3ffa..00000000 --- a/pygui/ETC/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -astropy==7.0.0 -matplotlib==3.10.0 -numpy==2.2.1 -scipy==1.15.0 -synphot==1.5.0 diff --git a/pygui/ETC/test.py b/pygui/ETC/test.py new file mode 100644 index 00000000..95da5370 --- /dev/null +++ b/pygui/ETC/test.py @@ -0,0 +1,21 @@ +from muchospec_etc.main import main +from muchospec_etc.arguments import parser, check_inputs_add_units + +# Minimal valid argument set +cmd = """ +G 500 510 SNR 10 +-slit SET 0.5 +-seeing 1 500 +-airmass 1 +-skymag 21.4 +-mag 18 +-magsystem AB +-magfilter match +""" + +args = parser.parse_args(cmd.split()) +check_inputs_add_units(args) + +result = main(args, quiet=True) + +print(result) diff --git a/pygui/calib/thrufocus b/pygui/calib/thrufocus index 2a1bb0e9..c330f156 100755 --- a/pygui/calib/thrufocus +++ b/pygui/calib/thrufocus @@ -31,6 +31,10 @@ camera exptime 10000 # 3. Camera BOI camera boi R 410 200 camera boi I 580 200 +camera boi G 360 200 +### Add G channel BOI +### Add U channel BOI once BOI with binning works. + # 3.5 Get some information from the camera and set some information imnum0=(`camera imnum`[1]) @@ -41,16 +45,21 @@ focusbase="focus_internal_`date +%g%m%d_%H%M%S`" camera basename $focusbase # 4. Turn on lamp and wait -fociI=( 4.4 4.5 4.6 4.7 4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 ) +fociI=( 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 5.0 5.1 5.2 5.3 ) fociR=( 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 3.1 ) +fociG=( 2.8 2.9 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 ) +### Add fociU focpos="1 2 3 4 5 6 7 8 9 10 11 12" echo $foci for fp in $focpos; do # set the focus # focus set I $focus - echo " FOC I ${fociI[$fp]} R ${fociR[$fp]} No `camera imnum`" + echo " FOC I ${fociI[$fp]} R ${fociR[$fp]} G ${fociG[$fp]} No `camera imnum`" focus set I ${fociI[$fp]} focus set R ${fociR[$fp]} + focus set G ${fociG[$fp]} + ### focus set G ${fociG[$fp]} + ### focus set U ..... echo sleep 4 imnum1=(`camera imnum`[1]) @@ -75,17 +84,30 @@ calib set door=close # revert BOI camera boi R full camera boi I full +camera boi G full + +#Add camera boi U full + +# now set the foci to nominal values, just because we can + +focus set G 3.35 +focus set R 2.45 +focus set I 4.75 + # now run the analysis script. + + + # now display the results allfiles="$focusbase*.fits" cd /home/observer/focus/ -/home/developer/Software/run/focus_spec.py /data/latest/$allfiles -fa x -fk FOCUS --range $imnum0 $imnum1 +/home/developer/Software/run/focus_spec.py /data/latest/$allfiles -fa x -fk FOCUS -G /home/observer/focus/gfocus.reg -eog focus_spec_*.png +eog focus_spec_?.png echo "Done." diff --git a/pygui/control_tab.py b/pygui/control_tab.py index 7758a06b..860e6445 100644 --- a/pygui/control_tab.py +++ b/pygui/control_tab.py @@ -22,9 +22,7 @@ def __init__(self, parent): # services self.logic_service = LogicService(self.parent) - # ----------------------------- - # Style helpers (centralized) - # ----------------------------- + # Style helpers def _style_enabled_green(self, btn: QPushButton): btn.setEnabled(True) btn.setStyleSheet(""" @@ -74,9 +72,8 @@ def _apply_shutdown_style(self): self.startup_shutdown_button.setText("Shutdown") self._style_black(self.startup_shutdown_button) - # ----------------------------- + # UI construction - # ----------------------------- def create_control_tab(self): control_layout = QVBoxLayout() @@ -286,9 +283,7 @@ def create_row5(self): row5_widget.setLayout(row5_layout) return row5_widget - # ----------------------------- - # Utility wiring - # ----------------------------- + # Utils def add_separator_line(self, layout): """Thin divider line between rows.""" separator = QFrame() @@ -307,9 +302,8 @@ def connect_input_fields(self): self.bin_spect_box.textChanged.connect(self.on_input_changed) self.bin_spat_box.textChanged.connect(self.on_input_changed) - # ----------------------------- + # Button slots / actions - # ----------------------------- def on_repeat_button_click(self): print("Repeating now...") self.parent.send_command("repeat\n") @@ -438,11 +432,10 @@ def on_abort_button_click(self): self._style_disabled_gray(self.offset_to_target_button) self._style_disabled_gray(self.continue_button) - # ----------------------------- - # DB update helpers (unchanged logic) - # ----------------------------- + def on_confirm_changes(self): - """Confirm input changes and push updates; also enables Go button.""" + """Confirm input changes, update DB, update GUI row, enable Go button.""" + exposure_time = self.exposure_time_box.text() slit_width = self.slit_width_box.text() slit_angle = self.slit_angle_box.text() @@ -450,9 +443,13 @@ def on_confirm_changes(self): bin_spect = self.bin_spect_box.text() bin_spat = self.bin_spat_box.text() + + # DB Updates if exposure_time and slit_width and slit_angle and num_of_exposures and bin_spect and bin_spat: - print(f"Confirmed Exposure Time: {exposure_time}, Slit Width: {slit_width}, " - f"Slit Angle: {slit_angle}, Number of Exposures: {num_of_exposures}") + print( + f"Confirmed Exposure Time: {exposure_time}, Slit Width: {slit_width}, " + f"Slit Angle: {slit_angle}, Number of Exposures: {num_of_exposures}" + ) self.on_exposure_time_changed() self.on_slit_width_changed() self.on_slit_angle_changed() @@ -462,7 +459,10 @@ def on_confirm_changes(self): self._style_disabled_gray(self.confirm_button) elif exposure_time and slit_width: - print(f"Confirmed Exposure Time: {exposure_time}, Slit Width: {slit_width}, Slit Angle: {slit_angle}") + print( + f"Confirmed Exposure Time: {exposure_time}, " + f"Slit Width: {slit_width}, Slit Angle: {slit_angle}" + ) self.on_exposure_time_changed() self.on_slit_width_changed() QSound.play("sound/exposure_slit_width_set.wav") @@ -488,10 +488,39 @@ def on_confirm_changes(self): else: print("Please enter valid values for all fields.") - if getattr(self.parent, "current_target_list_name", None): - print(f"Current target list: {self.parent.current_target_list_name}") - self.logic_service.update_target_table_with_list(self.parent.current_target_list_name) + # Update the selected row in the table + table = self.parent.layout_service.target_list_display + selected = table.selectionModel().selectedRows() + + if selected: + row = selected[0].row() + + headers = [ + table.horizontalHeaderItem(i).text() + for i in range(table.columnCount()) + ] + + if "EXPTIME" in headers and exposure_time: + col = headers.index("EXPTIME") + table.item(row, col).setText("SET " + exposure_time) + + if "SLITWIDTH" in headers and slit_width: + col = headers.index("SLITWIDTH") + table.item(row, col).setText("SET " + slit_width) + + if "NEXP" in headers and num_of_exposures: + col = headers.index("NEXP") + table.item(row, col).setText(num_of_exposures) + + if "BINSPECT" in headers and bin_spect: + col = headers.index("BINSPECT") + table.item(row, col).setText(bin_spect) + + if "BINSPAT" in headers and bin_spat: + col = headers.index("BINSPAT") + table.item(row, col).setText(bin_spat) + # Enable Go button self._style_enabled_green(self.go_button) def on_exposure_time_changed(self): diff --git a/pygui/etc_popup.py b/pygui/etc_popup.py index a70ea9b0..9d82406d 100644 --- a/pygui/etc_popup.py +++ b/pygui/etc_popup.py @@ -1,340 +1,634 @@ -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QSpacerItem, QSizePolicy, QFrame +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, + QLabel, QLineEdit, QComboBox, QCheckBox, + QPushButton, QSizePolicy, QFrame +) from PyQt5.QtCore import Qt -import subprocess import re +import subprocess class EtcPopup(QDialog): + def __init__(self, parent=None): super().__init__(parent) - + + self.FIELD_HEIGHT = 34 + self.FIELD_WIDTH = 220 + self.LABEL_WIDTH = 150 + self.INPUT_COLUMN_OFFSET = 190 + self.setWindowTitle("ETC") - self.setFixedSize(600, 600) # Increased width for better alignment + self.resize(900, 700) - # Main layout for the dialog - self.main_layout = QVBoxLayout() - self.main_layout.setSpacing(12) - self.main_layout.setContentsMargins(10, 10, 10, 10) + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(14) + + self.channel_ranges = { + "U": ("3250", "4330"), + "G": ("4330", "5850"), + "R": ("5850", "7600"), + "I": ("7700", "9340"), + } - # Initialize the form components self.init_widgets() self.init_layout() - self.setLayout(self.main_layout) def init_widgets(self): - """Initialize all the widgets needed for the form.""" - # Create input fields and widgets - self.magnitude_input = QLineEdit() - self.filter_dropdown = QComboBox() - self.filter_dropdown.addItems(["U", "G", "R", "I"]) - self.system_field = QLineEdit("AB") - self.system_field.setReadOnly(True) - - self.sky_mag_input = QLineEdit() - self.snr_input = QLineEdit() - - self.slit_width_input = QLineEdit() - self.slit_dropdown = QComboBox() - self.slit_dropdown.addItems(["SET", "LOSS", "SNR", "RES", "AUTO"]) - - self.range_input_start = QLineEdit() - self.range_input_end = QLineEdit() + + def line(): + w = QLineEdit() + w.setMinimumHeight(self.FIELD_HEIGHT) + w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + return w + def combo(items): + c = QComboBox() + c.addItems(items) + + c.setMinimumHeight(self.FIELD_HEIGHT) + c.setFixedWidth(self.FIELD_WIDTH) + + c.setMaxVisibleItems(4) # exactly the number of items + c.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) + + return c + + self.exptime_input = line() + self.snr_input = line() + + self.snr_mode = QComboBox() + self.snr_mode.addItems([ + "Fixed EXPTIME", + "Solve for EXPTIME" + ]) + self.snr_mode.setMinimumHeight(self.FIELD_HEIGHT) + self.slit_width_input = line() + self.resolution_input = line() + self.res_mode = QComboBox() + self.res_mode.addItems([ + "Fixed slit width", + "RES", + "AUTO" + ]) + self.res_mode.setMinimumHeight(self.FIELD_HEIGHT) + + self.channel_dropdown = combo(["R", "I", "U", "G"]) + self.channel_dropdown.currentTextChanged.connect(self.update_channel_range) + self.spatial_dropdown = combo(["1", "2", "3", "4", "5", "6"]) + self.spectral_dropdown = combo(["1", "2", "3", "4", "5", "6"]) + self.extract_dropdown = combo(["PSF", "2px", "4px", "6px", "8px", "10px"]) + + self.range_start = line() + self.range_end = line() + self.no_slicer_checkbox = QCheckBox("No Slicer") + self.no_slicer_checkbox.setChecked(True) + self.no_slicer_checkbox.setMinimumHeight(self.FIELD_HEIGHT) + + self.seeing_input = line() + self.seeing_wavelength = line() + self.sky_mag_input = line() + self.airmass_input = line() - self.seeing_input = QLineEdit() - self.airmass_input = QLineEdit() + self.magnitude_input = line() + self.abvega_dropdown = combo(["AB", "VEGA"]) + self.filter_dropdown = combo(["match", "U", "V", "R", "I"]) - self.exptime_input = QLineEdit() - self.resolution_input = QLineEdit() + self.extended_checkbox = QCheckBox("Extended Source") + self.extended_checkbox.setMinimumHeight(self.FIELD_HEIGHT) + + self.expert_field = line() + self.expert_field.setMaximumWidth(400) + self.expert_field.setPlaceholderText("Advanced parameters (for power users)") - # Buttons self.run_button = QPushButton("Run ETC") - self.save_button = QPushButton("Save") - self.save_button.setEnabled(False) # Initially disable the Save button + self.save_button = QPushButton("Apply to Target") - def init_layout(self): - """Add widgets to the layout.""" - # Add input rows for each section - self.main_layout.addLayout(self.create_input_row("Magnitude:", self.magnitude_input, self.filter_dropdown, self.system_field)) - - # Call create_sky_mag_snr_layout for Sky Mag and SNR fields - self.main_layout.addLayout(self.create_sky_mag_snr_layout()) - - self.main_layout.addLayout(self.create_input_row("Slit Width:", self.slit_width_input, self.slit_dropdown)) - self.main_layout.addLayout(self.create_range_layout()) - - # Modified the "Seeing" and "Airmass" row - self.main_layout.addLayout(self.create_seeing_airmass_layout()) - - # Modified the "Exp Time" and "Resolution" row - self.main_layout.addLayout(self.create_exptime_resolution_layout()) - - # Add a divider line - divider_line = QFrame() - divider_line.setFrameShape(QFrame.HLine) - divider_line.setFrameShadow(QFrame.Sunken) - self.main_layout.addWidget(divider_line) - - # Add buttons - self.add_buttons() - - # Add a spacer to ensure widgets aren't squished - spacer = QSpacerItem(20, 30, QSizePolicy.Minimum, QSizePolicy.Expanding) - self.main_layout.addItem(spacer) - - def create_input_row(self, label_text, *widgets): - """Create a horizontal layout with a label and widgets.""" - row_layout = QHBoxLayout() - label = self.create_aligned_label(label_text) - row_layout.addWidget(label) - for widget in widgets: - row_layout.addWidget(widget) - widget.setFixedHeight(35) - widget.setFixedWidth(110) - widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - return row_layout - - def create_sky_mag_snr_layout(self): - """Create a layout for Sky Mag and SNR with labels next to the input fields.""" - layout = QHBoxLayout() - - # Add Sky Mag label and input - sky_mag_label = self.create_aligned_label("Sky Mag:") - layout.addWidget(sky_mag_label) - layout.addWidget(self.sky_mag_input) - - # Add SNR label and input next to it - snr_label = self.create_aligned_label("SNR:") - layout.addWidget(snr_label) - layout.addWidget(self.snr_input) - - self.sky_mag_input.setFixedHeight(35) - self.sky_mag_input.setFixedWidth(110) - self.snr_input.setFixedHeight(35) - self.snr_input.setFixedWidth(110) - - return layout - - def create_seeing_airmass_layout(self): - """Create a layout for 'Seeing' and 'Airmass' next to each other.""" - layout = QHBoxLayout() - - # Add Seeing label and input - seeing_label = self.create_aligned_label("Seeing:") - layout.addWidget(seeing_label) - layout.addWidget(self.seeing_input) + self.run_button.setFixedSize(160, 50) + self.save_button.setFixedSize(160, 50) + self.run_button.clicked.connect(self.run_etc) + self.save_button.clicked.connect(self.save_etc) - # Add Airmass label and input next to it - airmass_label = self.create_aligned_label("Airmass:") - layout.addWidget(airmass_label) - layout.addWidget(self.airmass_input) + self.snr_mode.currentIndexChanged.connect(self.update_exptime_mode) + self.res_mode.currentIndexChanged.connect(self.update_resolution_mode) + self.extract_dropdown.currentTextChanged.connect(self.update_extract_mode) + + self.error_label = QLabel("") + self.error_label.setStyleSheet(""" + QLabel { + color: #cc0000; + font-weight: bold; + } + """) + self.error_label.setWordWrap(True) + + self.target_label = QLabel("No target selected") + self.target_label.setStyleSheet(""" + QLabel { + font-weight: bold; + font-size: 14px; + color: #dddddd; + } + """) + self.target_label.setAlignment(Qt.AlignCenter) + # initialize state + self.update_exptime_mode() + self.update_resolution_mode() - self.seeing_input.setFixedHeight(35) - self.seeing_input.setFixedWidth(110) - self.airmass_input.setFixedHeight(35) - self.airmass_input.setFixedWidth(110) + # Default ETC values + self.channel_dropdown.setCurrentText("R") + self.filter_dropdown.setCurrentText("match") + self.extract_dropdown.setCurrentText("8px") + self.spatial_dropdown.setCurrentText("2") - return layout + self.seeing_input.setText("1.5") + self.seeing_wavelength.setText("6400") + self.seeing_wavelength.setEnabled(False) + self.airmass_input.setText("1") + self.sky_mag_input.setText("21.4") + self.magnitude_input.setText("18") - def create_exptime_resolution_layout(self): - """Create a layout for 'Exp Time' and 'Resolution' next to each other.""" - layout = QHBoxLayout() - - # Add Exp Time label and input - exptime_label = self.create_aligned_label("Exp Time:") - layout.addWidget(exptime_label) - layout.addWidget(self.exptime_input) - - # Add Resolution label and input next to it - resolution_label = self.create_aligned_label("Resolution:") - layout.addWidget(resolution_label) - layout.addWidget(self.resolution_input) - - self.exptime_input.setFixedHeight(35) - self.exptime_input.setFixedWidth(110) - self.resolution_input.setFixedHeight(35) - self.resolution_input.setFixedWidth(110) - - return layout - - def create_range_layout(self): - """Create the range row layout.""" - range_layout = QHBoxLayout() - range_layout.setSpacing(10) - range_layout.addWidget(self.create_aligned_label("Range:")) - range_layout.addWidget(self.range_input_start) - range_layout.addWidget(QLabel("-")) - range_layout.addWidget(self.range_input_end) - range_layout.addWidget(self.no_slicer_checkbox) - return range_layout - - def create_aligned_label(self, text): - """Create a label with right alignment.""" - label = QLabel(text) - label.setFixedWidth(110) - label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - label.setStyleSheet("font-size: 14pt;") - return label - - def add_buttons(self): - """Add buttons to the layout.""" - button_row_layout = QHBoxLayout() - button_row_layout.setSpacing(10) - - self.run_button.setFixedSize(110, 45) - self.run_button.clicked.connect(self.run_etc) + self.update_channel_range(self.channel_dropdown.currentText()) - self.save_button.setFixedSize(100, 45) - self.save_button.clicked.connect(self.save_etc) + def label(self, text): + l = QLabel(text) + l.setFixedWidth(self.LABEL_WIDTH) + return l + + def hline(self): + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setStyleSheet("color:#555;margin-top:10px;margin-bottom:10px;") + return line + + def etc_row(self, l_label=None, l_widget=None, r_label=None, r_widget=None): + + row = QHBoxLayout() + row.setSpacing(10) + + def label(text): + lab = QLabel(text) + lab.setFixedWidth(self.INPUT_COLUMN_OFFSET) + return lab + + if l_label: + row.addWidget(label(l_label)) + + if l_widget: + row.addWidget(l_widget) + + if r_label: + row.addSpacing(60) + row.addWidget(label(r_label)) + + if r_widget: + row.addWidget(r_widget) + + row.addStretch() + + return row + + def init_layout(self): + + L = self.main_layout + L.addWidget(self.target_label) + L.addWidget(self.hline()) + + # EXPOSURE + exp = QGridLayout() + exp.setHorizontalSpacing(24) + exp.setVerticalSpacing(14) + + exp.setColumnMinimumWidth(0, 180) + exp.setColumnMinimumWidth(2, 140) + + exp.setColumnStretch(1, 1) + exp.setColumnStretch(3, 1) + + # Row 1 + exp.addWidget(self.label("Exp. Time (s)"), 0, 0) + exp.addWidget(self.exptime_input, 0, 1) + + exp.addWidget(self.label("SNR"), 0, 2) + exp.addWidget(self.snr_input, 0, 3) + exp.addWidget(self.snr_mode, 0, 4) + + # Row 2 + exp.addWidget(self.label("Slit width (arcsec)"), 1, 0) + exp.addWidget(self.slit_width_input, 1, 1) + + exp.addWidget(self.label("Resolution"), 1, 2) + exp.addWidget(self.resolution_input, 1, 3) + exp.addWidget(self.res_mode, 1, 4) + + L.addLayout(exp) + L.addWidget(self.hline()) + + # CHANNEL / CCD + chan = QGridLayout() + chan.setHorizontalSpacing(40) + chan.setVerticalSpacing(16) + + chan.addWidget(QLabel("Channel"), 0, 0) + chan.addWidget(self.channel_dropdown, 0, 1) + + snr = QHBoxLayout() + snr.addWidget(self.range_start) + snr.addWidget(self.range_end) + + chan.addWidget(QLabel("SNR Range (Ã…)"), 1, 0) + chan.addLayout(snr, 1, 1) + + chan.addWidget(self.no_slicer_checkbox, 2, 0) + + chan.addWidget(QLabel("Bin Spatial"), 0, 2) + chan.addWidget(self.spatial_dropdown, 0, 3) + + chan.addWidget(QLabel("Bin Spectral"), 1, 2) + chan.addWidget(self.spectral_dropdown, 1, 3) + + chan.addWidget(QLabel("Extract Spatial"), 2, 2) + chan.addWidget(self.extract_dropdown, 2, 3) + + L.addLayout(chan) + L.addWidget(self.hline()) + + # CONDITIONS + cond = QGridLayout() + cond.setHorizontalSpacing(40) + cond.setVerticalSpacing(16) + + cond.addWidget(QLabel("Seeing (arcsec)"), 0, 0) + cond.addWidget(self.seeing_input, 0, 1) + + cond.addWidget(QLabel("Seeing Pivot (Ã…)"), 0, 2) + cond.addWidget(self.seeing_wavelength, 0, 3) + + cond.addWidget(QLabel("Sky (mag/arcsec²)"), 1, 0) + cond.addWidget(self.sky_mag_input, 1, 1) + + cond.addWidget(QLabel("Airmass"), 1, 2) + cond.addWidget(self.airmass_input, 1, 3) + + L.addLayout(cond) + L.addWidget(self.hline()) + + # TARGET MODEL + target = QGridLayout() + target.setHorizontalSpacing(40) + target.setVerticalSpacing(16) + + target.addWidget(QLabel("Magnitude"), 0, 0) + target.addWidget(self.magnitude_input, 0, 1) + + target.addWidget(QLabel("AB/Vega"), 0, 2) + target.addWidget(self.abvega_dropdown, 0, 3) + + target.addWidget(QLabel("Filter"), 0, 4) + target.addWidget(self.filter_dropdown, 0, 5) + + target.addWidget(self.extended_checkbox, 1, 0) - button_row_layout.addWidget(self.run_button) - button_row_layout.addWidget(self.save_button) + target.addWidget(QLabel("Expert"), 1, 2) + target.addWidget(self.expert_field, 1, 3, 1, 3) - # Add a spacer after the buttons to create margin below - spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - button_row_layout.addItem(spacer) + L.addLayout(target) + L.addWidget(self.hline()) - self.main_layout.addLayout(button_row_layout) + L.addWidget(self.error_label) + # BUTTONS + btn = QHBoxLayout() + btn.addStretch() + btn.addWidget(self.run_button) + btn.addSpacing(60) + btn.addWidget(self.save_button) + btn.addStretch() + + L.addLayout(btn) + + def update_exptime_mode(self): + + mode = self.snr_mode.currentText() + + if mode == "Fixed EXPTIME": + self.set_field_state(self.exptime_input, True) + self.set_field_state(self.snr_input, False) + + else: + self.set_field_state(self.exptime_input, False) + self.set_field_state(self.snr_input, True) + + def update_resolution_mode(self): + + mode = self.res_mode.currentText() + + if mode == "Fixed slit width": + self.set_field_state(self.slit_width_input, True) + self.set_field_state(self.resolution_input, False) + + elif mode == "RES": + self.set_field_state(self.slit_width_input, False) + self.set_field_state(self.resolution_input, True) + + else: # AUTO + self.set_field_state(self.slit_width_input, False) + self.set_field_state(self.resolution_input, False) + + def set_field_state(self, field, enabled): + + field.setEnabled(enabled) + + if enabled: + field.setStyleSheet("") + else: + field.clear() + field.setStyleSheet(""" + QLineEdit { + border: 1px solid #cccccc; + } + """) + + def update_channel_range(self, channel): + + if channel in self.channel_ranges: + + start, end = self.channel_ranges[channel] + + self.range_start.setText(start) + self.range_end.setText(end) + + def update_extract_mode(self, mode): + if mode == "PSF": + self.no_slicer_checkbox.setChecked(True) + def validate_inputs(self): - """Validates user inputs in the ETC tab and highlights invalid fields.""" - - # Helper function to check if the input is a valid float and not empty + """Validate numeric inputs and highlight invalid fields.""" + def is_valid_float(text): - if text.strip() == '': # Check if the text is empty + if text.strip() == "": return False try: - float(text) # Try to convert to float + float(text) return True except ValueError: - return False # Return False if the conversion fails - - # Reset all fields to default state (clear previous error highlighting) - self.magnitude_input.setStyleSheet("") - self.sky_mag_input.setStyleSheet("") - self.snr_input.setStyleSheet("") - self.slit_width_input.setStyleSheet("") - self.range_input_start.setStyleSheet("") - self.range_input_end.setStyleSheet("") - + return False + + fields = [ + self.magnitude_input, + self.sky_mag_input, + self.snr_input, + self.slit_width_input, + self.range_start, + self.range_end, + self.seeing_input, + self.airmass_input, + ] + + # clear previous errors + for f in fields: + f.setStyleSheet("") + try: - # Check if all numeric inputs are valid + if not is_valid_float(self.magnitude_input.text()): - self.magnitude_input.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Magnitude must be a valid number.") - magnitude = float(self.magnitude_input.text()) + self.magnitude_input.setStyleSheet("border:1px solid red;") + raise ValueError("Magnitude must be a number") if not is_valid_float(self.sky_mag_input.text()): - self.sky_mag_input.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Sky Mag must be a valid number.") - sky_mag = float(self.sky_mag_input.text()) - - if not is_valid_float(self.snr_input.text()): - self.snr_input.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("SNR must be a valid number.") - snr = float(self.snr_input.text()) - - if not is_valid_float(self.slit_width_input.text()): - self.slit_width_input.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Slit Width must be a valid number.") - slit_width = float(self.slit_width_input.text()) - - if not is_valid_float(self.range_input_start.text()): - self.range_input_start.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Range Start must be a valid number.") - range_start = float(self.range_input_start.text()) - - if not is_valid_float(self.range_input_end.text()): - self.range_input_end.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Range End must be a valid number.") - range_end = float(self.range_input_end.text()) - - # Ensure range_start is less than range_end - if range_start >= range_end: - self.range_input_start.setStyleSheet("border: 1px solid red;") # Highlight invalid field - self.range_input_end.setStyleSheet("border: 1px solid red;") # Highlight invalid field - raise ValueError("Range start must be less than range end.") - - # Check for valid values (you can adjust this for your specific needs) - if magnitude <= 0 or sky_mag <= 0 or snr <= 0 or slit_width <= 0: - raise ValueError("Magnitude, Sky Mag, SNR, and Slit Width must be positive values.") - + self.sky_mag_input.setStyleSheet("border:1px solid red;") + raise ValueError("Sky Mag must be a number") + + if self.snr_input.isEnabled(): + if not is_valid_float(self.snr_input.text()): + self.snr_input.setStyleSheet("border:1px solid red;") + raise ValueError("SNR must be a number") + + if self.slit_width_input.isEnabled(): + if not is_valid_float(self.slit_width_input.text()): + self.slit_width_input.setStyleSheet("border:1px solid red;") + raise ValueError("Slit width must be a number") + + if not is_valid_float(self.range_start.text()): + self.range_start.setStyleSheet("border:1px solid red;") + raise ValueError("Range start must be a number") + + if not is_valid_float(self.range_end.text()): + self.range_end.setStyleSheet("border:1px solid red;") + raise ValueError("Range end must be a number") + + if self.resolution_input.isEnabled(): + if not is_valid_float(self.resolution_input.text()): + self.resolution_input.setStyleSheet("border:1px solid red;") + raise ValueError("Resolution must be a number") + + start = float(self.range_start.text()) + end = float(self.range_end.text()) + + if start >= end: + self.range_start.setStyleSheet("border:1px solid red;") + self.range_end.setStyleSheet("border:1px solid red;") + raise ValueError("Range start must be less than range end") + self.save_button.setEnabled(True) - return True # All inputs are valid + return True except ValueError as e: - # Show error message - error_msg = f"Invalid input: {str(e)}" - print(error_msg) - return False # Input is invalid - + print(f"Invalid input: {e}") + return False def run_etc(self): - """Handles the logic for the 'Run ETC' button.""" - - # Validate inputs before running the command - if not self.validate_inputs(): - return # If inputs are invalid, do not proceed - - # Collecting all necessary data from input fields - filter_value = self.filter_dropdown.currentText() # e.g., "G" - magnitude_value = self.magnitude_input.text() # e.g., "18.0" - sky_mag_value = self.sky_mag_input.text() # e.g., "21.4" - snr_value = self.snr_input.text() # e.g., "10" - slit_width_value = self.slit_width_input.text() # e.g., "0.5" - slit_option = self.slit_dropdown.currentText() # e.g., "SET X" - seeing_value = self.seeing_input.text() - airmass_value = self.airmass_input.text() - mag_system_value = self.system_field.text() # e.g., "AB" - mag_filter_value = "match" # e.g., "match" - - # Handling the range inputs - range_start_value = self.range_input_start.text() - range_end_value = self.range_input_end.text() - # Construct the command string - command = f"python3 ETC/ETC_main.py {filter_value} {range_start_value} {range_end_value} SNR {snr_value} " \ - f"-slit {slit_option} {slit_width_value} -seeing {seeing_value} 500 -airmass {airmass_value} " \ - f"-skymag {sky_mag_value} -mag {magnitude_value} -magsystem {mag_system_value} -magfilter {mag_filter_value}" + if not self.validate_inputs(): + return + self.error_label.setText("") + + channel = self.channel_dropdown.currentText() + + wrange_start = str(float(self.range_start.text()) / 10) + wrange_end = str(float(self.range_end.text()) / 10) + + mag = self.magnitude_input.text() + magsystem = self.abvega_dropdown.currentText() + magfilter = self.filter_dropdown.currentText() + + sky_mag = self.sky_mag_input.text() + seeing = self.seeing_input.text() + airmass = self.airmass_input.text() + + # EXPTIME / SNR solve mode + snr_mode = self.snr_mode.currentText() + + if snr_mode == "Fixed EXPTIME": + solve_param = "EXPTIME" + solve_value = self.exptime_input.text() + else: + solve_param = "SNR" + solve_value = self.snr_input.text() + + # SLIT / RESOLUTION mode + res_mode = self.res_mode.currentText() + + if res_mode == "Fixed slit width": + slit_mode = ["-slit", "SET", self.slit_width_input.text()] + + elif res_mode == "RES": + slit_mode = ["-slit", "RES", self.resolution_input.text()] + + else: + slit_mode = ["-slit", "AUTO"] + + + # Build command + cmd = [ + "python3", + "ETC/ETC_main.py", + channel, + wrange_start, + wrange_end, + solve_param, + solve_value, + *slit_mode, + "-seeing", seeing, "640", + "-airmass", airmass, + "-skymag", sky_mag, + "-mag", mag, + "-magsystem", magsystem, + "-magfilter", magfilter + ] + + # slicer option + if self.no_slicer_checkbox.isChecked(): + cmd.append("-noslicer") + + # binning options + cmd.extend(["-binspect", self.spectral_dropdown.currentText()]) + cmd.extend(["-binspat", self.spatial_dropdown.currentText()]) + # extract aperture -> fastSNR option + extract_mode = self.extract_dropdown.currentText() + + if extract_mode != "PSF": + fastsnr_value = int(extract_mode.replace("px", "")) // 2 + cmd.extend(["-fastSNR", str(fastsnr_value)]) + + # expert option + expert = self.expert_field.text().strip() + if expert: + cmd.extend(expert.split()) + + print("Running ETC command:") + print(" ".join(cmd)) - # Print the command for debugging - print(f"Running command: {command}") - - # Run the command and capture the output try: - result = subprocess.run(command, shell=True, capture_output=True, text=True) - output = result.stdout.strip() # Get the output from the command - print(f"Command output: {output}") - # Extract EXPTIME and RESOLUTION from the output using regex - exptime_match = re.search(r"EXPTIME=([0-9.]+) s", output) - resolution_match = re.search(r"RESOLUTION=([0-9.]+)", output) + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + + output = result.stdout + print(output) + + exptime_match = re.search(r"EXPTIME=([0-9.]+)", output) + res_match = re.search(r"RESOLUTION=([0-9.]+)", output) + snr_match = re.search(r"SNR=([0-9.]+)", output) + slitwidth_match = re.search(r"SLITWIDTH=([0-9.]+)", output) if exptime_match: exptime = float(exptime_match.group(1)) - exptime_rounded = round(exptime) # Round EXPTIME to the nearest integer - self.exptime_input.setText(str(exptime_rounded)) # Update GUI field with rounded EXPTIME + self.exptime_input.setText(str(exptime)) - if resolution_match: - resolution = float(resolution_match.group(1)) - resolution_rounded = round(resolution) # Round RESOLUTION to the nearest integer - self.resolution_input.setText(str(resolution_rounded)) # Update GUI field with rounded RESOLUTION + if res_match: + resolution = float(res_match.group(1)) + self.resolution_input.setText(str(resolution)) + + if snr_match: + snr = float(snr_match.group(1)) + self.snr_input.setText(str(snr)) + + if slitwidth_match: + slitwidth = float(slitwidth_match.group(1)) + self.slit_width_input.setText(str(slitwidth)) except subprocess.CalledProcessError as e: - # Handle errors if the command fails - print(f"Error running ETC: {e}") - - # Display the result in the results display (GUI) - result_text = f"Running ETC with the following parameters:\n{command}\n\n" \ - f"EXPTIME: {self.exptime_input.text()}\n" \ - f"RESOLUTION: {self.resolution_input.text()}" - print(result_text) + + stderr = e.stderr or "" + + error_line = "" + + for line in stderr.splitlines(): + if "ETC_main.py: error:" in line: + error_line = line + break + + if error_line: + self.error_label.setText(error_line) + else: + self.error_label.setText("ETC failed. See terminal for details.") + + print("ETC failed") + print(e.stdout) + print(e.stderr) + self.save_button.setEnabled(True) def save_etc(self): + + # Prevent execution if no target selected + if not getattr(self.parent, "current_observation_id", None): + print("No target selected, ETC results cannot be applied.") + return + exptime = self.exptime_input.text() resolution = self.resolution_input.text() - if (self.parent.current_observation_id): - self.logic_service.send_update_to_db(self.parent.current_observation_id, "OTMexpt", exptime) - self.logic_service.send_update_to_db(self.parent.current_observation_id, "exptime", exptime) - self.logic_service.send_update_to_db(self.parent.current_observation_id, "OTMres", resolution) - self.save_button.setEnabled(False) + slit_width = self.slit_width_input.text() + + # Exposure time + self.logic_service.send_update_to_db( + self.parent.current_observation_id, "OTMexpt", exptime + ) + + self.logic_service.send_update_to_db( + self.parent.current_observation_id, "exptime", "SET " + exptime + ) + + # Resolution + self.logic_service.send_update_to_db( + self.parent.current_observation_id, "OTMres", resolution + ) + + # Slit width + if slit_width: + self.logic_service.send_update_to_db( + self.parent.current_observation_id, "OTMslitwidth", slit_width + ) + + self.logic_service.send_update_to_db( + self.parent.current_observation_id, "slitwidth", "SET " + slit_width + ) + + print("ETC values applied to target.") + + def load_target(self, target_data): + """ + Populate ETC fields from selected target. + target_data is a dict from the target table. + """ + + mag = target_data.get("MAG") + filt = target_data.get("FILTER") + + if mag: + self.magnitude_input.setText(str(mag)) + + if filt: + idx = self.filter_dropdown.findText(filt) + if idx >= 0: + self.filter_dropdown.setCurrentIndex(idx) + + def set_target_info(self, name, ra, dec): + + text = f"Target: {name} RA: {ra} Dec: {dec}" + self.target_label.setText(text) \ No newline at end of file diff --git a/pygui/layout_service.py b/pygui/layout_service.py index 84db5df2..e02087c8 100644 --- a/pygui/layout_service.py +++ b/pygui/layout_service.py @@ -1004,6 +1004,12 @@ def update_target_info(self): # Pass the dictionary of target data to LogicService print("Target Data:", target_data) # Print the full target data for the selected row # self.parent.logic_service.update_target_list_table(target_data) + if hasattr(self.parent, "etc_popup") and self.parent.etc_popup is not None: + self.parent.etc_popup.set_target_info( + target_data.get("NAME", ""), + target_data.get("RA", ""), + target_data.get("DECL", "") + ) # Call to set the column widths (adjust them as needed) self.set_column_widths() diff --git a/pygui/ngps_gui.py b/pygui/ngps_gui.py index 7bdcc47a..4ddb6ec1 100644 --- a/pygui/ngps_gui.py +++ b/pygui/ngps_gui.py @@ -299,9 +299,25 @@ def open_calibration_gui(self): self.calibration_gui.activateWindow() def open_etc_popup(self): - """Opens the EtcPopup when the button is clicked.""" - self.etc_popup = EtcPopup(self) # Pass the parent as the current MainWindow - self.etc_popup.exec_() + + if not hasattr(self, "etc_popup") or self.etc_popup is None: + self.etc_popup = EtcPopup(self) + + table = self.layout_service.target_list_display + selected = table.selectionModel().selectedRows() + + if selected: + row = selected[0].row() + + name = table.item(row, 0).text() + ra = table.item(row, 1).text() + dec = table.item(row, 2).text() + + self.etc_popup.set_target_info(name, ra, dec) + + self.etc_popup.show() + self.etc_popup.raise_() + self.etc_popup.activateWindow() def show_popup(self, message): """Show a popup message on the screen.""" @@ -460,6 +476,16 @@ def _init_daemon_polling(self): self.refresh_daemon_states_from_ps() self._daemon_timer.start() + def update_etc_target(self, target_data): + + if hasattr(self, "etc_popup") and self.etc_popup is not None: + + name = target_data.get("NAME", "") + ra = target_data.get("RA", "") + dec = target_data.get("DECL", "") + + self.etc_popup.set_target_info(name, ra, dec) + if __name__ == '__main__': app = QApplication(sys.argv) From dba9f557aee7139c524b45a4e62e8f233bca5026 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 7 Apr 2026 15:17:44 -0700 Subject: [PATCH 02/37] Issue 363 Slicecam Acquire v2 (PR #402) Implements slicecam fine acquisition * Introduced slicecam_math.cpp/h implementing centroid detection, WCS pix2world conversion, and angular offset calculations. * Added a do_fineacquire loop to the Slicecam interface to automate target centering based on configurable aimpoints and background regions. * Added FINE_ACQUIRE_AIMPOINT and FINE_ACQUIRE_BACKGROUND parameters to the daemon configuration. * Replaces receiving a JSON message with the existing PUB-SUB mechanism * Updated target_offset logic to better distinguish between TCS-direct offsets and ACAM-guided goal offsets. * Renamed dothread_acquisition to do_acam_acquire and added atomic tracking for fine-acquisition locks and guiding states. --- Config/slicecamd.cfg.in | 19 + acamd/acam_interface.cpp | 465 ++++---- acamd/acam_interface.h | 23 +- acamd/acamd.cpp | 4 +- common/common.h | 28 + common/message_keys.h | 29 +- common/slicecamd_commands.h | 2 + emulator/power.cpp | 2 +- sequencerd/CMakeLists.txt | 1 + sequencerd/sequence.cpp | 344 +++--- sequencerd/sequence.h | 21 +- sequencerd/sequence_acquisition.cpp | 120 +++ sequencerd/sequencerd.cpp | 6 +- slicecamd/CMakeLists.txt | 8 +- slicecamd/guimanager.h | 121 +++ slicecamd/slicecam_camera.cpp | 1032 ++++++++++++++++++ slicecamd/slicecam_camera.h | 88 ++ slicecamd/slicecam_interface.cpp | 1560 ++++++++------------------- slicecamd/slicecam_interface.h | 232 ++-- slicecamd/slicecam_math.cpp | 431 ++++++++ slicecamd/slicecam_math.h | 72 ++ slicecamd/slicecam_server.cpp | 4 + slicecamd/slicecamd.cpp | 7 +- thermald/thermal_interface.cpp | 2 +- thermald/thermal_interface.h | 1 + 25 files changed, 2887 insertions(+), 1735 deletions(-) create mode 100644 sequencerd/sequence_acquisition.cpp create mode 100644 slicecamd/guimanager.h create mode 100644 slicecamd/slicecam_camera.cpp create mode 100644 slicecamd/slicecam_camera.h create mode 100644 slicecamd/slicecam_math.cpp create mode 100644 slicecamd/slicecam_math.h diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index 53873240..e3abb098 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -58,6 +58,25 @@ PUSH_GUI_SETTINGS=/home/developer/Software/GuiderGUI/push_settings_slicev.sh # PUSH_GUI_IMAGE=/home/developer/Software/GuiderGUI/push_image.sh +# FINE_ACQUIRE_AIMPOINT=( ) +# camera and location of aimpoint for fine acquisition +# which camera must be { L R } +# x-coordinate (col) +# y-coordinate (row) +# aimpoint x,y may be fractional +# +FINE_ACQUIRE_AIMPOINT=(L 150.0 115.5) + +# FINE_ACQUIRE_BACKGROUND=( ) +# defines the bounds of the region for background coorection +# for fine acquisition centroiding +# x-coordinate lower left +# x-coordinate lower right +# y-coordinate upper left +# y-coordinate upper right +# +FINE_ACQUIRE_BACKGROUND=(80 165 30 210) + # SkySimulator options: # SKYSIM_IMAGE_SIZE= where is integer # Sets the keyword argument "IMAGE_SIZE=" diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 5d314eb6..f19fbfcf 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -26,8 +26,8 @@ namespace Acam { * */ long Camera::emulator( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::emulator"; - std::stringstream message; + const char* function = "Acam::Camera::emulator"; + std::ostringstream message; // Help // @@ -81,8 +81,8 @@ namespace Acam { * */ long Camera::open( int sn ) { - std::string function = "Acam::Camera::open"; - std::stringstream message; + const char* function = "Acam::Camera::open"; + std::ostringstream message; long error=NO_ERROR; // Opens the Andor and initializes SDK @@ -178,8 +178,8 @@ namespace Acam { * */ long Camera::imflip( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::imflip"; - std::stringstream message; + const char* function = "Acam::Camera::imflip"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -262,8 +262,8 @@ namespace Acam { * */ long Camera::imrot( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::imrot"; - std::stringstream message; + const char* function = "Acam::Camera::imrot"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -366,7 +366,7 @@ namespace Acam { * @return ERROR | NO_ERROR */ long Camera::set_fan( int mode ) { - const std::string function="Acam::Camera::set_fan"; + const char* function="Acam::Camera::set_fan"; // Andor must be connected // @@ -392,8 +392,8 @@ namespace Acam { * */ long Camera::gain( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::gain"; - std::stringstream message; + const char* function = "Acam::Camera::gain"; + std::ostringstream message; long error = NO_ERROR; int gain = -999; @@ -532,7 +532,6 @@ namespace Acam { * */ int Camera::gain() { - std::string function = "Acam::Camera::gain"; std::string svalue; int ivalue=0; this->gain( "", svalue ); @@ -555,8 +554,8 @@ namespace Acam { * */ long Camera::speed( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::speed"; - std::stringstream message; + const char* function = "Acam::Camera::speed"; + std::ostringstream message; long error = NO_ERROR; float hori=-1, vert=-1; @@ -656,8 +655,8 @@ namespace Acam { * */ long Camera::temperature( std::string args, std::string &retstring ) { - std::string function = "Acam::Camera::temperature"; - std::stringstream message; + const char* function = "Acam::Camera::temperature"; + std::ostringstream message; long error = NO_ERROR; int temp = 999; @@ -780,8 +779,8 @@ namespace Acam { * */ long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { - std::string function = "Acam::Camera::write_frame"; - std::stringstream message; + const char* function = "Acam::Camera::write_frame"; + std::ostringstream message; long error = NO_ERROR; // Nothing to do if not Andor image data @@ -850,13 +849,7 @@ namespace Acam { * */ long Interface::test_image( ) { - std::string function = "Acam::Interface::test_image"; - std::stringstream message; - long error = NO_ERROR; - - error = this->camera.andor.test(); - - return error; + return this->camera.andor.test(); } /***** Acam::Camera::test_image *********************************************/ @@ -870,7 +863,7 @@ namespace Acam { * */ long Astrometry::initialize_python() { - std::string function = "Acam::Astrometry::initialize_python"; + const char* function = "Acam::Astrometry::initialize_python"; if ( ! py_instance.is_initialized() ) { logwrite( function, "ERROR could not initialize Python" ); @@ -895,7 +888,7 @@ namespace Acam { this->pQualityModule = PyImport_Import( pModuleNameQuality ); if ( this->pAstrometryModule == nullptr || this->pQualityModule == nullptr ) { - std::stringstream message; + std::ostringstream message; message << "ERROR could not import Python module(s):"; if ( this->pAstrometryModule == nullptr ) message << " " << PYTHON_ASTROMETRY_MODULE; if ( this->pQualityModule == nullptr ) message << " " << PYTHON_IMAGEQUALITY_MODULE; @@ -928,8 +921,8 @@ namespace Acam { * */ long Astrometry::image_quality( ) { - std::string function = "Acam::Astrometry::image_quality"; - std::stringstream message; + const char* function = "Acam::Astrometry::image_quality"; + std::ostringstream message; if ( !this->python_initialized ) { logwrite( function, "ERROR Python is not initialized" ); @@ -1079,8 +1072,8 @@ namespace Acam { * */ long Astrometry::solve( std::string imagename_in, std::vector solverargs_in ) { - std::string function = "Acam::Astrometry::solve"; - std::stringstream message; + const char* function = "Acam::Astrometry::solve"; + std::ostringstream message; if ( !this->python_initialized ) { logwrite( function, "ERROR Python is not initialized" ); @@ -1343,8 +1336,8 @@ namespace Acam { * */ long Interface::bin( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::bin"; - std::stringstream message; + const char* function = "Acam::Interface::bin"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -1433,28 +1426,26 @@ namespace Acam { * */ void Interface::publish_snapshot() { + // force-publish status + this->publish_status(true); - // assemble the telemetry into a json message - // nlohmann::json jmessage_out; - - jmessage_out["source"] = "acamd"; // source of this telemetry + jmessage_out[Key::SOURCE] = Topic::ACAMD; int ccdtemp=99; - this->camera.andor.get_temperature( ccdtemp ); // temp is int - jmessage_out["TANDOR_ACAM"] = ( this->isopen("camera") ? - static_cast(ccdtemp) : // but the database wants floats - NAN ); - - jmessage_out["ACAM_FILTER"] = ( this->isopen("motion" ) ? - this->motion.get_current_filtername() : - "not_connected" ); - jmessage_out["ACAM_COVER"] = ( this->isopen("motion" ) ? - this->motion.get_current_coverpos() : - "not_connected" ); + this->camera.andor.get_temperature( ccdtemp ); // temp is int + jmessage_out[Key::Acamd::TANDOR] = ( this->isopen("camera") ? + static_cast(ccdtemp) : // but the database wants floats + NAN ); + jmessage_out[Key::Acamd::FILTER] = ( this->isopen("motion" ) ? + this->motion.get_current_filtername() : + "not_connected" ); + jmessage_out[Key::Acamd::COVER] = ( this->isopen("motion" ) ? + this->motion.get_current_coverpos() : + "not_connected" ); try { - this->publisher->publish( jmessage_out ); + this->publisher->publish( jmessage_out, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Acam::Interface::publish_snapshot", @@ -1465,9 +1456,60 @@ namespace Acam { /***** Acam::Interface::publish_snapshot ************************************/ + /***** Acam::Interface::publish_status **************************************/ + /** + * @brief publishes my acam-related (important) status on change + * @details This publishes a JSON message containing important telemetry. + * @param[in] force optional (default=false) forces publish irrespective of change + * + */ + void Interface::publish_status(bool force) { + const std::string acquire_mode = this->target.acquire_mode_string(); + const bool is_acquired = this->target.is_acquired.load(); + const int nacquired = this->target.nacquired; + const int attempts = this->target.attempts; + + // unless forced, only publish if there was a change in any one of these + // + if ( !force && + acquire_mode == this->last_status.acquire_mode && + is_acquired == this->last_status.is_acquired && + nacquired == this->last_status.nacquired && + attempts == this->last_status.attempts ) return; + + this->last_status.acquire_mode = acquire_mode; + this->last_status.is_acquired = is_acquired; + this->last_status.nacquired = nacquired; + this->last_status.attempts = attempts; + + // assemble the telemetry into a json message + // + nlohmann::json jmessage_out; + jmessage_out[Key::SOURCE] = Topic::ACAMD; + jmessage_out[Key::Acamd::ACQUIRE_MODE] = this->target.acquire_mode_string(); + jmessage_out[Key::Acamd::IS_ACQUIRED] = this->target.is_acquired.load(); + jmessage_out[Key::Acamd::NACQUIRED] = this->target.nacquired; + jmessage_out[Key::Acamd::ATTEMPTS] = this->target.attempts; + jmessage_out[Key::Acamd::SEEING] = this->astrometry.get_seeing(); + jmessage_out[Key::Acamd::BACKGROUND] = this->astrometry.get_background(); + + try { + this->publisher->publish( jmessage_out, Topic::ACAMD ); + } + catch ( const std::exception &e ) { + logwrite( "Acam::Interface::publish_status", + "ERROR publishing message: "+std::string(e.what()) ); + return; + } + } + /***** Acam::Interface::publish_status **************************************/ + + /***** Acam::Interface::request_snapshot ************************************/ /** - * @brief sends request for snapshot + * @brief publises request for snapshot + * @details publishing Topic::SNAPSHOT induces subscribers to publish a + * snapshot of their telemetry * */ void Interface::request_snapshot() { @@ -1479,7 +1521,7 @@ namespace Acam { } } try { - this->publisher->publish( jmessage, "_snapshot" ); + this->publisher->publish( jmessage, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Acam::Interface::request_snapshot", @@ -1493,6 +1535,8 @@ namespace Acam { /***** Acam::Interface::wait_for_snapshots **********************************/ /** * @brief wait for everyone to publish their snaphots + * @details When forcing subscribers to publish their telemetry, + * this waits until they have done so. * */ bool Interface::wait_for_snapshots() { @@ -1517,7 +1561,7 @@ namespace Acam { if (all_received) return true; if (std::chrono::steady_clock::now() - start_time > timeout) { - std::stringstream message; + std::ostringstream message; message << "ERROR timeout waiting for telemetry from:"; for ( const auto &[topic,status] : snapshot_status ) { if (!status) message << " " << topic; @@ -1534,31 +1578,30 @@ namespace Acam { /***** Acam::Interface::handletopic_snapshot ********************************/ /** - * @brief publishes snapshot of my telemetry + * @brief what to do when the topic is Topic::ACAMD * @details This publishes a JSON message containing a snapshot of my - * telemetry info when the subscriber receives the "_snapshot" - * topic and the payload contains my daemon name. + * telemetry info when the subscriber receives the Topic::SNAPSHOT + * topic and the payload contains my name. * @param[in] jmessage_in subscribed-received JSON message * */ void Interface::handletopic_snapshot( const nlohmann::json &jmessage_in ) { - // If my name is in the jmessage then publish my snapshot - // - if ( jmessage_in.contains( Acam::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage_in.contains( "test" ) ) { - logwrite( "Acamd::Interface::handletopic_snapshot", jmessage_in.dump() ); - } + if ( jmessage_in.contains( Topic::ACAMD ) ) this->publish_snapshot(); } /***** Acam::Interface::handletopic_snapshot ********************************/ + /***** Acam::Interface::handletopic_tcsd ************************************/ + /** + * @brief what to do when the topic is Topic::TCSD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["tcsd"]=true; + snapshot_status[Topic::TCSD]=true; } // extract and store values in the class // @@ -1584,27 +1627,41 @@ namespace Acam { this->database.add_key_val( "focus", telem.telfocus ); this->database.add_key_val( "AIRMASS", telem.airmass ); } + /***** Acam::Interface::handletopic_tcsd ************************************/ + /***** Acam::Interface::handletopic_targetinfo ******************************/ + /** + * @brief what to do when the topic is Topic::TARGETINFO + * @details This receives target info + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_targetinfo( const nlohmann::json &jmessage ) { + { + std::lock_guard lock(snapshot_mtx); + snapshot_status[Topic::TARGETINFO]=true; + } this->database.add_from_json( jmessage, "OBS_ID" ); this->database.add_from_json( jmessage, "NAME" ); this->database.add_from_json( jmessage, "POINTMODE" ); this->database.add_from_json( jmessage, "RA" ); this->database.add_from_json( jmessage, "DECL" ); } + /***** Acam::Interface::handletopic_targetinfo ******************************/ /***** Acam::Interface::handletopic_slitd ***********************************/ /** - * @brief handles topic subscription to slitd + * @brief what to do when the topic is Topic::SLITD + * @details This receives slitd telemetry * @param[in] jmessage incoming json message * */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["slitd"]=true; + snapshot_status[Topic::SLITD]=true; } this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); @@ -1612,97 +1669,6 @@ namespace Acam { /***** Acam::Interface::handletopic_slitd ***********************************/ - /***** Acam::Interface::handle_json_message *********************************/ - /** - * @brief parses incoming telemetry messages - * @details Requesting telemetry from another daemon returns a serialized - * JSON message which needs to be passed in here to parse it. - * @param[in] message_in incoming serialized JSON message (as a string) - * @return ERROR | NO_ERROR - * - */ - long Interface::handle_json_message( std::string message_in ) { - const std::string function="Acam::Interface::handle_json_message"; - std::stringstream message; - - // nothing to do if the message is empty - // - if ( message_in.empty() ) { - logwrite( function, "ERROR empty JSON message" ); - return ERROR; - } - - try { - nlohmann::json jmessage = nlohmann::json::parse( message_in ); - std::string messagetype; - - // jmessage must not contain key "error" and must contain key "messagetype" - // - if ( !jmessage.contains("error") ) { - if ( jmessage.contains("messagetype") && jmessage["messagetype"].is_string() ) { - messagetype = jmessage["messagetype"]; - } - else { - logwrite( function, "ERROR received JSON message with missing or invalid messagetype" ); - return ERROR; - } - } - else { - logwrite( function, "ERROR in JSON message" ); - return ERROR; - } - - // no errors, so disseminate the message contents based on the message type - // - if ( messagetype == "tcsinfo" ) { - this->database.add_from_json( jmessage, "CASANGLE" ); - this->database.add_from_json( jmessage, "TELRA", "RAtel" ); - this->database.add_from_json( jmessage, "TELDEC", "DECLtel" ); - this->database.add_from_json( jmessage, "AZ" ); - this->database.add_from_json( jmessage, "TELFOCUS", "focus" ); - this->database.add_from_json( jmessage, "AIRMASS" ); - } - else - if ( messagetype == "targetinfo" ) { - this->database.add_from_json( jmessage, "OBS_ID" ); - this->database.add_from_json( jmessage, "NAME" ); - this->database.add_from_json( jmessage, "POINTMODE" ); - this->database.add_from_json( jmessage, "RA" ); - this->database.add_from_json( jmessage, "DECL" ); - } - else - if ( messagetype == "slitinfo" ) { - float slitw, slito; - Common::extract_telemetry_value( message_in, "SLITW", slitw ); - this->camera.fitsinfo.fitskeys.addkey( "SLITW", slitw, "slit width in arcsec" ); - Common::extract_telemetry_value( message_in, "SLITO", slito ); - this->camera.fitsinfo.fitskeys.addkey( "SLITO", slito, "slit offset in arcsec" ); - } - else - if ( messagetype == "test" ) { - } - else { - message.str(""); message << "ERROR received unhandled JSON message type \"" << messagetype << "\""; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const nlohmann::json::parse_error &e ) { - message.str(""); message << "ERROR json exception parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - - return NO_ERROR; - } - /***** Acam::Interface::handle_json_message *********************************/ - - /***** Acam::Interface::initialize_python_objects ***************************/ /** * @brief provides interface to initialize Python objects in the class @@ -1727,8 +1693,8 @@ namespace Acam { * */ long Interface::configure_interface( Config &config ) { - std::string function = "Acam::Interface::configure_interface"; - std::stringstream message; + const char* function = "Acam::Interface::configure_interface"; + std::ostringstream message; int applied=0; long error = NO_ERROR; @@ -2025,8 +1991,8 @@ namespace Acam { * */ long Interface::open( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::open"; - std::stringstream message; + const char* function = "Acam::Interface::open"; + std::ostringstream message; long error = NO_ERROR; std::vector arglist; std::string component, camarg; @@ -2118,15 +2084,14 @@ namespace Acam { // If serial number not specified as an arg then open the s/n specified // in the config file. // - int sn; + int sn=-1; if ( camarg.empty() ) sn = this->camera.andor.camera_info.serial_number; else { try { sn = std::stoi( camarg ); } catch( const std::exception &e ) { - message.str(""); message << "ERROR parsing serial number from \"" << camarg << "\": " << e.what(); - logwrite( function, message.str() ); + logwrite(function, "ERROR parsing serial number from '"+camarg+"': "+std::string(e.what())); error = ERROR; } } @@ -2174,8 +2139,8 @@ namespace Acam { * */ long Interface::isopen( std::string component, bool &state, std::string &retstring ) { - std::string function = "Acam::Interface::isopen"; - std::stringstream message; + const char* function = "Acam::Interface::isopen"; + std::ostringstream message; // Help // @@ -2255,8 +2220,8 @@ namespace Acam { this->close("",dontcare); } long Interface::close( std::string component, std::string &help ) { - std::string function = "Acam::Interface::close"; - std::stringstream message; + const char* function = "Acam::Interface::close"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -2325,8 +2290,8 @@ namespace Acam { * */ long Interface::tcs_init( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::tcs_init"; - std::stringstream message; + const char* function = "Acam::Interface::tcs_init"; + std::ostringstream message; long error = NO_ERROR; // If shutting down then stop the focus monitoring thread first @@ -2415,8 +2380,8 @@ namespace Acam { * */ long Interface::framegrab_fix( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::framegrab_fix"; - std::stringstream message; + const char* function = "Acam::Interface::framegrab_fix"; + std::ostringstream message; // Help // @@ -2473,7 +2438,7 @@ namespace Acam { * */ long Interface::saveframes( std::string args, std::string &retstring ) { - const std::string function = "Acam::Interface::saveframes"; + const char* function = "Acam::Interface::saveframes"; // Help // @@ -2526,7 +2491,7 @@ namespace Acam { * */ long Interface::skipframes( std::string args, std::string &retstring ) { - const std::string function = "Acam::Interface::skipframes"; + const char* function = "Acam::Interface::skipframes"; // Help // @@ -2572,8 +2537,8 @@ namespace Acam { * */ long Interface::framegrab( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::framegrab"; - std::stringstream message; + const char* function = "Acam::Interface::framegrab"; + std::ostringstream message; long error = NO_ERROR; std::string _imagename = this->imagename; @@ -2678,8 +2643,8 @@ namespace Acam { * */ void Interface::dothread_framegrab( Acam::Interface &iface, const std::string whattodo, std::string sourcefile ) { - std::string function = "Acam::Interface::dothread_framegrab"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_framegrab"; + std::ostringstream message; long error = NO_ERROR; if ( iface.is_framegrab_running.load(std::memory_order_acquire) ) { @@ -2856,8 +2821,8 @@ namespace Acam { * */ long Interface::guider_settings_control( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::guider_settings_control"; - std::stringstream message; + const char* function = "Acam::Interface::guider_settings_control"; + std::ostringstream message; // Help // @@ -3100,8 +3065,8 @@ namespace Acam { * */ long Interface::acquire( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::acquire"; - std::stringstream message; + const char* function = "Acam::Interface::acquire"; + std::ostringstream message; // Help // @@ -3340,8 +3305,8 @@ logwrite( function, message.str() ); * */ long Target::acquire( Acam::TargetAcquisitionModes requested_mode ) { - std::string function = "Acam::Target::acquire"; - std::stringstream message; + const char* function = "Acam::Target::acquire"; + std::ostringstream message; // reset guide offset filtering parameters // @@ -3412,6 +3377,8 @@ logwrite( function, message.str() ); this->acquire_mode = requested_mode; + iface->publish_status(); + return NO_ERROR; } /***** Acam::Target::acquire ************************************************/ @@ -3437,8 +3404,8 @@ logwrite( function, message.str() ); * */ long Target::do_acquire() { - std::string function = "Acam::Target::do_acquire"; - std::stringstream message; + const char* function = "Acam::Target::do_acquire"; + std::ostringstream message; // Do nothing, return immediately if no acquisition mode selected // or if stop_acquisition is set. @@ -3797,6 +3764,8 @@ logwrite( function, message.str() ); logwrite( function, "ERROR writing to database: "+std::string(e.what()) ); } + iface->publish_status(); + return error; } /***** Acam::Target::do_acquire *********************************************/ @@ -3865,8 +3834,8 @@ logwrite( function, message.str() ); * */ void Interface::dothread_set_filter( Acam::Interface &iface, std::string filter_req ) { - std::string function = "Acam::Interface::dothread_set_filter"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_set_filter"; + std::ostringstream message; // get current filter, used to determine if it changed // @@ -3916,9 +3885,9 @@ logwrite( function, message.str() ); * */ void Interface::dothread_set_focus( Acam::Interface &iface, double focus_req ) { - std::string function = "Acam::Interface::dothread_set_focus"; - std::stringstream message; /***** + const char* function = "Acam::Interface::dothread_set_focus"; + std::ostringstream message; // get current focus, used to determine if it changed // double focus_og; @@ -3973,8 +3942,8 @@ logwrite( function, message.str() ); * */ void Interface::dothread_fpoffset( Acam::Interface &iface ) { - std::string function = "Acam::Interface::dothread_fpoffset"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_fpoffset"; + std::ostringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); logwrite( function, message.str() ); @@ -4000,8 +3969,8 @@ logwrite( function, message.str() ); */ void Interface::dothread_monitor_focus( Acam::Interface &iface ) { /***** - std::string function = "Acam::Interface::dothread_monitor_focus"; - std::stringstream message; + const char* function = "Acam::Interface::dothread_monitor_focus"; + std::ostringstream message; if ( iface.monitor_focus_state.load(std::memory_order_seq_cst) == Acam::FOCUS_MONITOR_RUNNING ) { logwrite( function, "thread already running" ); @@ -4089,8 +4058,8 @@ logwrite( function, message.str() ); * */ long Interface::shutdown( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::shutdown"; - std::stringstream message; + const char* function = "Acam::Interface::shutdown"; + std::ostringstream message; // Help // @@ -4148,8 +4117,8 @@ logwrite( function, message.str() ); * */ long Interface::test( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::test"; - std::stringstream message; + const char* function = "Acam::Interface::test"; + std::ostringstream message; std::vector tokens; long error = NO_ERROR; @@ -4735,8 +4704,8 @@ logwrite( function, message.str() ); * */ long Interface::exptime( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::exptime"; - std::stringstream message; + const char* function = "Acam::Interface::exptime"; + std::ostringstream message; long error=NO_ERROR; if ( args == "?" || args == "help" ) { @@ -4813,8 +4782,8 @@ logwrite( function, message.str() ); * */ long Interface::fan_mode( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::fan_mode"; - std::stringstream message; + const char* function = "Acam::Interface::fan_mode"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -4886,11 +4855,7 @@ logwrite( function, message.str() ); * */ long Interface::image_quality( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::image_quality"; - std::stringstream message; - // Help - // if ( args == "?" ) { retstring = ACAMD_QUALITY; retstring.append( "\n" ); @@ -4922,8 +4887,8 @@ logwrite( function, message.str() ); * */ long Interface::solve( std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::solve"; - std::stringstream message; + const char* function = "Acam::Interface::solve"; + std::ostringstream message; long error = NO_ERROR; std::string _imagename; std::string _wcsname; @@ -5052,11 +5017,8 @@ logwrite( function, message.str() ); * */ long Interface::collect_header_info() { - std::string function = "Acam::Interface::collect_header_info"; - std::stringstream message; - - // request external telemetry, results in struct telem. - // + // force subscribers to publish now, then wait + // esults in struct telem. this->request_snapshot(); this->wait_for_snapshots(); @@ -5200,8 +5162,8 @@ logwrite( function, message.str() ); * */ long Interface::target_coords( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::target_coords"; - std::stringstream message; + const char* function = "Acam::Interface::target_coords"; + std::ostringstream message; double _ra=NAN, _dec=NAN, _angle=NAN; std::string _name; @@ -5334,8 +5296,8 @@ logwrite( function, message.str() ); * */ long Interface::offset_cal( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::offset_cal"; - std::stringstream message; + const char* function = "Acam::Interface::offset_cal"; + std::ostringstream message; // Help // @@ -5407,7 +5369,7 @@ logwrite( function, message.str() ); // Form and send the acquire command. // This will change the target.acquire_mode to TARGET_ACQUIRE while it's acquiring. // - std::stringstream cmd; + std::ostringstream cmd; cmd << std::fixed << std::setprecision(6) << acam_ra << " " << acam_dec << " " << acam_angle << " acam"; error = this->acquire( cmd.str(), retstring ); @@ -5479,8 +5441,8 @@ logwrite( function, message.str() ); * */ long Interface::offset_goal( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::offset_goal"; - std::stringstream message; + const std::string function("Acam::Interface::offset_goal"); + std::ostringstream message; if ( args.empty() ) { message << this->target.dRA << " " << this->target.dDEC; @@ -5492,39 +5454,29 @@ logwrite( function, message.str() ); // if ( args == "?" ) { retstring = ACAMD_OFFSETGOAL; - retstring.append( " [ ]\n" ); + retstring.append( " [ [ fineguiding ]\n" ); retstring.append( " Apply offsets to the ACAM goal coordinates.\n" ); retstring.append( " These offsets are applied only while guiding. If omitted,\n" ); retstring.append( " the current offsets are returned. Units are in degrees.\n" ); + retstring.append( " The optional 'fineguiding' is used for slicecam fine acquisition.\n" ); return HELP; } - std::vector tokens; - Tokenize( args, tokens, " " ); + std::istringstream iss(args); - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected " ); + double dRA=NAN, dDEC=NAN; + if (!(iss >> dRA >> dDEC) || + (std::isnan(dRA) || std::isnan(dDEC)) ) { + logwrite( function, "ERROR expected [ fineguiding ]" ); retstring="invalid_argument"; return ERROR; } + this->target.dRA = dRA; + this->target.dDEC = dDEC; - // Convert the input string to double and save in the class - // - try { - double dRA = std::stod( tokens.at(0) ); - double dDEC = std::stod( tokens.at(1) ); - - if (std::isnan(dRA) || std::isnan(dDEC)) throw std::invalid_argument("NaN value encountered"); - - this->target.dRA = dRA; - this->target.dDEC = dDEC; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing " << args << ": " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } + // optional fineguiding flag used for slicecam fineacquisition mode + std::string flag; + bool is_fineguiding = (iss >> flag && flag == "fineguiding"); // Apply any dRA, dDEC goal offsets from the "put on slit" action to // acam_ra_goal, acam_dec_goal. These dRA,dDEC offsets can come from @@ -5540,14 +5492,23 @@ logwrite( function, message.str() ); retstring = message.str(); if ( this->target.acquire_mode == Acam::TARGET_GUIDE ) { - this->target.acquire_mode = Acam::TARGET_ACQUIRE; - this->target.nacquired = 0; - this->target.attempts = 0; - this->target.sequential_failures = 0; - this->target.timeout_time = std::chrono::steady_clock::now() - + std::chrono::duration(this->target.timeout); + // for slicecam fine aquisition/guiding, stay in TARGET_GUIDE but + // reset the filtering so the goal takes effect quickly + if ( is_fineguiding ) { + this->target.reset_offset_params(); + } + else { + this->target.acquire_mode = Acam::TARGET_ACQUIRE; + this->target.nacquired = 0; + this->target.attempts = 0; + this->target.sequential_failures = 0; + this->target.timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration(this->target.timeout); + } } + this->publish_status(); + return NO_ERROR; } /***** Acam::Interface::offset_goal *****************************************/ @@ -5562,8 +5523,8 @@ logwrite( function, message.str() ); * */ long Interface::put_on_slit( const std::string args, std::string &retstring ) { - std::string function = "Acam::Interface::put_on_slit"; - std::stringstream message; + const char* function = "Acam::Interface::put_on_slit"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -5677,7 +5638,7 @@ logwrite( function, message.str() ); return; } - std::stringstream fn; + std::ostringstream fn; fn << path << "/" << basename << "_" << std::setfill('0') << std::setw(5) << npreserve << ".fits"; // increment until a unique file is found so that it never overwrites diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 673d26ed..8c4b317e 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -24,6 +24,7 @@ #include "tcsd_client.h" #include "skyinfo.h" #include "database.h" +#include "message_keys.h" #ifdef ANDORSIM #include "andorsim.h" @@ -510,6 +511,13 @@ namespace Acam { std::mutex framegrab_mtx; std::condition_variable cv; + struct { + std::string acquire_mode = ""; + bool is_acquired = false; + int nacquired = 0; + int attempts = 0; + } last_status; + public: std::string motion_host; @@ -567,8 +575,10 @@ namespace Acam { nskip_preserve_frames(0), newframe_ready(false), snapshot_status { - {"tcsd", false}, - {"slitd", false} + {Topic::TCSD, false}, + {Topic::SLITD, false}, + {Topic::TARGETINFO, false}, + {Topic::ACAMD, false} }, subscriber(std::make_unique(context, Common::PubSub::Mode::SUB)), is_subscriber_thread_running(false), @@ -576,13 +586,13 @@ namespace Acam { { target.set_interface_instance( this ); ///< Set the Interface instance in Target topic_handlers = { - { "_snapshot", std::function( + { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, - { "tcsd", std::function( + { Topic::TCSD, std::function( [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, - { "targetinfo", std::function( + { Topic::TARGETINFO, std::function( [this](const nlohmann::json &msg) { handletopic_targetinfo(msg); } ) }, - { "slitd", std::function( + { Topic::SLITD, std::function( [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) } }; } @@ -645,6 +655,7 @@ namespace Acam { long bin( std::string args, std::string &retstring ); void publish_snapshot(); + void publish_status(bool force=false); void request_snapshot(); bool wait_for_snapshots(); long handle_json_message( std::string message_in ); diff --git a/acamd/acamd.cpp b/acamd/acamd.cpp index 52028d0d..0bc34dcb 100644 --- a/acamd/acamd.cpp +++ b/acamd/acamd.cpp @@ -174,7 +174,9 @@ int main(int argc, char **argv) { // initialize the pub/sub handler and give it time to start // - if ( acamd.interface.init_pubsub( {"tcsd", "targetinfo", "slitd"} ) == ERROR ) { + if ( acamd.interface.init_pubsub( { Topic::TCSD, + Topic::TARGETINFO, + Topic::SLITD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); acamd.exit_cleanly(); } diff --git a/common/common.h b/common/common.h index 960a3120..d540dbd3 100644 --- a/common/common.h +++ b/common/common.h @@ -733,6 +733,34 @@ namespace Common { } return; } + + template + T get_key(const std::string &keyname) const { + auto it = this->keydb.find(keyname); + if (it == this->keydb.end()) { + throw std::out_of_range("FitsKeys::get_key '"+keyname+"' not found"); + } + const std::string &val = it->second.keyvalue; + + try { + if constexpr(std::is_same_v) return std::stod(val); + else + if constexpr(std::is_same_v) return std::stof(val); + else + if constexpr(std::is_same_v) return std::stoi(val); + else + if constexpr(std::is_same_v) return std::stol(val); + else + if constexpr(std::is_same_v) return (val=="T"||val=="true"||val=="1"); + else + if constexpr(std::is_same_v) return val; + else + static_assert(std::is_same_v, "FitsKeys::get_key unsupported type"); + } + catch (const std::exception &e) { + throw std::runtime_error("FitsKeys::get_key '"+keyname+"' could not convert '"+val+"'"); + } + } }; /**************** Common::FitsKeys ******************************************/ diff --git a/common/message_keys.h b/common/message_keys.h index 0155cc8c..ab7d8a46 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -10,17 +10,44 @@ namespace Topic { inline const std::string SNAPSHOT = "_snapshot"; + inline const std::string TARGETINFO = "targetinfo"; inline const std::string TCSD = "tcsd"; - inline const std::string TARGETINFO = "tcsd"; inline const std::string SLITD = "slitd"; inline const std::string CAMERAD = "camerad"; + inline const std::string ACAMD = "acamd"; + inline const std::string SEQ_DAEMONSTATE = "seq_daemonstate"; + inline const std::string SEQ_SEQSTATE = "seq_seqstate"; + inline const std::string SEQ_THREADSTATE = "seq_threadstate"; + inline const std::string SEQ_WAITSTATE = "seq_waitstate"; + inline const std::string SLICECAMD = "slicecamd"; } namespace Key { inline const std::string SOURCE = "source"; + namespace Sequencer { + inline const std::string SEQSTATE = "seqstate"; + } + namespace Camerad { inline const std::string READY = "ready"; } + + namespace Acamd { + inline const std::string TANDOR = "tandor"; + inline const std::string FILTER = "filter"; + inline const std::string COVER = "cover"; + inline const std::string ACQUIRE_MODE = "acquire_mode"; + inline const std::string IS_ACQUIRED = "is_acquired"; + inline const std::string NACQUIRED = "nacquired"; + inline const std::string ATTEMPTS = "attempts"; + inline const std::string SEEING = "seeing"; + inline const std::string BACKGROUND = "background"; + } + + namespace Slicecamd { + inline const std::string FINEACQUIRE_LOCKED = "fineacquire_locked"; + inline const std::string FINEACQUIRE_RUNNING = "fineacquire_running"; + } } diff --git a/common/slicecamd_commands.h b/common/slicecamd_commands.h index 1ddad325..45423c2f 100644 --- a/common/slicecamd_commands.h +++ b/common/slicecamd_commands.h @@ -24,6 +24,7 @@ const std::string SLICECAMD_EMULATOR = "emulator"; ///< set/get Andor emulator const std::string SLICECAMD_EXIT = "exit"; ///< const std::string SLICECAMD_EXPTIME = "exptime"; ///< set/get camera exposure time const std::string SLICECAMD_FAN = "fan"; ///< set Andor fan mode +const std::string SLICECAMD_FINEACQUIRE = "fineacquire"; ///< fine acquisition const std::string SLICECAMD_GUISET = "guiset"; ///< set params for gui display const std::string SLICECAMD_INIT = "init"; ///< *** const std::string SLICECAMD_ISACQUIRED = "isacquired"; ///< is the target acquired? @@ -53,6 +54,7 @@ const std::vector SLICECAMD_SYNTAX = { SLICECAMD_TCSISCONNECTED+" [ ? ]", SLICECAMD_TCSISOPEN+" [ ? ]", " CAMERA COMMANDS:", + SLICECAMD_FINEACQUIRE+" [ ? | status | stop | start { L | R } ]", SLICECAMD_AVGFRAMES+" [ ? | ]", SLICECAMD_FRAMEGRAB+" [ ? | start | stop | one [ ] | status ]", SLICECAMD_FRAMEGRABFIX+" [ ? ]", diff --git a/emulator/power.cpp b/emulator/power.cpp index 55450108..ae21efbb 100644 --- a/emulator/power.cpp +++ b/emulator/power.cpp @@ -324,6 +324,7 @@ namespace PowerEmulator { std::cerr << get_timestamp() << function << "[DEBUG] plugmap for nps" << npsnum << ": " << it->first << " " << it->second << "\n"; } +#endif } catch( std::out_of_range &e ) { @@ -332,7 +333,6 @@ namespace PowerEmulator { retstring = retstream.str(); return( ERROR ); } -#endif return ( NO_ERROR ); } diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index fb685cac..dda2f133 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencerd.cpp ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp + ${SEQUENCER_DIR}/sequence_acquisition.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} ${PYTHON_DEV} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 7a265c19..5db8147d 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -22,7 +22,7 @@ namespace Sequencer { /** * @brief publishes snapshot of my telemetry * @details This publishes a JSON message containing a snapshot of my - * telemetry info when the subscriber receives the "_snapshot" + * telemetry info when the subscriber receives the Topic::SNAPSHOT * topic and the payload contains my daemon name. * @param[in] jmessage_in subscribed-received JSON message * @@ -43,7 +43,7 @@ namespace Sequencer { /***** Sequencer::Sequence::handletopic_camerad ****************************/ /** - * @brief handles camerad telemetry + * @brief handles Topic::CAMERAD telemetry * @param[in] jmessage subscribed-received JSON message * */ @@ -58,6 +58,40 @@ namespace Sequencer { /***** Sequencer::Sequence::handletopic_camerad ****************************/ + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + /** + * @brief handles Topic::SLICECAMD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_slicecamd(const nlohmann::json &jmessage) { + // set is_fineacquire_locked flag + bool fineacquirelocked; + Common::extract_telemetry_value( jmessage, Key::Slicecamd::FINEACQUIRE_LOCKED, fineacquirelocked ); + this->is_fineacquire_locked.store(fineacquirelocked, std::memory_order_relaxed); + std::lock_guard lock(this->fineacquire_mtx); + this->fineacquire_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + + + /***** Sequencer::Sequence::handletopic_acamd ******************************/ + /** + * @brief handles Topic::ACAMD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_acamd(const nlohmann::json &jmessage) { + // set is_acam_guiding flag + bool acquired; + Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); + this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + std::lock_guard lock(this->acam_mtx); + this->acam_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_acamd ******************************/ + + /***** Sequencer::Sequence::publish_snapshot *******************************/ /** * @brief publishes snapshot of my telemetry @@ -85,15 +119,15 @@ namespace Sequencer { */ void Sequence::publish_seqstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // sequencer state std::string seqstate( this->seq_state_manager.get_set_states() ); rtrim( seqstate ); - jmessage_out["seqstate"] = seqstate; + jmessage_out[Key::Sequencer::SEQSTATE] = seqstate; try { - this->publisher->publish( jmessage_out, "seq_seqstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_SEQSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_seqstate", @@ -114,7 +148,7 @@ namespace Sequencer { */ void Sequence::publish_waitstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // iterate through map of daemon state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not @@ -124,7 +158,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_waitstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_WAITSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_waitstate", @@ -144,7 +178,7 @@ namespace Sequencer { */ void Sequence::publish_daemonstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // iterate through map of daemon state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not @@ -154,7 +188,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_daemonstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_DAEMONSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_daemonstate", @@ -174,7 +208,7 @@ namespace Sequencer { */ void Sequence::publish_threadstate() { nlohmann::json jmessage_out; - jmessage_out["source"] = Sequencer::DAEMON_NAME; + jmessage_out[Key::SOURCE] = Sequencer::DAEMON_NAME; // iterate through map of thread state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not @@ -184,7 +218,7 @@ namespace Sequencer { } try { - this->publisher->publish( jmessage_out, "seq_threadstate" ); + this->publisher->publish( jmessage_out, Topic::SEQ_THREADSTATE ); } catch ( const std::exception &e ) { logwrite( "Sequencer::Sequence::publish_threadstate", @@ -373,6 +407,45 @@ namespace Sequencer { } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + /** + * @brief waits for the user to click a button, or cancel + * @details Use this when you just want to slow things down or get a + * cup of coffee instead of observing. + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_user() { + const std::string function("Sequencer::Sequence::wait_for_user"); + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); + + this->async.enqueue_and_log( function, "NOTICE: waiting for USER to send \"continue\" signal" ); + + while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); + } + + this->async.enqueue_and_log( function, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) + +" signal!" ); + } // end scope for wait_state = WAIT_USER + + if ( this->cancel_flag.load() ) { + this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + return ABORT; + } + + this->is_usercontinue.store(false); + + this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -513,10 +586,11 @@ namespace Sequencer { worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) } }; } + else { + // For any other pointmode (SLIT, or empty, which assumes SLIT), all // subsystems are readied. // - else { // set pointmode explicitly, in case it's empty this->target.pointmode = Acam::POINTMODE_SLIT; @@ -569,55 +643,36 @@ namespace Sequencer { break; } -/*** 12/17/24 move acquisition elsewhere? - * - * logwrite( function, "starting acquisition thread" ); ///< TODO @todo log to telemetry! - - * this->seq_state.set( Sequencer::SEQ_WAIT_ACQUIRE ); - * this->broadcast_seqstate(); - * std::thread( &Sequencer::Sequence::dothread_acquisition, this ).detach(); - ***/ - - // If not a calibration target then introduce a pause for the user - // to make adjustments, send offsets, etc. + // If not a calibration target then acquire, first acam then slicecam // if ( !this->target.iscal ) { - // waiting for user signal (or cancel) - // - // The sequencer is effectively paused waiting for user input. This - // gives the user a chance to ensure the correct target is on the slit, - // select offset stars, etc. - // - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - - this->async.enqueue_and_log( function, "NOTICE: waiting for USER to send \"continue\" signal" ); - - while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); + // start ACAM acquisition. If it fails then wait for user to continue or cancel. + if ( this->do_acam_acquire() != NO_ERROR ) { + this->async.enqueue_and_log( function, "WARNING acam acquisition failed" ); + if (this->wait_for_user()==ABORT) { + this->async.enqueue_and_log( function, "NOTICE: cancelled" ); + return; + } } - - this->async.enqueue_and_log( function, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) - +" signal!" ); - } // end scope for wait_state = WAIT_USER - - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; + else + // start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log( function, "WARNING slicecam fine acquisition failed" ); } + } - this->is_usercontinue.store(false); - - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); - - // Ensure slit offset is in "expose" position - // - auto slitset = std::async(std::launch::async, &Sequence::slit_set, this, Sequencer::VSM_EXPOSE); + if ( !this->target.iscal ) { + // send offsets. wait for user if that fails to continue or cancel. + if ( this->target_offset() == ERROR ) { + if (this->wait_for_user()==ABORT) { + this->async.enqueue_and_log( function, "NOTICE: cancelled" ); + return; + } + } + // ensure slit offset is in "expose" position when needed try { - error |= slitset.get(); + error |= this->slit_set(Sequencer::VSM_EXPOSE); } catch (const std::exception& e) { logwrite( function, "ERROR slit offset exception: "+std::string(e.what()) ); @@ -2029,10 +2084,6 @@ namespace Sequencer { // if ( this->cancel_flag.load() ) return NO_ERROR; - // if ontarget (not cancelled) then acquire target - // - if ( !this->cancel_flag.load() ) this->acamd.command( ACAMD_ACQUIRE ); - this->is_ontarget.store(false); // remember the last target that was tracked on @@ -2526,141 +2577,6 @@ namespace Sequencer { /***** Sequencer::Sequence::modify_exptime **********************************/ - /***** Sequencer::Sequence::dothread_acquisition ****************************/ - /** - * @brief performs the acqusition sequence - * @details this gets called by the move_to_target thread - * - * This function is spawned in a thread. - * - */ - void Sequence::dothread_acquisition() { - const std::string function("Sequencer::Sequence::dothread_acquisition"); - std::stringstream message; - std::stringstream cmd; - std::string reply; - long error = NO_ERROR; - - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); - - // Before sending target coordinates to ACAM, - // convert them to decimal and to ACAM coordinates. - // (fpoffsets.coords_* are always in degrees) - // - double ra_in = radec_to_decimal( this->target.ra_hms ) * TO_DEGREES; - double dec_in = radec_to_decimal( this->target.dec_dms ); - double angle_in = this->target.slitangle; - - // can't be NaN - // - bool ra_isnan = std::isnan( ra_in ); - bool dec_isnan = std::isnan( dec_in ); - - if ( ra_isnan || dec_isnan ) { - message.str(""); message << "ERROR: converting"; - if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } - if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } - message << " to decimal"; - this->async.enqueue_and_log( function, message.str() ); - this->thread_error_manager.set( THR_MOVE_TO_TARGET ); - return; - } - -// // Before sending the target coords to the ACAM, -// // convert them from to ACAM coordinates. -// // -// double ra_out, dec_out, angle_out; -// error = this->target.fpoffsets.compute_offset( this->target.pointmode, "ACAM", -// ra_in, dec_in, angle_in, -// ra_out, dec_out, angle_out ); -// -// // Send the ACQUIRE command to acamd, which requires -// // the target coordinates (from the database). -// // -// message.str(""); message << "starting target acquisition " << ra_out << " " -// << dec_out << " " -// << angle_out << " " -// << this->target.name; - message.str(""); message << "starting target acquisition " << ra_in << " " - << dec_in << " " - << angle_in << " " - << this->target.name; - logwrite( function, message.str() ); - cmd.str(""); cmd << ACAMD_ACQUIRE << " " << ra_in << " " - << dec_in << " " - << angle_in << " "; - - error = this->acamd.command( cmd.str(), reply ); - -/***** DONT CARE ABOUT ERRORS NOW -- NO CONDITION ON ACQ SUCCESS - if ( error != NO_ERROR ) { - this->thread_error_manager.set( THR_ACQUISITION ); // report error - message.str(""); message << "ERROR acquiring target"; - this->async.enqueue_and_log( function, message.str() ); - this->seq_state.clear( Sequencer::SEQ_WAIT_ACQUIRE ); // clear ACQUIRE bit - this->broadcast_seqstate(); - return; - } - - // The reply contains the timeout. - // Acam's acquisition sequence uses that timeout but the Sequencer - // will also use it here, so that it knows when to stop asking acamd - // for its acquisition status. - // - double timeout; - try { - timeout = std::stod( reply ); - } catch( std::out_of_range &e ) { - message.str(""); message << "ERROR parsing timeout \"" << reply << "\" from acam: " << e.what(); - logwrite( function, message.str() ); - this->thread_error_manager.set( THR_ACQUISITION ); // report any error - return; - } - - auto timeout_time = std::chrono::steady_clock::now() - + std::chrono::duration(timeout); - - reply.clear(); - - // Poll acamd while it is acquiring. Once finished, get the state. - // - bool acquiring = true; - do { - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - if (error==NO_ERROR) error = this->acamd.command( ACAMD_ACQUIRE, reply ); - acquiring = ( reply.find("acquiring") != std::string::npos ); - } while ( error==NO_ERROR && - acquiring && - std::chrono::steady_clock::now() < timeout_time ); - - // Acquisition loop complete so get the state - // - error = this->acamd.command( ACAMD_ISACQUIRED, reply ); - this->target.acquired = ( reply.find("true") != std::string::npos ); - - // set message - // - if ( std::chrono::steady_clock::now() >= timeout_time ) { // Timeout - this->thread_error_manager.set( THR_ACQUISITION ); - message.str(""); message << "ERROR failed to acquire within timeout"; - } - else - if ( error!=NO_ERROR ) { // Error polling - this->thread_error_manager.set( THR_ACQUISITION ); - message.str(""); message << "ERROR acquiring target"; - } - else { // Success - message.str(""); message << "NOTICE: target " << ( this->target.acquired ? "acquired" : "not acquired" ); - } - - this->async.enqueue_and_log( function, message.str() ); // log message -*****/ - - } - /***** Sequencer::Sequence::dothread_acquisition ****************************/ - - /***** Sequencer::Sequence::startup *****************************************/ /** * @brief performs nightly startup @@ -3443,36 +3359,34 @@ namespace Sequencer { */ long Sequence::target_offset() { const std::string function("Sequencer::Sequence::target_offset"); - long error=NO_ERROR; - bool is_guiding = false; - std::string reply; - if ( this->acamd.command( ACAMD_ACQUIRE, reply ) == NO_ERROR ) { - if ( reply.find( "guiding" ) != std::string::npos ) is_guiding = true; - } - else { - logwrite( function, "ERROR reading ACAM guide state, falling back to TCS offset" ); - } + // nothing to do if both ra and dec offsets are zero + if (this->target.offset_ra == 0.0 && + this->target.offset_dec == 0.0) return NO_ERROR; + + // zero TCS offsets before applying target offset + long error = this->tcsd.command( TCSD_ZERO_OFFSETS ); - if ( is_guiding ) { + // when ACAM is guiding, offsets are handled by changing his goal + if (error==NO_ERROR && this->is_acam_guiding.load()) { // ACAMD_OFFSETGOAL expects degrees; target offsets are arcsec const double dra_deg = this->target.offset_ra / 3600.0; const double ddec_deg = this->target.offset_dec / 3600.0; std::stringstream cmd; cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << dra_deg << " " << ddec_deg; error = this->acamd.command( cmd.str() ); - logwrite( function, "sent "+cmd.str()+" (guiding)" ); - return error; + } + else + // if ACAM is not guiding then I send the offsets directly to the TCS + if (error==NO_ERROR) { + std::ostringstream cmd; + cmd << TCSD_PTOFFSET << " " << this->target.offset_ra << " " << this->target.offset_dec; + error = this->tcsd.command( cmd.str() ); } - error = this->tcsd.command( TCSD_ZERO_OFFSETS ); - - std::stringstream cmd; - cmd << TCSD_PTOFFSET << " " << this->target.offset_ra << " " << this->target.offset_dec; - - error |= this->tcsd.command( cmd.str() ); - - logwrite( function, "sent "+cmd.str() ); + std::ostringstream oss; + oss << (error==NO_ERROR?"":"ERROR ") << "target offsets" << (error==NO_ERROR ? " " : " not ") << "applied"; + logwrite(function, oss.str()); return error; } @@ -4622,8 +4536,8 @@ namespace Sequencer { // Finally, spawn the acquisition thread // - logwrite( function, "spawning dothread_acquisition..." ); - if (error==NO_ERROR) std::thread( &Sequencer::Sequence::dothread_acquisition, this ).detach(); + logwrite( function, "spawning do_acam_acquire..." ); + if (error==NO_ERROR) std::thread( &Sequencer::Sequence::do_acam_acquire, this ).detach(); } else diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index c931cce2..ab86cc45 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -288,6 +288,8 @@ namespace Sequencer { std::atomic cancel_flag{false}; std::atomic is_ontarget{false}; ///< remotely set by the TCS operator to indicate that the target is ready std::atomic is_usercontinue{false}; ///< remotely set by the user to continue + std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? + std::atomic is_acam_guiding{false}; ///< is acam guiding? /** @brief safely runs function in a detached thread using lambda to catch exceptions */ @@ -339,6 +341,10 @@ namespace Sequencer { topic_handlers = { { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, + { Topic::ACAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, + { Topic::SLICECAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_slicecamd(msg); } ) }, { Topic::CAMERAD, std::function( [this](const nlohmann::json &msg) { handletopic_camerad(msg); } ) } }; @@ -384,6 +390,10 @@ namespace Sequencer { /// std::mutex tcs_ontarget_mtx; /// std::condition_variable tcs_ontarget_cv; + std::mutex fineacquire_mtx; + std::condition_variable fineacquire_cv; + std::mutex acam_mtx; + std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; std::mutex wait_mtx; @@ -456,6 +466,8 @@ namespace Sequencer { void handletopic_snapshot( const nlohmann::json &jmessage ); void handletopic_camerad( const nlohmann::json &jmessage ); + void handletopic_acamd( const nlohmann::json &jmessage ); + void handletopic_slicecamd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); void publish_seqstate(); @@ -546,9 +558,9 @@ namespace Sequencer { void stop_exposure(); ///< stop exposure timer in progress long repeat_exposure(); ///< repeat the last exposure void modify_exptime( double exptime_in ); ///< modify exptime while exposure running - void dothread_acquisition(); /// performs the acquisition sequence when signalled void dothread_test(); + long wait_for_user(); ///< wait for the user or cancel void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params @@ -558,6 +570,13 @@ namespace Sequencer { long focus_set(); long flexure_set(); + /** + * these are in sequence_acquisition.cpp + */ + long do_acam_acquire(); + long do_slicecam_fineacquire(); + + long acam_init(); ///< initializes connection to acamd long calib_init(); ///< initializes connection to calibd long camera_init(); ///< initializes connection to camerad diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp new file mode 100644 index 00000000..7beab093 --- /dev/null +++ b/sequencerd/sequence_acquisition.cpp @@ -0,0 +1,120 @@ +/** + * @file sequence_acquisition.cpp + * @brief target acquisition code for the Sequence class + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::do_acam_acquire **********************************/ + /** + * @brief trigger ACAM acquisition and wait until guiding state reached + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_acam_acquire() { + const std::string function("Sequencer::Sequence::do_acam_acquire"); + std::string reply; + + ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); + + // form and send the ACQUIRE command to ACAM + // + double ra_in = radec_to_decimal( this->target.ra_hms ) * TO_DEGREES; + double dec_in = radec_to_decimal( this->target.dec_dms ); + double angle_in = this->target.slitangle; + + if ( std::isnan(ra_in) || std::isnan(dec_in) ) { + this->async.enqueue_and_log( function, "ERROR converting target coordinates to decimal" ); + return ERROR; + } + + std::ostringstream cmd; + cmd << ACAMD_ACQUIRE << " " << ra_in << " " << dec_in << " " << angle_in; + + this->async.enqueue_and_log( function, "NOTICE: starting ACAM acquisition" ); + + if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { + this->async.enqueue_and_log( function, "ERROR sending acquire command to acamd" ); + return ERROR; + } + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for is_acam_guiding (I subscribe to this) + // or cancel, or timeout + // + std::unique_lock lock(this->acam_mtx); + this->acam_cv.wait(lock, [&]() { + return this->is_acam_guiding.load() || this->cancel_flag.load() || + (use_timeout && std::chrono::steady_clock::now() > timeout_time); + }); + + if (this->cancel_flag.load()) return ABORT; + if (use_timeout && !this->is_acam_guiding.load()) { + this->async.enqueue_and_log(function, "ERROR ACAM acquisition timed out!"); + return TIMEOUT; + } + + this->async.enqueue_and_log(function, "ACAM target acquired"); + return NO_ERROR; + } + /***** Sequencer::Sequence::do_acam_acquire **********************************/ + + + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + /** + * @brief trigger SLICECAM fine acquisition and wait until locked + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_slicecam_fineacquire() { + const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); + + ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); + + // TODO don't hard-code the arguments here: + std::string reply; + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start L", reply ) != NO_ERROR) { + this->async.enqueue_and_log(function, "ERROR starting slicecam fine acquisition"); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + this->async.enqueue_and_log(function, "slicecam fine acquisition mode: "+reply); + return ERROR; + } + + this->async.enqueue_and_log(function, "NOTICE: slicecam fine acquisition started"); + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for is_fineacquire_locked (I subscribe to this) + // or cancel, or timeout + // + std::unique_lock lock(this->fineacquire_mtx); + this->fineacquire_cv.wait(lock, [&]() { + return this->is_fineacquire_locked.load() || this->cancel_flag.load() || + (use_timeout && std::chrono::steady_clock::now() > timeout_time); + }); + + if (this->cancel_flag.load()) return ABORT; + if (use_timeout && !this->is_fineacquire_locked.load()) { + this->async.enqueue_and_log(function, "ERROR slicecam fine acquisition timed out!"); + return TIMEOUT; + } + + this->async.enqueue_and_log(function, "slicecam fine acquisition target acquired"); + return NO_ERROR; + } + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + +} diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 841bb4f0..3b9ba63f 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -127,9 +127,11 @@ int main(int argc, char **argv) { sequencerd.exit_cleanly(); } - // initialize the pub/sub handler + // initialize the pub-sub handler with my subscriber topics // - if ( sequencerd.sequence.init_pubsub( {"camerad"} ) == ERROR ) { + if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, + Topic::ACAMD, + Topic::SLICECAMD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); } diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index ecfcfcd0..42969138 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -34,10 +34,12 @@ include_directories( ${PROJECT_BASE_DIR}/tcsd ) include_directories( ${PYTHON_DEV} ) add_executable(slicecamd - ${SLICECAMD_DIR}/slicecamd.cpp - ${SLICECAMD_DIR}/slicecam_server.cpp - ${SLICECAMD_DIR}/slicecam_interface.cpp + ${SLICECAMD_DIR}/slicecamd.cpp + ${SLICECAMD_DIR}/slicecam_server.cpp + ${SLICECAMD_DIR}/slicecam_interface.cpp + ${SLICECAMD_DIR}/slicecam_camera.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp + ${SLICECAMD_DIR}/slicecam_math.cpp ${PYTHON_DEV} ) diff --git a/slicecamd/guimanager.h b/slicecamd/guimanager.h new file mode 100644 index 00000000..2758fe41 --- /dev/null +++ b/slicecamd/guimanager.h @@ -0,0 +1,121 @@ +/** --------------------------------------------------------------------------- + * @file guimanager.h + * @brief slicecam display GUI manager + * @author David Hale + * + */ + +#pragma once + +/***** Slicecam ***************************************************************/ +/** + * @namespace Slicecam + * @brief namespace for slicer cameras + * + */ +namespace Slicecam { + + /***** Slicecam::GUIManager *************************************************/ + /** + * @class GUIManager + * @brief defines functions and settings for the display GUI + * + */ + class GUIManager { + private: + const std::string camera_name = "slicev"; + std::atomic update; ///push_settings=sh; } + + // sets the private variable push_image, call on config + inline void set_push_image( std::string sh ) { this->push_image=sh; } + + // sets the update flag true + inline void set_update() { this->update.store( true ); } + + /** + * @fn get_update + * @brief returns the update flag then clears it + * @return boolean true|false + */ + inline bool get_update() { return this->update.exchange( false ); } + + /** + * @fn get_message_string + * @brief returns a formatted message of all gui settings + * @details This message is the return string to guideset command. + * @return string in form of + */ + std::string get_message_string() { + std::ostringstream oss; + if ( this->exptime < 0 ) oss << "ERR"; else { oss << std::fixed << std::setprecision(3) << this->exptime; } + oss << " "; + if ( this->gain < 1 ) oss << "ERR"; else { oss << std::fixed << std::setprecision(3) << this->gain; } + oss << " "; + if ( this->bin < 1 ) oss << "x"; else { oss << std::fixed << std::setprecision(3) << this->bin; } + oss << " "; + if ( std::isnan(this->navg) ) oss << "NaN"; else { oss << std::fixed << std::setprecision(2) << this->navg; } + return oss.str(); + } + + /** + * @brief calls the push_settings script with the formatted message string + * @details the script pushes the settings to the Guider GUI + */ + void push_gui_settings() { + const std::string function("Slicecam::GUIManager::push_gui_settings"); + std::ostringstream cmd; + cmd << push_settings << " " + << ( get_update() ? "true" : "false" ) << " " + << get_message_string(); + + if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { + logwrite( function, "ERROR updating GUI" ); + } + } + + void send_fifo_warning(const std::string &message) { + const std::string fifo_name("/tmp/.slicev_warning.fifo"); + std::ofstream fifo(fifo_name); + if (!fifo.is_open()) { + logwrite("Slicecam::GUIManager::send_fifo_warning", "failed to open " + fifo_name + " for writing"); + } + else { + fifo << message << std::endl; + fifo.close(); + } + } + + /** + * @brief calls the push_image script with the formatted message string + * @details the script pushes the indicated file to the Guider GUI display + * @param[in] filename fits file to send + */ + void push_gui_image( std::string_view filename ) { + const std::string function("Slicecam::GUIManager::push_gui_image"); + std::ostringstream cmd; + cmd << push_image << " " + << camera_name << " " + << filename; + + if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { + logwrite( function, "ERROR pushing image to GUI" ); + } + } + }; + /***** Slicecam::GUIManager *************************************************/ +} diff --git a/slicecamd/slicecam_camera.cpp b/slicecamd/slicecam_camera.cpp new file mode 100644 index 00000000..2c6963ca --- /dev/null +++ b/slicecamd/slicecam_camera.cpp @@ -0,0 +1,1032 @@ +/** + * @file slicecam_camera.cpp + * @brief this contains the implementation for Slicecam::Camera code + * @author David Hale + * + * This file contains the code for the Camera class in the Slicecam namespace, + * which deals directly with the camera. + * + */ + +#include "slicecam_camera.h" + +namespace Slicecam { + + /***** Slicecam::Camera::emulator *******************************************/ + /** + * @brief enable/disable Andor emulator + * @param[in] args optional state { ? help true false } + * @param[out] retstring return status { true false } + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::emulator( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::emulator"; + std::stringstream message; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_EMULATOR; + retstring.append( " [ ] [ true | false ]\n" ); + retstring.append( " Enable Andor emulator.\n" ); + retstring.append( " If the optional is omitted then command applies to both cameras.\n" ); + retstring.append( " If the optional { true false } argument is omitted then the current\n" ); + retstring.append( " state is returned.\n" ); + return HELP; + } + + std::vector tokens; + + Tokenize( args, tokens, " " ); + + if ( tokens.size() == 0 ) { + } + else + if ( tokens.size() == 1 ) { + } + else + if ( tokens.size() == 2 ) { + } + else { + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // Set the Andor state + // + if ( args == "true" ) { + for ( const auto &pair : this->andor ) { + pair.second->andor_emulator( true ); + } + } + else + if ( args == "false" ) { + for ( const auto &pair : this->andor ) { + pair.second->andor_emulator( false ); + } + } + else + if ( ! args.empty() ) { + message.str(""); message << "ERROR unrecognized arg " << args << ": expected \"true\" or \"false\""; + logwrite( function, message.str() ); + return ERROR; + } + + // Set the return string + // + message.str(""); + for ( const auto &pair : this->andor ) { + std::string_view which_andor = pair.second->get_andor_object(); + if ( which_andor == "sim" ) message << "true "; + else + if ( which_andor == "sdk" ) message << "false "; + else { + retstring="unknown "; + } + } + + retstring = message.str(); + + rtrim( retstring ); + + return NO_ERROR; + } + /***** Slicecam::Camera::emulator *******************************************/ + + + /***** Slicecam::Camera::open ***********************************************/ + /** + * @brief open connection to Andor and initialize SDK + * @param[in] which optionally specify which camera to open + * @param[in] args optional args to send to camera(s) + * @return ERROR | NO_ERROR + * + */ + long Camera::open( std::string which, std::string args ) { + std::string function = "Slicecam::Camera::open"; + std::stringstream message; + long error=NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // Get a map of camera handles, indexed by serial number. + // This must be called before open() because open() uses handles. + // + if ( this->handlemap.size() == 0 ) { + error = this->andor.begin()->second->get_handlemap( this->handlemap ); + } + + if (error==ERROR) { + logwrite( function, "ERROR no camera handles found!" ); + return ERROR; + } + + // make sure each configured Andor has an associated handle for his s/n + // + for ( const auto &pair : this->andor ) { + auto it = this->handlemap.find(pair.second->camera_info.serial_number); + if ( it == this->handlemap.end() ) { + message.str(""); message << "ERROR no camera handle found for s/n " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + return ERROR; + } + pair.second->camera_info.handle = this->handlemap[pair.second->camera_info.serial_number]; + } + + long ret; + + // Loop through all defined Andors + // + for ( const auto &pair : this->andor ) { + + // get a copy of the Andor::DefaultValues object for + // the currently indexed andor + // + auto cfg = this->default_config[pair.first]; + + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // otherwise, open this camera if not already open + // + if ( !pair.second->is_open() ) { + if ( ( ret=pair.second->open( pair.second->camera_info.handle )) != NO_ERROR ) { + message.str(""); message << "ERROR opening slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error = ret; // preserve the error state for the return value but try all + continue; + } + } + + // Now set up for single scan readout -- cannot software-trigger acquisition to + // support continuous readout for multiple cameras in the same process. + // + error |= pair.second->set_acquisition_mode( 1 ); // single scan + error |= pair.second->set_read_mode( 4 ); // image mode + error |= pair.second->set_vsspeed( 4.33 ); // vertical shift speed + error |= pair.second->set_hsspeed( 1.0 ); // horizontal shift speed + error |= pair.second->set_shutter( "open" ); // shutter always open + error |= pair.second->set_imrot( cfg.rotstr ); // set imrot to configured value + error |= pair.second->set_imflip( cfg.hflip, cfg.vflip ); // set imflip to configured value + error |= pair.second->set_binning( cfg.hbin, cfg.vbin ); // set binning to configured value + error |= pair.second->set_temperature( cfg.setpoint ); // set temp setpoint to configured value + error |= this->set_gain(pair.first, 1); + error |= this->set_exptime(pair.first, 1 ); + + if ( error != NO_ERROR ) { + message.str(""); message << "ERROR configuring slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + } + } + + return error; + } + /***** Slicecam::Camera::open ***********************************************/ + + + /***** Slicecam::Camera::close **********************************************/ + /** + * @brief close connection to Andor + * @return ERROR or NO_ERROR + * + */ + long Camera::close() { + std::string function = "Slicecam::Camera::close"; + std::stringstream message; + long error=NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // loop through and close all (configured) Andors + // + for ( const auto &pair : this->andor ) { + long ret = pair.second->close(); + if ( ret != NO_ERROR ) { + message.str(""); message << "ERROR closing slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error = ret; // preserve the error state for the return value + } + } + + this->handlemap.clear(); + + return error; + } + /***** Slicecam::Camera::close **********************************************/ + + + /***** Slicecam::Camera::bin ************************************************/ + /** + * @brief set camera binning + * @param[in] hbin horizontal binning factor + * @param[in] vbin vertical binning factor + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::bin( const int hbin, const int vbin ) { + std::string function = "Slicecam::Camera::bin"; + std::stringstream message; + long error = NO_ERROR; + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + return ERROR; + } + + // all configured Andors must have been initialized + // + for ( const auto &pair : this->andor ) { + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Set the binning parameters now for each sequentially + // + for ( auto &[name, cam] : this->andor ) { + error |= cam->set_binning( hbin, vbin ); + } + + return error; + } + /***** Slicecam::Camera::bin ************************************************/ + + + /***** Slicecam::Camera::set_fan ********************************************/ + /** + * @brief set fan mode + * @param[in] which { L R } + * @param[in] mode { 0 1 2 } + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::set_fan( std::string which, int mode ) { + std::string function = "Slicecam::Camera::set_fan"; + std::stringstream message; + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + return ERROR; + } + + // make sure requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + return ERROR; + } + + // set the mode + // + return this->andor[which]->set_fan( mode ); + } + /***** Slicecam::Camera::set_fan ********************************************/ + + + /***** Slicecam::Camera::imflip *********************************************/ + /** + * @brief set or get camera image flip + * @param[in] args optionally contains (0=false 1=true) + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::imflip( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::imflip"; + std::stringstream message; + long error = NO_ERROR; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_IMFLIP; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get CCD image flip for camera = L | R.\n" ); + retstring.append( " and indicate to flip horizontally and\n" ); + retstring.append( " vertically, respectively. Set these =1 to enable flipping,\n" ); + retstring.append( " or =0 to disable flipping the indicated axis. When setting\n" ); + retstring.append( " either, both must be supplied. If both omitted then the\n" ); + retstring.append( " current flip states are returned.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // tokenize the args to get the camera name and the flip args + // + std::vector tokens; + Tokenize( args, tokens, " " ); + + if ( tokens.size() != 3 ) { + logwrite( function, "ERROR expected 3 args L|R " ); + retstring="invalid_argument"; + return ERROR; + } + + std::string which; + int hflip, vflip; + + try { + which = tokens.at(0); + hflip = std::stoi(tokens.at(1)); + vflip = std::stoi(tokens.at(2)); + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR processing args: " << e.what(); + logwrite( function, message.str() ); + retstring="argument_exception"; + return ERROR; + } + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + retstring="invalid_argument"; + return ERROR; + } + + // make sure the requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + retstring="not_open"; + return ERROR; + } + + // perform the flip + // + error = this->andor[which]->set_imflip( hflip, vflip ); + + if ( error == NO_ERROR ) { + hflip = this->andor[which]->camera_info.hflip; + vflip = this->andor[which]->camera_info.vflip; + } + + message.str(""); message << hflip << " " << vflip; + retstring = message.str(); + logwrite( function, retstring ); + + return error; + } + /***** Slicecam::Camera::imflip *********************************************/ + + + /***** Slicecam::Camera::imrot **********************************************/ + /** + * @brief set camera image rotation + * @param[in] args optionally contains "cw" or "ccw" + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::imrot( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::imrot"; + std::stringstream message; + long error = NO_ERROR; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_IMROT; + retstring.append( " [ ]\n" ); + retstring.append( " Set CCD image rotation for camera where is { none cw ccw }\n" ); + retstring.append( " is L | R\n" ); + retstring.append( " and \"cw\" will rotate 90 degrees clockwise,\n" ); + retstring.append( " \"ccw\" will rotate 90 degrees counter-clockwise,\n" ); + retstring.append( " \"none\" will set the rotation to none.\n" ); + retstring.append( " If used in conjuction with \"" + SLICECAMD_IMFLIP + "\" the rotation will\n" ); + retstring.append( " occur before the flip regardless of which order the commands are\n" ); + retstring.append( " sent. 180 degree rotation can be achieved using the \"" + SLICECAMD_IMFLIP + "\"\n" ); + retstring.append( " command by selecting both horizontal and vertical flipping.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // tokenize the args to get the camera name and the rot arg + // + std::vector tokens; + Tokenize( args, tokens, " " ); + + if ( tokens.size() != 2 ) { + logwrite( function, "ERROR expected 2 args L|R " ); + retstring="invalid_argument"; + return ERROR; + } + + std::string which; + int rotdir; + + // assign the numeric rotdir value from the string argument + // + try { + which = tokens.at(0); + // convert to lowercase + std::transform( tokens.at(1).begin(), tokens.at(1).end(), tokens.at(1).begin(), ::tolower ); + if ( tokens.at(1) == "none" ) rotdir = 0; + else + if ( tokens.at(1) == "cw" ) rotdir = 1; + else + if ( tokens.at(1) == "ccw" ) rotdir = 2; + else { + message.str(""); message << "ERROR bad arg " << tokens.at(1) << ": expected { none cw ccw }"; + logwrite( function, message.str() ); + return ERROR; + } + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR processing args: " << e.what(); + logwrite( function, message.str() ); + retstring="argument_exception"; + return ERROR; + } + + // make sure requested camera is in the map + // + auto it = this->andor.find( which ); + if ( it == this->andor.end() ) { + message.str(""); message << "ERROR invalid camera name \"" << which << "\""; + logwrite( function, message.str() ); + retstring="invalid_argument"; + return ERROR; + } + + // make sure requested camera is open + // + if ( ! this->andor[which]->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << which + << " S/N " << this->andor[which]->camera_info.serial_number; + logwrite( function, message.str() ); + retstring="not_open"; + return ERROR; + } + + // perform the image rotation + // + error = this->andor[which]->set_imrot( rotdir ); + + return error; + } + /***** Slicecam::Camera::imrot **********************************************/ + + + /***** Slicecam::Camera::set_exptime ****************************************/ + /** + * @brief set exposure time + * @details This will stop an acquisition in progress before setting the + * exposure time. The actual exposure time is returned in the + * reference argument. + * @param[in] fval reference to exposure time + * @return ERROR | NO_ERROR + * + * This function is overloaded + * + */ + long Camera::set_exptime( float &fval ) { + return set_exptime("", fval); + } + /***** Slicecam::Camera::set_exptime ****************************************/ + long Camera::set_exptime( std::string which, float &fval ) { + + long error = NO_ERROR; + + for ( const auto &pair : this->andor ) { + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // Ensure aquisition has stopped + // + error |= pair.second->abort_acquisition(); + + // Set the exposure time on the Andor. + // This will modify val with actual exptime. + // + if (error==NO_ERROR) error |= pair.second->set_exptime( fval ); + std::stringstream message; + message.str(""); message << "[DEBUG] set exptime to " << fval + << " for camera " << pair.second->camera_info.camera_name; + logwrite( "Slicecam::Camera::set_exptime", message.str() ); + } + + return error; + } + /***** Slicecam::Camera::set_exptime ****************************************/ + /** + * @brief set exposure time + * @details This overloaded version takes an rvalue reference to accept a + * temporary float used to call the other set_exptime function. + * Use this to set exptime with an rvalue instead of an lvalue. + * @param[in] fval rvalue reference to exposure time + * @return ERROR | NO_ERROR + */ + long Camera::set_exptime( float &&fval ) { + return set_exptime("", fval); + } + long Camera::set_exptime( std::string which, float &&fval ) { + float retval=fval; + return set_exptime(which, retval); + } + /***** Slicecam::Camera::set_exptime ****************************************/ + + + /***** Slicecam::Camera::set_gain *******************************************/ + /** + * @brief set or get the CCD gain + * @details The output amplifier is automatically set based on gain. + * If gain=1 then set to conventional amp and if gain > 1 + * then set the EMCCD gain register. + * @param[in] args optionally contains new gain + * @param[out] retstring return string contains temp, setpoint, and status + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::set_gain( int &gain ) { + return set_gain("", gain); + } + long Camera::set_gain( std::string which, int &gain ) { + std::string function = "Slicecam::Camera::set_gain"; + std::stringstream message; + long error = NO_ERROR; + + // get gain range + // + int low=999, high=-1; + error = this->andor.begin()->second->get_emgain_range( low, high ); + + // Loop through all defined Andors + // + for ( const auto &pair : this->andor ) { + // If a "which" was specified AND it's not this one, then skip it + // + if ( !which.empty() && pair.first != which ) continue; + + // camera must be open + // + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + continue; + } + + message.str(""); message << "[DEBUG] set gain to " << gain + << " for camera " << pair.second->camera_info.camera_name; + logwrite( function, message.str() ); + + if ( error==NO_ERROR && gain == 1 ) { + error = pair.second->set_output_amplifier( Andor::AMPTYPE_CONV ); + if (error==NO_ERROR) { + for ( const auto &pair : this->andor ) { + pair.second->camera_info.gain = 1; + } + } + else { message << "ERROR gain not set"; } + } + else + if ( error==NO_ERROR && gain >= low && gain <= high ) { + error |= pair.second->set_output_amplifier( Andor::AMPTYPE_EMCCD ); + if (error==NO_ERROR) pair.second->set_emgain( gain ); + if (error==NO_ERROR) pair.second->camera_info.gain = gain; + else { message << "ERROR gain not set"; } + } + else + if ( error==NO_ERROR ) { + message.str(""); message << "ERROR: gain " << gain << " outside range { 1, " + << low << ":" << high << " }"; + error = ERROR; + } + if ( !message.str().empty() ) logwrite( function, message.str() ); + + // The image gets flipped when the EM gain is used. + // This flips it back. + // + if (error==NO_ERROR && pair.first=="L") { + if (gain>1) { + error=this->andor["L"]->set_imflip( (default_config["L"].hflip==1?0:1), default_config["L"].vflip ); + } + else error=this->andor["L"]->set_imflip( default_config["L"].hflip, default_config["L"].vflip ); + } + if (error==NO_ERROR && pair.first=="R") { + if (gain>1) { + error=this->andor["R"]->set_imflip( (default_config["R"].hflip==1?0:1), default_config["R"].vflip ); + } + else error=this->andor["R"]->set_imflip( default_config["R"].hflip, default_config["R"].vflip ); + } + } + + // Regardless of setting the gain, always return what's in the camera + // + gain = this->andor.begin()->second->camera_info.gain; + + return error; + } + long Camera::set_gain( int &&gain ) { + return set_gain("", gain); + } + long Camera::set_gain( std::string which, int &&gain ) { + return set_gain(which, gain); + } + /***** Slicecam::Camera::set_gain *******************************************/ + + + /***** Slicecam::Camera::speed **********************************************/ + /** + * @brief set or get the CCD clocking speeds + * @param[in] args optionally contains new clocking speeds + * @param[out] retstring return string contains clocking speeds + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::speed( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::speed"; + std::stringstream message; + long error = NO_ERROR; + float hori=-1, vert=-1; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_SPEED; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get CCD clocking speeds for horizontal and vertical \n" ); + retstring.append( " If speeds are omitted then the current speeds are returned.\n" ); +/*** + if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { + auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map + retstring.append( " Current amp type is " ); + retstring.append( ( cam->camera_info.amptype == Andor::AMPTYPE_EMCCD ? "EMCCD\n" : "conventional\n" ) ); + retstring.append( " Select from {" ); + for ( const auto &hspeed : cam->camera_info.hsspeeds[ cam->camera_info.amptype] ) { + retstring.append( " " ); + retstring.append( std::to_string( hspeed ) ); + } + retstring.append( " }\n" ); + retstring.append( " Select from {" ); + for ( const auto &vspeed : cam->camera_info.vsspeeds ) { + retstring.append( " " ); + retstring.append( std::to_string( vspeed ) ); + } + retstring.append( " }\n" ); + retstring.append( " Units are MHz\n" ); + } + else { + retstring.append( " Open connection to camera to see possible speeds.\n" ); + } +***/ + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // all configured Andors must have been initialized + // +// for ( const auto &pair : this->andor ) { +// if ( ! pair.second->is_open() ) { + for ( auto &[name, cam] : this->andor ) { + if ( ! cam->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << cam->camera_info.camera_name + << " S/N " << cam->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Parse args if present + // + if ( !args.empty() ) { + + std::vector tokens; + Tokenize( args, tokens, " " ); + + // There must be only two args (the speeds) + // + if ( tokens.size() != 2 ) { + logwrite( function, "ERROR expected speeds" ); + return ERROR; + } + + // Parse the gain from the token + // + try { + hori = std::stof( tokens.at(0) ); + vert = std::stof( tokens.at(1) ); + } + catch ( std::out_of_range &e ) { + message.str(""); message << "ERROR reading speeds: " << e.what(); + error = ERROR; + } + catch ( std::invalid_argument &e ) { + message.str(""); message << "ERROR reading speeds: " << e.what(); + error = ERROR; + } + if (error==ERROR) logwrite( function, message.str() ); + + for ( const auto &pair : this->andor ) { + if (error!=ERROR ) error = pair.second->set_hsspeed( hori ); + if (error!=ERROR ) error = pair.second->set_vsspeed( vert ); + } + } + + message.str(""); + + for ( auto &[name, cam] : this->andor ) { + if ( ( cam->camera_info.hspeed < 0 ) || + ( cam->camera_info.vspeed < 0 ) ) { + message.str(""); message << "ERROR speeds not set for camera " << cam->camera_info.camera_name; + logwrite( function, message.str() ); + error = ERROR; + } + + message << cam->camera_info.camera_name << " " + << cam->camera_info.hspeed << " " << cam->camera_info.vspeed << " "; + } + + retstring = message.str(); + logwrite( function, retstring ); + + return error; + } + /***** Slicecam::Camera::speed **********************************************/ + + + /***** Slicecam::Camera::temperature ****************************************/ + /** + * @brief set or get the camera temperature setpoint + * @param[in] args optionally contains new setpoint + * @param[out] retstring return string contains + * @return ERROR | NO_ERROR | HELP + * + */ + long Camera::temperature( std::string args, std::string &retstring ) { + std::string function = "Slicecam::Camera::temperature"; + std::stringstream message; + long error = NO_ERROR; + int temp = 999; + + // Help + // + if ( args == "?" || args == "help" ) { + retstring = SLICECAMD_TEMP; + retstring.append( " [ ]\n" ); + retstring.append( " Set or get camera temperature in integer degrees C,\n" ); +/*** + if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { + auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map + retstring.append( " where is in range { " ); + retstring.append( std::to_string( cam->camera_info.temp_min ) ); + retstring.append( " " ); + retstring.append( std::to_string( cam->camera_info.temp_max ) ); + retstring.append( " }.\n" ); + } + else { + retstring.append( " open connection to camera to see acceptable range.\n" ); + } +***/ + retstring.append( " If optional is provided then the camera setpoint is changed,\n" ); + retstring.append( " else the current temperature, setpoint, and status are returned.\n" ); + retstring.append( " Format of return value is \n" ); + retstring.append( " Camera cooling is turned on/off automatically, as needed.\n" ); + return HELP; + } + + // If the STL map of Andors is empty then something went wrong + // with the configuration. + // + if ( this->andor.empty() ) { + logwrite( function, "ERROR no cameras defined" ); + retstring="bad_config"; + return ERROR; + } + + // all configured Andors must have been initialized + // + for ( const auto &pair : this->andor ) { + if ( ! pair.second->is_open() ) { + message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name + << " S/N " << pair.second->camera_info.serial_number; + logwrite( function, message.str() ); + error=ERROR; + } + } + if ( error==ERROR ) return ERROR; + + // Parse args if present + // + if ( !args.empty() ) { + + std::vector tokens; + Tokenize( args, tokens, " " ); + + // There can be only one arg (the requested temperature) + // + if ( tokens.size() != 1 ) { + logwrite( function, "ERROR too many arguments" ); + return ERROR; + } + + // Convert the temperature to int and set the temperature. + // Cooling will be automatically enabled/disabled as needed. + // + try { + temp = static_cast( std::round( std::stof( tokens.at(0) ) ) ); + for ( const auto &pair : this->andor ) { + message.str(""); message << "[DEBUG] set temp to " << temp + << " for camera " << pair.second->camera_info.camera_name; + logwrite( function, message.str() ); + error |= pair.second->set_temperature( temp ); + } + } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR setting temperature: " << e.what(); + error = ERROR; + } + } + if (error==ERROR) logwrite( function, message.str() ); + + // Regardless of setting the temperature, always read it. + // + message.str(""); + for ( const auto &pair : this->andor ) { + error |= pair.second->get_temperature(temp); + message << pair.second->camera_info.camera_name << " " << temp << " " + << pair.second->camera_info.setpoint << " " + << pair.second->camera_info.temp_status << " "; + } + logwrite( function, message.str() ); + + retstring = message.str(); + + return error; + } + /***** Slicecam::Camera::temperature ****************************************/ + + + /***** Slicecam::Camera::write_frame ****************************************/ + /** + * @brief write the Andor image data to FITS file + * @return ERROR or NO_ERROR + * + */ + long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { + std::string function = "Slicecam::Camera::write_frame"; + std::stringstream message; + + long error = NO_ERROR; + + fitsinfo.fits_name = outfile; +// fitsinfo.datatype = USHORT_IMG; + fitsinfo.datatype = FLOAT_IMG; + + fits_file.copy_info( fitsinfo ); // copy from fitsinfo to the fits_file class + + error = fits_file.open_file(); // open the fits file for writing + + if ( !source_file.empty() ) { + if (error==NO_ERROR) error = fits_file.copy_header_from( source_file ); + } + else { + if (error==NO_ERROR) error = fits_file.create_header(); // create basic header + } + + for ( auto &[name, cam] : this->andor ) { + cam->camera_info.section_size = cam->camera_info.axes[0] * cam->camera_info.axes[1]; + if ( cam->camera_info.section_size == 0 ) { + message.str(""); message << "ERROR section size 0 for slicecam " << cam->camera_info.camera_name; + logwrite( function, message.str() ); + error = ERROR; + break; + } + // cam is passed by reference + // + fits_file.write_image( cam ); // write the image data + } + + fits_file.close_file(); // close the file + + // This is the one extra call that is outside the normal workflow. + // If emulator is enabled then the skysim generator will create a simulated + // image. The image written above by fits_file.write_image() is used as + // input to skysim because it contains the correct WCS headers, but will + // ultimately be overwritten by the simulated image. + // + // Need only to make one call since it will generate a multi-extension + // image. + // + if ( !this->andor.empty() ) { + if ( this->andor.begin()->second->is_emulated() && _tcs_online ) { + this->andor.begin()->second->simulate_frame( fitsinfo.fits_name, + true, // multi-extension + this->simsize ); + } + } + + outfile = fitsinfo.fits_name; + + return error; + } + /***** Slicecam::Camera::write_frame ****************************************/ + + + std::vector Camera::get_image(const std::string &which) { + auto it = this->andor.find(which); + if (it==this->andor.end() || it->second==nullptr) return {}; + const auto &cam = it->second; +// if (cam->is_emulated()) { return this->read_from_file(which); +// logwrite("Slicecam::Camera::get_image", "[DEBUG] PROBLEM: is_emulated=false"); + const float* buf = cam->get_avg_data(); + if (buf==nullptr) return {}; + const long npix = cam->camera_info.axes[0]*cam->camera_info.axes[1]; + return std::vector(buf, buf+npix); + } + + + std::vector Camera::read_from_file(const std::string &extname) { + return{}; + } + std::vector Camera::read_from_file(const std::string &extname, long &ncols, long &nrows) { + const char* function = "Slicecam::Camera::read_image"; + try { + std::unique_ptr pInfile(new CCfits::FITS(fitsinfo.fits_name, CCfits::Read, false)); + CCfits::ExtHDU& ext = pInfile->extension(extname); + ncols = ext.axis(0); + nrows = ext.axis(1); + std::valarray tmp; + ext.read(tmp); + return std::vector(std::begin(tmp), std::end(tmp)); + } + catch (const CCfits::FitsException &e) { + logwrite(function, "ERROR CCfits: "+std::string(e.message())); + return {}; + } + catch (const std::exception &e) { + logwrite(function, "ERROR: "+std::string(e.what())); + return {}; + } + } +} diff --git a/slicecamd/slicecam_camera.h b/slicecamd/slicecam_camera.h new file mode 100644 index 00000000..f608fe2b --- /dev/null +++ b/slicecamd/slicecam_camera.h @@ -0,0 +1,88 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_camera.h + * @brief slicecam camera include + * @details defines the Slicecam Camera class + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "common.h" +#include "logentry.h" +#include "slicecam_fits.h" +#include "slicecamd_commands.h" + +/***** Slicecam ***************************************************************/ +/** + * @namespace Slicecam + * @brief namespace for slicer cameras + * + */ +namespace Slicecam { + + /***** Slicecam::Camera *****************************************************/ + /** + * @class Camera + * @brief Camera class + * + * This class is used for communicating with the slicecam camera directly (which is an Andor) + * + */ + class Camera { + private: + std::unique_ptr simdata; + int simsize; /// for the sky simulator + std::map handlemap; + + public: + Camera() : simsize(1024) { }; + + FITS_file fits_file; /// instantiate a FITS container object + FitsInfo fitsinfo; + + std::mutex framegrab_mutex; + + std::map> andor; ///< container for Andor::Interface objects + + std::map default_config; ///< container to hold defaults for each camera + + inline void copy_info() { fits_file.copy_info( fitsinfo ); } + inline void set_simsize( int val ) { if ( val > 0 ) this->simsize = val; else throw std::out_of_range("simsize must be greater than 0"); } + + inline long init_handlemap() { + this->handlemap.clear(); + return this->andor.begin()->second->get_handlemap( this->handlemap ); + } + + long emulator( std::string args, std::string &retstring ); + long open( std::string which, std::string args ); + long close(); + long get_frame(); + long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); + std::vector get_image(const std::string &which); + std::vector read_from_file(const std::string &which); + std::vector read_from_file(const std::string &which, long &ncols, long &nrows); + long bin( const int hbin, const int vbin ); + long set_fan( std::string which, int mode ); + long imflip( std::string args, std::string &retstring ); + long imrot( std::string args, std::string &retstring ); + long set_gain( int &gain ); + long set_gain( std::string which, int &gain ); + long set_gain( int &&gain ); + long set_gain( std::string which, int &&gain ); + long set_exptime( float &val ); + long set_exptime( std::string which, float &val ); + long set_exptime( float &&val ); + long set_exptime( std::string which, float &&val ); + long speed( std::string args, std::string &retstring ); + long temperature( std::string args, std::string &retstring ); + }; + /***** Slicecam::Camera *****************************************************/ +} diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index c793944f..fca6d53f 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -9,6 +9,7 @@ */ #include "slicecam_interface.h" +#include "slicecam_math.h" namespace Slicecam { @@ -16,985 +17,282 @@ namespace Slicecam { int npreserve=0; ///< counter used for Interface::preserve_framegrab() - /***** Slicecam::Camera::emulator *******************************************/ + /***** Slicecam::Interface::fineacquire *************************************/ /** - * @brief enable/disable Andor emulator - * @param[in] args optional state { ? help true false } - * @param[out] retstring return status { true false } - * @return ERROR | NO_ERROR | HELP + * @brief user-interface to start/stop fine target acquisition + * @param[in] args contains stop | + * @param[out] return string + * @return ERROR|NO_ERROR|HELP * */ - long Camera::emulator( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::emulator"; - std::stringstream message; + long Interface::fineacquire(std::string args, std::string &retstring) { + const char* function = "Slicecam::Interface::fineacquire"; + std::ostringstream message; // Help - // if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_EMULATOR; - retstring.append( " [ ] [ true | false ]\n" ); - retstring.append( " Enable Andor emulator.\n" ); - retstring.append( " If the optional is omitted then command applies to both cameras.\n" ); - retstring.append( " If the optional { true false } argument is omitted then the current\n" ); - retstring.append( " state is returned.\n" ); + retstring = SLICECAMD_FINEACQUIRE; + retstring.append( " stop | start [ { L | R } ] | [ status ]\n" ); + retstring.append( " start or stop fine target acquisition.\n" ); + retstring.append( " aimpoint is optional and uses configuration by default, but\n" ); + retstring.append( " if specified must contain both L or R to specify which camera,\n" ); + retstring.append( " and aimpoint , coordinates, which may be fractional pixels.\n" ); + retstring.append( " No argument (or optional 'status') returns status.\n" ); return HELP; } std::vector tokens; + Tokenize(args, tokens, " "); + const std::string action = tokens.empty() ? "status" : tokens.at(0); - Tokenize( args, tokens, " " ); - - if ( tokens.size() == 0 ) { - } - else - if ( tokens.size() == 1 ) { - } - else - if ( tokens.size() == 2 ) { - } - else { - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // Set the Andor state - // - if ( args == "true" ) { - for ( const auto &pair : this->andor ) { - pair.second->andor_emulator( true ); - } - } - else - if ( args == "false" ) { - for ( const auto &pair : this->andor ) { - pair.second->andor_emulator( false ); - } + // empty args returns status + if (action=="status") { + retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + return NO_ERROR; } else - if ( ! args.empty() ) { - message.str(""); message << "ERROR unrecognized arg " << args << ": expected \"true\" or \"false\""; - logwrite( function, message.str() ); - return ERROR; - } - - // Set the return string - // - message.str(""); - for ( const auto &pair : this->andor ) { - std::string_view which_andor = pair.second->get_andor_object(); - if ( which_andor == "sim" ) message << "true "; - else - if ( which_andor == "sdk" ) message << "false "; - else { - retstring="unknown "; - } - } - - retstring = message.str(); - - rtrim( retstring ); - - return NO_ERROR; - } - /***** Slicecam::Camera::emulator *******************************************/ - - - /***** Slicecam::Camera::open ***********************************************/ - /** - * @brief open connection to Andor and initialize SDK - * @param[in] which optionally specify which camera to open - * @param[in] args optional args to send to camera(s) - * @return ERROR | NO_ERROR - * - */ - long Camera::open( std::string which, std::string args ) { - std::string function = "Slicecam::Camera::open"; - std::stringstream message; - long error=NO_ERROR; - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // Get a map of camera handles, indexed by serial number. - // This must be called before open() because open() uses handles. - // - if ( this->handlemap.size() == 0 ) { - error = this->andor.begin()->second->get_handlemap( this->handlemap ); - } - - if (error==ERROR) { - logwrite( function, "ERROR no camera handles found!" ); - return ERROR; - } - - // make sure each configured Andor has an associated handle for his s/n - // - for ( const auto &pair : this->andor ) { - auto it = this->handlemap.find(pair.second->camera_info.serial_number); - if ( it == this->handlemap.end() ) { - message.str(""); message << "ERROR no camera handle found for s/n " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - return ERROR; - } - pair.second->camera_info.handle = this->handlemap[pair.second->camera_info.serial_number]; - } - - long ret; - - // Loop through all defined Andors - // - for ( const auto &pair : this->andor ) { - - // get a copy of the Andor::DefaultValues object for - // the currently indexed andor - // - auto cfg = this->default_config[pair.first]; - - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // otherwise, open this camera if not already open - // - if ( !pair.second->is_open() ) { - if ( ( ret=pair.second->open( pair.second->camera_info.handle )) != NO_ERROR ) { - message.str(""); message << "ERROR opening slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error = ret; // preserve the error state for the return value but try all - continue; - } - } - - // Now set up for single scan readout -- cannot software-trigger acquisition to - // support continuous readout for multiple cameras in the same process. - // - error |= pair.second->set_acquisition_mode( 1 ); // single scan - error |= pair.second->set_read_mode( 4 ); // image mode - error |= pair.second->set_vsspeed( 4.33 ); // vertical shift speed - error |= pair.second->set_hsspeed( 1.0 ); // horizontal shift speed - error |= pair.second->set_shutter( "open" ); // shutter always open - error |= pair.second->set_imrot( cfg.rotstr ); // set imrot to configured value - error |= pair.second->set_imflip( cfg.hflip, cfg.vflip ); // set imflip to configured value - error |= pair.second->set_binning( cfg.hbin, cfg.vbin ); // set binning to configured value - error |= pair.second->set_temperature( cfg.setpoint ); // set temp setpoint to configured value - error |= this->set_gain(pair.first, 1); - error |= this->set_exptime(pair.first, 1 ); - - if ( error != NO_ERROR ) { - message.str(""); message << "ERROR configuring slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - } - } - - return error; - } - /***** Slicecam::Camera::open ***********************************************/ - - - /***** Slicecam::Camera::close **********************************************/ - /** - * @brief close connection to Andor - * @return ERROR or NO_ERROR - * - */ - long Camera::close() { - std::string function = "Slicecam::Camera::close"; - std::stringstream message; - long error=NO_ERROR; - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // loop through and close all (configured) Andors - // - for ( const auto &pair : this->andor ) { - long ret = pair.second->close(); - if ( ret != NO_ERROR ) { - message.str(""); message << "ERROR closing slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error = ret; // preserve the error state for the return value - } - } - - this->handlemap.clear(); - - return error; - } - /***** Slicecam::Camera::close **********************************************/ - - - /***** Slicecam::Camera::bin ************************************************/ - /** - * @brief set camera binning - * @param[in] hbin horizontal binning factor - * @param[in] vbin vertical binning factor - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::bin( const int hbin, const int vbin ) { - std::string function = "Slicecam::Camera::bin"; - std::stringstream message; - long error = NO_ERROR; - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - return ERROR; - } - - // all configured Andors must have been initialized - // - for ( const auto &pair : this->andor ) { - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; + // stop fine acquisition + if (action=="stop") { + if (!this->is_fineacquire_running.load(std::memory_order_acquire)) { + logwrite(function, "stopped"); } - } - if ( error==ERROR ) return ERROR; - - // Set the binning parameters now for each sequentially - // - for ( auto &[name, cam] : this->andor ) { - error |= cam->set_binning( hbin, vbin ); - } - - return error; - } - /***** Slicecam::Camera::bin ************************************************/ - - - /***** Slicecam::Camera::set_fan ********************************************/ - /** - * @brief set fan mode - * @param[in] which { L R } - * @param[in] mode { 0 1 2 } - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::set_fan( std::string which, int mode ) { - std::string function = "Slicecam::Camera::set_fan"; - std::stringstream message; - - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); - return ERROR; - } - - // make sure requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - return ERROR; - } - - // set the mode - // - return this->andor[which]->set_fan( mode ); - } - /***** Slicecam::Camera::set_fan ********************************************/ - - - /***** Slicecam::Camera::imflip *********************************************/ - /** - * @brief set or get camera image flip - * @param[in] args optionally contains (0=false 1=true) - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::imflip( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::imflip"; - std::stringstream message; - long error = NO_ERROR; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_IMFLIP; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get CCD image flip for camera = L | R.\n" ); - retstring.append( " and indicate to flip horizontally and\n" ); - retstring.append( " vertically, respectively. Set these =1 to enable flipping,\n" ); - retstring.append( " or =0 to disable flipping the indicated axis. When setting\n" ); - retstring.append( " either, both must be supplied. If both omitted then the\n" ); - retstring.append( " current flip states are returned.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // tokenize the args to get the camera name and the flip args - // - std::vector tokens; - Tokenize( args, tokens, " " ); - - if ( tokens.size() != 3 ) { - logwrite( function, "ERROR expected 3 args L|R " ); - retstring="invalid_argument"; - return ERROR; - } - - std::string which; - int hflip, vflip; - - try { - which = tokens.at(0); - hflip = std::stoi(tokens.at(1)); - vflip = std::stoi(tokens.at(2)); - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR processing args: " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } - - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); - retstring="invalid_argument"; - return ERROR; - } - - // make sure the requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - retstring="not_open"; - return ERROR; - } - - // perform the flip - // - error = this->andor[which]->set_imflip( hflip, vflip ); - - if ( error == NO_ERROR ) { - hflip = this->andor[which]->camera_info.hflip; - vflip = this->andor[which]->camera_info.vflip; - } - - message.str(""); message << hflip << " " << vflip; - retstring = message.str(); - logwrite( function, retstring ); - - return error; - } - /***** Slicecam::Camera::imflip *********************************************/ - - - /***** Slicecam::Camera::imrot **********************************************/ - /** - * @brief set camera image rotation - * @param[in] args optionally contains "cw" or "ccw" - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::imrot( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::imrot"; - std::stringstream message; - long error = NO_ERROR; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_IMROT; - retstring.append( " [ ]\n" ); - retstring.append( " Set CCD image rotation for camera where is { none cw ccw }\n" ); - retstring.append( " is L | R\n" ); - retstring.append( " and \"cw\" will rotate 90 degrees clockwise,\n" ); - retstring.append( " \"ccw\" will rotate 90 degrees counter-clockwise,\n" ); - retstring.append( " \"none\" will set the rotation to none.\n" ); - retstring.append( " If used in conjuction with \"" + SLICECAMD_IMFLIP + "\" the rotation will\n" ); - retstring.append( " occur before the flip regardless of which order the commands are\n" ); - retstring.append( " sent. 180 degree rotation can be achieved using the \"" + SLICECAMD_IMFLIP + "\"\n" ); - retstring.append( " command by selecting both horizontal and vertical flipping.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // tokenize the args to get the camera name and the rot arg - // - std::vector tokens; - Tokenize( args, tokens, " " ); - - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected 2 args L|R " ); - retstring="invalid_argument"; - return ERROR; - } - - std::string which; - int rotdir; - - // assign the numeric rotdir value from the string argument - // - try { - which = tokens.at(0); - // convert to lowercase - std::transform( tokens.at(1).begin(), tokens.at(1).end(), tokens.at(1).begin(), ::tolower ); - if ( tokens.at(1) == "none" ) rotdir = 0; - else - if ( tokens.at(1) == "cw" ) rotdir = 1; - else - if ( tokens.at(1) == "ccw" ) rotdir = 2; else { - message.str(""); message << "ERROR bad arg " << tokens.at(1) << ": expected { none cw ccw }"; - logwrite( function, message.str() ); - return ERROR; + this->is_fineacquire_running.store(false); + this->publish_status(); + logwrite(function, "stop requested"); } + retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + return NO_ERROR; } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR processing args: " << e.what(); - logwrite( function, message.str() ); - retstring="argument_exception"; - return ERROR; - } + else - // make sure requested camera is in the map - // - auto it = this->andor.find( which ); - if ( it == this->andor.end() ) { - message.str(""); message << "ERROR invalid camera name \"" << which << "\""; - logwrite( function, message.str() ); + // not empty, stop or start is an error + if (action != "start" || tokens.size() < 2) { + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; } - // make sure requested camera is open - // - if ( ! this->andor[which]->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << which - << " S/N " << this->andor[which]->camera_info.serial_number; - logwrite( function, message.str() ); - retstring="not_open"; - return ERROR; - } - - // perform the image rotation - // - error = this->andor[which]->set_imrot( rotdir ); - - return error; - } - /***** Slicecam::Camera::imrot **********************************************/ - - - /***** Slicecam::Camera::set_exptime ****************************************/ - /** - * @brief set exposure time - * @details This will stop an acquisition in progress before setting the - * exposure time. The actual exposure time is returned in the - * reference argument. - * @param[in] fval reference to exposure time - * @return ERROR | NO_ERROR - * - * This function is overloaded - * - */ - long Camera::set_exptime( float &fval ) { - return set_exptime("", fval); - } - /***** Slicecam::Camera::set_exptime ****************************************/ - long Camera::set_exptime( std::string which, float &fval ) { - - long error = NO_ERROR; - - for ( const auto &pair : this->andor ) { - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // Ensure aquisition has stopped - // - error |= pair.second->abort_acquisition(); - - // Set the exposure time on the Andor. - // This will modify val with actual exptime. - // - if (error==NO_ERROR) error |= pair.second->set_exptime( fval ); - std::stringstream message; - message.str(""); message << "[DEBUG] set exptime to " << fval - << " for camera " << pair.second->camera_info.camera_name; - logwrite( "Slicecam::Camera::set_exptime", message.str() ); - } - - return error; - } - /***** Slicecam::Camera::set_exptime ****************************************/ - /** - * @brief set exposure time - * @details This overloaded version takes an rvalue reference to accept a - * temporary float used to call the other set_exptime function. - * Use this to set exptime with an rvalue instead of an lvalue. - * @param[in] fval rvalue reference to exposure time - * @return ERROR | NO_ERROR - */ - long Camera::set_exptime( float &&fval ) { - return set_exptime("", fval); - } - long Camera::set_exptime( std::string which, float &&fval ) { - float retval=fval; - return set_exptime(which, retval); - } - /***** Slicecam::Camera::set_exptime ****************************************/ - - - /***** Slicecam::Camera::set_gain *******************************************/ - /** - * @brief set or get the CCD gain - * @details The output amplifier is automatically set based on gain. - * If gain=1 then set to conventional amp and if gain > 1 - * then set the EMCCD gain register. - * @param[in] args optionally contains new gain - * @param[out] retstring return string contains temp, setpoint, and status - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::set_gain( int &gain ) { - return set_gain("", gain); - } - long Camera::set_gain( std::string which, int &gain ) { - std::string function = "Slicecam::Camera::set_gain"; - std::stringstream message; - long error = NO_ERROR; - - // get gain range - // - int low=999, high=-1; - error = this->andor.begin()->second->get_emgain_range( low, high ); - - // Loop through all defined Andors - // - for ( const auto &pair : this->andor ) { - // If a "which" was specified AND it's not this one, then skip it - // - if ( !which.empty() && pair.first != which ) continue; - - // camera must be open - // - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - continue; - } - - message.str(""); message << "[DEBUG] set gain to " << gain - << " for camera " << pair.second->camera_info.camera_name; - logwrite( function, message.str() ); - - if ( error==NO_ERROR && gain == 1 ) { - error = pair.second->set_output_amplifier( Andor::AMPTYPE_CONV ); - if (error==NO_ERROR) { - for ( const auto &pair : this->andor ) { - pair.second->camera_info.gain = 1; - } - } - else { message << "ERROR gain not set"; } - } - else - if ( error==NO_ERROR && gain >= low && gain <= high ) { - error |= pair.second->set_output_amplifier( Andor::AMPTYPE_EMCCD ); - if (error==NO_ERROR) pair.second->set_emgain( gain ); - if (error==NO_ERROR) pair.second->camera_info.gain = gain; - else { message << "ERROR gain not set"; } - } - else - if ( error==NO_ERROR ) { - message.str(""); message << "ERROR: gain " << gain << " outside range { 1, " - << low << ":" << high << " }"; - error = ERROR; - } - if ( !message.str().empty() ) logwrite( function, message.str() ); - - // The image gets flipped when the EM gain is used. - // This flips it back. - // - if (error==NO_ERROR && pair.first=="L") { - if (gain>1) { - error=this->andor["L"]->set_imflip( (default_config["L"].hflip==1?0:1), default_config["L"].vflip ); - } - else error=this->andor["L"]->set_imflip( default_config["L"].hflip, default_config["L"].vflip ); - } - if (error==NO_ERROR && pair.first=="R") { - if (gain>1) { - error=this->andor["R"]->set_imflip( (default_config["R"].hflip==1?0:1), default_config["R"].vflip ); - } - else error=this->andor["R"]->set_imflip( default_config["R"].hflip, default_config["R"].vflip ); - } - } - - // Regardless of setting the gain, always return what's in the camera - // - gain = this->andor.begin()->second->camera_info.gain; - - return error; - } - long Camera::set_gain( int &&gain ) { - return set_gain("", gain); - } - long Camera::set_gain( std::string which, int &&gain ) { - return set_gain(which, gain); - } - /***** Slicecam::Camera::set_gain *******************************************/ - - - /***** Slicecam::Camera::speed **********************************************/ - /** - * @brief set or get the CCD clocking speeds - * @param[in] args optionally contains new clocking speeds - * @param[out] retstring return string contains clocking speeds - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::speed( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::speed"; - std::stringstream message; - long error = NO_ERROR; - float hori=-1, vert=-1; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_SPEED; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get CCD clocking speeds for horizontal and vertical \n" ); - retstring.append( " If speeds are omitted then the current speeds are returned.\n" ); -/*** - if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { - auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map - retstring.append( " Current amp type is " ); - retstring.append( ( cam->camera_info.amptype == Andor::AMPTYPE_EMCCD ? "EMCCD\n" : "conventional\n" ) ); - retstring.append( " Select from {" ); - for ( const auto &hspeed : cam->camera_info.hsspeeds[ cam->camera_info.amptype] ) { - retstring.append( " " ); - retstring.append( std::to_string( hspeed ) ); - } - retstring.append( " }\n" ); - retstring.append( " Select from {" ); - for ( const auto &vspeed : cam->camera_info.vsspeeds ) { - retstring.append( " " ); - retstring.append( std::to_string( vspeed ) ); - } - retstring.append( " }\n" ); - retstring.append( " Units are MHz\n" ); - } - else { - retstring.append( " Open connection to camera to see possible speeds.\n" ); - } -***/ - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; - return ERROR; - } - - // all configured Andors must have been initialized - // -// for ( const auto &pair : this->andor ) { -// if ( ! pair.second->is_open() ) { - for ( auto &[name, cam] : this->andor ) { - if ( ! cam->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << cam->camera_info.camera_name - << " S/N " << cam->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - } - } - if ( error==ERROR ) return ERROR; - - // Parse args if present - // - if ( !args.empty() ) { - - std::vector tokens; - Tokenize( args, tokens, " " ); + // at this point, action=="start" - // There must be only two args (the speeds) - // - if ( tokens.size() != 2 ) { - logwrite( function, "ERROR expected speeds" ); - return ERROR; - } - - // Parse the gain from the token - // - try { - hori = std::stof( tokens.at(0) ); - vert = std::stof( tokens.at(1) ); - } - catch ( std::out_of_range &e ) { - message.str(""); message << "ERROR reading speeds: " << e.what(); - error = ERROR; - } - catch ( std::invalid_argument &e ) { - message.str(""); message << "ERROR reading speeds: " << e.what(); - error = ERROR; - } - if (error==ERROR) logwrite( function, message.str() ); - - for ( const auto &pair : this->andor ) { - if (error!=ERROR ) error = pair.second->set_hsspeed( hori ); - if (error!=ERROR ) error = pair.second->set_vsspeed( vert ); - } - } - - message.str(""); - - for ( auto &[name, cam] : this->andor ) { - if ( ( cam->camera_info.hspeed < 0 ) || - ( cam->camera_info.vspeed < 0 ) ) { - message.str(""); message << "ERROR speeds not set for camera " << cam->camera_info.camera_name; - logwrite( function, message.str() ); - error = ERROR; - } - - message << cam->camera_info.camera_name << " " - << cam->camera_info.hspeed << " " << cam->camera_info.vspeed << " "; - } - - retstring = message.str(); - logwrite( function, retstring ); - - return error; - } - /***** Slicecam::Camera::speed **********************************************/ - - - /***** Slicecam::Camera::temperature ****************************************/ - /** - * @brief set or get the camera temperature setpoint - * @param[in] args optionally contains new setpoint - * @param[out] retstring return string contains - * @return ERROR | NO_ERROR | HELP - * - */ - long Camera::temperature( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Camera::temperature"; - std::stringstream message; - long error = NO_ERROR; - int temp = 999; - - // Help - // - if ( args == "?" || args == "help" ) { - retstring = SLICECAMD_TEMP; - retstring.append( " [ ]\n" ); - retstring.append( " Set or get camera temperature in integer degrees C,\n" ); -/*** - if ( !this->andor.empty() && this->andor.begin()->second->is_open() ) { - auto cam = this->andor.begin()->second; // make a smart pointer to the first andor in the map - retstring.append( " where is in range { " ); - retstring.append( std::to_string( cam->camera_info.temp_min ) ); - retstring.append( " " ); - retstring.append( std::to_string( cam->camera_info.temp_max ) ); - retstring.append( " }.\n" ); - } - else { - retstring.append( " open connection to camera to see acceptable range.\n" ); - } -***/ - retstring.append( " If optional is provided then the camera setpoint is changed,\n" ); - retstring.append( " else the current temperature, setpoint, and status are returned.\n" ); - retstring.append( " Format of return value is \n" ); - retstring.append( " Camera cooling is turned on/off automatically, as needed.\n" ); - return HELP; - } - - // If the STL map of Andors is empty then something went wrong - // with the configuration. - // - if ( this->andor.empty() ) { - logwrite( function, "ERROR no cameras defined" ); - retstring="bad_config"; + // don't allow someone to run this if already running + if (this->is_fineacquire_running.load(std::memory_order_acquire)) { + logwrite(function, "ERROR target acquisition already running"); + retstring="running"; return ERROR; } - // all configured Andors must have been initialized - // - for ( const auto &pair : this->andor ) { - if ( ! pair.second->is_open() ) { - message.str(""); message << "ERROR no connection to slicecam " << pair.second->camera_info.camera_name - << " S/N " << pair.second->camera_info.serial_number; - logwrite( function, message.str() ); - error=ERROR; - } - } - if ( error==ERROR ) return ERROR; - - // Parse args if present - // - if ( !args.empty() ) { - - std::vector tokens; - Tokenize( args, tokens, " " ); + // framegrabbing must be running + if (!this->is_framegrab_running.load(std::memory_order_acquire)) { + logwrite(function, "ERROR framegrabbing is not running"); + retstring="stopped"; + return ERROR; + } - // There can be only one arg (the requested temperature) - // - if ( tokens.size() != 1 ) { - logwrite( function, "ERROR too many arguments" ); - return ERROR; - } + // ACAM must be guiding + if (!this->is_acam_guiding.load(std::memory_order_acquire)) { + logwrite(function, "ERROR ACAM is not guiding"); + retstring="stopped"; + return ERROR; + } - // Convert the temperature to int and set the temperature. - // Cooling will be automatically enabled/disabled as needed. - // + // are optional but if specified then require all three + if (tokens.size() > 1 && tokens.size() != 4) { + logwrite(function, "ERROR ACAM is not guiding"); + retstring="stopped"; + return ERROR; + } + else + // override the default camera and aimpoint if provided + if (tokens.size() > 1 && tokens.size() == 4) { try { - temp = static_cast( std::round( std::stof( tokens.at(0) ) ) ); - for ( const auto &pair : this->andor ) { - message.str(""); message << "[DEBUG] set temp to " << temp - << " for camera " << pair.second->camera_info.camera_name; - logwrite( function, message.str() ); - error |= pair.second->set_temperature( temp ); + const std::string which = tokens.at(1); + double x = std::stod(tokens.at(2)); + double y = std::stod(tokens.at(3)); + if ( (which != "L" && which != "R") || + std::isnan(x) || std::isnan(y) || x<0 || y<0) { + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); + retstring="invalid_argument"; + return ERROR; } + this->fineacquire_state.which = which; + this->fineacquire_state.aimpoint = { x, y }; } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR setting temperature: " << e.what(); - error = ERROR; + catch (const std::exception &e) { + logwrite(function, "ERROR expected stop | start [ { L | R } ] : "+std::string(e.what())); + retstring="invalid_argument"; + return ERROR; } } - if (error==ERROR) logwrite( function, message.str() ); - // Regardless of setting the temperature, always read it. - // - message.str(""); - for ( const auto &pair : this->andor ) { - error |= pair.second->get_temperature(temp); - message << pair.second->camera_info.camera_name << " " << temp << " " - << pair.second->camera_info.setpoint << " " - << pair.second->camera_info.temp_status << " "; - } - logwrite( function, message.str() ); + // start the state machine + this->fineacquire_state.reset(); + this->is_fineacquire_locked.store(false, std::memory_order_release); + this->is_fineacquire_running.store(true, std::memory_order_release); - retstring = message.str(); + // publishes status on change only + this->publish_status(); - return error; + logwrite(function, "fine target acquisition enabled"); + + retstring=is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + + return NO_ERROR; } - /***** Slicecam::Camera::temperature ****************************************/ + /***** Slicecam::Interface::fineacquire *************************************/ - /***** Slicecam::Camera::write_frame ****************************************/ + /***** Slicecam::Interface::do_fineacquire **********************************/ /** - * @brief write the Andor image data to FITS file - * @return ERROR or NO_ERROR + * @brief Evaluates fine acquisition natively per-frame + * @details Called synchronously inside dothread_framegrab. Acts as a + * state machine to accumulate samples, calculate medians, and + * publish acam goal corrections, to which acam responds by + * sending offsets to the telescope. + * @param[in] which "L" or "R" indicates which camera * */ - long Camera::write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ) { - std::string function = "Slicecam::Camera::write_frame"; - std::stringstream message; + void Interface::do_fineacquire() { + const char* function = "Slicecam::Interface::do_fineacquire"; - long error = NO_ERROR; + // skip frames if we are waiting for the telescope to settle after a move + // + if (this->fineacquire_state.skip_frames > 0) { + this->fineacquire_state.skip_frames--; + return; + } - fitsinfo.fits_name = outfile; -// fitsinfo.datatype = USHORT_IMG; - fitsinfo.datatype = FLOAT_IMG; + const std::string which = this->fineacquire_state.which; - fits_file.copy_info( fitsinfo ); // copy from fitsinfo to the fits_file class + // get a reference to the requested slicecam and + // a pointer to that image buffer + // + auto it = this->camera.andor.find(which); + if (it==this->camera.andor.end() || it->second==nullptr) { + logwrite(function, "slicecam '"+which+"' not found!"); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); + logwrite(function, "fine target acquisition disabled"); + return; + } + auto* cam = it->second.get(); + long ncols = cam->camera_info.axes[0]; + long nrows = cam->camera_info.axes[1]; - error = fits_file.open_file(); // open the fits file for writing + const std::vector img_data = cam->is_emulated() + ? this->camera.read_from_file(which, ncols, nrows) + : this->camera.get_image(which); - if ( !source_file.empty() ) { - if (error==NO_ERROR) error = fits_file.copy_header_from( source_file ); + if (img_data.empty()) { + logwrite(function, "no image data for slicecam '"+which+"'"); + return; } - else { - if (error==NO_ERROR) error = fits_file.create_header(); // create basic header + + // find the star centroid near the aim point + // + Point centroid; + + if ( Math::calculate_centroid( img_data, ncols, nrows, + this->fineacquire_state.bg_region, + this->fineacquire_state.aimpoint, + centroid) != NO_ERROR ) { + logwrite(function, "WARNING: failed to find centroid, skipping frame"); + return; } - for ( auto &[name, cam] : this->andor ) { - cam->camera_info.section_size = cam->camera_info.axes[0] * cam->camera_info.axes[1]; - if ( cam->camera_info.section_size == 0 ) { - message.str(""); message << "ERROR section size 0 for slicecam " << cam->camera_info.camera_name; - logwrite( function, message.str() ); - error = ERROR; - break; - } - // cam is passed by reference - // - fits_file.write_image( cam ); // write the image data + // convert centroid pixel -> sky using WCS from FITS header + // + World star_sky; + try { + Math::pix2world( cam->fitskeys, centroid, star_sky ); + } + catch (const std::exception &e) { + logwrite(function, "WARNING pix2world (centroid) failed: "+std::string(e.what())+", skipping frame"); + return; + } + if ( !std::isfinite( star_sky.ra ) || !std::isfinite( star_sky.dec ) ) { + logwrite( function, "WARNING pix2world returned non-finite coords, skipping frame" ); + return; } - fits_file.close_file(); // close the file + // convert aim point pixel -> sky + // + World aimpoint_sky; + try { + Math::pix2world( cam->fitskeys, this->fineacquire_state.aimpoint, aimpoint_sky ); + } + catch (const std::exception &e) { + logwrite(function, "WARNING pix2world (aimpoint) failed: "+std::string(e.what())+", skipping frame"); + return; + } - // This is the one extra call that is outside the normal workflow. - // If emulator is enabled then the skysim generator will create a simulated - // image. The image written above by fits_file.write_image() is used as - // input to skysim because it contains the correct WCS headers, but will - // ultimately be overwritten by the simulated image. + // compute dRA,dDEC in degrees and accumulate // - // Need only to make one call since it will generate a multi-extension - // image. + std::pair offsets; + + Math::calculate_acquisition_offsets( star_sky, aimpoint_sky, offsets ); + + this->fineacquire_state.dra_samp.push_back( offsets.first ); + this->fineacquire_state.ddec_samp.push_back( offsets.second ); + + // wait until there are enough samples to evaluate a move // - if ( !this->andor.empty() ) { - if ( this->andor.begin()->second->is_emulated() && _tcs_online ) { - this->andor.begin()->second->simulate_frame( fitsinfo.fits_name, - true, // multi-extension - this->simsize ); - } + const int max_samples = this->fineacquire_state.max_samples; + if ( static_cast(this->fineacquire_state.dra_samp.size()) < max_samples ) { + return; } - outfile = fitsinfo.fits_name; + // calculate median from accumulated samples + // + std::vector dra_sorted = this->fineacquire_state.dra_samp; + std::vector ddec_sorted = this->fineacquire_state.ddec_samp; - return error; + std::sort( dra_sorted.begin(), dra_sorted.end() ); + std::sort( ddec_sorted.begin(), ddec_sorted.end() ); + + const double med_dra = dra_sorted[ max_samples / 2 ]; + const double med_ddec = ddec_sorted[ max_samples / 2 ]; + + // convert to arcsec only for the threshold comparison and logging + // + const double offset_arcsec = std::hypot( med_dra, med_ddec ) * 3600.0; + + // convergence check + // + if ( offset_arcsec <= this->fineacquire_state.goal_arcsec ) { + logwrite( function, "fine acquisition converged" ); + this->is_fineacquire_locked.store( true, std::memory_order_release ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->fineacquire_state.reset(); + this->publish_status(); + return; + } + + // send gain-weighted offsets to acam + // + std::ostringstream oss; + oss << "offset dRA=" << med_dra * 3600.0 + << " dDEC=" << med_ddec * 3600.0 + << " arcsec (r=" << offset_arcsec + << " arcsec) -- applying correction"; + logwrite( function, oss.str() ); + + const double cmd_dra = this->fineacquire_state.gain * med_dra; + const double cmd_ddec = this->fineacquire_state.gain * med_ddec; + + if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { + logwrite( function, "ERROR failed to send offset to ACAM" ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); + return; + } + + // reset samples and skip a couple of frames for telescope settling + this->fineacquire_state.reset(); + this->fineacquire_state.skip_frames = 2; } - /***** Slicecam::Camera::write_frame ****************************************/ + /***** Slicecam::Interface::do_fineacquire **********************************/ /***** Slicecam::Interface::bin *********************************************/ @@ -1006,8 +304,8 @@ namespace Slicecam { * */ long Interface::bin( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::bin"; - std::stringstream message; + const char* function = "Slicecam::Interface::bin"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -1093,23 +391,51 @@ namespace Slicecam { /***** Slicecam::Interface::bin *********************************************/ + /***** Slicecam::Interface::handletopic_snapshot ****************************/ + /** + * @brief what to do when the topic is Topic::SLICECAMD + * @details This publishes a JSON message containing a snapshot of my + * telemetry info when the subscriber receives the Topic::SNAPSHOT + * topic and the payload contains my name. + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_snapshot( const nlohmann::json &jmessage ) { - // If my name is in the jmessage then publish my snapshot - // - if ( jmessage.contains( Slicecam::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage.contains( "test" ) ) { - logwrite( "Slicecam::Interface::handletopic_snapshot", jmessage.dump() ); + if ( jmessage.contains(Topic::SLICECAMD) ) this->publish_snapshot(); + } + /***** Slicecam::Interface::handletopic_snapshot ****************************/ + + + /***** Slicecam::Interface::handletopic_acamd *******************************/ + /** + * @brief what to do when the topic is Topic::ACAMD + * @param[in] jmessage subscribed-received JSON message + * + */ + void Interface::handletopic_acamd(const nlohmann::json &jmessage) { + { + std::lock_guard lock(snapshot_mtx); + snapshot_status[Topic::ACAMD]=true; } + // set is_acam_guiding flag + bool acquired; + Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); + this->is_acam_guiding.store(acquired, std::memory_order_relaxed); } + /***** Slicecam::Interface::handletopic_acamd *******************************/ + /***** Slicecam::Interface::handletopic_slitd *******************************/ + /** + * @brief what to do when the topic is Topic::SLITD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["slitd"]=true; + snapshot_status[Topic::SLITD]=true; } Common::extract_telemetry_value( jmessage, "SLITO", telem.slitoffset ); Common::extract_telemetry_value( jmessage, "SLITW", telem.slitwidth ); @@ -1117,12 +443,20 @@ namespace Slicecam { this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); } + /***** Slicecam::Interface::handletopic_slitd *******************************/ + /***** Slicecam::Interface::handletopic_tcsd ********************************/ + /** + * @brief what to do when the topic is Topic::TCSD + * @details This receives tcs telemetry + * @param[in] jmessage_in subscribed-received JSON message + * + */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { { std::lock_guard lock(snapshot_mtx); - snapshot_status["tcsd"]=true; + snapshot_status[Topic::TCSD]=true; } // extract and store values in the class // @@ -1139,6 +473,43 @@ namespace Slicecam { Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); } + /***** Slicecam::Interface::handletopic_tcsd ********************************/ + + + /***** Slicecam::Interface::publish_status **********************************/ + /** + * @brief publishes my important status on change + * @details This publishes a JSON message containing important telemetry. + * @param[in] force optional (default=false) forces publish irrespective of change + * + */ + void Interface::publish_status(bool force) { + const bool is_fineacquire_running_now = this->is_fineacquire_running.load(); + const bool is_fineacquire_locked_now = this->is_fineacquire_locked.load(); + + // unless forced, only publish if there was a change + // + if ( !force && + is_fineacquire_running_now == this->last_status.is_fineacquire_running && + is_fineacquire_locked_now == this->last_status.is_fineacquire_locked) return; + + this->last_status.is_fineacquire_running = is_fineacquire_running_now; + this->last_status.is_fineacquire_locked = is_fineacquire_locked_now; + + nlohmann::json jmessage_out; + jmessage_out[Key::SOURCE] = Topic::SLICECAMD; + jmessage_out[Key::Slicecamd::FINEACQUIRE_RUNNING] = this->is_fineacquire_running.load(); + jmessage_out[Key::Slicecamd::FINEACQUIRE_LOCKED] = this->is_fineacquire_locked.load(); + + try { + this->publisher->publish(jmessage_out, Topic::SLICECAMD); + } + catch (const std::exception &e) { + logwrite("Slicecam::Interface::publish_status", + "ERROR publishing status: "+std::string(e.what())); + } + } + /***** Slicecam::Interface::publish_status **********************************/ /***** Slicecam::Interface::publish_snapshot ********************************/ @@ -1149,8 +520,11 @@ namespace Slicecam { * */ void Interface::publish_snapshot() { + // force-publish status + this->publish_status(true); + nlohmann::json jmessage_out; - jmessage_out["source"] = "slicecamd"; + jmessage_out[Key::SOURCE] = Topic::SLICECAMD; for ( const auto &[name, cam] : this->camera.andor ) { std::string key="TANDOR_SCAM_"+name; @@ -1183,7 +557,7 @@ namespace Slicecam { } try { - this->publisher->publish( jmessage_out, "_snapshot" ); + this->publisher->publish( jmessage_out, Topic::SNAPSHOT ); } catch ( const std::exception &e ) { logwrite( "Slicecam::Interface::request_snapshot", @@ -1197,6 +571,8 @@ namespace Slicecam { /***** Slicecam::Interface::wait_for_snapshots ******************************/ /** * @brief wait for everyone to publish their snaphots + * @details When forcing subscribers to publish their telemetry, + * this waits until they have done so. * */ bool Interface::wait_for_snapshots() { @@ -1221,7 +597,7 @@ namespace Slicecam { if (all_received) return true; if (std::chrono::steady_clock::now() - start_time > timeout) { - std::stringstream message; + std::ostringstream message; message << "ERROR timeout waiting for telemetry from:"; for ( const auto &[topic,status] : snapshot_status ) { if (!status) message << " " << topic; @@ -1245,8 +621,8 @@ namespace Slicecam { * */ long Interface::configure_interface( Config &config ) { - std::string function = "Slicecam::Interface::configure_interface"; - std::stringstream message; + const char* function = "Slicecam::Interface::configure_interface"; + std::ostringstream message; int applied=0; long error = NO_ERROR; @@ -1333,21 +709,21 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "PUSH_GUI_SETTINGS" ) ) { this->gui_manager.set_push_settings( config.arg[entry] ); message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "PUSH_GUI_IMAGE" ) ) { this->gui_manager.set_push_image( config.arg[entry] ); message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; logwrite( function, message.str() ); applied++; } - + else if ( starts_with( config.param[entry], "TCSD_PORT" ) ) { int port; try { @@ -1363,7 +739,7 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } - + else if ( config.param[entry] == "SKYSIM_IMAGE_SIZE" ) { try { this->camera.set_simsize( std::stoi( config.arg[entry] ) ); @@ -1377,8 +753,38 @@ namespace Slicecam { logwrite( function, message.str() ); applied++; } + else + if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT=" ) { + std::string which; + double x,y; + std::istringstream iss(config.arg[entry]); + if (!(iss >> which >> x >> y)) { + logwrite(function, "ERROR invalid FINE_ACQUIRE_AIMPOINT='"+config.arg[entry]+"' expected "); + return ERROR; + } + this->fineacquire_state.which = which; + this->fineacquire_state.aimpoint = { x, y }; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND=" ) { + long x1, x2, y1, y2; + std::istringstream iss(config.arg[entry]); + if (!(iss >> x1 >> x2 >> y1 >> y2)) { + logwrite(function, "ERROR invalid FINE_ACQUIRE_BACKGROUND='"+config.arg[entry]+"' expected "); + return ERROR; + } + this->fineacquire_state.bg_region = { x1, x2, y1, y2 }; + } } + + // FINE_ACQUIRE parameters must have been configured properly + // + if (!this->fineacquire_state.is_valid()) { + logwrite(function, "ERROR bad or missing FINE_ACQUIRE configuration"); + return ERROR; + } + message.str(""); message << "applied " << applied << " configuration lines to the slicecam interface"; logwrite(function, message.str()); @@ -1396,8 +802,6 @@ namespace Slicecam { * */ long Interface::open( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::open"; - std::stringstream message; if ( args == "?" || args == "help" ) { retstring = SLICECAMD_OPEN; @@ -1429,6 +833,9 @@ namespace Slicecam { long error = this->tcs_init( "", retstring ); std::this_thread::sleep_for(std::chrono::milliseconds(500)); + // open connection to acamd + error |= this->acamd_init(); + if ( this->camera.open( which, args ) == NO_ERROR ) { // open the camera error |= this->framegrab( "start", retstring ); // start frame grabbing if open succeeds std::thread( &Slicecam::GUIManager::push_gui_settings, &gui_manager ).detach(); // force display refresh @@ -1454,8 +861,8 @@ namespace Slicecam { * */ long Interface::isopen( std::string which, bool &state, std::string &retstring ) { - std::string function = "Slicecam::Interface::isopen"; - std::stringstream message; + const char* function = "Slicecam::Interface::isopen"; + std::ostringstream message; // Help // @@ -1519,19 +926,22 @@ namespace Slicecam { /***** Slicecam::Interface::close *******************************************/ /** - * @brief closes slicecams - * @param[in] args optionally request help - * @param[out] retstring contains return string for help - * @return ERROR | NO_ERROR | HELP + * @brief closes slicecams, internal use * */ void Interface::close() { std::string dontcare; this->close("",dontcare); } + /***** Slicecam::Interface::close *******************************************/ + /** + * @brief closes slicecams + * @param[in] args optionally request help + * @param[out] retstring contains return string for help + * @return ERROR | NO_ERROR | HELP + * + */ long Interface::close( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::close"; - std::stringstream message; long error = NO_ERROR; // Help @@ -1564,8 +974,8 @@ namespace Slicecam { * */ long Interface::tcs_init( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::tcs_init"; - std::stringstream message; + const char* function = "Slicecam::Interface::tcs_init"; + std::ostringstream message; // Send command to tcs daemon client. If help was requested then that // request is passed on here to tcsd.init() so this could return HELP. @@ -1609,6 +1019,36 @@ namespace Slicecam { /***** Slicecam::Interface::tcs_init ****************************************/ + /***** Slicecam::Interface::acamd_init **************************************/ + /** + * @brief initialize connection to acamd + * @return ERROR | NO_ERROR + * + */ + long Interface::acamd_init() { + const char* function = "Slicecam::Interface::acam_init"; + + // If not connected to acamd then try to connect to the daemon. + std::string retstring; + if (this->acamd.is_connected(retstring) != NO_ERROR) { + logwrite(function, "ERROR no response from acamd"); + return NO_ERROR; + } + + // Not connected, try to connect + if ( retstring.find("false") != std::string::npos ) { + logwrite( function, "connecting to acamd" ); + if (this->acamd.connect() != NO_ERROR) { + logwrite( function, "ERROR unable to connect to acamd" ); + return NO_ERROR; + } + logwrite( function, "connected to acamd" ); + } + return NO_ERROR; + } + /***** Slicecam::Interface::acamd_init **************************************/ + + /***** Slicecam::Interface::saveframes **************************************/ /** * @brief set/get number of frame grabs to save during target acquisition @@ -1618,7 +1058,7 @@ namespace Slicecam { * */ long Interface::saveframes( std::string args, std::string &retstring ) { - const std::string function = "Slicecam::Interface::saveframes"; + const char* function = "Slicecam::Interface::saveframes"; // Help // @@ -1671,8 +1111,8 @@ namespace Slicecam { * */ long Interface::framegrab_fix( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::framegrab_fix"; - std::stringstream message; + const char* function = "Slicecam::Interface::framegrab_fix"; + std::ostringstream message; // Help // @@ -1754,8 +1194,8 @@ namespace Slicecam { * */ long Interface::framegrab( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::framegrab"; - std::stringstream message; + const char* function = "Slicecam::Interface::framegrab"; + std::ostringstream message; std::string _imagename = this->imagename; // Help @@ -1861,8 +1301,8 @@ namespace Slicecam { * */ void Interface::dothread_framegrab( const std::string whattodo, const std::string sourcefile ) { - std::string function = "Slicecam::Interface::dothread_framegrab"; - std::stringstream message; + const char* function = "Slicecam::Interface::dothread_framegrab"; + std::ostringstream message; long error = NO_ERROR; // For any whattodo that will take an image, when running the Andor emulator, @@ -1941,13 +1381,18 @@ namespace Slicecam { collect_header_info( cam ); } + // write to FITS file if (error==NO_ERROR) error = this->camera.write_frame( sourcefile, this->imagename, - this->tcs_online.load(std::memory_order_acquire) ); // write to FITS file + this->tcs_online.load(std::memory_order_acquire) ); + + // run the fine target acquisition if enabled + if ( is_fineacquire_running.load() ) { do_fineacquire(); } this->framegrab_time = std::chrono::steady_clock::time_point::min(); - this->gui_manager.push_gui_image( this->imagename ); // send frame to GUI + // send frame to GUI + this->gui_manager.push_gui_image( this->imagename ); // Normally, framegrabs are overwritten to the same file. // This optionally saves them at the requested cadence by @@ -2011,7 +1456,7 @@ namespace Slicecam { return; } - std::stringstream fn; + std::ostringstream fn; fn << path << "/" << basename << "_" << std::setfill('0') << std::setw(5) << npreserve << ".fits"; // increment until a unique file is found so that it never overwrites @@ -2051,8 +1496,8 @@ namespace Slicecam { * */ long Interface::gui_settings_control( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::gui_settings_control"; - std::stringstream message; + const char* function = "Slicecam::Interface::gui_settings_control"; + std::ostringstream message; auto info = this->camera.andor.begin()->second->camera_info; // Help @@ -2247,8 +1692,8 @@ namespace Slicecam { * */ void Interface::dothread_fpoffset( Slicecam::Interface &iface ) { - std::string function = "Slicecam::Interface::dothread_fpoffset"; - std::stringstream message; + const char* function = "Slicecam::Interface::dothread_fpoffset"; + std::ostringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); logwrite( function, message.str() ); @@ -2265,66 +1710,71 @@ namespace Slicecam { /***** Slicecam::Interface::dothread_fpoffset *******************************/ - /***** Slicecam::Interface::get_acam_guide_state ****************************/ + /***** Slicecam::Interface::offset_acam_goal ********************************/ /** - * @brief asks if ACAM is guiding - * @details The only way this can fail is if acam is connected but returns - * an error reading the state. - * @param[out] is_guiding bool guiding state + * @brief applies offset to ACAM goal, or move telescope directly + * @details When guiding is enabled, the offsets will be applied to the ACAM + * goal so that the ACAM will guide on the offset position. When + * not guiding, the offsets are sent directly to the TCS as PT offsets. + * @param[in] offsets pair { dRA, dDEC } * @return ERROR | NO_ERROR * */ - long Interface::get_acam_guide_state( bool &is_guiding ) { - std::string function = "Slicecam::Interface::get_acam_guide_state"; - std::stringstream message; - long error = NO_ERROR; - std::string retstring; + long Interface::offset_acam_goal(const std::pair &offsets, std::optional fineacquire) { + const char* function = "Slicecam::Interface::offset_acam_goal"; - // If not connected to acamd then try to connect to the daemon. - // If there's an error in doing this then assume acamd is not even - // running, in which case the guiding cannot be running. + auto [ra_off, dec_off] = offsets; // local copy + + bool is_fineacquire=false; + if (fineacquire) is_fineacquire = *fineacquire; + + // If ACAM is guiding then slicecam must not move the telescope, + // but must allow ACAM to perform the offset. // - error = this->acamd.is_connected(retstring); - if ( error == ERROR ) { - logwrite( function, "ERROR no response from acamd -- will assume guiding is inactive" ); - is_guiding = false; - return NO_ERROR; - } + bool is_guiding = this->is_acam_guiding.load(); - // Not connected, try to connect + // send the offsets now // - if ( retstring.find("false") != std::string::npos ) { - logwrite( function, "connecting to acamd" ); - error = this->acamd.connect(); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR unable to connect to acamd -- will assume guiding is inactive" ); - is_guiding=false; - return NO_ERROR; + if ( is_guiding ) { + // Send to acamd if guiding + // + std::ostringstream cmd; + cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; + + // add fineguiding arg when used for fine acquisition mode + if (is_fineacquire) cmd << " fineguiding"; + + if (this->acamd.command( cmd.str() ) != NO_ERROR) { + logwrite( function, "ERROR adding offset to acam goal" ); + return ERROR; } - logwrite( function, "connected to acamd" ); } + else + if ( !is_guiding && this->tcs_online.load(std::memory_order_acquire) && this->tcsd.client.is_open() ) { + // offsets are in degrees, convert to arcsec (required for PT command) + // + ra_off *= 3600.; + dec_off *= 3600.; - // Is acamd guiding? At this point slicecam is connected to acamd, so - // consider an error here as a fault and don't continute. - // - error = this->acamd.send( ACAMD_ACQUIRE, retstring ); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guiding state from ACAM" ); - return ERROR; + // Send them directly to the TCS when not guiding + // + if ( this->tcsd.pt_offset( ra_off, dec_off, OFFSETRATE ) != NO_ERROR ) { + logwrite( function, "ERROR offsetting telescope" ); + return ERROR; + } } - - // If guiding is in the return string then it is enabled. - // - if ( retstring.find( "guiding" ) != std::string::npos ) { - is_guiding = true; + else if ( !is_guiding ) { + logwrite( function, "ERROR not connected to tcsd" ); + return ERROR; } - else is_guiding = false; - message.str(""); message << "acam is" << ( is_guiding ? " " : " not " ) << "guiding"; + std::ostringstream message; + message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec"; + logwrite(function, message.str()); return NO_ERROR; } - /***** Slicecam::Interface::get_acam_guide_state ****************************/ + /***** Slicecam::Interface::offset_acam_goal ********************************/ /***** Slicecam::Interface::put_on_slit *************************************/ @@ -2338,8 +1788,8 @@ namespace Slicecam { * */ long Interface::put_on_slit( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::put_on_slit"; - std::stringstream message; + const char* function = "Slicecam::Interface::put_on_slit"; + std::ostringstream message; // Help // @@ -2390,55 +1840,9 @@ namespace Slicecam { return ERROR; } - // If ACAM is guiding then slicecam must not move the telescope, - // but must allow ACAM to perform the offset. - // - bool is_guiding; - long error = this->get_acam_guide_state( is_guiding ); - - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guide state" ); - return ERROR; - } - // send the offsets now // - if ( is_guiding ) { - // Send to acamd if guiding - // - std::stringstream cmd; - cmd << ACAMD_OFFSETGOAL << " " << std::fixed << std::setprecision(6) << ra_off << " " << dec_off; - error = this->acamd.command( cmd.str() ); - if ( error != NO_ERROR ) { - logwrite( function, "ERROR adding offset to acam goal" ); - return ERROR; - } - } - else - if ( !is_guiding && this->tcs_online.load(std::memory_order_acquire) && this->tcsd.client.is_open() ) { - // offsets are in degrees, convert to arcsec (required for PT command) - // - ra_off *= 3600.; - dec_off *= 3600.; - - // Send them directly to the TCS when not guiding - // - if ( this->tcsd.pt_offset( ra_off, dec_off, OFFSETRATE ) != NO_ERROR ) { - logwrite( function, "ERROR offsetting telescope" ); - retstring="tcs_error"; - return ERROR; - } - } - else if ( !is_guiding ) { - logwrite( function, "ERROR not connected to tcsd" ); - retstring="tcs_not_connected"; - return ERROR; - } - - message.str(""); message << "requested offsets dRA=" << ra_off << " dDEC=" << dec_off << " arcsec"; - logwrite( function, message.str() ); - - return NO_ERROR; + return this->offset_acam_goal( { ra_off, dec_off } ); } /***** Slicecam::Interface::put_on_slit *************************************/ @@ -2452,8 +1856,8 @@ namespace Slicecam { * */ long Interface::shutdown( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::shutdown"; - std::stringstream message; + const char* function = "Slicecam::Interface::shutdown"; + std::ostringstream message; // Help // @@ -2489,7 +1893,7 @@ namespace Slicecam { * */ long Interface::shutter(const std::string args, std::string &retstring) { - const std::string function("Slicecam::Interface::shutter"); + const char* function("Slicecam::Interface::shutter"); // Help if ( args == "?" || args == "help" ) { @@ -2545,8 +1949,8 @@ namespace Slicecam { * */ long Interface::test( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::test"; - std::stringstream message; + const char* function = "Slicecam::Interface::test"; + std::ostringstream message; std::vector tokens; long error = NO_ERROR; @@ -2760,9 +2164,7 @@ namespace Slicecam { retstring.append( " Return the acam guiding state\n" ); return HELP; } - bool is_guiding; - error = this->get_acam_guide_state( is_guiding ); - message.str(""); message << "request returned " << ( error==ERROR ? "ERROR" : "NO_ERROR" ) << ": guiding is " << ( is_guiding ? "on" : "off" ); + message.str(""); message << ": ACAM guiding is " << ( this->is_acam_guiding.load() ? "on" : "off" ); retstring = message.str(); } else @@ -2778,16 +2180,8 @@ namespace Slicecam { retstring="invalid_argument"; return ERROR; } - bool is_guiding; - long error = this->get_acam_guide_state( is_guiding ); - - if ( error != NO_ERROR ) { - logwrite( function, "ERROR getting guide state" ); - retstring="acamd_error"; - return ERROR; - } - if ( !is_guiding ) { + if ( !this->is_acam_guiding.load() ) { logwrite( function, "ERROR acam is not guiding" ); retstring="not_guiding"; return ERROR; @@ -2864,8 +2258,8 @@ namespace Slicecam { * */ long Interface::exptime( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::exptime"; - std::stringstream message; + const char* function = "Slicecam::Interface::exptime"; + std::ostringstream message; if ( args == "?" || args == "help" ) { retstring = SLICECAMD_EXPTIME; @@ -2941,8 +2335,8 @@ namespace Slicecam { * */ long Interface::fan_mode( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::fan_mode"; - std::stringstream message; + const char* function = "Slicecam::Interface::fan_mode"; + std::ostringstream message; long error = NO_ERROR; // Help @@ -3036,8 +2430,8 @@ namespace Slicecam { * */ long Interface::gain( std::string args, std::string &retstring ) { - std::string function = "Slicecam::Interface::gain"; - std::stringstream message; + const char* function = "Slicecam::Interface::gain"; + std::ostringstream message; long error = NO_ERROR; int gain = -999; @@ -3144,8 +2538,8 @@ namespace Slicecam { * */ long Interface::collect_header_info( std::unique_ptr &slicecam ) { - std::string function = "Slicecam::Interface::collect_header_info"; - std::stringstream message; + const char* function = "Slicecam::Interface::collect_header_info"; + std::ostringstream message; std::string cam = slicecam->camera_info.camera_name; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 7ec87161..0854d1a2 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -24,6 +25,9 @@ #include "acamd_commands.h" #include "tcsd_client.h" #include "skyinfo.h" +#include "slicecam_camera.h" +#include "slicecam_math.h" +#include "message_keys.h" #define PYTHON_PATH "/home/developer/Software/Python:/home/developer/Software/Python/acam_skyinfo" #define PYTHON_ASTROMETRY_MODULE "astrometry" @@ -37,6 +41,8 @@ #include "andor.h" #endif +#include "guimanager.h" + /***** Slicecam ***************************************************************/ /** * @namespace Slicecam @@ -53,170 +59,43 @@ namespace Slicecam { class Interface; // forward declaration - /***** Slicecam::Camera *****************************************************/ + /***** Slicecam::FineAcqState ***********************************************/ /** - * @class Camera - * @brief Camera class - * - * This class is used for communicating with the slicecam camera directly (which is an Andor) + * @brief Persistent state for the fine-acquisition per-frame state machine * + * @details + * which - which camera to use ("L" or "R") + * aimpoint - desired star location on the chip, FITS 1-based pixels. + * This is the pixel analogue of CF's --goal-x / --goal-y. + * The star is driven toward this pixel, not toward an ra/dec. + * bg_region - background estimation ROI, 1-based inclusive. + * Matches the --bg-x1/x2/y1/y2 defaults in slicecamd.cfg. + * dra_samp - accumulated dRA*cos(dec) samples in degrees + * ddec_samp - accumulated dDEC samples in degrees + * max_samples - samples to gather before evaluating a move + * goal_arcsec - convergence threshold; loop stops when median offset + * magnitude falls below this value + * gain - fraction of the median offset commanded each cycle (0..1) + * skip_frames - counts down frames to discard while telescope settles + * after a commanded move */ - class Camera { - private: - uint16_t* image_data; - int simsize; /// for the sky simulator - std::map handlemap; - - public: - Camera() : image_data( nullptr ), simsize(1024) { }; - - FITS_file fits_file; /// instantiate a FITS container object - FitsInfo fitsinfo; - - std::mutex framegrab_mutex; - - std::map> andor; ///< container for Andor::Interface objects - - std::map default_config; ///< container to hold defaults for each camera - - inline void copy_info() { fits_file.copy_info( fitsinfo ); } - inline void set_simsize( int val ) { if ( val > 0 ) this->simsize = val; else throw std::out_of_range("simsize must be greater than 0"); } - - inline long init_handlemap() { - this->handlemap.clear(); - return this->andor.begin()->second->get_handlemap( this->handlemap ); - } - - long emulator( std::string args, std::string &retstring ); - long open( std::string which, std::string args ); - long close(); - long get_frame(); - long write_frame( std::string source_file, std::string &outfile, const bool _tcs_online ); - long bin( const int hbin, const int vbin ); - long set_fan( std::string which, int mode ); - long imflip( std::string args, std::string &retstring ); - long imrot( std::string args, std::string &retstring ); - long set_gain( int &gain ); - long set_gain( std::string which, int &gain ); - long set_gain( int &&gain ); - long set_gain( std::string which, int &&gain ); - long set_exptime( float &val ); - long set_exptime( std::string which, float &val ); - long set_exptime( float &&val ); - long set_exptime( std::string which, float &&val ); - long speed( std::string args, std::string &retstring ); - long temperature( std::string args, std::string &retstring ); + struct FineAcqState { + std::string which; + Point aimpoint; ///< 1-based pixel aim point + Rect bg_region; ///< background ROI (1-based) + std::vector dra_samp; ///< dRA*cos(dec) samples, degrees + std::vector ddec_samp; ///< dDEC samples, degrees + int max_samples = 10; ///< samples before evaluating a move + double goal_arcsec = 0.3; ///< convergence threshold, arcsec + double gain = 0.7; ///< gain applied to commanded offset + int skip_frames = 0; ///< frames to skip after a telescope move + + void reset() { dra_samp.clear(); ddec_samp.clear(); skip_frames = 0; } + bool is_valid() const noexcept { + return !which.empty() && aimpoint.is_valid() && bg_region.is_valid(); + } }; - /***** Slicecam::Camera *****************************************************/ - - - /***** Slicecam::GUIManager *************************************************/ - /** - * @class GUIManager - * @brief defines functions and settings for the display GUI - * - */ - class GUIManager { - private: - const std::string camera_name = "slicev"; - std::atomic update; ///push_settings=sh; } - - // sets the private variable push_image, call on config - inline void set_push_image( std::string sh ) { this->push_image=sh; } - - // sets the update flag true - inline void set_update() { this->update.store( true ); return; } - - /** - * @fn get_update - * @brief returns the update flag then clears it - * @return boolean true|false - */ - inline bool get_update() { return this->update.exchange( false ); } - - /** - * @fn get_message_string - * @brief returns a formatted message of all gui settings - * @details This message is the return string to guideset command. - * @return string in form of - */ - std::string get_message_string() { - std::stringstream message; - if ( this->exptime < 0 ) message << "ERR"; else { message << std::fixed << std::setprecision(3) << this->exptime; } - message << " "; - if ( this->gain < 1 ) message << "ERR"; else { message << std::fixed << std::setprecision(3) << this->gain; } - message << " "; - if ( this->bin < 1 ) message << "x"; else { message << std::fixed << std::setprecision(3) << this->bin; } - message << " "; - if ( std::isnan(this->navg) ) message << "NaN"; else { message << std::fixed << std::setprecision(2) << this->navg; } - return message.str(); - } - - /** - * @brief calls the push_settings script with the formatted message string - * @details the script pushes the settings to the Guider GUI - */ - void push_gui_settings() { - std::string function = "Slicecam::GUIManager::push_gui_settings"; - std::stringstream cmd; - cmd << push_settings << " " - << ( get_update() ? "true" : "false" ) << " " - << get_message_string(); - - if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { - logwrite( function, "ERROR updating GUI" ); - } - - return; - } - - void send_fifo_warning(const std::string &message) { - const std::string fifo_name("/tmp/.slicev_warning.fifo"); - std::ofstream fifo(fifo_name); - if (!fifo.is_open()) { - logwrite("Slicecam::GUIManager::send_fifo_warning", "failed to open " + fifo_name + " for writing"); - } - else { - fifo << message << std::endl; - fifo.close(); - } - } - - /** - * @brief calls the push_image script with the formatted message string - * @details the script pushes the indicated file to the Guider GUI display - * @param[in] filename fits file to send - */ - void push_gui_image( std::string_view filename ) { - std::string function = "Slicecam::GUIManager::push_gui_image"; - std::stringstream cmd; - cmd << push_image << " " - << camera_name << " " - << filename; - - if ( std::system( cmd.str().c_str() ) && errno!=ECHILD ) { - logwrite( function, "ERROR pushing image to GUI" ); - } - - return; - } - }; - /***** Slicecam::GUIManager *************************************************/ + /***** Slicecam::FineAcqState ***********************************************/ /***** Slicecam::Interface **************************************************/ @@ -240,6 +119,8 @@ namespace Slicecam { std::mutex framegrab_mtx; std::condition_variable cv; + FineAcqState fineacquire_state; + public: std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint @@ -255,6 +136,9 @@ namespace Slicecam { std::atomic should_framegrab_run; ///< set if framegrab loop should run std::atomic is_framegrab_running; ///< set if framegrab loop is running + std::atomic is_fineacquire_running; ///< set if fine target acquisition is running + std::atomic is_fineacquire_locked; ///< set when fine acquire target acquired + std::atomic is_acam_guiding; ///< is acam guiding? /** these are set by Interface::saveframes() */ @@ -281,6 +165,11 @@ namespace Slicecam { std::mutex snapshot_mtx; std::unordered_map snapshot_status; + struct { + bool is_fineacquire_running=false; + bool is_fineacquire_locked=false; + } last_status; + GUIManager gui_manager; Interface() @@ -292,16 +181,23 @@ namespace Slicecam { should_subscriber_thread_run(false), should_framegrab_run(false), is_framegrab_running(false), + is_fineacquire_running(false), + is_fineacquire_locked(false), + is_acam_guiding(false), nsave_preserve_frames(0), nskip_preserve_frames(0), - snapshot_status { { "slitd", false }, {"tcsd", false} } + snapshot_status { { Topic::SLITD, false }, + { Topic::TCSD, false }, + { Topic::ACAMD, false } } { topic_handlers = { - { "_snapshot", std::function( + { Topic::SNAPSHOT, std::function( [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) }, - { "tcsd", std::function( + { Topic::ACAMD, std::function( + [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, + { Topic::TCSD, std::function( [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, - { "slitd", std::function( + { Topic::SLITD, std::function( [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) } }; } @@ -339,12 +235,17 @@ namespace Slicecam { void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void handletopic_snapshot( const nlohmann::json &jmessage ); + void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slitd( const nlohmann::json &jmessage ); void handletopic_tcsd( const nlohmann::json &jmessage ); + void publish_status(bool force=false); void publish_snapshot(); void request_snapshot(); bool wait_for_snapshots(); + long fineacquire(std::string args, std::string &retstring); + void do_fineacquire(); + long avg_frames( std::string args, std::string &retstring ); long bin( std::string args, std::string &retstring ); long test_image(); /// @@ -354,6 +255,7 @@ namespace Slicecam { void close(); long close( std::string args, std::string &retstring ); long tcs_init( std::string args, std::string &retstring ); /// initialize connection to TCS + long acamd_init(); long saveframes( std::string args, std::string &retstring ); void alert_framegrabbing_stopped(const int &waitms); long framegrab( std::string args ); /// wrapper to control Andor frame grabbing @@ -371,7 +273,7 @@ namespace Slicecam { long fan_mode( std::string args, std::string &retstring ); long gain( std::string args, std::string &retstring ); - long get_acam_guide_state( bool &is_guiding ); + long offset_acam_goal(const std::pair &offsets, std::optional fineacquire=std::nullopt); long collect_header_info( std::unique_ptr &slicecam ); diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp new file mode 100644 index 00000000..a87d9e61 --- /dev/null +++ b/slicecamd/slicecam_math.cpp @@ -0,0 +1,431 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_math.cpp + * @brief slicecam math utilities implementation for fine target acquisition + * @details Implements centroid detection, WCS pixel-to-sky conversion, and + * fine-acquisition offset calculation for the slicecam fine-acquire + * loop. Direct C++ translation of CF's ngps_acq.c. + * @author David Hale, Christoffer Fremling + * + */ + +#include "slicecam_math.h" + +#include +#include +#include +#include + +namespace Slicecam { + + // --------------------------------------------------------------------------- + // Internal helpers (file scope only) + // --------------------------------------------------------------------------- + + /** + * @brief Build a normalized 1-D Gaussian kernel, half-radius = ceil(3*sigma). + * @param[in] sigma Gaussian sigma in pixels (clamped to >= 0.2) + * @param[out] radius_out half-radius r; full kernel length = 2r+1 + */ + static std::vector make_gaussian_kernel( double sigma, int &radius_out ) { + if ( sigma < 0.2 ) sigma = 0.2; + const int r = std::max( 1, static_cast( std::ceil( 3.0 * sigma ) ) ); + const int len = 2 * r + 1; + std::vector k( len ); + double sum = 0.0; + for ( int i = -r; i <= r; i++ ) { + const double x = static_cast(i) / sigma; + k[i + r] = std::exp( -0.5 * x * x ); + sum += k[i + r]; + } + if ( sum <= 0.0 ) sum = 1.0; + for ( auto &v : k ) v /= sum; + radius_out = r; + return k; + } + + /** + * @brief Sum of squares of 1-D kernel coefficients. + * For a separable 2-D Gaussian: sigma_filt = sigma_raw * kernel_sum_sq. + */ + static double kernel_sum_sq( const std::vector &k ) { + double s = 0.0; + for ( auto v : k ) s += v * v; + return s; + } + + /** + * @brief Separable 2-D Gaussian convolution on a (w x h) patch. + * Border handling: clamp (mirror-pad would be better but matches CF). + */ + static void convolve_separable( const std::vector &in, + std::vector &tmp, + std::vector &out, + int w, int h, + const std::vector &k, int r ) { + // horizontal pass: in -> tmp + for ( int y = 0; y < h; y++ ) { + for ( int x = 0; x < w; x++ ) { + double acc = 0.0; + for ( int dx = -r; dx <= r; dx++ ) { + const int xx = std::max( 0, std::min( w - 1, x + dx ) ); + acc += static_cast( in[y * w + xx] ) * k[dx + r]; + } + tmp[y * w + x] = static_cast( acc ); + } + } + // vertical pass: tmp -> out + for ( int y = 0; y < h; y++ ) { + for ( int x = 0; x < w; x++ ) { + double acc = 0.0; + for ( int dy = -r; dy <= r; dy++ ) { + const int yy = std::max( 0, std::min( h - 1, y + dy ) ); + acc += static_cast( tmp[yy * w + x] ) * k[dy + r]; + } + out[y * w + x] = static_cast( acc ); + } + } + } + + /** + * @brief SExtractor-like background and sigma estimation. + * @details Translation of CF's bg_sigma_sextractor_like(). + * 1. Initial estimate: median and MAD-derived sigma. + * 2. Iterative 3-sigma clipping around the original median, + * with early exit when sigma converges (rel change < 1%). + * 3. Background via mode = 2.5*median - 1.5*mean, + * falling back to median if distribution is skewed. + */ + static void bg_sigma( const std::vector &samples, + double &bkg_out, double &sigma_out ) { + bkg_out = 0.0; + sigma_out = 1.0; + + const size_t ns = samples.size(); + if ( ns < 64 ) return; + + // samples must be sorted on entry + std::vector sorted = samples; + std::sort( sorted.begin(), sorted.end() ); + + const double median = ( ns % 2 ) + ? static_cast( sorted[ns / 2] ) + : 0.5 * ( static_cast( sorted[ns / 2 - 1] ) + + static_cast( sorted[ns / 2] ) ); + + // MAD -> initial sigma + std::vector dev( ns ); + for ( size_t i = 0; i < ns; i++ ) + dev[i] = std::abs( sorted[i] - static_cast( median ) ); + std::sort( dev.begin(), dev.end() ); + + const double mad = ( ns % 2 ) + ? static_cast( dev[ns / 2] ) + : 0.5 * ( static_cast( dev[ns / 2 - 1] ) + + static_cast( dev[ns / 2] ) ); + double sigma = 1.4826 * mad; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + + // Iterative 3-sigma clipping, always centered on original median + double mean = median; + double sigma_prev = sigma; + + for ( int it = 0; it < 8; it++ ) { + const double lo = median - 3.0 * sigma; + const double hi = median + 3.0 * sigma; + double sum = 0.0, sum2 = 0.0; + long cnt = 0; + for ( float v : sorted ) { + if ( v < lo || v > hi ) continue; + const double dv = static_cast( v ); + sum += dv; + sum2 += dv * dv; + cnt++; + } + if ( cnt < 32 ) break; + + mean = sum / static_cast( cnt ); + const double var = ( sum2 / static_cast( cnt ) ) - mean * mean; + sigma = ( var > 0.0 ) ? std::sqrt( var ) : 0.0; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) { sigma = sigma_prev; break; } + + const double rel = std::abs( sigma - sigma_prev ) + / ( sigma_prev > 0.0 ? sigma_prev : 1.0 ); + sigma_prev = sigma; + if ( rel < 0.01 ) break; + } + + // SExtractor mode estimator: 2.5*median - 1.5*mean + // Fall back to median if distribution is too skewed + double bkg = 2.5 * median - 1.5 * mean; + if ( sigma > 0.0 && ( mean - median ) / sigma > 0.3 ) bkg = median; + if ( !std::isfinite( bkg ) ) bkg = median; + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) sigma = 1.0; + + bkg_out = bkg; + sigma_out = sigma; + } + + + /***** Slicecam::Math::calculate_centroid ************************************/ + /** + * @brief compute the centroid of the brightest source near an aim point + * @details Direct C++ translation of CF's detect_star_near_goal() in + * ngps_acq.c, with parameter defaults matching AUTOACQ_ARGS. + * + * Step 1: SExtractor-like background and sigma estimation over + * the background ROI. + * + * Step 2: extract the search ROI (= background ROI), background- + * subtract, and apply separable Gaussian smoothing + * (filt_sigma_pix = 1.2). + * + * Step 3: scan the filtered patch for local maxima that exceed + * the SNR threshold, have >= 4 adjacent pixels above threshold + * in the raw residual, and are within 40 pixels of the aim point. + * Among all qualifying candidates, select the brightest. + * + * Step 4: refine to sub-pixel centroid via iterative Gaussian- + * windowed first-moment (centroid_sigma_pix = 2.0, 12 iterations, + * eps = 0.01 px). + * + * All pixel coordinates are FITS 1-based on input and output. + * Internally everything is 0-based. + * + */ + long Math::calculate_centroid( const std::vector &image, + long ncols, long nrows, + Rect background, + Point aimpoint, + Point ¢roid ) { + if ( image.empty() || ncols <= 0 || nrows <= 0 ) return ERROR; + + // Convert 1-based inclusive ROI to 0-based, clamped + const long bx1 = std::max( 0L, background.x1 - 1 ); + const long bx2 = std::min( ncols - 1L, background.x2 - 1 ); + const long by1 = std::max( 0L, background.y1 - 1 ); + const long by2 = std::min( nrows - 1L, background.y2 - 1 ); + + if ( bx2 < bx1 || by2 < by1 ) return ERROR; + + // Search ROI = background ROI (no separate search ROI configured) + const long sx1 = bx1, sx2 = bx2, sy1 = by1, sy2 = by2; + + // --- Step 1: background and sigma estimation --- + std::vector samples; + samples.reserve( static_cast( (bx2 - bx1 + 1) * (by2 - by1 + 1) ) ); + for ( long y = by1; y <= by2; y++ ) + for ( long x = bx1; x <= bx2; x++ ) + samples.push_back( image[y * ncols + x] ); + + double bkg = 0.0, sigma = 1.0; + bg_sigma( samples, bkg, sigma ); + + if ( !std::isfinite( sigma ) || sigma <= 0.0 ) return ERROR; + + // --- Step 2: background-subtract the search patch and smooth --- + const int w = static_cast( sx2 - sx1 + 1 ); + const int h = static_cast( sy2 - sy1 + 1 ); + if ( w <= 3 || h <= 3 ) return ERROR; + + std::vector patch( w * h ); + std::vector tmp( w * h ); + std::vector filt( w * h ); + + for ( int yy = 0; yy < h; yy++ ) { + const long y = sy1 + yy; + for ( int xx = 0; xx < w; xx++ ) { + const long x = sx1 + xx; + // keep negatives — filter uses them too (matches CF) + patch[yy * w + xx] = static_cast( + static_cast( image[y * ncols + x] ) - bkg ); + } + } + + // filt_sigma_pix = 1.2 (CF default) + int kr = 0; + const std::vector kernel = make_gaussian_kernel( 1.2, kr ); + convolve_separable( patch, tmp, filt, w, h, kernel, kr ); + + const double sumsq1d = kernel_sum_sq( kernel ); + const double sigma_filt = ( sumsq1d > 0.0 ) ? sigma * sumsq1d : sigma; + + // SNR thresholds (snr_thresh = 3.0, CF default) + const double thr_filt = 3.0 * sigma_filt; // threshold in filtered image + const double thr_raw = 3.0 * sigma; // threshold for adjacency check + + // Aim point in 0-based image coordinates + const double goal_x0 = aimpoint.x - 1.0; + const double goal_y0 = aimpoint.y - 1.0; + const double max_dist = 40.0; // pixels; CF's --max-dist default + + // --- Step 3: find best local maximum in the filtered patch --- + double best_val = -1.0e300; + long best_x = -1; + long best_y = -1; + + // skip border pixels (yy=0, yy=h-1, xx=0, xx=w-1) — local max test + // requires all 4 neighbours to exist + for ( int yy = 1; yy < h - 1; yy++ ) { + for ( int xx = 1; xx < w - 1; xx++ ) { + const float v = filt[yy * w + xx]; + + // must exceed detection threshold in filtered image + if ( static_cast( v ) < thr_filt ) continue; + + // must be a local maximum in the filtered image (4-connected) + if ( v < filt[ yy * w + (xx - 1)] ) continue; + if ( v < filt[ yy * w + (xx + 1)] ) continue; + if ( v < filt[(yy - 1) * w + xx ] ) continue; + if ( v < filt[(yy + 1) * w + xx ] ) continue; + + // convert patch coordinates to full-image coordinates (0-based) + const long x0 = sx1 + xx; + const long y0 = sy1 + yy; + + // must be within max_dist pixels of the aim point + const double dxg = static_cast( x0 ) - goal_x0; + const double dyg = static_cast( y0 ) - goal_y0; + if ( std::hypot( dxg, dyg ) > max_dist ) continue; + + // adjacency check in the raw residual patch: need >= 4 of 8 neighbours + // above the raw threshold (rejects hot pixels and cosmic rays) + int nadj = 0; + for ( int dy = -1; dy <= 1; dy++ ) { + for ( int dx = -1; dx <= 1; dx++ ) { + if ( dx == 0 && dy == 0 ) continue; + if ( static_cast( patch[(yy + dy) * w + (xx + dx)] ) > thr_raw ) + nadj++; + } + } + if ( nadj < 4 ) continue; // min_adjacent = 4; CF's --min-adj default + + // rank by raw (not filtered) peak value — brightest candidate wins + const double rawv = static_cast( patch[yy * w + xx] ); + if ( rawv > best_val ) { + best_val = rawv; + best_x = x0; + best_y = y0; + } + } + } + + { + std::ostringstream oss; + oss << "[DEBUG] bkg=" << bkg << " sigma=" << sigma + << " best_val=" << best_val << " best_x=" << best_x << " best_y=" << best_y; + logwrite("Slicecam::Math::calculate_centroid", oss.str()); + } + + if ( best_x < 0 ) return ERROR; // no source found + + // --- Step 4: iterative Gaussian-windowed first-moment centroid --- + // + // centroid_halfwin = 4 (CF's --centroid-hw default) + // centroid_sigma_pix = 2.0 (CF's default, NOTE: different from filt_sigma) + // centroid_maxiter = 12 (CF's default) + // centroid_eps_pix = 0.01 + // + const int hw = 4; + const double s2 = 2.0 * 2.0; // centroid_sigma_pix^2 + + double cx = static_cast( best_x ); + double cy = static_cast( best_y ); + double sumI = 0.0; + + for ( int it = 0; it < 12; it++ ) { + const long xlo = std::max( sx1, static_cast( cx ) - hw ); + const long xhi = std::min( sx2, static_cast( cx ) + hw ); + const long ylo = std::max( sy1, static_cast( cy ) - hw ); + const long yhi = std::min( sy2, static_cast( cy ) + hw ); + + double sumX = 0.0, sumY = 0.0; + sumI = 0.0; + + for ( long y = ylo; y <= yhi; y++ ) { + for ( long x = xlo; x <= xhi; x++ ) { + const double I = static_cast( image[y * ncols + x] ) - bkg; + if ( I <= 0.0 ) continue; + const double dx = static_cast( x ) - cx; + const double dy = static_cast( y ) - cy; + const double wgt = std::exp( -0.5 * ( dx * dx + dy * dy ) / s2 ) * I; + sumI += wgt; + sumX += wgt * static_cast( x ); + sumY += wgt * static_cast( y ); + } + } + + if ( sumI <= 0.0 ) break; + + const double ncx = sumX / sumI; + const double ncy = sumY / sumI; + const double shift = std::hypot( ncx - cx, ncy - cy ); + cx = ncx; + cy = ncy; + + if ( shift < 0.01 ) break; // centroid_eps_pix + } + + if ( sumI <= 0.0 || !std::isfinite( cx ) || !std::isfinite( cy ) ) return ERROR; + + // Return in FITS 1-based coordinates + centroid.x = cx + 1.0; + centroid.y = cy + 1.0; + + return NO_ERROR; + } + /***** Slicecam::Math::calculate_centroid ************************************/ + + + /***** Slicecam::Math::pix2world *********************************************/ + /** + * @brief convert 1-based pixel coordinate to sky (RA/DEC) in degrees + * @details Applies the standard FITS affine WCS: + * u = pix.x - CRPIX1 + * v = pix.y - CRPIX2 + * world.ra = CRVAL1 + CDELT1 * (PC1_1 * u + PC1_2 * v) + * world.dec = CRVAL2 + CDELT2 * (PC2_1 * u + PC2_2 * v) + * Valid for the gnomonic (TAN) projection over a small field. + * Throws if any required WCS key is absent. + * + */ + void Math::pix2world( const Common::FitsKeys &keys, Point pix, World &world ) { + const double crpix1 = keys.get_key( "CRPIX1" ); + const double crpix2 = keys.get_key( "CRPIX2" ); + const double crval1 = keys.get_key( "CRVAL1" ); + const double crval2 = keys.get_key( "CRVAL2" ); + const double cdelt1 = keys.get_key( "CDELT1" ); + const double cdelt2 = keys.get_key( "CDELT2" ); + const double pc1_1 = keys.get_key( "PC1_1" ); + const double pc1_2 = keys.get_key( "PC1_2" ); + const double pc2_1 = keys.get_key( "PC2_1" ); + const double pc2_2 = keys.get_key( "PC2_2" ); + + const double u = pix.x - crpix1; + const double v = pix.y - crpix2; + + world.ra = crval1 + cdelt1 * ( pc1_1 * u + pc1_2 * v ); + world.dec = crval2 + cdelt2 * ( pc2_1 * u + pc2_2 * v ); + } + /***** Slicecam::Math::pix2world *********************************************/ + + + /***** Slicecam::Math::calculate_acquisition_offsets *************************/ + /** + * @brief compute (dRA*cos(dec), dDEC) offset to move star onto aim point + * @details Returns (star - goal) as a true on-sky angular offset in degrees. + * Applying this offset to the telescope pointing moves the star + * onto the aim point. + * + */ + void Math::calculate_acquisition_offsets( World star, World goal, + std::pair &offsets ) { + double dra = star.ra - goal.ra; + while ( dra > 180.0 ) dra -= 360.0; + while ( dra < -180.0 ) dra += 360.0; + + const double cosdec = std::cos( goal.dec * M_PI / 180.0 ); + offsets = { dra * cosdec, star.dec - goal.dec }; + } + /***** Slicecam::Math::calculate_acquisition_offsets *************************/ + +} diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h new file mode 100644 index 00000000..74fb6cdb --- /dev/null +++ b/slicecamd/slicecam_math.h @@ -0,0 +1,72 @@ +/** --------------------------------------------------------------------------- + * @file slicecam_math.h + * @brief slicecam math utilities + * @details Declares structs and the Math class used for centroid detection, + * WCS pixel-to-sky conversion, and fine-acquisition offset calculation. + * @author David Hale, Christoffer Fremling + * + */ + +#pragma once + +#include +#include +#include +#include +#include "common.h" ///< for Common::FitsKeys + +namespace Slicecam { + + struct Point { ///< pixel coordinate + double x = 0.0; double y = 0.0; + bool is_valid() const noexcept { + return !std::isnan(x) && !std::isnan(y) && + x >= 0.0 && y >= 0.0; + } + }; + + struct Rect { ///< rectangular region + long x1 = 1; long x2 = 1; long y1 = 1; long y2 = 1; + bool is_valid() const noexcept { + return x1>0 && x2>0 && y1>0 && y2>0 && x1 != x2 && y1 != y2; + } + }; + + struct World { ///< sky coordinates + double ra = 0.0; double dec = 0.0; + bool is_valid() const noexcept { + return !std::isnan(ra) && !std::isnan(dec) && + ra >= 0.0 && dec >= 0.0; + } + }; + + /***** Slicecam::Math *******************************************************/ + /** + * @brief Static math utilities for slicecam fine acquisition + * + */ + class Math { + public: + /** + * @brief compute the centroid of the brightest source near an aim point + */ + static long calculate_centroid( const std::vector &image, + long cols, long rows, + Rect background, + Point aimpoint, + Point ¢roid ); + /** + * @brief convert pixel coordinates to sky coordinates using WCS keys + */ + static void pix2world( const Common::FitsKeys &keys, Point pix, World &world ); + + /** + * @brief compute the (dRA, dDEC) offset from a goal position to a star + */ + static void calculate_acquisition_offsets( World star, World goal, + std::pair &offsets ); + + }; + /***** Slicecam::Math *******************************************************/ + +} diff --git a/slicecamd/slicecam_server.cpp b/slicecamd/slicecam_server.cpp index 226227de..8ec19e6d 100644 --- a/slicecamd/slicecam_server.cpp +++ b/slicecamd/slicecam_server.cpp @@ -531,6 +531,10 @@ namespace Slicecam { ret = this->interface.fan_mode( args, retstring ); } else + if ( cmd == SLICECAMD_FINEACQUIRE ) { + ret = this->interface.fineacquire( args, retstring ); + } + else if ( cmd == SLICECAMD_GAIN ) { ret = this->interface.gain( args, retstring ); // set gain if (ret==NO_ERROR) this->interface.gui_settings_control(); // update GUI display igores ret diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index 332c51d2..82279803 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -146,14 +146,13 @@ int main(int argc, char **argv) { // initialize the pub/sub handler, which // takes a list of subscription topics // - if ( slicecamd.interface.init_pubsub({"slitd", "tcsd"}) == ERROR ) { + if ( slicecamd.interface.init_pubsub( { Topic::SLITD, + Topic::ACAMD, + Topic::TCSD }) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); slicecamd.exit_cleanly(); } - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - slicecamd.interface.publish_snapshot(); - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); slicecamd.interface.request_snapshot(); diff --git a/thermald/thermal_interface.cpp b/thermald/thermal_interface.cpp index bfc5b438..9f59a09c 100644 --- a/thermald/thermal_interface.cpp +++ b/thermald/thermal_interface.cpp @@ -154,7 +154,7 @@ namespace Thermal { // no errors, so disseminate the message contents based on the message type // if ( messagetype == "acaminfo" ) { - this->process_key( jmessage, "TANDOR_ACAM" ); + this->process_key( jmessage, Key::Acamd::TANDOR ); } else if ( messagetype == "slicecaminfo" ) { diff --git a/thermald/thermal_interface.h b/thermald/thermal_interface.h index fa246006..9d3d0c8d 100644 --- a/thermald/thermal_interface.h +++ b/thermald/thermal_interface.h @@ -8,6 +8,7 @@ #pragma once +#include "message_keys.h" #include "network.h" #include "logentry.h" #include "common.h" From f671b4877dee26b9bd3ab42db79706f0fdb31b70 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 30 Mar 2026 15:27:29 -0700 Subject: [PATCH 03/37] first steps making helpers that will aid in sequencer scripting --- sequencerd/sequence.cpp | 448 ++++++++++++++++++++++++---------------- sequencerd/sequence.h | 15 +- 2 files changed, 286 insertions(+), 177 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 5db8147d..fc3de240 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -18,6 +18,199 @@ namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command + /***** Sequencer::Sequence::run ********************************************/ + /** + * @brief + * @param[in] op Operation + * @param[in] function function name of operation for logging + * + */ + long Sequence::run( const Operation &op, + const std::string &function ) { + long error=NO_ERROR; + try { + error = op.func(); + + if (error != NO_ERROR) { + this->async.enqueue_and_log(function, "ERROR in "+op.name); + } + } + catch (const std::exception &e) { + logwrite(function, "ERROR in "+op.name+": "+e.what()); + } + return error; + } + /***** Sequencer::Sequence::run ********************************************/ + + + /***** Sequencer::Sequence::run_parallel ***********************************/ + /** + * @brief + * @param[in] op Operation + * @param[in] function function name of operation for logging + * + */ + long Sequence::run_parallel( const std::vector &ops, + const std::string &function ) { + + // start a thread for each operation + // + std::vector> futures; + for (const auto &op : ops) { + futures.emplace_back(std::async(std::launch::async, [this, &op, function]() { + try { + return op.func(); + } + catch (const std::exception &e) { + logwrite(function, "ERROR in "+op.name+": "+e.what()); + return ERROR; + } + })); + } + + long error=NO_ERROR; + + // wait for each thread to complete + // + for (size_t i=0; i ops; + + if (this->target.pointmode == Acam::POINTMODE_ACAM) { + this->dotype("ONE"); + ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + } + else { + this->target.pointmode = Acam::POINTMODE_SLIT; + + // these are the default operations prior to exposure, + // they can be done in parallel + ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + } + + // wait for threads + error = run_parallel(ops, caller); + + // early exit on error + if (error != NO_ERROR) return error; + + if (this->cancel_flag.load()) return ABORT; + + // ---------- POINTMODE-ACAM EXIT ---------------------- + + // If pointmode is ACAM then the user has chosen to put the star on ACAM, in + // which case the assumption is made that nothing else matters. This special + // mode of operation only points the telescope. + // + if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { + this->async.enqueue_and_log(caller, "NOTICE: target list processing has stopped"); + return NO_ERROR; + } + + // if not a calibration target then acquire, first acam then slicecam + if (!this->target.iscal) { + + // ---------- TARGET ACQUISITION --------------------- + + // start ACAM acquisition. If it fails then wait for user to continue or cancel. + if ( this->do_acam_acquire() != NO_ERROR ) { + + this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); + return ABORT; + } + } + else + // start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + + this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); + } + + // ---------- TARGET OFFSETS ------------------------- + + // send offsets. wait for user if that fails or cancelled + if (this->target_offset() == ERROR) { + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log(caller, "NOTICE: cancelled"); + return ABORT; + } + } + + // ---------- SLIT POSITON FOR EXPOSE ---------------- + + // ensure slit offset is in "expose" position when needed + try { + error |= this->slit_set(Sequencer::VSM_EXPOSE); + } + catch (const std::exception &e) { + logwrite(caller, "ERROR slit offset exception: "+std::string(e.what())); + return ERROR; + } + } + + // ---------- EXPOSURE --------------------------------- + + logwrite(caller, "starting exposure"); + + // Start the exposure in a thread... + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); + try { + error = start_exposure.get(); + } + catch (const std::exception& e) { + logwrite( caller, "ERROR start_exposure exception: "+std::string(e.what()) ); + return ERROR; + } + + // wait for the exposure to end (naturally or cancelled) + // + logwrite( caller, "waiting for exposure" ); + if (error==NO_ERROR) error = this->wait_for_exposure(caller); + + // If not using frame transfer then wait for readout, too + // + if (error==NO_ERROR && !this->is_science_frame_transfer) { + logwrite( caller, "waiting for readout" ); + error = this->wait_for_readout(caller); + } + + return error; + } + /***** Sequencer::Sequence::run_default_sequence ***************************/ + + /***** Sequencer::Sequence::handletopic_snapshot ***************************/ /** * @brief publishes snapshot of my telemetry @@ -412,40 +605,88 @@ namespace Sequencer { * @brief waits for the user to click a button, or cancel * @details Use this when you just want to slow things down or get a * cup of coffee instead of observing. + * @param[in] caller reference to caller's name for logging * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_user() { - const std::string function("Sequencer::Sequence::wait_for_user"); + long Sequence::wait_for_user(const std::string &caller) { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - this->async.enqueue_and_log( function, "NOTICE: waiting for USER to send \"continue\" signal" ); + this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); } - this->async.enqueue_and_log( function, "NOTICE: received " + this->async.enqueue_and_log( caller, "NOTICE: received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) +" signal!" ); } // end scope for wait_state = WAIT_USER if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); + this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); return ABORT; } this->is_usercontinue.store(false); - this->async.enqueue_and_log( function, "NOTICE: received USER continue signal!" ); - return NO_ERROR; } /***** Sequencer::Sequence::wait_for_user ***********************************/ + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + /** + * @brief waits for exposure completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_exposure(const std::string &caller) { + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + + + /***** Sequencer::Sequence::wait_for_readout ********************************/ + /** + * @brief waits for readout completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_readout(const std::string &caller) { + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_readout ********************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -569,174 +810,17 @@ namespace Sequencer { break; } - // get the threads going -- - // - // These things can all be done in parallel, just have to sync up at the end. - // - - // threads to start, pair their ThreadStatusBit with the function to call - std::vector>> worker_threads; - - // If pointmode is ACAM then the user has chosen to put the star on ACAM, in - // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->dotype( "ONE" ); - worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) } }; - - } - else { - - // For any other pointmode (SLIT, or empty, which assumes SLIT), all - // subsystems are readied. - // - // set pointmode explicitly, in case it's empty - this->target.pointmode = Acam::POINTMODE_SLIT; - - // threads to start, pair their ThreadStatusBit with the function to call - // - worker_threads = { { THR_MOVE_TO_TARGET, std::bind(&Sequence::move_to_target, this) }, - { THR_CAMERA_SET, std::bind(&Sequence::camera_set, this) }, - { THR_FOCUS_SET, std::bind(&Sequence::focus_set, this) }, - { THR_FLEXURE_SET, std::bind(&Sequence::flexure_set, this) }, - { THR_CALIB_SET, std::bind(&Sequence::calib_set, this) }, - // for CAL targets, slit comes from database, otherwise use VSM acquire position - { THR_SLIT_SET, std::bind(&Sequence::slit_set, this, - this->target.iscal ? Sequencer::VSM_DATABASE : Sequencer::VSM_ACQUIRE) } - }; - } - - // pair their ThreadStatusBit with their future - std::vector>> worker_futures; - - // start all of the threads - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete. these can be cancelled. + // default observation sequence // - for ( auto &[thr, future] : worker_futures) { - try { - error |= future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - return; - } - } - - logwrite(function, "DONE waiting on threads"); + error = run_default_sequence(function); - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: sequence cancelled" ); - return; - } - - // For pointmode ACAM, there is nothing to be done so get out - // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->async.enqueue_and_log( function, "NOTICE: target list processing has stopped" ); + if (error != NO_ERROR) { + this->thread_error_manager.set(THR_SEQUENCE_START); break; } - // If not a calibration target then acquire, first acam then slicecam - // - if ( !this->target.iscal ) { - - // start ACAM acquisition. If it fails then wait for user to continue or cancel. - if ( this->do_acam_acquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING acam acquisition failed" ); - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - else - // start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log( function, "WARNING slicecam fine acquisition failed" ); - } - } - - if ( !this->target.iscal ) { - // send offsets. wait for user if that fails to continue or cancel. - if ( this->target_offset() == ERROR ) { - if (this->wait_for_user()==ABORT) { - this->async.enqueue_and_log( function, "NOTICE: cancelled" ); - return; - } - } - // ensure slit offset is in "expose" position when needed - try { - error |= this->slit_set(Sequencer::VSM_EXPOSE); - } - catch (const std::exception& e) { - logwrite( function, "ERROR slit offset exception: "+std::string(e.what()) ); - return; - } - } - - logwrite( function, "starting exposure" ); ///< TODO @todo log to telemetry! - - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error |= start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR repeat_exposure exception: "+std::string(e.what()) ); - return; - } - - // wait for the exposure to end (naturally or cancelled) - // - logwrite( function, "waiting for exposure" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || this->cancel_flag.load() ); } ); - } - - // When an exposure is aborted then it will be marked as UNASSIGNED - // - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: exposure cancelled" ); - error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); - message.str(""); message << ( error==NO_ERROR ? "" : "ERROR " ) << "marking target " << this->target.name - << " id " << this->target.obsid << " order " << this->target.obsorder - << " as " << Sequencer::TARGET_UNASSIGNED; - logwrite( function, message.str() ); - return; - } - - this->async.enqueue_and_log( function, "NOTICE: done waiting for expose" ); - message.str(""); message << "exposure complete for target " << this->target.name - << " id " << this->target.obsid << " order " << this->target.obsorder; - logwrite( function, message.str() ); - - // If not using frame transfer then wait for readout, too - // - if (!this->is_science_frame_transfer) { - logwrite( function, "waiting for readout" ); - while ( !this->cancel_flag.load() && wait_state_manager.is_set( Sequencer::SEQ_WAIT_READOUT ) ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( !wait_state_manager.is_set(SEQ_WAIT_READOUT) || this->cancel_flag.load() ); } ); - } - } - - // Now that we're done waiting, check for errors or abort - // - if ( this->thread_error_manager.are_any_set() ) { - message.str(""); message << "ERROR stopping sequencer because the following thread(s) had an error: " - << this->thread_error_manager.get_set_states(); - logwrite( function, message.str() ); + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(function, "NOTICE: sequence cancelled"); break; } @@ -747,14 +831,24 @@ namespace Sequencer { // Update this target's state in the database // - error = this->target.update_state( Sequencer::TARGET_COMPLETE ); // update the active target table + if (error==NO_ERROR) error = this->target.update_state( Sequencer::TARGET_COMPLETE ); + else + if (error==ABORT) error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); + if (error==NO_ERROR) error = this->target.insert_completed(); // insert into the completed table if (error!=NO_ERROR) this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + std::ostringstream oss; + oss << "TARGETSTATE:" << this->target.state + << " TARGET:" << this->target.name + << " OBSID:" << this->target.obsid; + this->async.enqueue_and_log(function, oss.str()); + + // abort sequence on error + // + if ( this->thread_error_manager.are_any_set() ) break; // Check the "dotype" -- // If this was "do one" then do_once is set and get out now. @@ -767,9 +861,11 @@ namespace Sequencer { } // end while true if ( this->thread_error_manager.are_any_set() ) { - logwrite( function, "requesting stop because an error was detected" ); - if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { // If this target was flagged as active, - this->target.update_state( Sequencer::TARGET_UNASSIGNED ); // then change it to unassigned on error. + logwrite(function, "ERROR stopping sequencer due to error in: "+ + this->thread_error_manager.get_set_states()); + // If this target was flagged as active, then change it to unassigned on error. + if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { + this->target.update_state( Sequencer::TARGET_UNASSIGNED ); } this->thread_error_manager.clear_all(); // clear the thread error state this->do_once.store(true); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index ab86cc45..de3975b2 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -291,6 +291,12 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + struct Operation { + std::string name; + ThreadStatusBits thr; + std::function func; + }; + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), const std::string &function) { @@ -456,6 +462,11 @@ namespace Sequencer { float slitoffsetacquire; ///< "virtual slit mode" offset for acquire float slitwidthacquire; ///< "virtual slit mode" width for acquire + // new stuff + long run(const Operation &op, const std::string &function); + long run_parallel(const std::vector &ops, const std::string &function); + long run_default_sequence(const std::string &caller); + // publish/subscribe functions // long init_pubsub(const std::initializer_list &topics={}) { @@ -560,7 +571,9 @@ namespace Sequencer { void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_user(); ///< wait for the user or cancel + long wait_for_user(const std::string &function); ///< wait for the user or cancel + long wait_for_exposure(const std::string &function); ///< wait for exposure completion or cancel + long wait_for_readout(const std::string &function); ///< wait for readout completion or cancel void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params From 2eeb0163abe40c386d6b2a0242bb5f175230a570 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 31 Mar 2026 09:02:33 -0700 Subject: [PATCH 04/37] adds a sequencial operations wrapper --- sequencerd/sequence.cpp | 222 ++++++++++++++++++++++++++-------------- sequencerd/sequence.h | 1 + 2 files changed, 144 insertions(+), 79 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index fc3de240..41dd9d52 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -20,49 +20,88 @@ namespace Sequencer { /***** Sequencer::Sequence::run ********************************************/ /** - * @brief - * @param[in] op Operation - * @param[in] function function name of operation for logging + * @brief executes a single operation + * @param[in] op Operation + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT * */ long Sequence::run( const Operation &op, - const std::string &function ) { + const std::string &caller ) { long error=NO_ERROR; try { error = op.func(); if (error != NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR in "+op.name); + this->async.enqueue_and_log(caller, "ERROR in "+op.name); } } catch (const std::exception &e) { - logwrite(function, "ERROR in "+op.name+": "+e.what()); + logwrite(caller, "ERROR in "+op.name+": "+e.what()); } return error; } /***** Sequencer::Sequence::run ********************************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ + /** + * @brief executes operations in sequence, one at a time + * @param[in] op vector of Operations to execute + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_sequence( const std::vector &ops, + const std::string &caller ) { + + for (const auto &op : ops) { + + if (this->cancel_flag.load()) return ABORT; + + logwrite(caller, "starting "+op.name); + + try { + long error; + if ( (error = op.func()) != NO_ERROR ) { + std::ostringstream oss; + oss << (error==ABORT ? "cancelled" : "ERROR") << " in " << op.name; + logwrite(caller, oss.str()); + return error; + } + } + catch (const std::exception &e) { + logwrite(caller, "ERROR in "+op.name+": "+std::string(e.what())); + return ERROR; + } + } + return NO_ERROR; + } + /***** Sequencer::Sequence::run_sequence ***********************************/ + + /***** Sequencer::Sequence::run_parallel ***********************************/ /** - * @brief - * @param[in] op Operation - * @param[in] function function name of operation for logging + * @brief executes operations in parallel threads + * @details This will return only when all have completed. + * @param[in] op vector of Operations to execute + * @param[in] caller name of calling function for logging + * @return ERROR|NO_ERROR|ABORT * */ long Sequence::run_parallel( const std::vector &ops, - const std::string &function ) { + const std::string &caller ) { // start a thread for each operation // std::vector> futures; for (const auto &op : ops) { - futures.emplace_back(std::async(std::launch::async, [this, &op, function]() { + futures.emplace_back(std::async(std::launch::async, [this, &op, caller]() { try { return op.func(); } catch (const std::exception &e) { - logwrite(function, "ERROR in "+op.name+": "+e.what()); + logwrite(caller, "ERROR in "+op.name+": "+e.what()); return ERROR; } })); @@ -75,10 +114,10 @@ namespace Sequencer { for (size_t i=0; i ops; + std::vector par_ops; + // If pointmode is ACAM then the user has chosen to put the star on ACAM, in + // which case the assumption is made that nothing else matters. This special + // mode of operation only points the telescope. + // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; } else { this->target.pointmode = Acam::POINTMODE_SLIT; + // ---------- RUN THESE IN PARALLEL ------------------ + // these are the default operations prior to exposure, // they can be done in parallel - ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; } - // wait for threads - error = run_parallel(ops, caller); + // execute in parallel threads and wait for completion + error = run_parallel(par_ops, caller); // early exit on error if (error != NO_ERROR) return error; @@ -131,80 +182,89 @@ namespace Sequencer { return NO_ERROR; } + // ---------- RUN THESE IN SERIES ---------------------- + + std::vector seq_ops; + // if not a calibration target then acquire, first acam then slicecam if (!this->target.iscal) { // ---------- TARGET ACQUISITION --------------------- - // start ACAM acquisition. If it fails then wait for user to continue or cancel. - if ( this->do_acam_acquire() != NO_ERROR ) { + seq_ops.push_back( { "acam_acquire", THR_ACQUISITION, + [this,caller]() { - this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + // start ACAM acquisition. + if ( this->do_acam_acquire() != NO_ERROR ) { - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); - return ABORT; - } - } - else - // start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); - this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); - } + // If acquisition fails, wait for user to continue or cancel. + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); + return ABORT; + } + } + else + // ACAM acquire success, start SLICECAM fine acquisition + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + + // slicecam fine acquire failure is not fatal + this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); + } + } } ); // ---------- TARGET OFFSETS ------------------------- - // send offsets. wait for user if that fails or cancelled - if (this->target_offset() == ERROR) { + seq_ops.push_back( { "target_offset", THR_MOVE_TO_TARGET, + [this,caller]() { - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log(caller, "NOTICE: cancelled"); - return ABORT; - } - } + // send offsets. wait for user if that fails or cancelled + if (this->target_offset() == ERROR) { + + if (this->wait_for_user(caller)==ABORT) { + this->async.enqueue_and_log(caller, "NOTICE: cancelled"); + return ABORT; + } + } + } } ); // ---------- SLIT POSITON FOR EXPOSE ---------------- - // ensure slit offset is in "expose" position when needed - try { - error |= this->slit_set(Sequencer::VSM_EXPOSE); - } - catch (const std::exception &e) { - logwrite(caller, "ERROR slit offset exception: "+std::string(e.what())); - return ERROR; - } + seq_ops.push_back( { "slit_expose", THR_SLIT_SET, + [this]() { + // This was moved to VSM_ACQUIRE initially, then VSM_EXPOSE after acquisition. + return this->slit_set(Sequencer::VSM_EXPOSE); + } } ); } + // end if iscal // ---------- EXPOSURE --------------------------------- - logwrite(caller, "starting exposure"); + seq_ops.push_back( { "trigger_exposure", THR_TRIGGER_EXPOSURE, + [this, caller]() { - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error = start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( caller, "ERROR start_exposure exception: "+std::string(e.what()) ); - return ERROR; - } + logwrite(caller, "starting exposure"); - // wait for the exposure to end (naturally or cancelled) - // - logwrite( caller, "waiting for exposure" ); - if (error==NO_ERROR) error = this->wait_for_exposure(caller); + // Start the exposure in a thread... + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); + long ret = start_exposure.get(); - // If not using frame transfer then wait for readout, too - // - if (error==NO_ERROR && !this->is_science_frame_transfer) { - logwrite( caller, "waiting for readout" ); - error = this->wait_for_readout(caller); - } + if (ret==NO_ERROR) ret = this->wait_for_exposure(caller); + + if (ret==NO_ERROR && !this->is_science_frame_transfer) { + ret = this->wait_for_readout(caller); + } + return ret; + } } ); + + // ---------- RUN THE SEQUENCE NOW --------------------- + + error = run_sequence(seq_ops, caller); return error; } @@ -645,6 +705,7 @@ namespace Sequencer { * */ long Sequence::wait_for_exposure(const std::string &caller) { + logwrite(caller, "waiting for exposure"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { std::unique_lock lock(cv_mutex); @@ -670,6 +731,7 @@ namespace Sequencer { * */ long Sequence::wait_for_readout(const std::string &caller) { + logwrite(caller, "waiting for readout"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { std::unique_lock lock(cv_mutex); @@ -703,7 +765,7 @@ namespace Sequencer { */ void Sequence::sequence_start(std::string obsid_in="") { const std::string function("Sequencer::Sequence::sequence_start"); - std::stringstream message; + std::ostringstream message; std::string reply; std::string targetstatus; TargetInfo::TargetState targetstate; @@ -793,7 +855,9 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state << " TARGET:" << this->target.name << " OBSID:" << this->target.obsid; + message.str(""); message << "TARGETSTATE:" << this->target.state + << " TARGET:" << this->target.name + << " OBSID:" << this->target.obsid; this->async.enqueue( message.str() ); #ifdef LOGLEVEL_DEBUG logwrite( function, "[DEBUG] target found, starting threads" ); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index de3975b2..8c954279 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -464,6 +464,7 @@ namespace Sequencer { // new stuff long run(const Operation &op, const std::string &function); + long run_sequence(const std::vector &ops, const std::string &function); long run_parallel(const std::vector &ops, const std::string &function); long run_default_sequence(const std::string &caller); From 3f699b25e83f569ecd4a97488e491b25c05217a2 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 31 Mar 2026 11:47:57 -0700 Subject: [PATCH 05/37] adds one more level of abstraction, Operation Blocks, to aid in scripting --- sequencerd/sequence.cpp | 230 +++++++++++++++++++++++----------------- sequencerd/sequence.h | 22 +++- 2 files changed, 149 insertions(+), 103 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 41dd9d52..9110b1d2 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -29,6 +29,8 @@ namespace Sequencer { long Sequence::run( const Operation &op, const std::string &caller ) { long error=NO_ERROR; + logwrite(caller, "starting "+op.name); + try { error = op.func(); @@ -38,6 +40,7 @@ namespace Sequencer { } catch (const std::exception &e) { logwrite(caller, "ERROR in "+op.name+": "+e.what()); + error = ERROR; } return error; } @@ -92,40 +95,83 @@ namespace Sequencer { long Sequence::run_parallel( const std::vector &ops, const std::string &caller ) { + std::vector> futures; + // start a thread for each operation // - std::vector> futures; for (const auto &op : ops) { - futures.emplace_back(std::async(std::launch::async, [this, &op, caller]() { - try { - return op.func(); - } - catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name+": "+e.what()); - return ERROR; - } - })); + futures.emplace_back(std::async(std::launch::async, op.func)); } - long error=NO_ERROR; + long error = NO_ERROR; - // wait for each thread to complete + // wait for all threads, collect errors // - for (size_t i=0; i &blocks, + const std::string &caller, + bool continue_on_error ) { + long error = NO_ERROR; + + for (const auto &block : blocks) { + if (this->cancel_flag.load()) return ABORT; + + // PARALLEL Blocks are executed in parallel threads + // + if (block.type == OperationType::PARALLEL) { + long ret = run_parallel(block.operations, caller); + error |= ret; + + if (ret != NO_ERROR && !continue_on_error) return error; + } + // SERIAL Blocks are executed one at a time + // + else { + for (const auto &op : block.operations) { + if (this->cancel_flag.load()) return ABORT; + + long ret = run(op, caller); + error |= ret; + + if (ret != NO_ERROR && !continue_on_error) return error; + } + } + } + + return error; + } + /***** Sequencer::Sequence::run_operation_blocks ***************************/ + + /***** Sequencer::Sequence::run_default_sequence ***************************/ /** * @brief executes a default observation sequence @@ -135,9 +181,9 @@ namespace Sequencer { */ long Sequence::run_default_sequence(const std::string &caller) { - long error = NO_ERROR; + std::vector blocks; - std::vector par_ops; + // ---------- RUN THESE IN PARALLEL -------------------- // If pointmode is ACAM then the user has chosen to put the star on ACAM, in // which case the assumption is made that nothing else matters. This special @@ -145,128 +191,113 @@ namespace Sequencer { // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } }; + blocks.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; - // ---------- RUN THESE IN PARALLEL ------------------ - // these are the default operations prior to exposure, // they can be done in parallel - par_ops = { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } }; + blocks.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, + [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } } } ); } - // execute in parallel threads and wait for completion - error = run_parallel(par_ops, caller); - - // early exit on error - if (error != NO_ERROR) return error; - - if (this->cancel_flag.load()) return ABORT; - - // ---------- POINTMODE-ACAM EXIT ---------------------- - - // If pointmode is ACAM then the user has chosen to put the star on ACAM, in - // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. + // Early Exit for pointmode=ACAM // - if ( this->target.pointmode == Acam::POINTMODE_ACAM ) { - this->async.enqueue_and_log(caller, "NOTICE: target list processing has stopped"); - return NO_ERROR; + if (this->target.pointmode == Acam::POINTMODE_ACAM) { + return run_operation_blocks(blocks, caller); } // ---------- RUN THESE IN SERIES ---------------------- - std::vector seq_ops; - // if not a calibration target then acquire, first acam then slicecam if (!this->target.iscal) { // ---------- TARGET ACQUISITION --------------------- - seq_ops.push_back( { "acam_acquire", THR_ACQUISITION, - [this,caller]() { - - // start ACAM acquisition. - if ( this->do_acam_acquire() != NO_ERROR ) { + blocks.push_back( { OperationType::SERIAL, + { { "acam_acquire", THR_ACQUISITION, [this,caller]() { - this->async.enqueue_and_log( caller, "WARNING acam acquisition failed" ); + // if ACAM acquisition fails, wait for user to continue or cancel + if ( this->do_acam_acquire() != NO_ERROR ) { + return this->wait_for_user(caller); + } + else return NO_ERROR; + } }, - // If acquisition fails, wait for user to continue or cancel. - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log( caller, "NOTICE: cancelled" ); - return ABORT; + { "slicecam_fineacquire", THR_ACQUISITION, [this,caller]() { + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); } + return NO_ERROR; // slicecam fine acquire is never fatal + } } } - else - // ACAM acquire success, start SLICECAM fine acquisition - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + } ); + } - // slicecam fine acquire failure is not fatal - this->async.enqueue_and_log( caller, "WARNING slicecam fine acquisition failed" ); - } - } } ); + if (!this->target.iscal) { // ---------- TARGET OFFSETS ------------------------- - seq_ops.push_back( { "target_offset", THR_MOVE_TO_TARGET, - [this,caller]() { + blocks.push_back( { OperationType::SERIAL, + { { "target_offset", THR_MOVE_TO_TARGET, [this,caller]() { - // send offsets. wait for user if that fails or cancelled - if (this->target_offset() == ERROR) { - - if (this->wait_for_user(caller)==ABORT) { - this->async.enqueue_and_log(caller, "NOTICE: cancelled"); - return ABORT; + // if offsets fail, wait for user to continue or cancel + if (this->target_offset() != NO_ERROR) { + return this->wait_for_user(caller); } + else return NO_ERROR; + } } } - } } ); + } ); + } + + if (!this->target.iscal) { // ---------- SLIT POSITON FOR EXPOSE ---------------- - seq_ops.push_back( { "slit_expose", THR_SLIT_SET, - [this]() { - // This was moved to VSM_ACQUIRE initially, then VSM_EXPOSE after acquisition. - return this->slit_set(Sequencer::VSM_EXPOSE); - } } ); + blocks.push_back( { OperationType::SERIAL, + { { "slit_expose", THR_SLIT_SET, [this]() { + return this->slit_set(Sequencer::VSM_EXPOSE); } } + } + } ); } - // end if iscal // ---------- EXPOSURE --------------------------------- - seq_ops.push_back( { "trigger_exposure", THR_TRIGGER_EXPOSURE, - [this, caller]() { - - logwrite(caller, "starting exposure"); + blocks.push_back( { OperationType::SERIAL, + { { "trigger_exposure", THR_EXPOSURE, [this]() { - // Start the exposure in a thread... - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - long ret = start_exposure.get(); + // set the EXPOSE bit here, outside of the trigger_exposure function, because that + // function only triggers the exposure -- it doesn't block waiting for the exposure. + // + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit + return trigger_exposure(); + } }, - if (ret==NO_ERROR) ret = this->wait_for_exposure(caller); + { "wait_exposure", THR_EXPOSURE, [this,caller]() { + return this->wait_for_exposure(caller); + } }, - if (ret==NO_ERROR && !this->is_science_frame_transfer) { - ret = this->wait_for_readout(caller); + { "wait_readout", THR_EXPOSURE, [this,caller]() { + if (!this->is_science_frame_transfer) { + return this->wait_for_readout(caller); + } + else return NO_ERROR; + } } } - return ret; - } } ); + } ); // ---------- RUN THE SEQUENCE NOW --------------------- - error = run_sequence(seq_ops, caller); - - return error; + return run_operation_blocks(blocks, caller); } /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -4843,7 +4874,6 @@ namespace Sequencer { return( ERROR ); } - bool ispower = false; std::string reply; // power module must be initialized before any others. If this is not diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8c954279..c95b8dd3 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -188,6 +188,7 @@ namespace Sequencer { enum ThreadStatusBits : size_t { THR_SEQUENCER_ASYNC_LISTENER=0, THR_TRIGGER_EXPOSURE, + THR_EXPOSURE, THR_REPEAT_EXPOSURE, THR_STOP_EXPOSURE, THR_ABORT_PROCESS, @@ -232,6 +233,7 @@ namespace Sequencer { const std::map thread_names = { {THR_SEQUENCER_ASYNC_LISTENER, "async_listener"}, {THR_TRIGGER_EXPOSURE, "trigger_exposure"}, + {THR_EXPOSURE, "exposure"}, {THR_REPEAT_EXPOSURE, "repeat_exposure"}, {THR_STOP_EXPOSURE, "stop_exposure"}, {THR_ABORT_PROCESS, "abort_process"}, @@ -291,10 +293,21 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + enum class OperationType { + PARALLEL, + SERIAL + }; + struct Operation { - std::string name; - ThreadStatusBits thr; - std::function func; + std::string name; ///< name of this operation + ThreadStatusBits thr; ///< status bit of what is running + std::function func; ///< function that this operation calls + std::map params; ///< function parameters + }; + + struct OperationBlock { + OperationType type; + std::vector operations; }; /** @brief safely runs function in a detached thread using lambda to catch exceptions @@ -467,6 +480,9 @@ namespace Sequencer { long run_sequence(const std::vector &ops, const std::string &function); long run_parallel(const std::vector &ops, const std::string &function); long run_default_sequence(const std::string &caller); + long run_operation_blocks( const std::vector &blocks, + const std::string &caller, + bool continue_on_error=false ); // publish/subscribe functions // From 80e5a18cde70e5b9b072e8688a3bb01791e98517 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 3 Apr 2026 16:51:38 -0700 Subject: [PATCH 06/37] encapsulates more logic into blocks to aid scripting changes std::string to string_view where string not needed --- common/common.cpp | 10 +- common/common.h | 10 +- sequencerd/sequence.cpp | 282 +++++++++++++++------------- sequencerd/sequence.h | 25 ++- sequencerd/sequence_acquisition.cpp | 50 ++++- sequencerd/sequencer_server.cpp | 2 +- utils/logentry.cpp | 7 +- utils/logentry.h | 2 +- utils/network.cpp | 8 +- utils/network.h | 2 +- 10 files changed, 231 insertions(+), 167 deletions(-) diff --git a/common/common.cpp b/common/common.cpp index 3fc176eb..196d1fb0 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -59,7 +59,7 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log(std::string function, std::string message) { + void Queue::enqueue_and_log(std::string_view function, std::string_view message) { std::lock_guard lock(queue_mutex); message_queue.push(message); notifier.notify_one(); @@ -77,9 +77,9 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log( std::string tag, std::string function, std::string message ) { + void Queue::enqueue_and_log( std::string_view tag, std::string_view function, std::string_view message ) { std::lock_guard lock(queue_mutex); - std::stringstream qmessage; + std::ostringstream qmessage; qmessage << tag << ":" << message; message_queue.push(qmessage.str()); notifier.notify_one(); @@ -98,12 +98,12 @@ namespace Common { * If the queue is empty, wait untill an element is avaiable. * */ - std::string Queue::dequeue(void) { + std::string_view Queue::dequeue(void) { std::unique_lock lock(queue_mutex); while(message_queue.empty()) { notifier.wait(lock); // release lock as long as the wait and reaquire it afterwards. } - std::string message = message_queue.front(); + std::string_view message = message_queue.front(); message_queue.pop(); return message; } diff --git a/common/common.h b/common/common.h index d540dbd3..49d4db66 100644 --- a/common/common.h +++ b/common/common.h @@ -1112,7 +1112,7 @@ namespace Common { */ class Queue { private: - std::queue message_queue; + std::queue message_queue; mutable std::mutex queue_mutex; std::condition_variable notifier; bool is_running; @@ -1123,10 +1123,10 @@ namespace Common { void service_running(bool state) { this->is_running = state; }; ///< set service running bool service_running() { return this->is_running; }; ///< is the service running? - void enqueue_and_log(std::string function, std::string message); - void enqueue_and_log(std::string tag, std::string function, std::string message); - void enqueue(std::string message); ///< push an element into the queue. - std::string dequeue(void); ///< pop an element from the queue + void enqueue_and_log(std::string_view function, std::string_view message); + void enqueue_and_log(std::string_view tag, std::string_view function, std::string_view message); + void enqueue(std::string message_view); ///< push an element into the queue. + std::string_view dequeue(void); ///< pop an element from the queue }; /**************** Common::Queue *********************************************/ diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 9110b1d2..3c2a7641 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -27,7 +27,7 @@ namespace Sequencer { * */ long Sequence::run( const Operation &op, - const std::string &caller ) { + std::string_view caller ) { long error=NO_ERROR; logwrite(caller, "starting "+op.name); @@ -56,7 +56,7 @@ namespace Sequencer { * */ long Sequence::run_sequence( const std::vector &ops, - const std::string &caller ) { + std::string_view caller ) { for (const auto &op : ops) { @@ -93,7 +93,7 @@ namespace Sequencer { * */ long Sequence::run_parallel( const std::vector &ops, - const std::string &caller ) { + std::string_view caller ) { std::vector> futures; @@ -138,7 +138,7 @@ namespace Sequencer { * */ long Sequence::run_operation_blocks( const std::vector &blocks, - const std::string &caller, + std::string_view caller, bool continue_on_error ) { long error = NO_ERROR; @@ -179,7 +179,7 @@ namespace Sequencer { * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_default_sequence(const std::string &caller) { + long Sequence::run_default_sequence(std::string_view caller) { std::vector blocks; @@ -200,13 +200,14 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel blocks.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, - [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } } } ); + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, + { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, + { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, + { "slit_set", THR_SLIT_SET, [this]{ return slit_set(this->target.iscal ? VSM_DATABASE + : VSM_ACQUIRE); } } + } } ); } // Early Exit for pointmode=ACAM @@ -217,58 +218,17 @@ namespace Sequencer { // ---------- RUN THESE IN SERIES ---------------------- - // if not a calibration target then acquire, first acam then slicecam - if (!this->target.iscal) { - - // ---------- TARGET ACQUISITION --------------------- - - blocks.push_back( { OperationType::SERIAL, - { { "acam_acquire", THR_ACQUISITION, [this,caller]() { - - // if ACAM acquisition fails, wait for user to continue or cancel - if ( this->do_acam_acquire() != NO_ERROR ) { - return this->wait_for_user(caller); - } - else return NO_ERROR; - } }, - - { "slicecam_fineacquire", THR_ACQUISITION, [this,caller]() { - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); - } - return NO_ERROR; // slicecam fine acquire is never fatal - } } - } - } ); - } - - if (!this->target.iscal) { - - // ---------- TARGET OFFSETS ------------------------- - - blocks.push_back( { OperationType::SERIAL, - { { "target_offset", THR_MOVE_TO_TARGET, [this,caller]() { - - // if offsets fail, wait for user to continue or cancel - if (this->target_offset() != NO_ERROR) { - return this->wait_for_user(caller); - } - else return NO_ERROR; - } } - } - } ); - } - - if (!this->target.iscal) { + blocks.push_back( { OperationType::SERIAL, + { { "target_acquisition", THR_ACQUISITION, + [this,caller]() { return this->do_target_acquisition(caller); } }, - // ---------- SLIT POSITON FOR EXPOSE ---------------- + { "target_offset", THR_MOVE_TO_TARGET, + [this]() { return this->target_offset(); } }, - blocks.push_back( { OperationType::SERIAL, - { { "slit_expose", THR_SLIT_SET, [this]() { - return this->slit_set(Sequencer::VSM_EXPOSE); } } - } - } ); - } + { "slit_expose", THR_SLIT_SET, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } } + } + } ); // ---------- EXPOSURE --------------------------------- @@ -589,7 +549,7 @@ namespace Sequencer { * */ void Sequence::dothread_sequencer_async_listener( Sequencer::Sequence &seq, Network::UdpSocket udp ) { - const std::string function("Sequencer::Sequence::dothread_sequencer_async_listener"); + std::string_view function("Sequencer::Sequence::dothread_sequencer_async_listener"); ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_SEQUENCER_ASYNC_LISTENER ); @@ -691,6 +651,40 @@ namespace Sequencer { } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + /** + * @brief waits for the TCS Operator to click 'ontarget' + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_ontarget(std::string_view caller) { + // waiting for TCS Operator input (or cancel) + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); + + this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); + + while ( !this->cancel_flag.load() && + !this->is_ontarget.load() ) { + + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || + this->cancel_flag.load() ); } ); + } + + this->async.enqueue_and_log(caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") + : std::string("ontarget")) + +" signal!" ); + } + this->is_ontarget.store(false); + + return (this->cancel_flag.load() ? ABORT : NO_ERROR); + } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + + /***** Sequencer::Sequence::wait_for_user ***********************************/ /** * @brief waits for the user to click a button, or cancel @@ -700,7 +694,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_user(const std::string &caller) { + long Sequence::wait_for_user(std::string_view caller) { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); @@ -735,7 +729,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_exposure(const std::string &caller) { + long Sequence::wait_for_exposure(std::string_view caller) { logwrite(caller, "waiting for exposure"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { @@ -761,7 +755,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_readout(const std::string &caller) { + long Sequence::wait_for_readout(std::string_view caller) { logwrite(caller, "waiting for readout"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { @@ -780,6 +774,36 @@ namespace Sequencer { /***** Sequencer::Sequence::wait_for_readout ********************************/ + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + /** + * @brief waits for camera to be ready to expose, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_canexpose(std::string_view caller) { + logwrite(caller, "waiting for can_expose"); + + while ( !this->cancel_flag.load() && + !this->can_expose.load() ) { + + this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); + + std::unique_lock lock(this->camerad_mtx); + this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || + this->cancel_flag.load() ); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + + /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -795,7 +819,7 @@ namespace Sequencer { * */ void Sequence::sequence_start(std::string obsid_in="") { - const std::string function("Sequencer::Sequence::sequence_start"); + std::string_view function("Sequencer::Sequence::sequence_start"); std::ostringstream message; std::string reply; std::string targetstatus; @@ -905,7 +929,7 @@ namespace Sequencer { break; } - // default observation sequence + // ---------- default observation sequence ----------- // error = run_default_sequence(function); @@ -982,27 +1006,14 @@ namespace Sequencer { * */ long Sequence::camera_set() { - const std::string function("Sequencer::Sequence::camera_set"); + std::string_view function("Sequencer::Sequence::camera_set"); std::string reply; std::stringstream camcmd; long error=NO_ERROR; // wait until camera is ready to expose // - std::unique_lock lock(this->camerad_mtx); - if (!this->can_expose.load()) { - - this->async.enqueue_and_log(function, "NOTICE: waiting for camera to be ready to expose"); - - this->camerad_cv.wait( lock, [this]() { - return( this->can_expose.load() || this->cancel_flag.load() ); - } ); - - if (this->cancel_flag.load()) { - logwrite(function, "sequence cancelled"); - return NO_ERROR; - } - } + this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); @@ -1083,7 +1094,7 @@ namespace Sequencer { * */ long Sequence::slit_set(VirtualSlitMode mode) { - const std::string function("Sequencer::Sequence::slit_set"); + std::string_view function("Sequencer::Sequence::slit_set"); std::string reply, modestr; std::stringstream slitcmd, message; @@ -1140,7 +1151,7 @@ namespace Sequencer { * */ long Sequence::power_init() { - const std::string function("Sequencer::Sequence::power_init"); + std::string_view function("Sequencer::Sequence::power_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_POWER_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1167,7 +1178,7 @@ namespace Sequencer { * */ long Sequence::power_shutdown() { - const std::string function("Sequencer::Sequence::power_shutdown"); + std::string_view function("Sequencer::Sequence::power_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_POWER_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1190,7 +1201,7 @@ namespace Sequencer { * */ long Sequence::slit_init() { - const std::string function("Sequencer::Sequence::slit_init"); + std::string_view function("Sequencer::Sequence::slit_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); @@ -1256,7 +1267,7 @@ namespace Sequencer { * */ long Sequence::slit_shutdown() { - const std::string function("Sequencer::Sequence::slit_shutdown"); + std::string_view function("Sequencer::Sequence::slit_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1324,7 +1335,7 @@ namespace Sequencer { * */ long Sequence::slicecam_init() { - const std::string function("Sequencer::Sequence::slicecam_init"); + std::string_view function("Sequencer::Sequence::slicecam_init"); this->daemon_manager.clear( Sequencer::DAEMON_SLICECAM ); // slicecamd not ready @@ -1364,7 +1375,7 @@ namespace Sequencer { * */ long Sequence::acam_init() { - const std::string function("Sequencer::Sequence::acam_init"); + std::string_view function("Sequencer::Sequence::acam_init"); this->daemon_manager.clear( Sequencer::DAEMON_ACAM ); // acamd not ready @@ -1424,7 +1435,7 @@ namespace Sequencer { * */ long Sequence::slicecam_shutdown() { - const std::string function("Sequencer::Sequence::slicecam_shutdown"); + std::string_view function("Sequencer::Sequence::slicecam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1487,7 +1498,7 @@ namespace Sequencer { * */ long Sequence::acam_shutdown() { - const std::string function("Sequencer::Sequence::acam_shutdown"); + std::string_view function("Sequencer::Sequence::acam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1559,7 +1570,7 @@ namespace Sequencer { * */ long Sequence::calib_init() { - const std::string function("Sequencer::Sequence::calib_init"); + std::string_view function("Sequencer::Sequence::calib_init"); this->daemon_manager.clear( Sequencer::DAEMON_CALIB ); @@ -1633,7 +1644,7 @@ namespace Sequencer { * */ long Sequence::calib_shutdown() { - const std::string function("Sequencer::Sequence::calib_shutdown"); + std::string_view function("Sequencer::Sequence::calib_shutdown"); long error=NO_ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CALIB_SHUTDOWN ); @@ -1755,7 +1766,7 @@ namespace Sequencer { * */ long Sequence::tcs_shutdown() { - const std::string function("Sequencer::Sequence::tcs_shutdown"); + std::string_view function("Sequencer::Sequence::tcs_shutdown"); std::stringstream message; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_TCS_SHUTDOWN ); @@ -1797,7 +1808,7 @@ namespace Sequencer { * */ long Sequence::flexure_init() { - const std::string function("Sequencer::Sequence::flexure_init"); + std::string_view function("Sequencer::Sequence::flexure_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); @@ -1836,7 +1847,7 @@ namespace Sequencer { * */ long Sequence::flexure_shutdown() { - const std::string function("Sequencer::Sequence::flexure_shutdown"); + std::string_view function("Sequencer::Sequence::flexure_shutdown"); std::string reply; long error=NO_ERROR; @@ -1897,7 +1908,7 @@ namespace Sequencer { * */ long Sequence::focus_init() { - const std::string function("Sequencer::Sequence::focus_init"); + std::string_view function("Sequencer::Sequence::focus_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); @@ -1965,7 +1976,7 @@ namespace Sequencer { * */ long Sequence::focus_shutdown() { - const std::string function("Sequencer::Sequence::focus_shutdown"); + std::string_view function("Sequencer::Sequence::focus_shutdown"); std::string reply; long error=NO_ERROR; @@ -2026,7 +2037,7 @@ namespace Sequencer { * */ long Sequence::camera_init() { - const std::string function("Sequencer::Sequence::camera_init"); + std::string_view function("Sequencer::Sequence::camera_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2080,7 +2091,7 @@ namespace Sequencer { * */ long Sequence::camera_shutdown() { - const std::string function("Sequencer::Sequence::camera_shutdown"); + std::string_view function("Sequencer::Sequence::camera_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CAMERA_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2145,7 +2156,7 @@ namespace Sequencer { * */ long Sequence::move_to_target() { - const std::string function("Sequencer::Sequence::move_to_target"); + std::string_view function("Sequencer::Sequence::move_to_target"); std::stringstream message; long error=NO_ERROR; @@ -2299,7 +2310,7 @@ namespace Sequencer { * */ void Sequence::dothread_notify_tcs( Sequencer::Sequence &seq ) { - const std::string function("Sequencer::Sequence::dothread_notify_tcs"); + std::string_view function("Sequencer::Sequence::dothread_notify_tcs"); std::stringstream message; ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_NOTIFY_TCS ); @@ -2371,7 +2382,7 @@ namespace Sequencer { * */ long Sequence::focus_set() { - const std::string function("Sequencer::Sequence::focus_set"); + std::string_view function("Sequencer::Sequence::focus_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_SET ); @@ -2390,7 +2401,7 @@ namespace Sequencer { * */ long Sequence::flexure_set() { - const std::string function("Sequencer::Sequence::flexure_set"); + std::string_view function("Sequencer::Sequence::flexure_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_SET ); @@ -2409,7 +2420,7 @@ namespace Sequencer { * */ long Sequence::calib_set() { - const std::string function("Sequencer::Sequence::calib_set"); + std::string_view function("Sequencer::Sequence::calib_set"); std::stringstream message; ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIBRATOR_SET ); @@ -2494,7 +2505,7 @@ namespace Sequencer { * */ void Sequence::abort_process() { - const std::string function("Sequencer::Sequence::abort_process"); + std::string_view function("Sequencer::Sequence::abort_process"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); @@ -2529,7 +2540,7 @@ namespace Sequencer { * */ void Sequence::stop_exposure() { - const std::string function("Sequencer::Sequence::stop_exposure"); + std::string_view function("Sequencer::Sequence::stop_exposure"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); @@ -2577,7 +2588,7 @@ namespace Sequencer { * */ long Sequence::repeat_exposure() { - const std::string function("Sequencer::Sequence::repeat_exposure"); + std::string_view function("Sequencer::Sequence::repeat_exposure"); std::stringstream message; long error = NO_ERROR; @@ -2667,7 +2678,7 @@ namespace Sequencer { * */ long Sequence::trigger_exposure() { - const std::string function("Sequencer::Sequence::trigger_exposure"); + std::string_view function("Sequencer::Sequence::trigger_exposure"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -2721,7 +2732,7 @@ namespace Sequencer { * */ void Sequence::modify_exptime( double exptime_in ) { - const std::string function("Sequencer::Sequence::modify_exptime"); + std::string_view function("Sequencer::Sequence::modify_exptime"); std::stringstream message; std::string reply=""; long error = NO_ERROR; @@ -2775,7 +2786,7 @@ namespace Sequencer { * */ long Sequence::startup() { - const std::string function("Sequencer::Sequence::startup"); + std::string_view function("Sequencer::Sequence::startup"); std::stringstream message; long error=NO_ERROR; @@ -2984,7 +2995,7 @@ namespace Sequencer { * */ long Sequence::shutdown() { - const std::string function("Sequencer::Sequence::shutdown"); + std::string_view function("Sequencer::Sequence::shutdown"); long error=ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running @@ -3075,8 +3086,8 @@ namespace Sequencer { * @return ERROR or NO_ERROR * */ - long Sequence::parse_state( std::string whoami, std::string reply, bool &state ) { - const std::string function("Sequencer::Sequence::parse_state"); + long Sequence::parse_state( std::string_view whoami, std::string reply, bool &state ) { + std::string_view function("Sequencer::Sequence::parse_state"); std::stringstream message; // Tokenize the reply -- @@ -3133,7 +3144,7 @@ namespace Sequencer { * */ long Sequence::extract_tcs_value( std::string reply, int &value ) { - const std::string function("Sequencer::Sequence::extract_tcs_value"); + std::string_view function("Sequencer::Sequence::extract_tcs_value"); std::stringstream message; std::vector tokens; long error = ERROR; @@ -3221,7 +3232,7 @@ namespace Sequencer { * */ long Sequence::parse_tcs_generic( int value ) { - const std::string function("Sequencer::Sequence::parse_tcs_generic"); + std::string_view function("Sequencer::Sequence::parse_tcs_generic"); std::stringstream message; std::string tcsreply; std::vector tokens; @@ -3270,7 +3281,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args ) { - const std::string function("Sequencer::Sequence::dotype"); + std::string_view function("Sequencer::Sequence::dotype"); std::stringstream message; std::string dontcare; return this->dotype( args, dontcare ); @@ -3294,7 +3305,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args, std::string &retstring ) { - const std::string function("Sequencer::Sequence::dotype"); + std::string_view function("Sequencer::Sequence::dotype"); std::stringstream message; long error = NO_ERROR; @@ -3341,7 +3352,7 @@ namespace Sequencer { return this->get_dome_position( false, domeazi, telazi ); } long Sequence::get_dome_position( bool poll, double &domeazi, double &telazi ) { - const std::string function("Sequencer::Sequence::get_dome_position"); + std::string_view function("Sequencer::Sequence::get_dome_position"); std::stringstream message; std::string tcsreply; @@ -3403,7 +3414,7 @@ namespace Sequencer { return this->get_tcs_motion( false, state_out ); } long Sequence::get_tcs_motion( bool poll, std::string &state_out ) { - const std::string function("Sequencer::Sequence::get_tcs_motion"); + std::string_view function("Sequencer::Sequence::get_tcs_motion"); std::stringstream message; std::string tcsreply; @@ -3445,7 +3456,7 @@ namespace Sequencer { return this->get_tcs_coords_type( TCSD_WEATHER_COORDS, ra_h, dec_d ); } long Sequence::get_tcs_coords_type( std::string cmd, double &ra_h, double &dec_d ) { - const std::string function("Sequencer::Sequence::get_tcs_coords"); + std::string_view function("Sequencer::Sequence::get_tcs_coords"); std::stringstream message; std::string coordstring; @@ -3498,7 +3509,7 @@ namespace Sequencer { * */ long Sequence::get_tcs_cass( double &cass ) { - const std::string function("Sequencer::Sequencer::get_tcs_cass"); + std::string_view function("Sequencer::Sequencer::get_tcs_cass"); std::stringstream message; std::string tcsreply; @@ -3549,11 +3560,14 @@ namespace Sequencer { * */ long Sequence::target_offset() { - const std::string function("Sequencer::Sequence::target_offset"); + std::string_view function("Sequencer::Sequence::target_offset"); + + bool is_ra_zero = std::abs(this->target.offset_ra) < std::numeric_limits::epsilon(); + bool is_dec_zero = std::abs(this->target.offset_dec) < std::numeric_limits::epsilon(); - // nothing to do if both ra and dec offsets are zero - if (this->target.offset_ra == 0.0 && - this->target.offset_dec == 0.0) return NO_ERROR; + // nothing to do for calibrator or if both ra and dec offsets are zero + if ( this->target.iscal || + (is_ra_zero && is_dec_zero) ) return NO_ERROR; // zero TCS offsets before applying target offset long error = this->tcsd.command( TCSD_ZERO_OFFSETS ); @@ -3661,7 +3675,7 @@ namespace Sequencer { * */ long Sequence::handle_json_message( const std::string message_in ) { - const std::string function("Sequencer::Sequence::handle_json_message"); + std::string_view function("Sequencer::Sequence::handle_json_message"); std::stringstream message; if ( message_in.empty() ) { @@ -3744,7 +3758,7 @@ namespace Sequencer { * */ void Sequence::dothread_test_fpoffset() { - const std::string function("Sequencer::Sequence::dothread_fpoffset"); + std::string_view function("Sequencer::Sequence::dothread_fpoffset"); std::stringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); @@ -3779,7 +3793,7 @@ namespace Sequencer { } long Sequence::set_power_switch( PowerState reqstate, const std::string which, std::chrono::seconds delay ) { - const std::string function("Sequencer::Sequence::set_power_switch"); + std::string_view function("Sequencer::Sequence::set_power_switch"); long error=NO_ERROR; bool need_delay=false; @@ -3885,7 +3899,7 @@ namespace Sequencer { long Sequence::open_hardware( Common::DaemonClient &daemon, const std::string opencmd, const int opentimeout, bool &was_opened, bool forceopen ) { - const std::string function("Sequencer::Sequence::open_hardware"); + std::string_view function("Sequencer::Sequence::open_hardware"); const int maxattempts=3; ///< allow retries connecting to daemon bool isopen=false; std::string reply; @@ -3945,7 +3959,7 @@ namespace Sequencer { * */ long Sequence::connect_to_daemon( Common::DaemonClient &daemon ) { - const std::string function("Sequencer::Sequence::connect_to_daemon"); + std::string_view function("Sequencer::Sequence::connect_to_daemon"); // if not connected to the daemon then connect // @@ -3973,7 +3987,7 @@ namespace Sequencer { * */ long Sequence::daemon_restart(Common::DaemonClient &daemon) { - const std::string function("Sequencer::Sequence::daemon_restart"); + std::string_view function("Sequencer::Sequence::daemon_restart"); std::string command; // the daemon control script must have been specified in the config file @@ -4028,7 +4042,7 @@ namespace Sequencer { * */ long Sequence::test( std::string args, std::string &retstring ) { - const std::string function("Sequencer::Sequence::test"); + std::string_view function("Sequencer::Sequence::test"); std::stringstream message; std::vector tokens; long error = NO_ERROR; diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index c95b8dd3..92b2baa6 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -312,7 +312,7 @@ namespace Sequencer { /** @brief safely runs function in a detached thread using lambda to catch exceptions */ - void safe_thread(long (Sequence::*method)(), const std::string &function) { + void safe_thread(long (Sequence::*method)(), std::string_view function) { std::thread([this, method, function]() { try { (this->*method)(); @@ -476,12 +476,12 @@ namespace Sequencer { float slitwidthacquire; ///< "virtual slit mode" width for acquire // new stuff - long run(const Operation &op, const std::string &function); - long run_sequence(const std::vector &ops, const std::string &function); - long run_parallel(const std::vector &ops, const std::string &function); - long run_default_sequence(const std::string &caller); + long run(const Operation &op, std::string_view function); + long run_sequence(const std::vector &ops, std::string_view function); + long run_parallel(const std::vector &ops, std::string_view function); + long run_default_sequence(std::string_view caller); long run_operation_blocks( const std::vector &blocks, - const std::string &caller, + std::string_view caller, bool continue_on_error=false ); // publish/subscribe functions @@ -543,7 +543,7 @@ namespace Sequencer { bool is_ready() { return this->ready_to_start; } ///< returns the ready_to_start state, set true only after nightly startup long parse_calibration_target(); - long parse_state( std::string whoami, std::string reply, bool &state ); ///< parse true|false state from reply string + long parse_state( std::string_view whoami, std::string reply, bool &state ); ///< parse true|false state from reply string void dothread_test_fpoffset(); ///< for testing, calls Python function from thread long test( std::string args, std::string &retstring ); ///< handles test commands long extract_tcs_value( std::string reply, int &value ); ///< extract value returned by the TCS via tcsd @@ -588,9 +588,12 @@ namespace Sequencer { void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_user(const std::string &function); ///< wait for the user or cancel - long wait_for_exposure(const std::string &function); ///< wait for exposure completion or cancel - long wait_for_readout(const std::string &function); ///< wait for readout completion or cancel + long wait_for_ontarget(std::string_view caller); ///< wait for TCS Operator + long wait_for_user(std::string_view caller); ///< wait for the user or cancel + long wait_for_exposure(std::string_view caller); ///< wait for exposure completion or cancel + long wait_for_readout(std::string_view caller); ///< wait for readout completion or cancel + long wait_for_canexpose(std::string_view caller); ///< wait for camera can_expose + void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params @@ -605,6 +608,8 @@ namespace Sequencer { */ long do_acam_acquire(); long do_slicecam_fineacquire(); + long do_target_acquisition(std::string_view caller); + long do_target_virtualslit(VirtualSlitMode mode); long acam_init(); ///< initializes connection to acamd diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 7beab093..695c0423 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -16,7 +16,7 @@ namespace Sequencer { * */ long Sequence::do_acam_acquire() { - const std::string function("Sequencer::Sequence::do_acam_acquire"); + std::string_view function("Sequencer::Sequence::do_acam_acquire"); std::string reply; ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); @@ -75,7 +75,7 @@ namespace Sequencer { * */ long Sequence::do_slicecam_fineacquire() { - const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); + std::string_view function("Sequencer::Sequence::do_slicecam_fineacquire"); ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); @@ -117,4 +117,50 @@ namespace Sequencer { } /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + /** + * @brief performs target acquisition + * @details First acquire on ACAM, then run slicecam fineacquire + * @return NO_ERROR | ABORT + * + */ + long Sequence::do_target_acquisition(std::string_view caller) { + + if (this->target.iscal) return NO_ERROR; + + // ---------- ACAM acquire ----------------------------- + // + if ( this->do_acam_acquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING acam acquisition failed"); + + // on ACAM acquisition failure wait for user to continue or cancel + if ( this->wait_for_user(caller) == ABORT ) return ABORT; + + return NO_ERROR; // user chose to continue + } + + // ---------- SLICECAM fineacquire --------------------- + // + if ( this->do_slicecam_fineacquire() != NO_ERROR ) { + this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_target_acquisition ****************************/ + + + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + /** + * @brief move to virtual slit position + * @param[in] mode VirtualSlitMode + * @return NO_ERROR | NO_ERROR + * + */ + long Sequence::do_target_virtualslit(VirtualSlitMode mode) { + if (this->target.iscal) return NO_ERROR; + return this->slit_set(mode); + } + /***** Sequencer::Sequence::do_target_virtualslit ****************************/ } diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 06514cef..75a0f436 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1043,7 +1043,7 @@ namespace Sequencer { } while (1) { - std::string message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) + auto message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/utils/logentry.cpp b/utils/logentry.cpp index 0b95650e..6ab2ba79 100644 --- a/utils/logentry.cpp +++ b/utils/logentry.cpp @@ -175,13 +175,12 @@ void close_log() { * log filestream isn't open. * */ -void logwrite( const std::string &function, std::string message ) { - std::stringstream logmsg; - std::string timestamp = get_timestamp(); // get the current time (defined in utilities.h) +void logwrite( std::string_view function, std::string_view message ) { + std::ostringstream logmsg; std::lock_guard lock(loglock); // lock mutex to protect from multiple access - logmsg << timestamp << " (" << function << ") " << message << "\n"; + logmsg << get_timestamp() << " (" << function << ") " << message << "\n"; if (filestream.is_open()) { filestream << logmsg.str(); // send to the file stream (if open) diff --git a/utils/logentry.h b/utils/logentry.h index ddf95890..f296bcbf 100644 --- a/utils/logentry.h +++ b/utils/logentry.h @@ -21,6 +21,6 @@ extern unsigned int nextday; /// number of seconds long init_log( std::string logpath, std::string name ); /// initialize the logging system long init_log( std::string logpath, std::string name, bool stderr_in ); /// initialize the logging system void close_log(); /// close the log file stream -void logwrite(const std::string &function, std::string message); /// create a time-stamped log entry "message" from "function" +void logwrite(std::string_view function, std::string_view message); /// create a time-stamped log entry "message" from "function" #endif diff --git a/utils/network.cpp b/utils/network.cpp index 5b6a2402..caf3d36e 100644 --- a/utils/network.cpp +++ b/utils/network.cpp @@ -174,14 +174,14 @@ namespace Network { * @return 0 on success, -1 on error * */ - int UdpSocket::Send(std::string message) { - std::string function = "Network::UdpSocket::Send"; - std::stringstream errstm; + int UdpSocket::Send(std::string_view message) { + std::string_view function = "Network::UdpSocket::Send"; + std::ostringstream errstm; ssize_t nbytes; if ( !this->is_running() ) return 0; // silently do nothing if the UDP multicast socket isn't running - if ( ( nbytes = sendto( this->fd, message.c_str(), (size_t)message.length(), 0, + if ( ( nbytes = sendto( this->fd, std::string(message).c_str(), (size_t)message.length(), 0, (struct sockaddr*) &this->addr, (socklen_t)sizeof(this->addr) ) ) < 0 ) { errstm << "error " << errno << " calling sendto: " << strerror(errno); logwrite(function, errstm.str()); diff --git a/utils/network.h b/utils/network.h index f2b5637a..0fea2000 100644 --- a/utils/network.h +++ b/utils/network.h @@ -168,7 +168,7 @@ namespace Network { std::string getgroup() { return this->group; }; ///< use to get group int Create(); ///< create a UDP multi-cast socket - int Send(std::string message); ///< transmit the message to the UDP socket + int Send(std::string_view message); ///< transmit the message to the UDP socket int Close(); ///< close the UDP socket connection int Listener(); ///< creates a UDP listener, returns a file descriptor ssize_t Receive( std::string &message ); ///< receive a UDP message from the Listener fd From 2925c7f0d9d20c6f024eb6dbff34dd219486b055 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 6 Apr 2026 10:07:54 -0700 Subject: [PATCH 07/37] adds signatures for scripting functions moves wait_for_XXXX wrappers to a separate file moves sequence building tools to a separate file --- common/sequencerd_commands.h | 4 + sequencerd/CMakeLists.txt | 2 + sequencerd/sequence.cpp | 378 ++++++++++---------------------- sequencerd/sequence.h | 70 +++++- sequencerd/sequence_builder.cpp | 65 ++++++ sequencerd/sequence_wait.cpp | 168 ++++++++++++++ sequencerd/sequencer_server.cpp | 16 ++ 7 files changed, 432 insertions(+), 271 deletions(-) create mode 100644 sequencerd/sequence_builder.cpp create mode 100644 sequencerd/sequence_wait.cpp diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index cf38c57d..adce01ee 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -16,9 +16,11 @@ const std::string SEQUENCERD_GUIDE = "guide"; const std::string SEQUENCERD_MODEXPTIME = "modexptime"; const std::string SEQUENCERD_ONTARGET = "ontarget"; const std::string SEQUENCERD_USERCONTINUE = "usercontinue"; +const std::string SEQUENCERD_OP = "op"; const std::string SEQUENCERD_PAUSE = "pause"; const std::string SEQUENCERD_REPEAT = "repeat"; const std::string SEQUENCERD_RESUME = "resume"; +const std::string SEQUENCERD_SCRIPT = "script"; const std::string SEQUENCERD_SHUTDOWN = "shutdown"; const std::string SEQUENCERD_START = "start"; const std::string SEQUENCERD_STARTONE = "startone"; @@ -54,11 +56,13 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_GUIDE, SEQUENCERD_MODEXPTIME+" ", SEQUENCERD_ONTARGET, + SEQUENCERD_OP, SEQUENCERD_PAUSE, SEQUENCERD_REPEAT, SEQUENCERD_RESUME, TELEMREQUEST+" [?]", SEQUENCERD_USERCONTINUE, + SEQUENCERD_SCRIPT, SEQUENCERD_SHUTDOWN, SEQUENCERD_START, SEQUENCERD_STARTONE, diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index dda2f133..b5804c76 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -38,6 +38,8 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp ${SEQUENCER_DIR}/sequence_acquisition.cpp + ${SEQUENCER_DIR}/sequence_wait.cpp + ${SEQUENCER_DIR}/sequence_builder.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} ${PYTHON_DEV} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 3c2a7641..598092ac 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -47,42 +47,6 @@ namespace Sequencer { /***** Sequencer::Sequence::run ********************************************/ - /***** Sequencer::Sequence::run_sequence ***********************************/ - /** - * @brief executes operations in sequence, one at a time - * @param[in] op vector of Operations to execute - * @param[in] caller name of calling function for logging - * @return ERROR|NO_ERROR|ABORT - * - */ - long Sequence::run_sequence( const std::vector &ops, - std::string_view caller ) { - - for (const auto &op : ops) { - - if (this->cancel_flag.load()) return ABORT; - - logwrite(caller, "starting "+op.name); - - try { - long error; - if ( (error = op.func()) != NO_ERROR ) { - std::ostringstream oss; - oss << (error==ABORT ? "cancelled" : "ERROR") << " in " << op.name; - logwrite(caller, oss.str()); - return error; - } - } - catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name+": "+std::string(e.what())); - return ERROR; - } - } - return NO_ERROR; - } - /***** Sequencer::Sequence::run_sequence ***********************************/ - - /***** Sequencer::Sequence::run_parallel ***********************************/ /** * @brief executes operations in parallel threads @@ -123,40 +87,40 @@ namespace Sequencer { /***** Sequencer::Sequence::run_parallel ***********************************/ - /***** Sequencer::Sequence::run_operation_blocks ***************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ /** - * @brief executes operation blocks - * @details An operation block contains a vector of operations paired - * with a type, SERIAL|PARALLEL which specifies how that block - * is to be executed. This executes a vector of Operation Blocks. - * The optional continue_on_error allows a block to continue, or - * stop immediately when any operation within the block fails. - * @param[in] blocks vector of OperationBlocks + * @brief executes a sequence, a collection of operation groups + * @details An operation group contains a vector of operations paired + * with a type, SERIAL|PARALLEL which specifies how that group + * is to be executed. This executes a vector of Operation Groups. + * The optional continue_on_error allows a group to continue, or + * stop immediately when any operation within the group fails. + * @param[in] groups vector of OperationGroups * @param[in] caller name of calling function for logging - * @param[in] continue_on_error continue or stop block execution on error + * @param[in] continue_on_error continue or stop group execution on error * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_operation_blocks( const std::vector &blocks, - std::string_view caller, - bool continue_on_error ) { + long Sequence::run_sequence( const std::vector &groups, + std::string_view caller, + bool continue_on_error ) { long error = NO_ERROR; - for (const auto &block : blocks) { + for (const auto &group : groups) { if (this->cancel_flag.load()) return ABORT; - // PARALLEL Blocks are executed in parallel threads + // PARALLEL Groups are executed in parallel threads // - if (block.type == OperationType::PARALLEL) { - long ret = run_parallel(block.operations, caller); + if (group.type == OperationType::PARALLEL) { + long ret = run_parallel(group.operations, caller); error |= ret; if (ret != NO_ERROR && !continue_on_error) return error; } - // SERIAL Blocks are executed one at a time + // SERIAL Groups are executed one at a time // else { - for (const auto &op : block.operations) { + for (const auto &op : group.operations) { if (this->cancel_flag.load()) return ABORT; long ret = run(op, caller); @@ -169,7 +133,7 @@ namespace Sequencer { return error; } - /***** Sequencer::Sequence::run_operation_blocks ***************************/ + /***** Sequencer::Sequence::run_sequence ***********************************/ /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -181,17 +145,18 @@ namespace Sequencer { */ long Sequence::run_default_sequence(std::string_view caller) { - std::vector blocks; + std::vector groups; // ---------- RUN THESE IN PARALLEL -------------------- // If pointmode is ACAM then the user has chosen to put the star on ACAM, in // which case the assumption is made that nothing else matters. This special - // mode of operation only points the telescope. + // mode of operation only points the telescope so this is the only operation + // added to the sequence. // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - blocks.push_back( { OperationType::PARALLEL, + groups.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { @@ -199,7 +164,7 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel - blocks.push_back( { OperationType::PARALLEL, + groups.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, @@ -210,56 +175,83 @@ namespace Sequencer { } } ); } - // Early Exit for pointmode=ACAM - // - if (this->target.pointmode == Acam::POINTMODE_ACAM) { - return run_operation_blocks(blocks, caller); - } - // ---------- RUN THESE IN SERIES ---------------------- - blocks.push_back( { OperationType::SERIAL, - { { "target_acquisition", THR_ACQUISITION, - [this,caller]() { return this->do_target_acquisition(caller); } }, + if (this->target.pointmode != Acam::POINTMODE_ACAM) { + groups.push_back( { OperationType::SERIAL, + { { "target_acquisition", THR_ACQUISITION, + [this,caller]() { return this->do_target_acquisition(caller); } }, - { "target_offset", THR_MOVE_TO_TARGET, - [this]() { return this->target_offset(); } }, + { "target_offset", THR_MOVE_TO_TARGET, + [this]() { return this->target_offset(); } }, - { "slit_expose", THR_SLIT_SET, - [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } } - } - } ); + { "slit_expose", THR_SLIT_SET, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - // ---------- EXPOSURE --------------------------------- + { "science_exposure", THR_EXPOSURE, + [this,caller]() { return this->do_exposure(caller); } } + } + } ); + } - blocks.push_back( { OperationType::SERIAL, - { { "trigger_exposure", THR_EXPOSURE, [this]() { + // ---------- RUN THE SEQUENCE NOW --------------------- - // set the EXPOSE bit here, outside of the trigger_exposure function, because that - // function only triggers the exposure -- it doesn't block waiting for the exposure. - // - this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); // set EXPOSE bit - return trigger_exposure(); - } }, + return run_sequence(groups, caller); + } + /***** Sequencer::Sequence::run_default_sequence ***************************/ - { "wait_exposure", THR_EXPOSURE, [this,caller]() { - return this->wait_for_exposure(caller); - } }, - { "wait_readout", THR_EXPOSURE, [this,caller]() { - if (!this->is_science_frame_transfer) { - return this->wait_for_readout(caller); - } - else return NO_ERROR; - } } - } - } ); + /***** Sequencer::Sequence::run_script *************************************/ + /** + * @brief executes a user script + * @param[in] filename filename of script + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::run_script(const std::string &filename) { + return NO_ERROR; + } + /***** Sequencer::Sequence::run_script *************************************/ - // ---------- RUN THE SEQUENCE NOW --------------------- - return run_operation_blocks(blocks, caller); + /***** Sequencer::Sequence::parse_script ***********************************/ + /** + * @brief parses a user script + * @param[in] filename filename of script + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::parse_script(const std::string &filename, + std::vector &out) { + return NO_ERROR; } - /***** Sequencer::Sequence::run_default_sequence ***************************/ + /***** Sequencer::Sequence::parse_script ***********************************/ + + + /***** Sequencer::Sequence::validate_sequence ******************************/ + /** + * @brief + * @param[in] + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::validate_sequence(const std::vector &groups) { + return NO_ERROR; + } + /***** Sequencer::Sequence::validate_sequence ******************************/ + + + /***** Sequencer::Sequence::handle_cli_operation ***************************/ + /** + * @brief handle incoming operation request + * @param[in] op the name of an operation + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::handle_cli_operation(const std::string &op) { + return NO_ERROR; + } + /***** Sequencer::Sequence::handle_cli_operation ***************************/ /***** Sequencer::Sequence::handletopic_snapshot ***************************/ @@ -642,168 +634,6 @@ namespace Sequencer { /***** Sequencer::Sequence::dothread_sequencer_async_listener ***************/ - void Sequence::dothread_test() { - logwrite( "Sequencer::Sequence::dothread_test", "here I am" ); - std::string targetstatus; - this->target.get_specified_target( "4430", targetstatus ); - logwrite( "Sequencer::Sequence::dothread_test", targetstatus ); - return; - } - - - /***** Sequencer::Sequence::wait_for_ontarget *******************************/ - /** - * @brief waits for the TCS Operator to click 'ontarget' - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_ontarget(std::string_view caller) { - // waiting for TCS Operator input (or cancel) - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - - this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); - - while ( !this->cancel_flag.load() && - !this->is_ontarget.load() ) { - - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || - this->cancel_flag.load() ); } ); - } - - this->async.enqueue_and_log(caller, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") - : std::string("ontarget")) - +" signal!" ); - } - this->is_ontarget.store(false); - - return (this->cancel_flag.load() ? ABORT : NO_ERROR); - } - /***** Sequencer::Sequence::wait_for_ontarget *******************************/ - - - /***** Sequencer::Sequence::wait_for_user ***********************************/ - /** - * @brief waits for the user to click a button, or cancel - * @details Use this when you just want to slow things down or get a - * cup of coffee instead of observing. - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_user(std::string_view caller) { - { - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - - this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); - - while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); - } - - this->async.enqueue_and_log( caller, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) - +" signal!" ); - } // end scope for wait_state = WAIT_USER - - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); - return ABORT; - } - - this->is_usercontinue.store(false); - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_user ***********************************/ - - - /***** Sequencer::Sequence::wait_for_exposure *******************************/ - /** - * @brief waits for exposure completion, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_exposure(std::string_view caller) { - logwrite(caller, "waiting for exposure"); - while (!this->cancel_flag.load() && - wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || - this->cancel_flag.load()); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_exposure *******************************/ - - - /***** Sequencer::Sequence::wait_for_readout ********************************/ - /** - * @brief waits for readout completion, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_readout(std::string_view caller) { - logwrite(caller, "waiting for readout"); - while (!this->cancel_flag.load() && - wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { - std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || - this->cancel_flag.load()); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_readout ********************************/ - - - /***** Sequencer::Sequence::wait_for_canexpose ******************************/ - /** - * @brief waits for camera to be ready to expose, or cancel - * @param[in] caller reference to caller's name for logging - * @return NO_ERROR on continue | ABORT on cancel - * - */ - long Sequence::wait_for_canexpose(std::string_view caller) { - logwrite(caller, "waiting for can_expose"); - - while ( !this->cancel_flag.load() && - !this->can_expose.load() ) { - - this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); - - std::unique_lock lock(this->camerad_mtx); - this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || - this->cancel_flag.load() ); } ); - } - - if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); - return ABORT; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::wait_for_canexpose ******************************/ - - /***** Sequencer::Sequence::sequence_start **********************************/ /** * @brief main sequence start thread @@ -840,6 +670,8 @@ namespace Sequencer { return; } + // ---------- SEQUENCER IS RUNNING --------------------- + // ScopedState thr_state( thread_state_manager, Sequencer::THR_SEQUENCE_START ); // this thread is running ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // state = RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit @@ -1011,8 +843,6 @@ namespace Sequencer { std::stringstream camcmd; long error=NO_ERROR; - // wait until camera is ready to expose - // this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); @@ -2683,6 +2513,8 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; + this->wait_for_canexpose(function); + ScopedState thr_state( thread_state_manager, Sequencer::THR_TRIGGER_EXPOSURE ); // Check tcs_preauth_time and set notify_tcs_next_target -- @@ -2721,6 +2553,30 @@ namespace Sequencer { /***** Sequencer::Sequence::trigger_exposure ********************************/ + /***** Sequencer::Sequence::do_exposure *************************************/ + /** + * @brief wrapper for performing science exposure + * @details Triggers an exposure and waits for the exposure and readout. + * This blocks until + * @param[in] caller name of calling function + * @return ERROR|NO_ERROR + * + */ + long Sequence::do_exposure(std::string_view caller) { + + this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); + + if ( this->trigger_exposure() != NO_ERROR ) return ERROR; + + if ( this->wait_for_exposure(caller) != NO_ERROR ) return ERROR; + + if ( this->wait_for_readout(caller) != NO_ERROR ) return ERROR; + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_exposure *************************************/ + + /***** Sequencer::Sequence::modify_exptime **********************************/ /** * @brief modify the exposure time while an exposure is running diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 92b2baa6..6292cd77 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -193,6 +193,7 @@ namespace Sequencer { THR_STOP_EXPOSURE, THR_ABORT_PROCESS, THR_SEQUENCE_START, + THR_RUN_SCRIPT, THR_MONITOR_READY_STATE, THR_CALIB_SET, THR_CAMERA_SET, @@ -238,6 +239,7 @@ namespace Sequencer { {THR_STOP_EXPOSURE, "stop_exposure"}, {THR_ABORT_PROCESS, "abort_process"}, {THR_SEQUENCE_START, "sequence_start"}, + {THR_RUN_SCRIPT, "run_script"}, {THR_MONITOR_READY_STATE, "monitor_ready_state"}, {THR_CALIB_SET, "calib_set"}, {THR_CAMERA_SET, "camera_set"}, @@ -293,23 +295,62 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + /** @brief operation type can be SERIAL or PARALLEL + */ enum class OperationType { PARALLEL, SERIAL }; + /** @brief map of parameter key=value pairs associated with operation + */ + struct OperationParams { + std::unordered_map map; + + bool has(const std::string &key) const { + return map.find(key) != map.end(); + } + + template + T get(const std::string &key, const T &default_val) const { + auto it = map.find(key); + if (it == map.end()) return default_val; + + if constexpr (std::is_same_v) { + return it->second; + } + else { + std::istringstream iss(it->second); + T val; + iss >> val; + return iss.fail() ? default_val : val; + } + } + }; + + /** @brief sequencer operation contains name, status bit, function and params + */ struct Operation { - std::string name; ///< name of this operation - ThreadStatusBits thr; ///< status bit of what is running - std::function func; ///< function that this operation calls - std::map params; ///< function parameters + std::string name; + ThreadStatusBits thr; + std::function func; + OperationParams params; }; - struct OperationBlock { + /** @brief a group of operations stored in a vector with the operation type + */ + struct OperationGroup { OperationType type; std::vector operations; }; + /** @brief associates a sequencer command with its parameters + */ + struct ParsedCommand { + std::string name; + OperationParams params; + }; + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), std::string_view function) { @@ -475,14 +516,22 @@ namespace Sequencer { float slitoffsetacquire; ///< "virtual slit mode" offset for acquire float slitwidthacquire; ///< "virtual slit mode" width for acquire - // new stuff + // ---------- sequencer scripting and execution tools -------------------- + // long run(const Operation &op, std::string_view function); - long run_sequence(const std::vector &ops, std::string_view function); long run_parallel(const std::vector &ops, std::string_view function); long run_default_sequence(std::string_view caller); - long run_operation_blocks( const std::vector &blocks, - std::string_view caller, - bool continue_on_error=false ); + long run_sequence( const std::vector &groups, + std::string_view caller, + bool continue_on_error=false ); + + long run_script(const std::string &filename); ///< run user script + long parse_script(const std::string &filename, + std::vector &out); ///< parse script into commands/args + long build_sequence(const std::vector &commands, + std::vector &sequence_out); ///< build sequence from parsed commands + long validate_sequence(const std::vector &groups); ///< validate sequence + long handle_cli_operation(const std::string &op); ///< handle incoming operation request // publish/subscribe functions // @@ -582,6 +631,7 @@ namespace Sequencer { // These are various jobs that are done in their own threads // long trigger_exposure(); ///< trigger and wait for exposure + long do_exposure(std::string_view caller); ///< wrapper performs and waits for science exposure void abort_process(); ///< tries to abort everything void stop_exposure(); ///< stop exposure timer in progress long repeat_exposure(); ///< repeat the last exposure diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp new file mode 100644 index 00000000..4a7489a7 --- /dev/null +++ b/sequencerd/sequence_builder.cpp @@ -0,0 +1,65 @@ +/** + * @file sequence_builder.cpp + * @brief implementation for building sequences from operations + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::build_sequence *********************************/ + /** + * @brief build a sequence from parsed commands + * @param[in] commands vector of ParsedCommands + * @param[out] sequence_out the operation group to execute + * @return ERROR|NO_ERROR|ABORT + * + */ + long Sequence::build_sequence(const std::vector &commands, + std::vector &sequence_out) { + OperationGroup group; + + group.type = OperationType::SERIAL; // default is serial + + for (const auto &command : commands) { + + if (command.name == "begin_parallel") { + if (!group.operations.empty()) sequence_out.push_back(group); + group = { OperationType::PARALLEL, {} }; + continue; + } + else + if (command.name == "end_parallel") { + sequence_out.push_back(group); + group = { OperationType::SERIAL, {} }; // back to default + continue; + } + else + if (command.name == "move_to_target") { + group.operations.emplace_back( Operation { + "move_to_target", THR_MOVE_TO_TARGET, + [this,params=command.params]() { + if (params.has("ra") && params.has("dec")) { + this->target.ra_hms = params.get(std::string("ra"),std::string("")); + this->target.dec_dms = params.get(std::string("dec"),std::string("")); + } + return move_to_target(); + }, + command.params + }); + } + + else { + this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", + "ERROR unknown command '"+command.name+"'"); + } + } + if (!group.operations.empty()) sequence_out.push_back(group); + + return NO_ERROR; + } + /***** Sequencer::Sequence::build_sequence *********************************/ + +} diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp new file mode 100644 index 00000000..078c1046 --- /dev/null +++ b/sequencerd/sequence_wait.cpp @@ -0,0 +1,168 @@ +/** + * @file sequence_wait.cpp + * @brief wait wrappers used in the Sequence class + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + /** + * @brief waits for the TCS Operator to click 'ontarget' + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_ontarget(std::string_view caller) { + // waiting for TCS Operator input (or cancel) + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); + + this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); + + while ( !this->cancel_flag.load() && + !this->is_ontarget.load() ) { + + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || + this->cancel_flag.load() ); } ); + } + + this->async.enqueue_and_log(caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") + : std::string("ontarget")) + +" signal!" ); + } + this->is_ontarget.store(false); + + return (this->cancel_flag.load() ? ABORT : NO_ERROR); + } + /***** Sequencer::Sequence::wait_for_ontarget *******************************/ + + + /***** Sequencer::Sequence::wait_for_user ***********************************/ + /** + * @brief waits for the user to click a button, or cancel + * @details Use this when you just want to slow things down or get a + * cup of coffee instead of observing. + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_user(std::string_view caller) { + { + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); + + this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); + + while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); + } + + this->async.enqueue_and_log( caller, "NOTICE: received " + +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) + +" signal!" ); + } // end scope for wait_state = WAIT_USER + + if ( this->cancel_flag.load() ) { + this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); + return ABORT; + } + + this->is_usercontinue.store(false); + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_user ***********************************/ + + + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + /** + * @brief waits for exposure completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_exposure(std::string_view caller) { + logwrite(caller, "waiting for exposure"); + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_EXPOSE) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_exposure *******************************/ + + + /***** Sequencer::Sequence::wait_for_readout ********************************/ + /** + * @brief waits for readout completion, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_readout(std::string_view caller) { + + // don't have to wait for readout when using frame transfer + if ( this->is_science_frame_transfer ) return NO_ERROR; + + logwrite(caller, "waiting for readout"); + + while (!this->cancel_flag.load() && + wait_state_manager.is_set(Sequencer::SEQ_WAIT_READOUT)) { + std::unique_lock lock(cv_mutex); + this->cv.wait( lock, [this]() { return(!wait_state_manager.is_set(SEQ_WAIT_READOUT) || + this->cancel_flag.load()); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_readout ********************************/ + + + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + /** + * @brief waits for camera to be ready to expose, or cancel + * @param[in] caller reference to caller's name for logging + * @return NO_ERROR on continue | ABORT on cancel + * + */ + long Sequence::wait_for_canexpose(std::string_view caller) { + + this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); + + while ( !this->cancel_flag.load() && + !this->can_expose.load() ) { + + std::unique_lock lock(this->camerad_mtx); + this->camerad_cv.wait( lock, [this]() { return( this->can_expose.load() || + this->cancel_flag.load() ); } ); + } + + if (this->cancel_flag.load()) { + this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); + return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::wait_for_canexpose ******************************/ + +} diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 75a0f436..e0ed8961 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1376,6 +1376,22 @@ namespace Sequencer { } else + // handle incoming CLI operation request + // + if ( cmd == SEQUENCERD_OP ) { + std::thread( &Sequencer::Sequence::handle_cli_operation, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + + // run sequencer script + // + if ( cmd == SEQUENCERD_SCRIPT ) { + std::thread( &Sequencer::Sequence::run_script, std::ref(this->sequence), args ).detach(); + ret = NO_ERROR; + } + else + // Sequence "start" // if ( cmd == SEQUENCERD_START ) { From aaa47b29f796524507269dbd352ed491852a45a9 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 6 Apr 2026 14:41:04 -0700 Subject: [PATCH 08/37] implements the functions for script parsing and execution --- sequencerd/sequence.cpp | 165 +++++++++++++++++++++++++++----- sequencerd/sequence.h | 7 +- sequencerd/sequence_builder.cpp | 28 +++++- 3 files changed, 174 insertions(+), 26 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 598092ac..f1c719d9 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -101,12 +101,14 @@ namespace Sequencer { * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_sequence( const std::vector &groups, + long Sequence::run_sequence( const std::vector &sequence, std::string_view caller, bool continue_on_error ) { long error = NO_ERROR; - for (const auto &group : groups) { + logwrite(caller, "starting sequence"); + + for (const auto &group : sequence) { if (this->cancel_flag.load()) return ABORT; // PARALLEL Groups are executed in parallel threads @@ -131,6 +133,8 @@ namespace Sequencer { } } + logwrite(caller, "sequence complete"); + return error; } /***** Sequencer::Sequence::run_sequence ***********************************/ @@ -145,7 +149,7 @@ namespace Sequencer { */ long Sequence::run_default_sequence(std::string_view caller) { - std::vector groups; + std::vector sequence; // ---------- RUN THESE IN PARALLEL -------------------- @@ -156,15 +160,15 @@ namespace Sequencer { // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - groups.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); + sequence.push_back( { OperationType::PARALLEL, + { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; // these are the default operations prior to exposure, // they can be done in parallel - groups.push_back( { OperationType::PARALLEL, + sequence.push_back( { OperationType::PARALLEL, { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, @@ -178,17 +182,17 @@ namespace Sequencer { // ---------- RUN THESE IN SERIES ---------------------- if (this->target.pointmode != Acam::POINTMODE_ACAM) { - groups.push_back( { OperationType::SERIAL, - { { "target_acquisition", THR_ACQUISITION, + sequence.push_back( { OperationType::SERIAL, + { { "target_acquire", THR_ACQUISITION, [this,caller]() { return this->do_target_acquisition(caller); } }, { "target_offset", THR_MOVE_TO_TARGET, [this]() { return this->target_offset(); } }, - { "slit_expose", THR_SLIT_SET, + { "slit_set", THR_SLIT_SET, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - { "science_exposure", THR_EXPOSURE, + { "expose", THR_EXPOSURE, [this,caller]() { return this->do_exposure(caller); } } } } ); @@ -196,7 +200,7 @@ namespace Sequencer { // ---------- RUN THE SEQUENCE NOW --------------------- - return run_sequence(groups, caller); + return run_sequence(sequence, caller); } /***** Sequencer::Sequence::run_default_sequence ***************************/ @@ -209,7 +213,26 @@ namespace Sequencer { * */ long Sequence::run_script(const std::string &filename) { - return NO_ERROR; + std::string_view function("Sequencer::Sequence::run_script"); + + std::vector commands; + if ( parse_script(filename, commands) != NO_ERROR ) { + logwrite(function, "ERROR parsing '"+filename+"'"); + return ERROR; + } + + std::vector sequence; + if ( build_sequence(commands, sequence) != NO_ERROR ) { + logwrite(function, "ERROR building sequence from '"+filename+"'"); + return ERROR; + } + + if ( validate_sequence(sequence) != NO_ERROR ) { + logwrite(function, "ERROR validating sequence from '"+filename+"'"); + return ERROR; + } + + return run_sequence(sequence, function); } /***** Sequencer::Sequence::run_script *************************************/ @@ -217,25 +240,97 @@ namespace Sequencer { /***** Sequencer::Sequence::parse_script ***********************************/ /** * @brief parses a user script - * @param[in] filename filename of script - * @return ERROR|NO_ERROR|ABORT + * @details This parses a script, makes a ParsedCommand struct from each + * line, returning a vector of ParsedCommands. No validation is + * done here, only parsing. + * @param[in] filename filename of script + * @param[out] commands_out reference to vector of ParsedCommands + * @return ERROR|NO_ERROR * */ long Sequence::parse_script(const std::string &filename, - std::vector &out) { + std::vector &commands_out) { + std::ifstream file(filename); + if (!file.is_open()) { + logwrite("Sequencer::Sequence::parse_script", "ERROR opening '"+filename+"'"); + return ERROR; + } + + std::string line; + + while (std::getline(file, line)) { + + auto command = parse_command(line); + + if (command) commands_out.push_back(*command); + } + return NO_ERROR; } /***** Sequencer::Sequence::parse_script ***********************************/ + /***** Sequencer::Sequence::parse_command **********************************/ + /** + * @brief parses a single command line + * @details This parses a command and any parameters as key=val pairs + * from the supplied string and returns a ParsedCommand struct. + * @param[in] args string containing command and any optional arguments + * @return nullptr | ParsedCommand + * + */ + std::optional Sequence::parse_command(std::string &args) { + + // strip comments, everything after '#' + auto pos = args.find('#'); + if (pos != std::string::npos) args = args.substr(0, pos); + + std::istringstream iss(args); + std::string word; + + // first word is the command + if (!(iss >> word)) return std::nullopt; + + ParsedCommand command; + command.name = word; + + // any additional words are parameters, expected to be key=val pairs + while (iss >> word) { + auto eq = word.find('='); + if (eq != std::string::npos) { + std::string key = word.substr(0, eq); + std::string val = word.substr(eq+1); + command.params.map[key] = val; + } + } + + return command; + } + /***** Sequencer::Sequence::parse_command **********************************/ + + /***** Sequencer::Sequence::validate_sequence ******************************/ /** - * @brief - * @param[in] - * @return ERROR|NO_ERROR|ABORT + * @brief applies validation rules to sequence + * @param[in] sequence vector of OperationGroups + * @return ERROR|NO_ERROR * */ - long Sequence::validate_sequence(const std::vector &groups) { + long Sequence::validate_sequence(const std::vector &sequence) { + + // sequence is a vector of OperationGroups + for (const auto &group : sequence) { + + // group is a vector of Operations + for (const auto &op : group.operations) { + + if (op.name == "expose") { + } + else + if (op.name == "slit_set") { + } + } + } return NO_ERROR; } /***** Sequencer::Sequence::validate_sequence ******************************/ @@ -244,12 +339,38 @@ namespace Sequencer { /***** Sequencer::Sequence::handle_cli_operation ***************************/ /** * @brief handle incoming operation request - * @param[in] op the name of an operation + * @details This performs all the same steps for a single command as a + * sequence of one operation. This is inefficient for performing + * multiple steps. + * @param[in] args string containing command and any arguments * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::handle_cli_operation(const std::string &op) { - return NO_ERROR; + long Sequence::handle_cli_operation(std::string args) { + std::string_view function("Sequencer::Sequence::handle_cli_operation"); + + if (args.empty()) return ERROR; + + // build a mini-sequence of one command in order to validate it + // + auto commands = { *parse_command(args) }; + + std::vector sequence; + + if ( build_sequence(commands, sequence) != NO_ERROR ) return ERROR; + + if ( validate_sequence(sequence) != NO_ERROR ) return ERROR; + + if ( sequence.empty() || sequence[0].operations.empty() ) { + logwrite(function, "ERROR invalid command '"+args+"'"); + return ERROR; + } + + Operation op = sequence[0].operations[0]; + + // ---------- RUN THE COMMAND -------------------------- + // + return run(op, function); } /***** Sequencer::Sequence::handle_cli_operation ***************************/ diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 6292cd77..dcac10f0 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -527,11 +527,12 @@ namespace Sequencer { long run_script(const std::string &filename); ///< run user script long parse_script(const std::string &filename, - std::vector &out); ///< parse script into commands/args + std::vector &commands_out); ///< parse script into commands/args + std::optional parse_command(std::string &args); long build_sequence(const std::vector &commands, std::vector &sequence_out); ///< build sequence from parsed commands - long validate_sequence(const std::vector &groups); ///< validate sequence - long handle_cli_operation(const std::string &op); ///< handle incoming operation request + long validate_sequence(const std::vector &sequence); ///< validate sequence + long handle_cli_operation(std::string command); ///< handle incoming operation request // publish/subscribe functions // diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 4a7489a7..6b2d36ba 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -31,12 +31,14 @@ namespace Sequencer { continue; } else + if (command.name == "end_parallel") { sequence_out.push_back(group); - group = { OperationType::SERIAL, {} }; // back to default + group = { OperationType::SERIAL, {} }; continue; } else + if (command.name == "move_to_target") { group.operations.emplace_back( Operation { "move_to_target", THR_MOVE_TO_TARGET, @@ -50,6 +52,30 @@ namespace Sequencer { command.params }); } + else + + if (command.name == "slit_set") { + group.operations.emplace_back( Operation { + "slit_set", THR_SLIT_SET, + [this,params=command.params]() { + size_t mode = params.get("mode", VSM_DATABASE); + return slit_set(static_cast(mode)); + }, + command.params + }); + } + + else + + if (command.name == "expose") { + group.operations.emplace_back( Operation { + "expose", THR_SLIT_SET, + [this]() { + return do_exposure("placeholder"); + }, + {} + }); + } else { this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", From 39ab92719e38db64d263a5f6d1c7722a408ed9e5 Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 7 Apr 2026 16:13:12 -0700 Subject: [PATCH 09/37] * updates status publishing in slitd * adds subscriber topic handlers to sequencerd * introduces a command handler header-only lib (WIP, not implemented) --- camerad/astrocam.cpp | 3 +- camerad/camerad.cpp | 2 +- common/message_keys.h | 22 +++- sequencerd/command.h | 134 ++++++++++++++++++++++ sequencerd/command_rules.h | 37 ++++++ sequencerd/sequence.cpp | 189 ++++++++----------------------- sequencerd/sequence.h | 25 +++- sequencerd/sequence_builder.cpp | 2 +- sequencerd/sequencer_interface.h | 13 +++ sequencerd/sequencer_server.h | 2 +- sequencerd/sequencerd.cpp | 2 + slitd/slit_interface.cpp | 88 ++++++-------- slitd/slit_interface.h | 18 ++- slitd/slit_server.cpp | 18 +-- slitd/slitd.cpp | 2 +- 15 files changed, 332 insertions(+), 225 deletions(-) create mode 100644 sequencerd/command.h create mode 100644 sequencerd/command_rules.h diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 23d4db37..fb2ddea2 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -29,8 +29,9 @@ namespace AstroCam { nlohmann::json jmessage_out; // build JSON message with my telemetry - jmessage_out[Key::SOURCE] = "camerad"; + jmessage_out[Key::SOURCE] = Topic::CAMERAD; jmessage_out[Key::Camerad::READY] = this->can_expose.load(); + jmessage_out[Key::Camerad::SHUTTERTIME] = this->camera.shutter.get_duration(); // publish JSON message try { diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 2d7e0449..551a9a70 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -339,7 +339,7 @@ void async_main(Network::UdpSocket sock) { } while (1) { - std::string message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) + std::string_view message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/common/message_keys.h b/common/message_keys.h index ab7d8a46..13a4d7ff 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -31,7 +31,8 @@ namespace Key { } namespace Camerad { - inline const std::string READY = "ready"; + inline const std::string READY = "ready"; + inline const std::string SHUTTERTIME = "shuttime_sec"; } namespace Acamd { @@ -50,4 +51,23 @@ namespace Key { inline const std::string FINEACQUIRE_LOCKED = "fineacquire_locked"; inline const std::string FINEACQUIRE_RUNNING = "fineacquire_running"; } + + namespace Slitd { + inline const std::string SLITPOSA = "slitposa"; + inline const std::string SLITPOSB = "slitposb"; + inline const std::string SLITW = "slitw"; + inline const std::string SLITO = "slito"; + inline const std::string ISOPEN = "isopen"; + inline const std::string ISHOME = "ishome"; + } + + namespace Tcsd { + inline const std::string TELRA = "telra"; + inline const std::string TELDEC = "teldec"; + inline const std::string ALT = "alt"; + inline const std::string AZ = "az"; + inline const std::string AIRMASS = "airmass"; + inline const std::string CASANGLE = "casangle"; + } + } diff --git a/sequencerd/command.h b/sequencerd/command.h new file mode 100644 index 00000000..ebacb4bc --- /dev/null +++ b/sequencerd/command.h @@ -0,0 +1,134 @@ +/** + * @file command.h + * @brief header-only library for handling commands to daemons + * @details This provides a wrapper to form command and arg list strings. + * Also a wrapper that stores transition states, and validates that + * a command is allowed to be sent while in the current state. + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include + +/***** Sequencer **************************************************************/ +/** + * @brief namespace for the observation sequencer + * + */ +namespace Sequencer { + + /** + * @brief wrapper to form string from command and arglist + */ + struct Command { + std::string name; + std::vector arglist; + + std::string str() const { + std::string strung = name; + for (const auto &arg : arglist) { + strung += " " + arg; + } + return strung; + } + }; + + + /** + * @brief command specs right now just holds min/max number of allowed args + */ + struct CommandSpec { + int min_args; + int max_args; + }; + + using CommandSpecMap = std::unordered_map; + + + /** + * @brief structure contains command and states it can transition from->to + */ + template + struct Transition { + State from; + std::string command; + State to; + }; + + + /** + * @brief contains the functionality of the library + * @details This pairs specs and transitions with a command and holds + * the client object used to communiate with the daemon. + */ + template + class CommandClient { + private: + Common::DaemonClient &client; + const CommandSpecMap &specs; + State state; + const std::vector> &transitions; + + void validate_args( const Command &cmd ) const { + auto it = specs.find( cmd.name ); + if (it == specs.end()) throw std::runtime_error("unknown command: "+cmd.name); + int nargs = cmd.arglist.size(); + if (nargs < it->second.min_args || nargs > it->second.max_args) { + throw std::runtime_error("invalid arg count for "+cmd.name); + } + } + + void validate_order( const Command &cmd ) const { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) { + return; + } + } + throw std::runtime_error("invalid command order: "+cmd.name); + } + + void advance_state( const Command &cmd ) { + for (const auto &transition : transitions) { + if (transition.from == state && transition.command == cmd.name) { + state = transition.to; + return; + } + } + } + + public: + CommandClient( Common::DaemonClient &client, + const CommandSpecMap &specs, + State initial_state, + const std::vector> &transitions ) + : client(client), + specs(specs), + state(initial_state), + transitions(transitions) { } + + /** + * @brief primary interface to sending commands + * @details This validates the number of args, validates the + * transition state, that this command is allowed to be used + * in the current state, and sends the command. + * @param[in] cmd Command struct contains command and arglist + * @return return value from the client + * + */ + long send( const Command &cmd ) { + validate_args( cmd ); + validate_order( cmd ); + long ret = client.command( cmd.str() ); + advance_state( cmd ); + return ret; + } + + State get_state() const { return state; } + }; +} +/***** Sequencer **************************************************************/ diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h new file mode 100644 index 00000000..5ec60136 --- /dev/null +++ b/sequencerd/command_rules.h @@ -0,0 +1,37 @@ + +#pragma once + +#include "command.h" + +#include + +namespace Sequencer { + + enum class CameraState { + IDLE, + READY, + EXPOSING, + READING + }; + + const CommandSpecMap camerad_specs = { + { CAMERAD_ACTIVATE, {0, 4} }, + { CAMERAD_DEACTIVATE, {1, 4} }, + { CAMERAD_OPEN, {0, 1} }, + { CAMERAD_CLOSE, {0, 0} }, + { CAMERAD_EXPTIME, {0, 1} }, + { CAMERAD_EXPOSE, {0, 0} }, + { CAMERAD_READOUT, {0, 2} } + }; + + const std::vector> camerad_transitions = { + { CameraState::IDLE, CAMERAD_OPEN, CameraState::READY }, + { CameraState::READY, CAMERAD_ACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_DEACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPTIME, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPOSE, CameraState::EXPOSING }, + { CameraState::EXPOSING, CAMERAD_READOUT, CameraState::READING }, + { CameraState::READING, CAMERAD_READOUT, CameraState::READY } + }; + +} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index f1c719d9..564156f3 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -405,16 +405,34 @@ namespace Sequencer { * */ void Sequence::handletopic_camerad(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::EXPTIME, Key::Camerad::SHUTTERTIME, jmessage ); if (jmessage.contains(Key::Camerad::READY)) { int isready = jmessage[Key::Camerad::READY].get(); this->can_expose.store(isready, std::memory_order_relaxed); - std::lock_guard lock(camerad_mtx); - this->camerad_cv.notify_all(); } + + std::lock_guard lock(camerad_mtx); + this->camerad_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_camerad ****************************/ + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + /** + * @brief handles Topic::SLITD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_slitd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::SLITWIDTH, Key::Slitd::SLITW, jmessage ); + this->target.column_from_json( DBCol::SLITOFFSET, Key::Slitd::SLITO, jmessage ); + + std::lock_guard lock(slitd_mtx); + this->slitd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_slitd ******************************/ + + /***** Sequencer::Sequence::handletopic_slicecamd **************************/ /** * @brief handles Topic::SLICECAMD telemetry @@ -426,12 +444,33 @@ namespace Sequencer { bool fineacquirelocked; Common::extract_telemetry_value( jmessage, Key::Slicecamd::FINEACQUIRE_LOCKED, fineacquirelocked ); this->is_fineacquire_locked.store(fineacquirelocked, std::memory_order_relaxed); + std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } /***** Sequencer::Sequence::handletopic_slicecamd **************************/ + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + /** + * @brief handles Topic::TCSD telemetry + * @param[in] jmessage subscribed-received JSON message + * + */ + void Sequence::handletopic_tcsd(const nlohmann::json &jmessage) { + this->target.column_from_json( DBCol::TELRA, Key::Tcsd::TELRA, jmessage ); + this->target.column_from_json( DBCol::TELDECL, Key::Tcsd::TELDEC, jmessage ); + this->target.column_from_json( DBCol::ALT, Key::Tcsd::ALT, jmessage ); + this->target.column_from_json( DBCol::AZ, Key::Tcsd::AZ, jmessage ); + this->target.column_from_json( DBCol::AIRMASS, Key::Tcsd::AIRMASS, jmessage ); + this->target.column_from_json( DBCol::CASANGLE, Key::Tcsd::CASANGLE, jmessage ); + + std::lock_guard lock(tcsd_mtx); + this->tcsd_cv.notify_all(); + } + /***** Sequencer::Sequence::handletopic_tcsd *******************************/ + + /***** Sequencer::Sequence::handletopic_acamd ******************************/ /** * @brief handles Topic::ACAMD telemetry @@ -443,6 +482,7 @@ namespace Sequencer { bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } @@ -769,7 +809,7 @@ namespace Sequencer { * @param[in] obsid_in optional obsid, specify for single-target observation * */ - void Sequence::sequence_start(std::string obsid_in="") { + void Sequence::sequence_start(std::string obsid_in) { std::string_view function("Sequencer::Sequence::sequence_start"); std::ostringstream message; std::string reply; @@ -896,10 +936,7 @@ namespace Sequencer { break; } - // before writing to the completed database table, get current - // telemetry from other daemons. - // - this->get_external_telemetry(); +// this->request_status(tcsd); // force tcsd to publish his status TODO WORK-IN-PROGRESS // Update this target's state in the database // @@ -989,6 +1026,7 @@ namespace Sequencer { // send two commands, one for each if (!activechans.str().empty()) { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); +/*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); throw std::runtime_error("camera returned "+reply); @@ -3013,8 +3051,8 @@ namespace Sequencer { { THR_CAMERA_SHUTDOWN, std::bind(&Sequence::camera_shutdown, this) }, { THR_FLEXURE_SHUTDOWN, std::bind(&Sequence::flexure_shutdown, this) }, { THR_FOCUS_SHUTDOWN, std::bind(&Sequence::focus_shutdown, this) }, - { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, - { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, + { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, + { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, { THR_TCS_SHUTDOWN, std::bind(&Sequence::tcs_shutdown, this) } }; @@ -3615,120 +3653,6 @@ namespace Sequencer { /***** Sequencer::Sequence::make_telemetry_message **************************/ - /***** Sequencer::Sequence::get_external_telemetry **************************/ - /** - * @brief collect telemetry from other daemon(s) - * @details This is used for any telemetry that I need to collect from - * another daemon. Common::collect_telemetry() sends a command - * to the daemon, which will respond with a JSON message. The - * daemon(s) to contact are configured with the TELEM_PROVIDER - * key in the config file. - * - */ - void Sequence::get_external_telemetry() { - // Loop through each configured telemetry provider. This requests - // their telemetry which is returned as a serialized json string - // held in retstring. - // - // handle_json_message() will parse the serialized json string. - // - std::string retstring; - for ( const auto &provider : this->telemetry_providers ) { - Common::collect_telemetry( provider, retstring ); - handle_json_message(retstring); - } - return; - } - /***** Sequencer::Sequence::get_external_telemetry **************************/ - - - /***** Sequencer::Sequence::handle_json_message *****************************/ - /** - * @brief parses incoming telemetry messages - * @details Requesting telemetry from another daemon returns a serialized - * JSON message which needs to be passed in here to parse it. - * @param[in] message_in incoming serialized JSON message (as a string) - * @return ERROR | NO_ERROR - * - */ - long Sequence::handle_json_message( const std::string message_in ) { - std::string_view function("Sequencer::Sequence::handle_json_message"); - std::stringstream message; - - if ( message_in.empty() ) { - logwrite( function, "ERROR empty JSON message" ); - return ERROR; - } - - try { - nlohmann::json jmessage = nlohmann::json::parse( message_in ); - std::string messagetype; - - // jmessage must not contain key "error" and must contain key "messagetype" - // - if ( !jmessage.contains("error") ) { - if ( jmessage.contains("messagetype") && jmessage["messagetype"].is_string() ) { - messagetype = jmessage["messagetype"]; - } - else { - logwrite( function, "ERROR received JSON message with missing or invalid messagetype" ); - return ERROR; - } - } - else { - logwrite( function, "ERROR in JSON message" ); - return ERROR; - } - - // No errors, so disseminate the message contents based on the message type. - // - // column_from_json( colname, jkey, jmessage ) will extract the value of - // expected type with key jkey from json string jmessage, and assign it - // to this->target.external_telemetry[colname] map. It is expected that - // "colname" is the column name in the database. - // - if ( messagetype == "camerainfo" ) { - this->target.column_from_json( "EXPTIME", "SHUTTIME_SEC", jmessage ); - } - else - if ( messagetype == "slitinfo" ) { - this->target.column_from_json( "SLITWIDTH", "SLITW", jmessage ); - this->target.column_from_json( "SLITOFFSET", "SLITO", jmessage ); - } - else - if ( messagetype == "tcsinfo" ) { - this->target.column_from_json( "TELRA", "TELRA", jmessage ); - this->target.column_from_json( "TELDECL", "TELDEC", jmessage ); - this->target.column_from_json( "ALT", "ALT", jmessage ); - this->target.column_from_json( "AZ", "AZ", jmessage ); - this->target.column_from_json( "AIRMASS", "AIRMASS", jmessage ); - this->target.column_from_json( "CASANGLE", "CASANGLE", jmessage ); - } - else - if ( messagetype == "test" ) { - } - else { - message.str(""); message << "ERROR received unhandled JSON message type \"" << messagetype << "\""; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const nlohmann::json::parse_error &e ) { - message.str(""); message << "ERROR json exception parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing message: " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - - return NO_ERROR; - } - /***** Sequencer::Sequence::handle_json_message *****************************/ - - /***** Sequencer::Sequence::dothread_test_fpoffset **************************/ /** * @brief for testing, calls a Python function from a thread @@ -4054,7 +3978,6 @@ namespace Sequencer { retstring.append( " fpoffset ? | \n" ); retstring.append( " getnext [ ? ]\n" ); retstring.append( " getobsid [ ? ]\n" ); - retstring.append( " gettelem [ ? ]\n" ); retstring.append( " isready [ ? ]\n" ); retstring.append( " moveto [ ? | ]\n" ); retstring.append( " notify [ ? ]\n" ); @@ -4437,24 +4360,6 @@ namespace Sequencer { retstring = rts.str(); } else - // ---------------------------------------------------- - // gettelem -- get external telemetry - // ---------------------------------------------------- - // - if ( testname == "gettelem" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test gettelem\n"; - retstring.append( " Get external telemetry from other daemons.\n" ); - return HELP; - } - this->get_external_telemetry(); - message.str(""); - for ( const auto &[name,data] : this->target.external_telemetry ) { - message << "name=" << name << " valid=" << (data.valid?"T":"F") << " value=" << data.value << "\n"; - } - retstring = message.str(); - } - else // ---------------------------------------------------- // addrow -- insert a (fixed, hard-coded) row into the database diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index dcac10f0..fa5e7420 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -34,6 +34,10 @@ #include "tcsd_commands.h" #include "sequencerd_commands.h" #include "message_keys.h" +/*** Work-In-Progress + * #include "command.h" + * #include "command_rules.h" + */ #include "tcs_constants.h" #include "acam_interface_shared.h" @@ -405,6 +409,10 @@ namespace Sequencer { [this](const nlohmann::json &msg) { handletopic_acamd(msg); } ) }, { Topic::SLICECAMD, std::function( [this](const nlohmann::json &msg) { handletopic_slicecamd(msg); } ) }, + { Topic::SLITD, std::function( + [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) }, + { Topic::TCSD, std::function( + [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, { Topic::CAMERAD, std::function( [this](const nlohmann::json &msg) { handletopic_camerad(msg); } ) } }; @@ -456,6 +464,10 @@ namespace Sequencer { std::condition_variable acam_cv; std::mutex camerad_mtx; std::condition_variable camerad_cv; + std::mutex slitd_mtx; + std::condition_variable slitd_cv; + std::mutex tcsd_mtx; + std::condition_variable tcsd_cv; std::mutex wait_mtx; std::condition_variable cv; std::mutex cv_mutex; @@ -510,6 +522,13 @@ namespace Sequencer { Common::DaemonClient slitd { "slitd" }; Common::DaemonClient tcsd { "tcsd" }; +/**** Work-In-Progress + CommandClient camerad_cmd { camerad, + camerad_specs, + CameraState::IDLE, + camerad_transitions}; +*****/ + std::map power_switch; ///< STL map of PowerSwitch objects maps all plugnames to each subsystem float slitoffsetexpose; ///< "virtual slit mode" offset for expose @@ -546,6 +565,8 @@ namespace Sequencer { void handletopic_camerad( const nlohmann::json &jmessage ); void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slicecamd( const nlohmann::json &jmessage ); + void handletopic_slitd( const nlohmann::json &jmessage ); + void handletopic_tcsd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); void publish_seqstate(); @@ -616,8 +637,6 @@ namespace Sequencer { long target_offset(); void make_telemetry_message( std::string &retstring ); ///< assembles my telemetry message - void get_external_telemetry(); ///< collect telemetry from another daemon - long handle_json_message( const std::string message_in ); ///< parses incoming telemetry messages long set_power_switch( PowerState state, const std::string which, std::chrono::seconds delay ); long check_power_switch( PowerState checkstate, const std::string which, bool &is_set ); @@ -645,7 +664,7 @@ namespace Sequencer { long wait_for_readout(std::string_view caller); ///< wait for readout completion or cancel long wait_for_canexpose(std::string_view caller); ///< wait for camera can_expose - void sequence_start(std::string obsid_in); ///< main sequence start thread. optional obsid_in for single target obs + void sequence_start(std::string obsid_in=""); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params long camera_set(); ///< sets camera according to target entry params long slit_set(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 6b2d36ba..cf1ba269 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -69,7 +69,7 @@ namespace Sequencer { if (command.name == "expose") { group.operations.emplace_back( Operation { - "expose", THR_SLIT_SET, + "expose", THR_EXPOSURE, [this]() { return do_exposure("placeholder"); }, diff --git a/sequencerd/sequencer_interface.h b/sequencerd/sequencer_interface.h index f4569e7c..fd59948b 100644 --- a/sequencerd/sequencer_interface.h +++ b/sequencerd/sequencer_interface.h @@ -26,6 +26,19 @@ #define ERROR_TARGETLIST_BAD_HEADER 1001 ///< TODO change this +namespace DBCol { + inline const std::string EXPTIME = "EXPTIME"; + inline const std::string SLITWIDTH = "SLITWIDTH"; + inline const std::string SLITOFFSET = "SLITOFFSET"; + + inline const std::string TELRA = "TELRA"; + inline const std::string TELDECL = "TELDECL"; + inline const std::string ALT = "ALT"; + inline const std::string AZ = "AZ"; + inline const std::string AIRMASS = "AIRMASS"; + inline const std::string CASANGLE = "CASANGLE"; +} + /***** Sequencer **************************************************************/ /** * @namespace Sequencer diff --git a/sequencerd/sequencer_server.h b/sequencerd/sequencer_server.h index f785d42d..aa3440a8 100644 --- a/sequencerd/sequencer_server.h +++ b/sequencerd/sequencer_server.h @@ -82,7 +82,7 @@ namespace Sequencer { // are initialized here. The names are useful just for logging. // this->sequence.calibd.name = "calibd"; - this->sequence.camerad.name = "camerad"; +// this->sequence.camerad.name = "camerad"; this->sequence.filterd.name = "filterd"; this->sequence.flexured.name = "flexured"; this->sequence.focusd.name = "focusd"; diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 3b9ba63f..26f43523 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -131,6 +131,8 @@ int main(int argc, char **argv) { // if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, Topic::ACAMD, + Topic::TCSD, + Topic::SLITD, Topic::SLICECAMD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); diff --git a/slitd/slit_interface.cpp b/slitd/slit_interface.cpp index 2569478c..96edc275 100644 --- a/slitd/slit_interface.cpp +++ b/slitd/slit_interface.cpp @@ -104,10 +104,10 @@ namespace Slit { std::string retstring; this->is_open( "", retstring ); - snapshot.isopen = ( retstring=="true" ? true : false ); - if ( snapshot.isopen ) { + status.isopen = ( retstring=="true" ? true : false ); + if ( status.isopen ) { this->is_home( "", retstring ); - snapshot.ishome = ( retstring=="true" ? true : false ); + status.ishome = ( retstring=="true" ? true : false ); } this->get( retstring ); @@ -285,14 +285,14 @@ namespace Slit { return HELP; } - if ( std::isnan(snapshot.width.arcsec()) ) { + if ( std::isnan(status.width.arcsec()) ) { logwrite( "Slit::Interface::offset", "ERROR width not previously set" ); retstring="undefined_width"; return ERROR; } std::stringstream cmd; - cmd << snapshot.width.arcsec() << " " << args; + cmd << status.width.arcsec() << " " << args; return this->set( cmd.str(), retstring ); } @@ -374,7 +374,7 @@ namespace Slit { else fval = std::round( fval * 10.0 ) / 10.0; // round to nearest tenth } reqwidth = SlitDimension( fval, unit ); - reqoffset = snapshot.offset; + reqoffset = status.offset; } if ( tokens.size() == 2 ) { if ( tokens.at(1).find("mm") != std::string::npos ) unit=Unit::MM; else unit=Unit::ARCSEC; @@ -502,30 +502,27 @@ namespace Slit { // this call reads the controller and returns the numeric values // - error = this->read_positions( poswidth, posoffset, snapshot.posA, snapshot.posB ); + error = this->read_positions( poswidth, posoffset, status.posA, status.posB ); // store the current readings in the class // - snapshot.width = SlitDimension( poswidth, Unit::MM ); - snapshot.offset = SlitDimension( posoffset, Unit::MM ); + status.width = SlitDimension( poswidth, Unit::MM ); + status.offset = SlitDimension( posoffset, Unit::MM ); // form the return value // std::stringstream s; if ( args=="mm" ) { - s << std::setprecision(2) << std::fixed << snapshot.width.mm() << " " - << std::setprecision(3) << snapshot.offset.mm() << " mm"; + s << std::setprecision(2) << std::fixed << status.width.mm() << " " + << std::setprecision(3) << status.offset.mm() << " mm"; } else { - s << std::setprecision(2) << std::fixed << snapshot.width.arcsec() << " " - << std::setprecision(3) << snapshot.offset.arcsec(); + s << std::setprecision(2) << std::fixed << status.width.arcsec() << " " + << std::setprecision(3) << status.offset.arcsec(); } retstring = s.str(); - message.str(""); message << "NOTICE:" << Slit::DAEMON_NAME << " " << retstring; - this->async.enqueue( message.str() ); - - this->publish_snapshot(); + this->publish_status(); return error; } @@ -713,55 +710,42 @@ namespace Slit { * @param[in] jmessage_in subscribed-received JSON message * */ - void Interface::handletopic_snapshot( const nlohmann::json &jmessage_in ) { - // If my name is in the jmessage then publish my snapshot - // - if ( jmessage_in.contains( Slit::DAEMON_NAME ) ) { - this->publish_snapshot(); - } - else - if ( jmessage_in.contains( "test" ) ) { - logwrite( "Slit::Interface::handletopic_snapshot", jmessage_in.dump() ); - } + void Interface::handletopic_snapshot( const nlohmann::json &jmessage ) { + if ( jmessage.contains(Topic::SLITD) ) this->publish_status(); } /***** Slit::Interface::handletopic_snapshot ********************************/ - /***** Slit::Interface::publish_snapshot ************************************/ + /***** Slit::Interface::publish_status **************************************/ /** - * @brief publishes snapshot of my telemetry - * @details This publishes a JSON message containing a snapshot of my - * telemetry. + * @brief publishes my status on change + * @param[in] force optional (default=false) force publish irrespective of change * */ - void Interface::publish_snapshot() { - std::string dontcare; - this->publish_snapshot(dontcare); - } - void Interface::publish_snapshot(std::string &retstring) { + void Interface::publish_status(bool force) { + + // unless forced, only publish if there was a change + if ( !force && this->status == this->last_published_status ) return; + nlohmann::json jmessage_out; - jmessage_out["source"] = "slitd"; - jmessage_out["ISOPEN"] = snapshot.isopen; - jmessage_out["ISHOME"] = snapshot.ishome; - jmessage_out["SLITW"] = snapshot.width.arcsec(); - jmessage_out["SLITO"] = snapshot.offset.arcsec(); - jmessage_out["SLITPOSA"] = snapshot.posA; - jmessage_out["SLITPOSB"] = snapshot.posB; - - // for backwards compatibility - jmessage_out["messagetype"] = "slitinfo"; - retstring=jmessage_out.dump(); - retstring.append(JEOF); + jmessage_out[Key::SOURCE] = Topic::SLITD; + jmessage_out[Key::Slitd::ISOPEN] = this->status.isopen; + jmessage_out[Key::Slitd::ISHOME] = this->status.ishome; + jmessage_out[Key::Slitd::SLITW] = this->status.width.arcsec(); + jmessage_out[Key::Slitd::SLITO] = this->status.offset.arcsec(); + jmessage_out[Key::Slitd::SLITPOSA] = this->status.posA; + jmessage_out[Key::Slitd::SLITPOSB] = this->status.posB; + + this->last_published_status = this->status; try { this->publisher->publish( jmessage_out ); } catch ( const std::exception &e ) { - logwrite( "Slit::Interface::publish_snapshot", - "ERROR publishing message: "+std::string(e.what()) ); - return; + logwrite( "Slit::Interface::publish_status", + "ERROR publishing status: "+std::string(e.what()) ); } } - /***** Slit::Interface::publish_snapshot ************************************/ + /***** Slit::Interface::publish_status **************************************/ } diff --git a/slitd/slit_interface.h b/slitd/slit_interface.h index 02111f81..235c2455 100644 --- a/slitd/slit_interface.h +++ b/slitd/slit_interface.h @@ -8,6 +8,7 @@ #pragma once +#include "message_keys.h" #include "network.h" #include "pi.h" #include "logentry.h" @@ -207,16 +208,24 @@ namespace Slit { SlitDimension minwidth; ///< set by config file SlitDimension center; ///< position of center in actuator units - typedef struct { + struct Status { SlitDimension width; SlitDimension offset; float posA=NAN; float posB=NAN; bool ishome=false; bool isopen=false; - } snapshot_t; - snapshot_t snapshot; + bool operator==(const Status& other) const { + return std::tie(width, offset, posA, posB, ishome, isopen) == + std::tie(other.width, other.offset, other.posA, other.posB, other.ishome, other.isopen); + } + + bool operator!=(const Status& other) const { return !(*this == other); } + }; + + Status status; + Status last_published_status; Common::Queue async; @@ -233,8 +242,7 @@ namespace Slit { void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void handletopic_snapshot( const nlohmann::json &jmessage ); - void publish_snapshot(); - void publish_snapshot(std::string &retstring); + void publish_status(bool force=false); long initialize_class(); long open(); ///< opens the PI socket connection diff --git a/slitd/slit_server.cpp b/slitd/slit_server.cpp index 3a298599..a24960cf 100644 --- a/slitd/slit_server.cpp +++ b/slitd/slit_server.cpp @@ -376,7 +376,7 @@ namespace Slit { } while (1) { - std::string message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) + std::string_view message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; @@ -597,22 +597,6 @@ namespace Slit { if ( cmd == SLITD_NATIVE ) { ret = this->interface.send_command( args, retstring ); } - else - - // send telemetry on request - // - if ( cmd == TELEMREQUEST ) { - if ( args=="?" || args=="help" ) { - retstring=TELEMREQUEST+"\n"; - retstring.append( " Returns a serialized JSON message containing telemetry\n" ); - retstring.append( " information, terminated with \"EOF\\n\".\n" ); - ret=HELP; - } - else { - this->interface.publish_snapshot(retstring); - ret = JSON; - } - } // unknown commands generate an error // diff --git a/slitd/slitd.cpp b/slitd/slitd.cpp index 23daaa3e..4fd6a1fb 100644 --- a/slitd/slitd.cpp +++ b/slitd/slitd.cpp @@ -127,7 +127,7 @@ int main(int argc, char **argv) { } std::this_thread::sleep_for( std::chrono::milliseconds(100) ); - slitd.interface.publish_snapshot(); + slitd.interface.publish_status(true); // This will pre-thread N_THREADS threads. // The 0th thread is reserved for the blocking port, and the rest are for the non-blocking port. From e2c366fcbe2902dce7d8b6e865011ba1f55b8b6e Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 8 Apr 2026 11:55:42 -0700 Subject: [PATCH 10/37] string/string_view cleanup --- Andor/andor.h | 2 +- Andor/andor_emulator.cpp | 4 +- Andor/andor_emulator.h | 2 +- acamd/acam_interface.h | 14 +++- camerad/camerad.cpp | 2 +- common/common.cpp | 10 +-- common/common.h | 10 +-- emulator/tcs.h | 2 +- sequencerd/command.h | 2 +- sequencerd/sequence.cpp | 114 ++++++++++++++-------------- sequencerd/sequence.h | 26 +++---- sequencerd/sequence_acquisition.cpp | 6 +- sequencerd/sequence_wait.cpp | 10 +-- slicecamd/guimanager.h | 2 +- slicecamd/slicecam_interface.h | 10 ++- slitd/slit_server.cpp | 2 +- tcsd/tcsd_client.cpp | 2 +- tcsd/tcsd_client.h | 2 +- utils/logentry.cpp | 2 +- utils/logentry.h | 2 +- utils/network.cpp | 4 +- utils/network.h | 2 +- 22 files changed, 122 insertions(+), 110 deletions(-) diff --git a/Andor/andor.h b/Andor/andor.h index 40616662..700dd4bb 100644 --- a/Andor/andor.h +++ b/Andor/andor.h @@ -517,7 +517,7 @@ namespace Andor { * @return "emulator" or "sdk" or "null" * */ - inline std::string_view get_andor_object() { + inline std::string_view get_andor_object() const { if ( this->andor == &emulator ) return ANDOR_OBJ_EMULATOR; else if ( this->andor == &sdk ) return ANDOR_OBJ_SDK; diff --git a/Andor/andor_emulator.cpp b/Andor/andor_emulator.cpp index df7a0e9d..dd41a1bb 100644 --- a/Andor/andor_emulator.cpp +++ b/Andor/andor_emulator.cpp @@ -1144,8 +1144,8 @@ namespace Andor { * @return ERROR | NO_ERROR * */ - long SkySim::generate_image( const std::string_view &headerfile, - const std::string_view &outputfile, + long SkySim::generate_image( const std::string headerfile, + const std::string outputfile, const float exptime, const bool ismex, const int simsize ) { diff --git a/Andor/andor_emulator.h b/Andor/andor_emulator.h index 09a6b594..8b8acb9a 100644 --- a/Andor/andor_emulator.h +++ b/Andor/andor_emulator.h @@ -41,7 +41,7 @@ namespace Andor { PyObject* pSkySimModule; - long generate_image( const std::string_view &headerfile, const std::string_view &outputfile, + long generate_image( const std::string headerfile, const std::string outputfile, const float exptime, const bool ismex, const int simsize ); void log_python_arguments(PyObject* pFunction, PyObject* pArgs, PyObject* pKwArgs); diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 8c4b317e..3cb5d4f6 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -286,7 +286,7 @@ namespace Acam { * @details the script pushes the indicated file to the Guider GUI display * @param[in] filename fits file to send */ - void push_guider_image( std::string_view filename ) { + void push_guider_image( std::string filename ) { std::string function = "Acam::GuideManager::push_guider_image"; std::stringstream cmd; cmd << push_image << " " @@ -304,7 +304,7 @@ namespace Acam { * @brief calls the push_message script with the supplied message string * @param[in] message message to send */ - void push_guider_message( std::string_view message ) { + void push_guider_message( std::string message ) { std::string function = "Acam::GuideManager::push_guider_message"; std::stringstream cmd; cmd << push_message << " " @@ -613,8 +613,14 @@ namespace Acam { inline std::string get_imagename() { return this->imagename; } inline std::string get_wcsname() { return this->wcsname; } - inline void set_imagename( std::string name_in ) { this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : name_in ); return; } - inline void set_wcsname( std::string name_in ) { this->wcsname = name_in; return; } + inline void set_imagename( std::string name_in ) { + this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : std::move(name_in) ); + return; + } + inline void set_wcsname( std::string name_in ) { + this->wcsname = std::move(name_in); + return; + } GuideManager guide_manager; diff --git a/camerad/camerad.cpp b/camerad/camerad.cpp index 551a9a70..2d7e0449 100644 --- a/camerad/camerad.cpp +++ b/camerad/camerad.cpp @@ -339,7 +339,7 @@ void async_main(Network::UdpSocket sock) { } while (1) { - std::string_view message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) + std::string message = server.camera.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/common/common.cpp b/common/common.cpp index 196d1fb0..3fc176eb 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -59,7 +59,7 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log(std::string_view function, std::string_view message) { + void Queue::enqueue_and_log(std::string function, std::string message) { std::lock_guard lock(queue_mutex); message_queue.push(message); notifier.notify_one(); @@ -77,9 +77,9 @@ namespace Common { * @param[in] message string to write * */ - void Queue::enqueue_and_log( std::string_view tag, std::string_view function, std::string_view message ) { + void Queue::enqueue_and_log( std::string tag, std::string function, std::string message ) { std::lock_guard lock(queue_mutex); - std::ostringstream qmessage; + std::stringstream qmessage; qmessage << tag << ":" << message; message_queue.push(qmessage.str()); notifier.notify_one(); @@ -98,12 +98,12 @@ namespace Common { * If the queue is empty, wait untill an element is avaiable. * */ - std::string_view Queue::dequeue(void) { + std::string Queue::dequeue(void) { std::unique_lock lock(queue_mutex); while(message_queue.empty()) { notifier.wait(lock); // release lock as long as the wait and reaquire it afterwards. } - std::string_view message = message_queue.front(); + std::string message = message_queue.front(); message_queue.pop(); return message; } diff --git a/common/common.h b/common/common.h index 49d4db66..d540dbd3 100644 --- a/common/common.h +++ b/common/common.h @@ -1112,7 +1112,7 @@ namespace Common { */ class Queue { private: - std::queue message_queue; + std::queue message_queue; mutable std::mutex queue_mutex; std::condition_variable notifier; bool is_running; @@ -1123,10 +1123,10 @@ namespace Common { void service_running(bool state) { this->is_running = state; }; ///< set service running bool service_running() { return this->is_running; }; ///< is the service running? - void enqueue_and_log(std::string_view function, std::string_view message); - void enqueue_and_log(std::string_view tag, std::string_view function, std::string_view message); - void enqueue(std::string message_view); ///< push an element into the queue. - std::string_view dequeue(void); ///< pop an element from the queue + void enqueue_and_log(std::string function, std::string message); + void enqueue_and_log(std::string tag, std::string function, std::string message); + void enqueue(std::string message); ///< push an element into the queue. + std::string dequeue(void); ///< pop an element from the queue }; /**************** Common::Queue *********************************************/ diff --git a/emulator/tcs.h b/emulator/tcs.h index be4d43ce..99e43820 100644 --- a/emulator/tcs.h +++ b/emulator/tcs.h @@ -136,7 +136,7 @@ namespace TcsEmulator { std::map map_returnval; std::map map_motionval; - long parse_command( const std::string cmd, std::string &retstring ); ///< parse commands for the TCS + long parse_command( std::string cmd, std::string &retstring ); ///< parse commands for the TCS }; /***** TcsEmulator::Interface ***********************************************/ diff --git a/sequencerd/command.h b/sequencerd/command.h index ebacb4bc..b9a37c5d 100644 --- a/sequencerd/command.h +++ b/sequencerd/command.h @@ -112,7 +112,7 @@ namespace Sequencer { transitions(transitions) { } /** - * @brief primary interface to sending commands + * @brief primary interface to sending commands * @details This validates the number of args, validates the * transition state, that this command is allowed to be used * in the current state, and sends the command. diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 564156f3..da073068 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -27,7 +27,7 @@ namespace Sequencer { * */ long Sequence::run( const Operation &op, - std::string_view caller ) { + std::string caller ) { long error=NO_ERROR; logwrite(caller, "starting "+op.name); @@ -57,7 +57,7 @@ namespace Sequencer { * */ long Sequence::run_parallel( const std::vector &ops, - std::string_view caller ) { + std::string caller ) { std::vector> futures; @@ -102,7 +102,7 @@ namespace Sequencer { * */ long Sequence::run_sequence( const std::vector &sequence, - std::string_view caller, + std::string caller, bool continue_on_error ) { long error = NO_ERROR; @@ -147,7 +147,7 @@ namespace Sequencer { * @return ERROR|NO_ERROR|ABORT * */ - long Sequence::run_default_sequence(std::string_view caller) { + long Sequence::run_default_sequence(std::string caller) { std::vector sequence; @@ -213,7 +213,7 @@ namespace Sequencer { * */ long Sequence::run_script(const std::string &filename) { - std::string_view function("Sequencer::Sequence::run_script"); + const std::string function("Sequencer::Sequence::run_script"); std::vector commands; if ( parse_script(filename, commands) != NO_ERROR ) { @@ -347,7 +347,7 @@ namespace Sequencer { * */ long Sequence::handle_cli_operation(std::string args) { - std::string_view function("Sequencer::Sequence::handle_cli_operation"); + const std::string function("Sequencer::Sequence::handle_cli_operation"); if (args.empty()) return ERROR; @@ -702,7 +702,7 @@ namespace Sequencer { * */ void Sequence::dothread_sequencer_async_listener( Sequencer::Sequence &seq, Network::UdpSocket udp ) { - std::string_view function("Sequencer::Sequence::dothread_sequencer_async_listener"); + const std::string function("Sequencer::Sequence::dothread_sequencer_async_listener"); ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_SEQUENCER_ASYNC_LISTENER ); @@ -810,7 +810,7 @@ namespace Sequencer { * */ void Sequence::sequence_start(std::string obsid_in) { - std::string_view function("Sequencer::Sequence::sequence_start"); + const std::string function("Sequencer::Sequence::sequence_start"); std::ostringstream message; std::string reply; std::string targetstatus; @@ -996,7 +996,7 @@ namespace Sequencer { * */ long Sequence::camera_set() { - std::string_view function("Sequencer::Sequence::camera_set"); + const std::string function("Sequencer::Sequence::camera_set"); std::string reply; std::stringstream camcmd; long error=NO_ERROR; @@ -1083,7 +1083,7 @@ namespace Sequencer { * */ long Sequence::slit_set(VirtualSlitMode mode) { - std::string_view function("Sequencer::Sequence::slit_set"); + const std::string function("Sequencer::Sequence::slit_set"); std::string reply, modestr; std::stringstream slitcmd, message; @@ -1140,7 +1140,7 @@ namespace Sequencer { * */ long Sequence::power_init() { - std::string_view function("Sequencer::Sequence::power_init"); + const std::string function("Sequencer::Sequence::power_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_POWER_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1167,7 +1167,7 @@ namespace Sequencer { * */ long Sequence::power_shutdown() { - std::string_view function("Sequencer::Sequence::power_shutdown"); + const std::string function("Sequencer::Sequence::power_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_POWER_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_POWER ); @@ -1190,7 +1190,7 @@ namespace Sequencer { * */ long Sequence::slit_init() { - std::string_view function("Sequencer::Sequence::slit_init"); + const std::string function("Sequencer::Sequence::slit_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); @@ -1256,7 +1256,7 @@ namespace Sequencer { * */ long Sequence::slit_shutdown() { - std::string_view function("Sequencer::Sequence::slit_shutdown"); + const std::string function("Sequencer::Sequence::slit_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1324,7 +1324,7 @@ namespace Sequencer { * */ long Sequence::slicecam_init() { - std::string_view function("Sequencer::Sequence::slicecam_init"); + const std::string function("Sequencer::Sequence::slicecam_init"); this->daemon_manager.clear( Sequencer::DAEMON_SLICECAM ); // slicecamd not ready @@ -1364,7 +1364,7 @@ namespace Sequencer { * */ long Sequence::acam_init() { - std::string_view function("Sequencer::Sequence::acam_init"); + const std::string function("Sequencer::Sequence::acam_init"); this->daemon_manager.clear( Sequencer::DAEMON_ACAM ); // acamd not ready @@ -1424,7 +1424,7 @@ namespace Sequencer { * */ long Sequence::slicecam_shutdown() { - std::string_view function("Sequencer::Sequence::slicecam_shutdown"); + const std::string function("Sequencer::Sequence::slicecam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1487,7 +1487,7 @@ namespace Sequencer { * */ long Sequence::acam_shutdown() { - std::string_view function("Sequencer::Sequence::acam_shutdown"); + const std::string function("Sequencer::Sequence::acam_shutdown"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -1559,7 +1559,7 @@ namespace Sequencer { * */ long Sequence::calib_init() { - std::string_view function("Sequencer::Sequence::calib_init"); + const std::string function("Sequencer::Sequence::calib_init"); this->daemon_manager.clear( Sequencer::DAEMON_CALIB ); @@ -1633,7 +1633,7 @@ namespace Sequencer { * */ long Sequence::calib_shutdown() { - std::string_view function("Sequencer::Sequence::calib_shutdown"); + const std::string function("Sequencer::Sequence::calib_shutdown"); long error=NO_ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CALIB_SHUTDOWN ); @@ -1755,7 +1755,7 @@ namespace Sequencer { * */ long Sequence::tcs_shutdown() { - std::string_view function("Sequencer::Sequence::tcs_shutdown"); + const std::string function("Sequencer::Sequence::tcs_shutdown"); std::stringstream message; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_TCS_SHUTDOWN ); @@ -1797,7 +1797,7 @@ namespace Sequencer { * */ long Sequence::flexure_init() { - std::string_view function("Sequencer::Sequence::flexure_init"); + const std::string function("Sequencer::Sequence::flexure_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); @@ -1836,7 +1836,7 @@ namespace Sequencer { * */ long Sequence::flexure_shutdown() { - std::string_view function("Sequencer::Sequence::flexure_shutdown"); + const std::string function("Sequencer::Sequence::flexure_shutdown"); std::string reply; long error=NO_ERROR; @@ -1897,7 +1897,7 @@ namespace Sequencer { * */ long Sequence::focus_init() { - std::string_view function("Sequencer::Sequence::focus_init"); + const std::string function("Sequencer::Sequence::focus_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); @@ -1965,7 +1965,7 @@ namespace Sequencer { * */ long Sequence::focus_shutdown() { - std::string_view function("Sequencer::Sequence::focus_shutdown"); + const std::string function("Sequencer::Sequence::focus_shutdown"); std::string reply; long error=NO_ERROR; @@ -2026,7 +2026,7 @@ namespace Sequencer { * */ long Sequence::camera_init() { - std::string_view function("Sequencer::Sequence::camera_init"); + const std::string function("Sequencer::Sequence::camera_init"); ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2080,7 +2080,7 @@ namespace Sequencer { * */ long Sequence::camera_shutdown() { - std::string_view function("Sequencer::Sequence::camera_shutdown"); + const std::string function("Sequencer::Sequence::camera_shutdown"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CAMERA_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); @@ -2145,7 +2145,7 @@ namespace Sequencer { * */ long Sequence::move_to_target() { - std::string_view function("Sequencer::Sequence::move_to_target"); + const std::string function("Sequencer::Sequence::move_to_target"); std::stringstream message; long error=NO_ERROR; @@ -2299,7 +2299,7 @@ namespace Sequencer { * */ void Sequence::dothread_notify_tcs( Sequencer::Sequence &seq ) { - std::string_view function("Sequencer::Sequence::dothread_notify_tcs"); + const std::string function("Sequencer::Sequence::dothread_notify_tcs"); std::stringstream message; ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_NOTIFY_TCS ); @@ -2371,7 +2371,7 @@ namespace Sequencer { * */ long Sequence::focus_set() { - std::string_view function("Sequencer::Sequence::focus_set"); + const std::string function("Sequencer::Sequence::focus_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_SET ); @@ -2390,7 +2390,7 @@ namespace Sequencer { * */ long Sequence::flexure_set() { - std::string_view function("Sequencer::Sequence::flexure_set"); + const std::string function("Sequencer::Sequence::flexure_set"); ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_SET ); @@ -2409,7 +2409,7 @@ namespace Sequencer { * */ long Sequence::calib_set() { - std::string_view function("Sequencer::Sequence::calib_set"); + const std::string function("Sequencer::Sequence::calib_set"); std::stringstream message; ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIBRATOR_SET ); @@ -2494,7 +2494,7 @@ namespace Sequencer { * */ void Sequence::abort_process() { - std::string_view function("Sequencer::Sequence::abort_process"); + const std::string function("Sequencer::Sequence::abort_process"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); @@ -2529,7 +2529,7 @@ namespace Sequencer { * */ void Sequence::stop_exposure() { - std::string_view function("Sequencer::Sequence::stop_exposure"); + const std::string function("Sequencer::Sequence::stop_exposure"); ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); @@ -2577,7 +2577,7 @@ namespace Sequencer { * */ long Sequence::repeat_exposure() { - std::string_view function("Sequencer::Sequence::repeat_exposure"); + const std::string function("Sequencer::Sequence::repeat_exposure"); std::stringstream message; long error = NO_ERROR; @@ -2667,7 +2667,7 @@ namespace Sequencer { * */ long Sequence::trigger_exposure() { - std::string_view function("Sequencer::Sequence::trigger_exposure"); + const std::string function("Sequencer::Sequence::trigger_exposure"); std::stringstream message; std::string reply; long error=NO_ERROR; @@ -2721,7 +2721,7 @@ namespace Sequencer { * @return ERROR|NO_ERROR * */ - long Sequence::do_exposure(std::string_view caller) { + long Sequence::do_exposure(std::string caller) { this->wait_state_manager.set( Sequencer::SEQ_WAIT_EXPOSE ); @@ -2747,7 +2747,7 @@ namespace Sequencer { * */ void Sequence::modify_exptime( double exptime_in ) { - std::string_view function("Sequencer::Sequence::modify_exptime"); + const std::string function("Sequencer::Sequence::modify_exptime"); std::stringstream message; std::string reply=""; long error = NO_ERROR; @@ -2801,7 +2801,7 @@ namespace Sequencer { * */ long Sequence::startup() { - std::string_view function("Sequencer::Sequence::startup"); + const std::string function("Sequencer::Sequence::startup"); std::stringstream message; long error=NO_ERROR; @@ -3010,7 +3010,7 @@ namespace Sequencer { * */ long Sequence::shutdown() { - std::string_view function("Sequencer::Sequence::shutdown"); + const std::string function("Sequencer::Sequence::shutdown"); long error=ERROR; ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running @@ -3101,8 +3101,8 @@ namespace Sequencer { * @return ERROR or NO_ERROR * */ - long Sequence::parse_state( std::string_view whoami, std::string reply, bool &state ) { - std::string_view function("Sequencer::Sequence::parse_state"); + long Sequence::parse_state( std::string whoami, std::string reply, bool &state ) { + const std::string function("Sequencer::Sequence::parse_state"); std::stringstream message; // Tokenize the reply -- @@ -3159,7 +3159,7 @@ namespace Sequencer { * */ long Sequence::extract_tcs_value( std::string reply, int &value ) { - std::string_view function("Sequencer::Sequence::extract_tcs_value"); + const std::string function("Sequencer::Sequence::extract_tcs_value"); std::stringstream message; std::vector tokens; long error = ERROR; @@ -3247,7 +3247,7 @@ namespace Sequencer { * */ long Sequence::parse_tcs_generic( int value ) { - std::string_view function("Sequencer::Sequence::parse_tcs_generic"); + const std::string function("Sequencer::Sequence::parse_tcs_generic"); std::stringstream message; std::string tcsreply; std::vector tokens; @@ -3296,7 +3296,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args ) { - std::string_view function("Sequencer::Sequence::dotype"); + const std::string function("Sequencer::Sequence::dotype"); std::stringstream message; std::string dontcare; return this->dotype( args, dontcare ); @@ -3320,7 +3320,7 @@ namespace Sequencer { * */ long Sequence::dotype( std::string args, std::string &retstring ) { - std::string_view function("Sequencer::Sequence::dotype"); + const std::string function("Sequencer::Sequence::dotype"); std::stringstream message; long error = NO_ERROR; @@ -3367,7 +3367,7 @@ namespace Sequencer { return this->get_dome_position( false, domeazi, telazi ); } long Sequence::get_dome_position( bool poll, double &domeazi, double &telazi ) { - std::string_view function("Sequencer::Sequence::get_dome_position"); + const std::string function("Sequencer::Sequence::get_dome_position"); std::stringstream message; std::string tcsreply; @@ -3429,7 +3429,7 @@ namespace Sequencer { return this->get_tcs_motion( false, state_out ); } long Sequence::get_tcs_motion( bool poll, std::string &state_out ) { - std::string_view function("Sequencer::Sequence::get_tcs_motion"); + const std::string function("Sequencer::Sequence::get_tcs_motion"); std::stringstream message; std::string tcsreply; @@ -3471,7 +3471,7 @@ namespace Sequencer { return this->get_tcs_coords_type( TCSD_WEATHER_COORDS, ra_h, dec_d ); } long Sequence::get_tcs_coords_type( std::string cmd, double &ra_h, double &dec_d ) { - std::string_view function("Sequencer::Sequence::get_tcs_coords"); + const std::string function("Sequencer::Sequence::get_tcs_coords"); std::stringstream message; std::string coordstring; @@ -3524,7 +3524,7 @@ namespace Sequencer { * */ long Sequence::get_tcs_cass( double &cass ) { - std::string_view function("Sequencer::Sequencer::get_tcs_cass"); + const std::string function("Sequencer::Sequencer::get_tcs_cass"); std::stringstream message; std::string tcsreply; @@ -3575,7 +3575,7 @@ namespace Sequencer { * */ long Sequence::target_offset() { - std::string_view function("Sequencer::Sequence::target_offset"); + const std::string function("Sequencer::Sequence::target_offset"); bool is_ra_zero = std::abs(this->target.offset_ra) < std::numeric_limits::epsilon(); bool is_dec_zero = std::abs(this->target.offset_dec) < std::numeric_limits::epsilon(); @@ -3659,7 +3659,7 @@ namespace Sequencer { * */ void Sequence::dothread_test_fpoffset() { - std::string_view function("Sequencer::Sequence::dothread_fpoffset"); + const std::string function("Sequencer::Sequence::dothread_fpoffset"); std::stringstream message; message.str(""); message << "calling fpoffsets.compute_offset() from thread: PyGILState=" << PyGILState_Check(); @@ -3694,7 +3694,7 @@ namespace Sequencer { } long Sequence::set_power_switch( PowerState reqstate, const std::string which, std::chrono::seconds delay ) { - std::string_view function("Sequencer::Sequence::set_power_switch"); + const std::string function("Sequencer::Sequence::set_power_switch"); long error=NO_ERROR; bool need_delay=false; @@ -3800,7 +3800,7 @@ namespace Sequencer { long Sequence::open_hardware( Common::DaemonClient &daemon, const std::string opencmd, const int opentimeout, bool &was_opened, bool forceopen ) { - std::string_view function("Sequencer::Sequence::open_hardware"); + const std::string function("Sequencer::Sequence::open_hardware"); const int maxattempts=3; ///< allow retries connecting to daemon bool isopen=false; std::string reply; @@ -3860,7 +3860,7 @@ namespace Sequencer { * */ long Sequence::connect_to_daemon( Common::DaemonClient &daemon ) { - std::string_view function("Sequencer::Sequence::connect_to_daemon"); + const std::string function("Sequencer::Sequence::connect_to_daemon"); // if not connected to the daemon then connect // @@ -3888,7 +3888,7 @@ namespace Sequencer { * */ long Sequence::daemon_restart(Common::DaemonClient &daemon) { - std::string_view function("Sequencer::Sequence::daemon_restart"); + const std::string function("Sequencer::Sequence::daemon_restart"); std::string command; // the daemon control script must have been specified in the config file @@ -3943,7 +3943,7 @@ namespace Sequencer { * */ long Sequence::test( std::string args, std::string &retstring ) { - std::string_view function("Sequencer::Sequence::test"); + const std::string function("Sequencer::Sequence::test"); std::stringstream message; std::vector tokens; long error = NO_ERROR; diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index fa5e7420..6492f7b1 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -357,7 +357,7 @@ namespace Sequencer { /** @brief safely runs function in a detached thread using lambda to catch exceptions */ - void safe_thread(long (Sequence::*method)(), std::string_view function) { + void safe_thread(long (Sequence::*method)(), std::string function) { std::thread([this, method, function]() { try { (this->*method)(); @@ -537,11 +537,11 @@ namespace Sequencer { // ---------- sequencer scripting and execution tools -------------------- // - long run(const Operation &op, std::string_view function); - long run_parallel(const std::vector &ops, std::string_view function); - long run_default_sequence(std::string_view caller); + long run(const Operation &op, std::string function); + long run_parallel(const std::vector &ops, std::string function); + long run_default_sequence(std::string caller); long run_sequence( const std::vector &groups, - std::string_view caller, + std::string caller, bool continue_on_error=false ); long run_script(const std::string &filename); ///< run user script @@ -614,7 +614,7 @@ namespace Sequencer { bool is_ready() { return this->ready_to_start; } ///< returns the ready_to_start state, set true only after nightly startup long parse_calibration_target(); - long parse_state( std::string_view whoami, std::string reply, bool &state ); ///< parse true|false state from reply string + long parse_state( std::string whoami, std::string reply, bool &state ); ///< parse true|false state from reply string void dothread_test_fpoffset(); ///< for testing, calls Python function from thread long test( std::string args, std::string &retstring ); ///< handles test commands long extract_tcs_value( std::string reply, int &value ); ///< extract value returned by the TCS via tcsd @@ -651,18 +651,18 @@ namespace Sequencer { // These are various jobs that are done in their own threads // long trigger_exposure(); ///< trigger and wait for exposure - long do_exposure(std::string_view caller); ///< wrapper performs and waits for science exposure + long do_exposure(std::string caller); ///< wrapper performs and waits for science exposure void abort_process(); ///< tries to abort everything void stop_exposure(); ///< stop exposure timer in progress long repeat_exposure(); ///< repeat the last exposure void modify_exptime( double exptime_in ); ///< modify exptime while exposure running void dothread_test(); - long wait_for_ontarget(std::string_view caller); ///< wait for TCS Operator - long wait_for_user(std::string_view caller); ///< wait for the user or cancel - long wait_for_exposure(std::string_view caller); ///< wait for exposure completion or cancel - long wait_for_readout(std::string_view caller); ///< wait for readout completion or cancel - long wait_for_canexpose(std::string_view caller); ///< wait for camera can_expose + long wait_for_ontarget(std::string caller); ///< wait for TCS Operator + long wait_for_user(std::string caller); ///< wait for the user or cancel + long wait_for_exposure(std::string caller); ///< wait for exposure completion or cancel + long wait_for_readout(std::string caller); ///< wait for readout completion or cancel + long wait_for_canexpose(std::string caller); ///< wait for camera can_expose void sequence_start(std::string obsid_in=""); ///< main sequence start thread. optional obsid_in for single target obs long calib_set(); ///< sets calib according to target entry params @@ -678,7 +678,7 @@ namespace Sequencer { */ long do_acam_acquire(); long do_slicecam_fineacquire(); - long do_target_acquisition(std::string_view caller); + long do_target_acquisition(std::string caller); long do_target_virtualslit(VirtualSlitMode mode); diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 695c0423..7da563c8 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -16,7 +16,7 @@ namespace Sequencer { * */ long Sequence::do_acam_acquire() { - std::string_view function("Sequencer::Sequence::do_acam_acquire"); + const std::string function("Sequencer::Sequence::do_acam_acquire"); std::string reply; ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); @@ -75,7 +75,7 @@ namespace Sequencer { * */ long Sequence::do_slicecam_fineacquire() { - std::string_view function("Sequencer::Sequence::do_slicecam_fineacquire"); + const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); @@ -125,7 +125,7 @@ namespace Sequencer { * @return NO_ERROR | ABORT * */ - long Sequence::do_target_acquisition(std::string_view caller) { + long Sequence::do_target_acquisition(std::string caller) { if (this->target.iscal) return NO_ERROR; diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp index 078c1046..711151fc 100644 --- a/sequencerd/sequence_wait.cpp +++ b/sequencerd/sequence_wait.cpp @@ -16,7 +16,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_ontarget(std::string_view caller) { + long Sequence::wait_for_ontarget(std::string caller) { // waiting for TCS Operator input (or cancel) { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); @@ -52,7 +52,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_user(std::string_view caller) { + long Sequence::wait_for_user(std::string caller) { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); @@ -87,7 +87,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_exposure(std::string_view caller) { + long Sequence::wait_for_exposure(std::string caller) { logwrite(caller, "waiting for exposure"); while (!this->cancel_flag.load() && wait_state_manager.is_set(Sequencer::SEQ_WAIT_EXPOSE)) { @@ -113,7 +113,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_readout(std::string_view caller) { + long Sequence::wait_for_readout(std::string caller) { // don't have to wait for readout when using frame transfer if ( this->is_science_frame_transfer ) return NO_ERROR; @@ -144,7 +144,7 @@ namespace Sequencer { * @return NO_ERROR on continue | ABORT on cancel * */ - long Sequence::wait_for_canexpose(std::string_view caller) { + long Sequence::wait_for_canexpose(std::string caller) { this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); diff --git a/slicecamd/guimanager.h b/slicecamd/guimanager.h index 2758fe41..d3856bd3 100644 --- a/slicecamd/guimanager.h +++ b/slicecamd/guimanager.h @@ -105,7 +105,7 @@ namespace Slicecam { * @details the script pushes the indicated file to the Guider GUI display * @param[in] filename fits file to send */ - void push_gui_image( std::string_view filename ) { + void push_gui_image( std::string filename ) { const std::string function("Slicecam::GUIManager::push_gui_image"); std::ostringstream cmd; cmd << push_image << " " diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 0854d1a2..0ca7625e 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -207,8 +207,14 @@ namespace Slicecam { inline std::string get_imagename() { return this->imagename; } inline std::string get_wcsname() { return this->wcsname; } - inline void set_imagename( std::string name_in ) { this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : name_in ); return; } - inline void set_wcsname( std::string name_in ) { this->wcsname = name_in; return; } + inline void set_imagename( std::string name_in ) { + this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : std::move(name_in) ); + return; + } + inline void set_wcsname( std::string name_in ) { + this->wcsname = std::move(name_in); + return; + } Slicecam::FitsInfo fitsinfo; diff --git a/slitd/slit_server.cpp b/slitd/slit_server.cpp index a24960cf..1c39d8f4 100644 --- a/slitd/slit_server.cpp +++ b/slitd/slit_server.cpp @@ -376,7 +376,7 @@ namespace Slit { } while (1) { - std::string_view message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) + std::string message = slit.interface.async.dequeue(); // get the latest message from the queue (blocks) retval = sock.Send(message); // transmit the message if (retval < 0) { std::stringstream errstm; diff --git a/tcsd/tcsd_client.cpp b/tcsd/tcsd_client.cpp index 4ea8b42b..c0598402 100644 --- a/tcsd/tcsd_client.cpp +++ b/tcsd/tcsd_client.cpp @@ -25,7 +25,7 @@ * @return ERROR | NO_ERROR | HELP * */ - long TcsDaemonClient::init( std::string_view which, std::string &retstring ) { + long TcsDaemonClient::init( std::string which, std::string &retstring ) { std::string function = "TcsDaemonClient::init"; std::stringstream message; std::string reply, tcsname; diff --git a/tcsd/tcsd_client.h b/tcsd/tcsd_client.h index 29d30ed6..679bce15 100644 --- a/tcsd/tcsd_client.h +++ b/tcsd/tcsd_client.h @@ -23,7 +23,7 @@ public: Common::DaemonClient client { "tcsd", '\n', '\n' }; - long init( std::string_view which, std::string &retstring ); + long init( std::string which, std::string &retstring ); long get_name( std::string &name, bool poll ); long get_name( std::string &name ); long poll_name( std::string &name ); diff --git a/utils/logentry.cpp b/utils/logentry.cpp index 6ab2ba79..b4acb65f 100644 --- a/utils/logentry.cpp +++ b/utils/logentry.cpp @@ -175,7 +175,7 @@ void close_log() { * log filestream isn't open. * */ -void logwrite( std::string_view function, std::string_view message ) { +void logwrite( std::string function, std::string message ) { std::ostringstream logmsg; std::lock_guard lock(loglock); // lock mutex to protect from multiple access diff --git a/utils/logentry.h b/utils/logentry.h index f296bcbf..a5c464e3 100644 --- a/utils/logentry.h +++ b/utils/logentry.h @@ -21,6 +21,6 @@ extern unsigned int nextday; /// number of seconds long init_log( std::string logpath, std::string name ); /// initialize the logging system long init_log( std::string logpath, std::string name, bool stderr_in ); /// initialize the logging system void close_log(); /// close the log file stream -void logwrite(std::string_view function, std::string_view message); /// create a time-stamped log entry "message" from "function" +void logwrite(std::string function, std::string message); /// create a time-stamped log entry "message" from "function" #endif diff --git a/utils/network.cpp b/utils/network.cpp index caf3d36e..6ad3e8eb 100644 --- a/utils/network.cpp +++ b/utils/network.cpp @@ -174,8 +174,8 @@ namespace Network { * @return 0 on success, -1 on error * */ - int UdpSocket::Send(std::string_view message) { - std::string_view function = "Network::UdpSocket::Send"; + int UdpSocket::Send(std::string message) { + std::string function = "Network::UdpSocket::Send"; std::ostringstream errstm; ssize_t nbytes; diff --git a/utils/network.h b/utils/network.h index 0fea2000..f2b5637a 100644 --- a/utils/network.h +++ b/utils/network.h @@ -168,7 +168,7 @@ namespace Network { std::string getgroup() { return this->group; }; ///< use to get group int Create(); ///< create a UDP multi-cast socket - int Send(std::string_view message); ///< transmit the message to the UDP socket + int Send(std::string message); ///< transmit the message to the UDP socket int Close(); ///< close the UDP socket connection int Listener(); ///< creates a UDP listener, returns a file descriptor ssize_t Receive( std::string &message ); ///< receive a UDP message from the Listener fd From a04b36e23ef50b2a493a5c3f3a33ddb7d7ac8f52 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 9 Apr 2026 16:26:20 -0700 Subject: [PATCH 11/37] applies new Operation model to startup and shutdown sequences --- sequencerd/sequence.cpp | 195 +++++++++++--------------------- sequencerd/sequence.h | 4 +- sequencerd/sequence_builder.cpp | 6 +- 3 files changed, 70 insertions(+), 135 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index da073068..eed8cf7c 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -29,17 +29,17 @@ namespace Sequencer { long Sequence::run( const Operation &op, std::string caller ) { long error=NO_ERROR; - logwrite(caller, "starting "+op.name); + logwrite(caller, "starting "+op.name()); try { error = op.func(); if (error != NO_ERROR) { - this->async.enqueue_and_log(caller, "ERROR in "+op.name); + this->async.enqueue_and_log(caller, "ERROR in "+op.name()); } } catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name+": "+e.what()); + logwrite(caller, "ERROR in "+op.name()+": "+e.what()); error = ERROR; } return error; @@ -74,10 +74,10 @@ namespace Sequencer { for (size_t i=0; i < futures.size(); ++i) { try { error |= futures[i].get(); - logwrite(caller, "completed "+ops[i].name); + logwrite(caller, "completed "+thread_names.at(ops[i].thr)); } catch (const std::exception &e) { - logwrite(caller, "ERROR in "+ops[i].name+": "+e.what()); + logwrite(caller, "ERROR in "+ops[i].name()+": "+e.what()); error |= ERROR; } } @@ -161,7 +161,7 @@ namespace Sequencer { if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); sequence.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); + { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; @@ -169,12 +169,12 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel sequence.push_back( { OperationType::PARALLEL, - { { "move_to_target", THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { "camera_set", THR_CAMERA_SET, [this]{ return camera_set(); } }, - { "focus_set", THR_FOCUS_SET, [this]{ return focus_set(); } }, - { "flexure_set", THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { "calib_set", THR_CALIB_SET, [this]{ return calib_set(); } }, - { "slit_set", THR_SLIT_SET, [this]{ return slit_set(this->target.iscal ? VSM_DATABASE + { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { THR_CAMERA_SET, [this]{ return camera_set(); } }, + { THR_FOCUS_SET, [this]{ return focus_set(); } }, + { THR_FLEXURE_SET, [this]{ return flexure_set(); } }, + { THR_CALIB_SET, [this]{ return calib_set(); } }, + { THR_SLIT_SET, [this]{ return slit_set(this->target.iscal ? VSM_DATABASE : VSM_ACQUIRE); } } } } ); } @@ -183,16 +183,16 @@ namespace Sequencer { if (this->target.pointmode != Acam::POINTMODE_ACAM) { sequence.push_back( { OperationType::SERIAL, - { { "target_acquire", THR_ACQUISITION, + { { THR_ACQUISITION, [this,caller]() { return this->do_target_acquisition(caller); } }, - { "target_offset", THR_MOVE_TO_TARGET, + { THR_MOVE_TO_TARGET, [this]() { return this->target_offset(); } }, - { "slit_set", THR_SLIT_SET, + { THR_SLIT_SET, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - { "expose", THR_EXPOSURE, + { THR_EXPOSURE, [this,caller]() { return this->do_exposure(caller); } } } } ); @@ -215,23 +215,31 @@ namespace Sequencer { long Sequence::run_script(const std::string &filename) { const std::string function("Sequencer::Sequence::run_script"); + // ---------- PARSE ------------------------------------------------------- + std::vector commands; if ( parse_script(filename, commands) != NO_ERROR ) { logwrite(function, "ERROR parsing '"+filename+"'"); return ERROR; } + // ---------- BUILD ------------------------------------------------------- + std::vector sequence; if ( build_sequence(commands, sequence) != NO_ERROR ) { logwrite(function, "ERROR building sequence from '"+filename+"'"); return ERROR; } + // ---------- VALIDATE ---------------------------------------------------- + if ( validate_sequence(sequence) != NO_ERROR ) { logwrite(function, "ERROR validating sequence from '"+filename+"'"); return ERROR; } + // ---------- RUN --------------------------------------------------------- + return run_sequence(sequence, function); } /***** Sequencer::Sequence::run_script *************************************/ @@ -324,10 +332,10 @@ namespace Sequencer { // group is a vector of Operations for (const auto &op : group.operations) { - if (op.name == "expose") { + if (op.name() == "expose") { } else - if (op.name == "slit_set") { + if (op.name() == "slit_set") { } } } @@ -2579,7 +2587,6 @@ namespace Sequencer { long Sequence::repeat_exposure() { const std::string function("Sequencer::Sequence::repeat_exposure"); std::stringstream message; - long error = NO_ERROR; // can only repeat when state is READY // @@ -2603,49 +2610,16 @@ namespace Sequencer { logwrite( function, targetstatus ); - // threads to start, pair their ThreadStatusBit with the function to call - std::vector>> worker_threads; - - worker_threads = { { THR_CAMERA_SET, std::bind(&Sequence::camera_set, this) }, -// { THR_SLIT_SET, std::bind(&Sequence::slit_set, this) } - }; - - // pair their ThreadStatusBit with their future - std::vector>> worker_futures; - - // start the threads - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } - - // wait for the threads to complete. these can be cancelled. - for ( auto &[thr, future] : worker_futures) { - try { - error |= future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); + std::vector sequence = { + { OperationType::SERIAL, { + { THR_SLIT_SET, + [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, + { THR_EXPOSURE, + [this,function]() { return this->do_exposure(function); } } } } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - return ERROR; - } - } - - if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( function, "NOTICE: cancelled repeat exposure" ); - return NO_ERROR; - } + }; - // Start the exposure in a thread... - // - auto start_exposure = std::async(std::launch::async, &Sequence::trigger_exposure, this); - try { - error |= start_exposure.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR repeat_exposure exception: "+std::string(e.what()) ); - return ERROR; - } - return NO_ERROR; + return run_sequence(sequence, function); } /***** Sequencer::Sequence::repeat_exposure *********************************/ @@ -2828,48 +2802,27 @@ namespace Sequencer { // Everything (except TCS) needs the power control to be running // so initialize the power control first. // - auto start_power = std::async(std::launch::async, &Sequence::power_init, this); - error = start_power.get(); + error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR starting power control. Will try to continue (but don't hold your breath)" ); + this->async.enqueue_and_log(function, "ERROR starting power control"); + return ERROR; } - // threads to start, pair their ThreadStatusBit with the function to call - // - std::vector>> worker_threads = { - { THR_CALIB_INIT, std::bind(&Sequence::calib_init, this) }, - { THR_CAMERA_INIT, std::bind(&Sequence::camera_init, this) }, - { THR_FLEXURE_INIT, std::bind(&Sequence::flexure_init, this) }, - { THR_FOCUS_INIT, std::bind(&Sequence::focus_init, this) }, - { THR_SLIT_INIT, std::bind(&Sequence::slit_init, this) }, - { THR_TCS_INIT, std::bind(&Sequence::tcs_init, this) } - }; - - std::vector>> worker_futures; - - // launch all of the worker threads listed in the vector + // run these in parallel // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } + error = run_parallel( { + { THR_CALIB_INIT, [this]{ return calib_init(); }, { } }, + { THR_CAMERA_INIT, [this]{ return camera_init(); }, { } }, + { THR_FLEXURE_INIT, [this]{ return flexure_init(); }, { } }, + { THR_FOCUS_INIT, [this]{ return focus_init(); }, { } }, + { THR_SLIT_INIT, [this]{ return slit_init(); }, { } }, + { THR_TCS_INIT, [this]{ return tcs_init(); }, { } } + }, function ); - // get() will block, waiting for the threads to complete - // - for ( auto &[thr, future] : worker_futures) { - try { - // wait for this worker to finish - if ( future.get() != NO_ERROR ) { - logwrite( function, "ERROR from "+Sequencer::thread_names.at(thr)); - error = ERROR; - } - else logwrite(function, Sequencer::thread_names.at(thr)+" success"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - error = ERROR; - break; - } + if ( error != NO_ERROR ) { + this->async.enqueue_and_log(function, "ERROR starting something"); // TODO need granularity here + return ERROR; } // Now the Andor cameras must be done individually, first slicecam, @@ -3037,48 +2990,28 @@ namespace Sequencer { // Everything (except TCS) needs the power control to be running // so make sure power control is initialized before continuing. // - auto start_power = std::async(std::launch::async, &Sequence::power_init, this); - if ( start_power.get() != NO_ERROR ) { + error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); + + if ( error != NO_ERROR ) { this->async.enqueue_and_log( function, "ERROR from power control. Will try to continue (but don't hold your breath)" ); } // container of shutdown threads to launch, // pair their ThreadStatusBit with the function to call // - std::vector>> worker_threads = { - { THR_ACAM_SHUTDOWN, std::bind(&Sequence::acam_shutdown, this) }, - { THR_CALIB_SHUTDOWN, std::bind(&Sequence::calib_shutdown, this) }, - { THR_CAMERA_SHUTDOWN, std::bind(&Sequence::camera_shutdown, this) }, - { THR_FLEXURE_SHUTDOWN, std::bind(&Sequence::flexure_shutdown, this) }, - { THR_FOCUS_SHUTDOWN, std::bind(&Sequence::focus_shutdown, this) }, - { THR_SLIT_SHUTDOWN, std::bind(&Sequence::slit_shutdown, this) }, - { THR_SLICECAM_SHUTDOWN, std::bind(&Sequence::slicecam_shutdown, this) }, - { THR_TCS_SHUTDOWN, std::bind(&Sequence::tcs_shutdown, this) } - }; - - std::vector>> worker_futures; - - // launch the shutdown threads - // - for ( const auto &[thr, func] : worker_threads ) { - worker_futures.emplace_back( thr, std::async(std::launch::async, func) ); - } + error = run_parallel( { + { THR_ACAM_SHUTDOWN, [this]{ return acam_shutdown(); }, { } }, + { THR_CALIB_SHUTDOWN, [this]{ return calib_shutdown(); }, { } }, + { THR_CAMERA_SHUTDOWN, [this]{ return camera_shutdown(); }, { } }, + { THR_FLEXURE_SHUTDOWN, [this]{ return flexure_shutdown(); }, { } }, + { THR_FOCUS_SHUTDOWN, [this]{ return focus_shutdown(); }, { } }, + { THR_SLIT_SHUTDOWN, [this]{ return slit_shutdown(); }, { } }, + { THR_SLICECAM_SHUTDOWN, [this]{ return slicecam_shutdown(); }, { } }, + { THR_TCS_SHUTDOWN, [this]{ return tcs_shutdown(); }, { } } + }, function ); - // wait for the threads to complete - // - for ( auto &[thr, future] : worker_futures) { - try { - error=future.get(); // wait for this worker to finish - logwrite( function, "NOTICE: worker "+Sequencer::thread_names.at(thr)+" completed"); - } - catch (const std::exception& e) { - logwrite( function, "ERROR: worker "+Sequencer::thread_names.at(thr)+" exception: "+std::string(e.what()) ); - error=ERROR; - } - } - - std::stringstream message; - if (error==NO_ERROR) { + std::ostringstream message; + if (error==NO_ERROR) { // TODO need granularity here message << "NOTICE: instrument is shut down"; } else { diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 6492f7b1..8fc3e929 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -335,10 +335,12 @@ namespace Sequencer { /** @brief sequencer operation contains name, status bit, function and params */ struct Operation { - std::string name; ThreadStatusBits thr; std::function func; OperationParams params; + std::string name() const { + return (thread_names.find(thr)==thread_names.end()?"":thread_names.at(thr)); + } }; /** @brief a group of operations stored in a vector with the operation type diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index cf1ba269..2eddc960 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -41,7 +41,7 @@ namespace Sequencer { if (command.name == "move_to_target") { group.operations.emplace_back( Operation { - "move_to_target", THR_MOVE_TO_TARGET, + THR_MOVE_TO_TARGET, [this,params=command.params]() { if (params.has("ra") && params.has("dec")) { this->target.ra_hms = params.get(std::string("ra"),std::string("")); @@ -56,7 +56,7 @@ namespace Sequencer { if (command.name == "slit_set") { group.operations.emplace_back( Operation { - "slit_set", THR_SLIT_SET, + THR_SLIT_SET, [this,params=command.params]() { size_t mode = params.get("mode", VSM_DATABASE); return slit_set(static_cast(mode)); @@ -69,7 +69,7 @@ namespace Sequencer { if (command.name == "expose") { group.operations.emplace_back( Operation { - "expose", THR_EXPOSURE, + THR_EXPOSURE, [this]() { return do_exposure("placeholder"); }, From 379a6a5998f8c995e37e9c6d8267c90f694cde45 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 10 Apr 2026 08:46:05 -0700 Subject: [PATCH 12/37] cleans up cancel_flag a bit --- sequencerd/sequence.cpp | 41 ++++++++++++++++++++--------------------- sequencerd/sequence.h | 16 ++++++++++++---- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index eed8cf7c..08f1a847 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -109,7 +109,7 @@ namespace Sequencer { logwrite(caller, "starting sequence"); for (const auto &group : sequence) { - if (this->cancel_flag.load()) return ABORT; + if (is_cancelled()) return ABORT; // PARALLEL Groups are executed in parallel threads // @@ -123,7 +123,7 @@ namespace Sequencer { // else { for (const auto &op : group.operations) { - if (this->cancel_flag.load()) return ABORT; + if (is_cancelled()) return ABORT; long ret = run(op, caller); error |= ret; @@ -855,7 +855,7 @@ namespace Sequencer { // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -939,7 +939,7 @@ namespace Sequencer { break; } - if (this->cancel_flag.load()) { + if (is_cancelled()) { this->async.enqueue_and_log(function, "NOTICE: sequence cancelled"); break; } @@ -2269,19 +2269,19 @@ namespace Sequencer { this->async.enqueue_and_log( function, "NOTICE: waiting for TCS operator to send \"ontarget\" signal" ); - while ( !this->cancel_flag.load() && !this->is_ontarget.load() ) { + while ( !is_cancelled() && !this->is_ontarget.load() ) { std::unique_lock lock(cv_mutex); - this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || this->cancel_flag.load() ); } ); + this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || is_cancelled() ); } ); } this->async.enqueue_and_log( function, "NOTICE: received " - +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) + +(is_cancelled() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); } // If waiting for TCS operator was cancelled then don't continue // - if ( this->cancel_flag.load() ) return NO_ERROR; + if ( is_cancelled() ) return NO_ERROR; this->is_ontarget.store(false); @@ -2442,7 +2442,7 @@ namespace Sequencer { << " cover=" << ( calinfo.calcover ? "open" : "close" ); logwrite( function, "calib: "+cmd.str() ); - if ( !this->cancel_flag.load() && + if ( !is_cancelled() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); @@ -2451,7 +2451,7 @@ namespace Sequencer { // set the internal calibration lamps // for ( const auto &[lamp,state] : calinfo.lamp ) { - if ( this->cancel_flag.load() ) break; + if ( is_cancelled() ) break; cmd.str(""); cmd << lamp << " " << (state?"on":"off"); message.str(""); message << "power " << cmd.str(); logwrite( function, message.str() ); @@ -2467,7 +2467,7 @@ namespace Sequencer { // // set the dome lamps // // // for ( const auto &[lamp,state] : calinfo.domelamp ) { -// if ( this->cancel_flag.load() ) break; +// if ( is_cancelled() ) break; // cmd.str(""); cmd << TCSD_NATIVE << " NPS " << lamp << " " << (state?1:0); // if ( this->tcsd.command( cmd.str() ) != NO_ERROR ) { // this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); @@ -2478,7 +2478,7 @@ namespace Sequencer { // set the lamp modulators // for ( const auto &[mod,state] : calinfo.lampmod ) { - if ( this->cancel_flag.load() ) break; + if ( is_cancelled() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); @@ -2486,7 +2486,7 @@ namespace Sequencer { } } - if ( this->cancel_flag.load() ) { + if ( is_cancelled() ) { this->async.enqueue_and_log( function, "NOTICE: abort may have left calib system partially set" ); } @@ -2506,7 +2506,7 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); - this->cancel_flag.store(false); + clear_cancel_flag(); // stop any exposure that may be in progress // @@ -2520,8 +2520,7 @@ namespace Sequencer { // set the cancel flag to stop any cancel-able tasks // - this->cancel_flag.store(true); - this->cv.notify_all(); + set_cancel_flag(); // drop into do-one to prevent auto increment to next target // @@ -2548,7 +2547,7 @@ namespace Sequencer { return; } - this->cancel_flag.store(false); + clear_cancel_flag(); // Send command to the camera to stop the exposure. // @@ -2601,7 +2600,7 @@ namespace Sequencer { // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -2795,7 +2794,7 @@ namespace Sequencer { // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -2979,7 +2978,7 @@ namespace Sequencer { // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); @@ -4495,7 +4494,7 @@ namespace Sequencer { // clear stop flags // - this->cancel_flag.store(false); + clear_cancel_flag(); this->is_ontarget.store(false); this->is_usercontinue.store(false); diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 8fc3e929..fb513657 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -332,14 +332,16 @@ namespace Sequencer { } }; - /** @brief sequencer operation contains name, status bit, function and params + /** @brief sequencer operation contains status bit, function and params */ struct Operation { ThreadStatusBits thr; std::function func; OperationParams params; - std::string name() const { - return (thread_names.find(thr)==thread_names.end()?"":thread_names.at(thr)); + const std::string &name() const { + static const std::string empty; + auto it = thread_names.find(thr); + return(it==thread_names.end() ? empty : it->second); } }; @@ -444,7 +446,13 @@ namespace Sequencer { this->is_usercontinue.store(false); } - inline void reset_cancel_flag() { this->cancel_flag.store(false); } + inline bool is_cancelled() const { return this->cancel_flag.load(std::memory_order_acquire); } + + inline void clear_cancel_flag() { this->cancel_flag.store(false, std::memory_order_release); } + inline void set_cancel_flag() { + this->cancel_flag.store(true, std::memory_order_release); + this->cv.notify_all(); + } std::map telemetry_providers; ///< map of port[daemon_name] for external telemetry providers From f10060d8bfd5edd9577695437d50230349f27614 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 10 Apr 2026 13:56:42 -0700 Subject: [PATCH 13/37] adds optional retry function to Operation struct and updates Sequencer::Sequence::run() to use this for retry on error, and uses this to tidy the startup() sequence --- sequencerd/CMakeLists.txt | 1 + sequencerd/sequence.cpp | 299 +++++++++++++---------------- sequencerd/sequence.h | 36 +++- sequencerd/sequence_operations.cpp | 166 ++++++++++++++++ 4 files changed, 331 insertions(+), 171 deletions(-) create mode 100644 sequencerd/sequence_operations.cpp diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index b5804c76..a365d772 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -40,6 +40,7 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequence_acquisition.cpp ${SEQUENCER_DIR}/sequence_wait.cpp ${SEQUENCER_DIR}/sequence_builder.cpp + ${SEQUENCER_DIR}/sequence_operations.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} ${PYTHON_DEV} diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 08f1a847..23a3516a 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -18,6 +18,42 @@ namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command + /***** Sequencer::Sequence::operation_sleep ********************************/ + /** + * @brief interruptable sleep for operations + * @details Use this if an Operation needs to sleep. Can be cancelled. + * @param[in] delay_ms delay in milliseconds + * + */ + void Sequence::operation_sleep(int delay_ms) { + std::unique_lock lock(cv_mutex); + cv.wait_for(lock, + std::chrono::milliseconds(delay_ms), + [this]() { return is_cancelled(); }); + } + /***** Sequencer::Sequence::operation_sleep ********************************/ + + + /***** Sequencer::Sequence::handle_operation_exception *********************/ + /** + * @brief logs exceptions thrown by operations + * @param[in] eptr exception pointer + * @param[in] name name of operation + * @param[in] caller calling function + * + */ + void Sequence::handle_operation_exception( std::exception_ptr eptr, + std::string name, std::string caller ) { + try { + if (eptr) std::rethrow_exception(eptr); + } + catch (const std::exception &e) { + logwrite(caller, "ERROR in "+name+": "+std::string(e.what())); + } + } + /***** Sequencer::Sequence::handle_operation_exception *********************/ + + /***** Sequencer::Sequence::run ********************************************/ /** * @brief executes a single operation @@ -29,20 +65,40 @@ namespace Sequencer { long Sequence::run( const Operation &op, std::string caller ) { long error=NO_ERROR; + int attempt=1; + logwrite(caller, "starting "+op.name()); - try { - error = op.func(); + while (!is_cancelled()) { - if (error != NO_ERROR) { - this->async.enqueue_and_log(caller, "ERROR in "+op.name()); + try { + error = op.func(); + if (error==NO_ERROR) return NO_ERROR; + if (error==ABORT || + is_cancelled()) return ABORT; + } + catch (...) { + error=ERROR; + handle_operation_exception( std::current_exception(), op.name(), caller ); + } + + if (attempt >= op.max_attempts) { + logwrite(caller, "ERROR "+op.name()+ + " failed after "+std::to_string(attempt)+ + " attempt(s)"); + return ERROR; + } + + ++attempt; + + if (op.on_retry) { + logwrite(caller, "retrying operation "+op.name()+ + " attempt "+std::to_string(attempt)); + if (!is_cancelled()) op.on_retry(); + if (!is_cancelled()) operation_sleep(op.retry_delay); } } - catch (const std::exception &e) { - logwrite(caller, "ERROR in "+op.name()+": "+e.what()); - error = ERROR; - } - return error; + return ( is_cancelled() ? ABORT : error ); } /***** Sequencer::Sequence::run ********************************************/ @@ -64,25 +120,43 @@ namespace Sequencer { // start a thread for each operation // for (const auto &op : ops) { - futures.emplace_back(std::async(std::launch::async, op.func)); + futures.emplace_back( std::async( std::launch::async, [this, op, &caller]() { + if (is_cancelled()) return ABORT; + return run(op, caller); + } ) ); } - long error = NO_ERROR; + bool is_error=false; + bool is_abort=false; - // wait for all threads, collect errors + // wait for all threads to complete before returning, + // logging each status as they arrive // for (size_t i=0; i < futures.size(); ++i) { + std::ostringstream oss; + long ret; try { - error |= futures[i].get(); - logwrite(caller, "completed "+thread_names.at(ops[i].thr)); + ret = futures[i].get(); + oss << ops[i].name(); + if (ret==ABORT) { is_abort=true; oss << " cancelled"; } + else + oss << " completed" << ( (ret==NO_ERROR) ? "" : " with error"); } catch (const std::exception &e) { - logwrite(caller, "ERROR in "+ops[i].name()+": "+e.what()); - error |= ERROR; + oss << " received exception: " << e.what(); + is_error=true; + } + catch (...) { + oss << " received unknown exception"; + is_error=true; } + logwrite(caller, oss.str()); } - return error; + if (is_abort) return ABORT; + if (is_error) return ERROR; + + return NO_ERROR; } /***** Sequencer::Sequence::run_parallel ***********************************/ @@ -102,8 +176,7 @@ namespace Sequencer { * */ long Sequence::run_sequence( const std::vector &sequence, - std::string caller, - bool continue_on_error ) { + std::string caller ) { long error = NO_ERROR; logwrite(caller, "starting sequence"); @@ -117,7 +190,8 @@ namespace Sequencer { long ret = run_parallel(group.operations, caller); error |= ret; - if (ret != NO_ERROR && !continue_on_error) return error; + if (ret != NO_ERROR && + group.on_error == OnError::STOP) return error; } // SERIAL Groups are executed one at a time // @@ -128,7 +202,8 @@ namespace Sequencer { long ret = run(op, caller); error |= ret; - if (ret != NO_ERROR && !continue_on_error) return error; + if (ret != NO_ERROR && + group.on_error == OnError::STOP) return error; } } } @@ -160,7 +235,7 @@ namespace Sequencer { // if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); - sequence.push_back( { OperationType::PARALLEL, + sequence.push_back( { OperationType::PARALLEL, OnError::STOP, { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { @@ -168,7 +243,7 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel - sequence.push_back( { OperationType::PARALLEL, + sequence.push_back( { OperationType::PARALLEL, OnError::STOP, { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, { THR_CAMERA_SET, [this]{ return camera_set(); } }, { THR_FOCUS_SET, [this]{ return focus_set(); } }, @@ -182,7 +257,7 @@ namespace Sequencer { // ---------- RUN THESE IN SERIES ---------------------- if (this->target.pointmode != Acam::POINTMODE_ACAM) { - sequence.push_back( { OperationType::SERIAL, + sequence.push_back( { OperationType::SERIAL, OnError::STOP, { { THR_ACQUISITION, [this,caller]() { return this->do_target_acquisition(caller); } }, @@ -359,9 +434,14 @@ namespace Sequencer { if (args.empty()) return ERROR; - // build a mini-sequence of one command in order to validate it - // - auto commands = { *parse_command(args) }; + // ----- build a mini-sequence of one command in order to validate it ----- + + auto parsed = parse_command(args); + if (!parsed) { + logwrite(function, "ERROR parsing '"+args+"'"); + return ERROR; + } + auto commands = { *parsed }; std::vector sequence; @@ -376,8 +456,8 @@ namespace Sequencer { Operation op = sequence[0].operations[0]; - // ---------- RUN THE COMMAND -------------------------- - // + // ---------- RUN THE COMMAND --------------------------------------------- + return run(op, function); } /***** Sequencer::Sequence::handle_cli_operation ***************************/ @@ -2610,7 +2690,7 @@ namespace Sequencer { logwrite( function, targetstatus ); std::vector sequence = { - { OperationType::SERIAL, { + { OperationType::SERIAL, OnError::STOP, { { THR_SLIT_SET, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, { THR_EXPOSURE, @@ -2776,7 +2856,6 @@ namespace Sequencer { long Sequence::startup() { const std::string function("Sequencer::Sequence::startup"); std::stringstream message; - long error=NO_ERROR; if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, Sequencer::SEQ_NOTREADY ) ) { message << "ERROR cannot perform system startup while " @@ -2798,147 +2877,33 @@ namespace Sequencer { this->is_ontarget.store(false); this->is_usercontinue.store(false); - // Everything (except TCS) needs the power control to be running - // so initialize the power control first. - // - error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); - - if ( error != NO_ERROR ) { - this->async.enqueue_and_log(function, "ERROR starting power control"); - return ERROR; - } - - // run these in parallel - // - error = run_parallel( { - { THR_CALIB_INIT, [this]{ return calib_init(); }, { } }, - { THR_CAMERA_INIT, [this]{ return camera_init(); }, { } }, - { THR_FLEXURE_INIT, [this]{ return flexure_init(); }, { } }, - { THR_FOCUS_INIT, [this]{ return focus_init(); }, { } }, - { THR_SLIT_INIT, [this]{ return slit_init(); }, { } }, - { THR_TCS_INIT, [this]{ return tcs_init(); }, { } } - }, function ); + Ops ops(this); - if ( error != NO_ERROR ) { - this->async.enqueue_and_log(function, "ERROR starting something"); // TODO need granularity here - return ERROR; - } + // ---------- DEFINE STARTUP SEQUENCE ------------------------------------- - // Now the Andor cameras must be done individually, first slicecam, - // then the acam. - // Sometimes the Andors lose connection with the driver and the only - // recovery seems to be power-cycling the Andor and restarting the - // daemon. Try up to maxattempts times if necessary. - // - const int maxattempts=3; - - // slicecam_init - { - long __error=NO_ERROR; // keep track of the error just for this scope - int attempt=1; - while (attempt <= maxattempts) { - try { - // launch slicecam_init async task and wait for result - std::async(std::launch::async, &Sequence::slicecam_init, this).get(); - logwrite(function, Sequencer::thread_names.at(THR_SLICECAM_INIT)+" success"); - break; + std::vector sequence = { + // stop on error in power_init because everything else needs that + { OperationType::SERIAL, OnError::STOP, + { ops.power_init() } + }, + // everything else can continue on error + { OperationType::PARALLEL, OnError::CONTINUE, + { ops.calib_init(), + ops.camera_init(), + ops.flexure_init(), + ops.focus_init(), + ops.slit_init(), + ops.tcs_init() } + }, + { OperationType::SERIAL, OnError::CONTINUE, + { ops.acam_init(), + ops.slicecam_init() } } - catch (const SlicecamException &e) { - logwrite( function, "ERROR slicecam_init exception: "+std::string(e.what()) ); - - // If there was an error with the SLICECAM cameras, turn them off, - // restart the slicecam daemon, then loop to try again. - if (attempt < maxattempts) { - if ( set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(5)) != NO_ERROR ) { - async.enqueue_and_log( function, "ERROR switching off slicecams" ); - __error=ERROR; - break; - } - logwrite(function, "slicecams powered off"); + }; - // restart slicecamd - __error=this->daemon_restart(this->slicecamd); + // ---------- RUN THE SEQUENCE -------------------------------------------- - logwrite(function, "retrying slicecam_init"); - ++attempt; - continue; - } - else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting slicecam" ); - __error=ERROR; - } - } - catch (const std::exception &e) { - logwrite( function, "ERROR slicecam_init exception: "+std::string(e.what()) ); - __error=ERROR; - break; - } - catch (...) { - logwrite(function, "ERROR unknown slicecam_init exception"); - __error=ERROR; - break; - } - } // end while - if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR slicecam not initialized" ); - error=ERROR; - } - } - - // acam_init - { - long __error=NO_ERROR; // keep track of the error just for this scope - int attempt=1; - while (attempt <= maxattempts) { - try { - // launch acam_init async task and wait for result - std::async(std::launch::async, &Sequence::acam_init, this).get(); - logwrite(function, Sequencer::thread_names.at(THR_ACAM_INIT)+" success"); - break; - } - catch (const AcamException &e) { - logwrite( function, "ERROR acam_init exception: "+std::string(e.what()) ); - - // If there was an error with the ACAM camera, turn it off, - // restart the acam daemon, then loop to try again. - if (e.code == ErrorCode::ERROR_ACAM_CAMERA) { - if (attempt < maxattempts) { - if ( set_power_switch(OFF, POWER_ACAM_CAM, std::chrono::seconds(5)) != NO_ERROR ) { - async.enqueue_and_log( function, "ERROR switching off acam camera" ); - __error=ERROR; - } - logwrite(function, "acam camera powered off"); - - // restart acamd - __error=this->daemon_restart(this->acamd); - - logwrite(function, "retrying acam_init"); - attempt++; - continue; - } - else { - async.enqueue_and_log( function, "ERROR exceeded max attempts starting acam" ); - __error=ERROR; - } - } - break; - } - catch (const std::exception &e) { - logwrite( function, "ERROR acam_init exception: "+std::string(e.what()) ); - __error=ERROR; - break; - } - catch (...) { - logwrite(function, "ERROR unknown acam_init exception"); - __error=ERROR; - break; - } - } // end while - if (__error == ERROR) { - async.enqueue_and_log( function, "ERROR acam not initialized" ); - error=ERROR; - } - } + long error = run_sequence(sequence, function); // change state to READY if all daemons ready w/o error if ( error==NO_ERROR && daemon_manager.are_all_set() ) { diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index fb513657..869727c8 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -343,12 +343,23 @@ namespace Sequencer { auto it = thread_names.find(thr); return(it==thread_names.end() ? empty : it->second); } + int max_attempts=1; + int retry_delay=0; + std::function on_retry; + }; + + /** @brief what to do with an OperationGroup on error + */ + enum class OnError { + CONTINUE, + STOP }; /** @brief a group of operations stored in a vector with the operation type */ struct OperationGroup { OperationType type; + OnError on_error; std::vector operations; }; @@ -359,6 +370,23 @@ namespace Sequencer { OperationParams params; }; + class Ops { + private: + Sequence* seq; + public: + explicit Ops(Sequence* seq); + + Operation acam_init(); + Operation calib_init(); + Operation camera_init(); + Operation flexure_init(); + Operation focus_init(); + Operation power_init(); + Operation slicecam_init(); + Operation slit_init(); + Operation tcs_init(); + }; + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), std::string function) { @@ -547,12 +575,13 @@ namespace Sequencer { // ---------- sequencer scripting and execution tools -------------------- // - long run(const Operation &op, std::string function); + void operation_sleep(int delay_ms); + void handle_operation_exception(std::exception_ptr eptr, std::string name, std::string caller); + long run(const Operation &op, std::string caller); long run_parallel(const std::vector &ops, std::string function); long run_default_sequence(std::string caller); long run_sequence( const std::vector &groups, - std::string caller, - bool continue_on_error=false ); + std::string caller ); long run_script(const std::string &filename); ///< run user script long parse_script(const std::string &filename, @@ -691,7 +720,6 @@ namespace Sequencer { long do_target_acquisition(std::string caller); long do_target_virtualslit(VirtualSlitMode mode); - long acam_init(); ///< initializes connection to acamd long calib_init(); ///< initializes connection to calibd long camera_init(); ///< initializes connection to camerad diff --git a/sequencerd/sequence_operations.cpp b/sequencerd/sequence_operations.cpp new file mode 100644 index 00000000..a43608d7 --- /dev/null +++ b/sequencerd/sequence_operations.cpp @@ -0,0 +1,166 @@ +/** + * @file sequence_operations.cpp + * @brief implementation of operations + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + Sequence::Ops::Ops(Sequence* seq) : seq(seq) { } + + /***** Sequencer::Sequence::Ops::acam_init *********************************/ + /** + * @brief defines the acam_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::acam_init() { + Sequence::Operation op; + op.thr = THR_ACAM_INIT; + op.func = [this]() { return seq->acam_init(); }; + op.params = { }; + op.max_attempts = 3; + op.retry_delay = 2000; + op.on_retry = [this]() { + long error=NO_ERROR; + error |= seq->set_power_switch(OFF, POWER_ACAM_CAM, std::chrono::seconds(5)); + error |= seq->daemon_restart(seq->acamd); + return error; + }; + return op; + } + /***** Sequencer::Sequence::Ops::acam_init *********************************/ + + + /***** Sequencer::Sequence::Ops::calib_init ********************************/ + /** + * @brief defines the calib_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::calib_init() { + Sequence::Operation op; + op.thr = THR_CALIB_INIT; + op.func = [this]() { return seq->calib_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::calib_init ********************************/ + + + /***** Sequencer::Sequence::Ops::camera_init *******************************/ + /** + * @brief defines the camera_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::camera_init() { + Sequence::Operation op; + op.thr = THR_CAMERA_INIT; + op.func = [this]() { return seq->camera_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::camera_init *******************************/ + + + /***** Sequencer::Sequence::Ops::flexure_init ******************************/ + /** + * @brief defines the flexure_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::flexure_init() { + Sequence::Operation op; + op.thr = THR_FLEXURE_INIT; + op.func = [this]() { return seq->flexure_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::flexure_init ******************************/ + + + /***** Sequencer::Sequence::Ops::focus_init ********************************/ + /** + * @brief defines the focus_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::focus_init() { + Sequence::Operation op; + op.thr = THR_FOCUS_INIT; + op.func = [this]() { return seq->focus_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::focus_init ********************************/ + + + /***** Sequencer::Sequence::Ops::power_init ********************************/ + /** + * @brief defines the power_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::power_init() { + Sequence::Operation op; + op.thr = THR_POWER_INIT; + op.func = [this]() { return seq->power_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::power_init ********************************/ + + + /***** Sequencer::Sequence::Ops::slicecam_init *****************************/ + /** + * @brief defines the slicecam_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slicecam_init() { + Sequence::Operation op; + op.thr = THR_ACAM_INIT; + op.func = [this]() { return seq->slicecam_init(); }; + op.params = { }; + op.max_attempts = 3; + op.retry_delay = 2000; + op.on_retry = [this]() { + long error=NO_ERROR; + error |= seq->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(5)); + error |= seq->daemon_restart(seq->slicecamd); + return error; + }; + return op; + } + /***** Sequencer::Sequence::Ops::slicecam_init *****************************/ + + + /***** Sequencer::Sequence::Ops::slit_init *********************************/ + /** + * @brief defines the slit_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slit_init() { + Sequence::Operation op; + op.thr = THR_SLIT_INIT; + op.func = [this]() { return seq->slit_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::slit_init *********************************/ + + + /***** Sequencer::Sequence::Ops::tcs_init **********************************/ + /** + * @brief defines the tcs_init operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::tcs_init() { + Sequence::Operation op; + op.thr = THR_TCS_INIT; + op.func = [this]() { return seq->tcs_init(); }; + return op; + } + /***** Sequencer::Sequence::Ops::tcs_init **********************************/ + +} From edc934c05f53bdeb07cef7ba93090b951ea4e7ee Mon Sep 17 00:00:00 2001 From: David Hale Date: Sun, 12 Apr 2026 23:51:52 -0700 Subject: [PATCH 14/37] * adds some functionality to Sequence::parse_script() * implements a generic ThreadPool class so that the sequencer isn't continuously creating new threads (something I should have done a long time ago) -- not tested yet --- sequencerd/sequence.cpp | 41 ++++++++++++---- sequencerd/sequence.h | 7 +++ utils/thread_pool.h | 102 ++++++++++++++++++++++++++++++++++++++++ utils/utilities.h | 4 ++ 4 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 utils/thread_pool.h diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 23a3516a..5e7d4dc9 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -116,11 +116,13 @@ namespace Sequencer { std::string caller ) { std::vector> futures; + futures.reserve(ops.size()); - // start a thread for each operation + // start each operation in a thread from the pool // for (const auto &op : ops) { - futures.emplace_back( std::async( std::launch::async, [this, op, &caller]() { + if (is_cancelled()) return ABORT; + futures.emplace_back( pool.enqueue( [this, op, caller]() { if (is_cancelled()) return ABORT; return run(op, caller); } ) ); @@ -333,19 +335,46 @@ namespace Sequencer { */ long Sequence::parse_script(const std::string &filename, std::vector &commands_out) { + const std::string function("Sequencer::Sequence::parse_script"); + std::ifstream file(filename); if (!file.is_open()) { - logwrite("Sequencer::Sequence::parse_script", "ERROR opening '"+filename+"'"); + logwrite(function, "ERROR opening '"+filename+"'"); return ERROR; } + commands_out.clear(); std::string line; + int linenum=0; while (std::getline(file, line)) { + ++linenum; + + // trim off leading and trailing spaces + lrtrim(line); + + // skip empty lines + if (line.empty()) continue; + + // skip comment lines + if (line[0] == '#') continue; + + // strip inline comments, everything after '#' + auto pos = line.find('#'); + if (pos != std::string::npos) line = line.substr(0, pos); auto command = parse_command(line); - if (command) commands_out.push_back(*command); + if (!command) continue; + + command->linenum = linenum; + + commands_out.push_back(std::move(command.value())); + } + + if (commands_out.empty()) { + logwrite(function, "ERROR empty script"); + return ERROR; } return NO_ERROR; @@ -364,10 +393,6 @@ namespace Sequencer { */ std::optional Sequence::parse_command(std::string &args) { - // strip comments, everything after '#' - auto pos = args.find('#'); - if (pos != std::string::npos) args = args.substr(0, pos); - std::istringstream iss(args); std::string word; diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 869727c8..319641a3 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -34,6 +34,7 @@ #include "tcsd_commands.h" #include "sequencerd_commands.h" #include "message_keys.h" +#include "thread_pool.h" /*** Work-In-Progress * #include "command.h" * #include "command_rules.h" @@ -50,6 +51,8 @@ */ namespace Sequencer { + constexpr size_t NTHREADS = 10; ///< number of simultaneous operation threads + /** * @enum ErrorCodes * @brief @@ -299,6 +302,8 @@ namespace Sequencer { std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + ThreadPool pool; + /** @brief operation type can be SERIAL or PARALLEL */ enum class OperationType { @@ -368,6 +373,7 @@ namespace Sequencer { struct ParsedCommand { std::string name; OperationParams params; + int linenum{0}; }; class Ops { @@ -411,6 +417,7 @@ namespace Sequencer { is_science_frame_transfer(false), notify_tcs_next_target(false), arm_readout_flag(false), + pool(NTHREADS), acquisition_timeout(0), acquisition_max_retrys(-1), tcs_offsetrate_ra(45), diff --git a/utils/thread_pool.h b/utils/thread_pool.h new file mode 100644 index 00000000..e18eb89c --- /dev/null +++ b/utils/thread_pool.h @@ -0,0 +1,102 @@ +/** + * @file thread_pool.h + * @brief general purpose thread pool + * @author David Hale + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ThreadPool { + private: + std::mutex mtx; + std::condition_variable cv_worker; + std::condition_variable cv_backlog; + std::vector workers; + std::queue> tasks; + size_t max_queue_size; + std::atomic stop; + + void worker_loop() { + while (true) { + std::function task; + { + std::unique_lock lock(mtx); + // blocks until work to do, or stop + cv_worker.wait( lock, [this]() { return stop || !tasks.empty(); } ); + if (stop && tasks.empty()) return; + + task = std::move(tasks.front()); + tasks.pop(); + } + // notify backlogged tasks waiting for a free slot + cv_backlog.notify_one(); + // execute the task outside the lock + task(); + } + } + + public: + explicit ThreadPool(size_t nthreads, size_t max_tasks=100) + : max_queue_size(max_tasks), + stop(false) { + if (nthreads==0) throw std::invalid_argument("ThreadPool requires at least one thread"); + if (max_queue_size==0) throw std::invalid_argument("max_queue_size must be at least 1"); + workers.reserve(nthreads); + for (size_t n=0; n < nthreads; ++n) { + workers.emplace_back( [this]() { this->worker_loop(); } ); + } + } + + ~ThreadPool() { + { + std::unique_lock lock(mtx); + // signal workers to drain and exit + stop=true; + } + // wake-up all blocked workers + cv_worker.notify_all(); + cv_backlog.notify_all(); + + // wait for clean shutdown + for (auto &thr : workers) { if (thr.joinable()) thr.join(); } + } + + // not copyable + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator=(const ThreadPool&) = delete; + + // accepts any callable + arguments, returns future + template + auto enqueue(F&& f, Args&&... args) -> std::future> { + using return_type = std::invoke_result_t; + // wrap in a packaged task + auto task = std::make_shared>( + [f = std::forward(f), + args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(std::move(f), std::move(args)); + }); + std::future ret = task->get_future(); + { + std::unique_lock lock(mtx); + // block until the queue has space + cv_backlog.wait( lock, [this]() { return stop || tasks.size() < max_queue_size; } ); + if (stop) throw std::runtime_error("job added to stopped ThreadPool"); + tasks.emplace( [task]() { (*task)(); } ); + } + cv_worker.notify_one(); + return ret; + } +}; diff --git a/utils/utilities.h b/utils/utilities.h index 7203c578..f53f10f9 100644 --- a/utils/utilities.h +++ b/utils/utilities.h @@ -144,6 +144,10 @@ double angular_separation( double ra1, double dec1, double ra2, double dec2 ); static inline void rtrim(std::string &s) { /// trim off trailing whitespace from a string s.erase( std::find_if( s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); } ).base(), s.end() ); } +static inline void ltrim(std::string &s) { /// trim off leading white space + s.erase(std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); } ) ); +} +static inline void lrtrim(std::string &s) { rtrim(s); ltrim(s); } inline bool caseCompareChar( char a, char b ) { return ( std::toupper(a) == std::toupper(b) ); } From cbd901312722fc25bdf14349d6b009f11193e2c4 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 13 Apr 2026 11:36:15 -0700 Subject: [PATCH 15/37] adds unit test for ThreadPool to Sequencer::Sequence::test --- sequencerd/sequence.cpp | 42 +++++++++++++++++++++++++++++++++++++++++ utils/thread_pool.h | 30 +++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 5e7d4dc9..cf6b39ed 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -3917,6 +3917,7 @@ namespace Sequencer { retstring.append( " tablenames [ ? ]\n" ); retstring.append( " threadoffset [ ? ]\n" ); retstring.append( " update ? | { pending | complete | unassigned }\n" ); + retstring.append( " threadpool [ ? ]\n" ); return HELP; } else @@ -4774,6 +4775,47 @@ namespace Sequencer { logwrite( function, message.str() ); } } + else + + // --------------------------------------------------------- + // threadpool -- unit test for thread pool + // --------------------------------------------------------- + // + if ( testname == "threadpool" ) { + if ( tokens.size() > 1 && tokens[1] == "?" ) { + retstring = SEQUENCERD_TEST; + retstring.append( " threadpool\n" ); + retstring.append( " unit test ThreadPool object\n" ); + return HELP; + } + std::vector> futures; + futures.reserve(10); + + // add 15 2+ sec timers to the pool + for (int i=0; i<15; i++) { + std::ostringstream oss; + oss << "pooling timer " << i << ". tasks=" << pool.get_active() << " backlog=" << pool.get_backlog(); + logwrite(function, oss.str()); + futures.emplace_back( pool.enqueue( [this,i]() { + PreciseTimer timer; + timer.delay( 2000+(i*200) ); + } ) ); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + + // wait for timer completion + for (size_t i=0; i < futures.size(); ++i) { + try { + futures[i].get(); + std::ostringstream oss; + oss << "timer " << i << " complete. tasks=" << pool.get_active() << " backlog=" << pool.get_backlog(); + logwrite(function, oss.str()); + } + catch (const std::exception &e) { + logwrite(function, "received exception: "+std::string(e.what())); + } + } + } else { // ---------------------------------------------------- diff --git a/utils/thread_pool.h b/utils/thread_pool.h index e18eb89c..e01c0892 100644 --- a/utils/thread_pool.h +++ b/utils/thread_pool.h @@ -28,6 +28,14 @@ class ThreadPool { std::queue> tasks; size_t max_queue_size; std::atomic stop; + std::atomic active_tasks; + + void log_exception(std::exception_ptr eptr) { + try { if (eptr) std::rethrow_exception(eptr); } + catch (const std::exception &e) { + std::cerr << get_timestamp() << "(ThreadPool::worker_loop) exception: " << e.what(); + } + } void worker_loop() { while (true) { @@ -40,18 +48,25 @@ class ThreadPool { task = std::move(tasks.front()); tasks.pop(); + active_tasks++; } // notify backlogged tasks waiting for a free slot cv_backlog.notify_one(); // execute the task outside the lock - task(); + try { task(); } + catch (...) { + log_exception(std::current_exception()); + } + active_tasks--; } } public: explicit ThreadPool(size_t nthreads, size_t max_tasks=100) : max_queue_size(max_tasks), - stop(false) { + stop(false), + active_tasks(0) + { if (nthreads==0) throw std::invalid_argument("ThreadPool requires at least one thread"); if (max_queue_size==0) throw std::invalid_argument("max_queue_size must be at least 1"); workers.reserve(nthreads); @@ -78,6 +93,17 @@ class ThreadPool { ThreadPool(const ThreadPool&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; + // return number of active tasks + size_t get_active() const { return active_tasks.load(std::memory_order_relaxed); } + + // return number of backlogged tasks + size_t get_backlog() { + std::lock_guard lock(mtx); + size_t backlog = tasks.size(); + size_t avail = workers.size() - active_tasks; + return (backlog > avail) ? (backlog-avail) : 0; + } + // accepts any callable + arguments, returns future template auto enqueue(F&& f, Args&&... args) -> std::future> { From 054930dddf1c588dddfe149f2027a73f0abec292 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 13 Apr 2026 16:04:57 -0700 Subject: [PATCH 16/37] adds functionality to sequence_builder --- sequencerd/sequence.cpp | 10 +-- sequencerd/sequence_builder.cpp | 114 ++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 46 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index cf6b39ed..67617b69 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -190,10 +190,10 @@ namespace Sequencer { // if (group.type == OperationType::PARALLEL) { long ret = run_parallel(group.operations, caller); - error |= ret; + error |= ret; // accumulate any errors if (ret != NO_ERROR && - group.on_error == OnError::STOP) return error; + group.on_error == OnError::STOP) return ret; } // SERIAL Groups are executed one at a time // @@ -202,17 +202,17 @@ namespace Sequencer { if (is_cancelled()) return ABORT; long ret = run(op, caller); - error |= ret; + error |= ret; // accumulate any errors if (ret != NO_ERROR && - group.on_error == OnError::STOP) return error; + group.on_error == OnError::STOP) return ret; } } } logwrite(caller, "sequence complete"); - return error; + return (error==NO_ERROR) ? NO_ERROR : ERROR; } /***** Sequencer::Sequence::run_sequence ***********************************/ diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 2eddc960..7eeab2a2 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -19,70 +19,102 @@ namespace Sequencer { */ long Sequence::build_sequence(const std::vector &commands, std::vector &sequence_out) { - OperationGroup group; + const std::string function("Sequencer::Sequence::build_sequence"); - group.type = OperationType::SERIAL; // default is serial + // local failure-reporting wrapper + auto fail = [&](const ParsedCommand &cmd, const std::string &msg) { + logwrite(function, "ERROR line "+std::to_string(cmd.linenum)+": "+msg); + return ERROR; + }; + + // group of operations currently being processed + std::optional current_group; + + sequence_out.clear(); for (const auto &command : commands) { - if (command.name == "begin_parallel") { - if (!group.operations.empty()) sequence_out.push_back(group); - group = { OperationType::PARALLEL, {} }; + const std::string &name = command.name; + + if (name == "on_error") { + if (!current_group) return fail(command, "'on_error' out of a group"); + if (!command.params.has("action")) return fail(command, "on_error requires action=stop|continue"); + + const std::string val = command.params.get("action",""); + + if (val == "stop") current_group->on_error = OnError::STOP; + else + if (val == "continue") current_group->on_error = OnError::CONTINUE; + else { + return fail(command, "on_error requires action=stop|continue"); + } + } + + if (name == "parallel:" || name == "serial:") { + + if (current_group) return fail(command, "group already open"); + + // create a new group + OperationGroup new_group; + new_group.on_error = OnError::STOP; + new_group.type = (name=="parallel:") ? OperationType::PARALLEL + : OperationType::SERIAL; + current_group = std::move(new_group); continue; } - else - if (command.name == "end_parallel") { - sequence_out.push_back(group); - group = { OperationType::SERIAL, {} }; + if (name == "end") { + if (!current_group) return fail(command, "unexpected 'end' outside group"); + + // end of group, add it to the sequence now + sequence_out.push_back(std::move(*current_group)); + + current_group.reset(); continue; } - else - if (command.name == "move_to_target") { - group.operations.emplace_back( Operation { - THR_MOVE_TO_TARGET, - [this,params=command.params]() { - if (params.has("ra") && params.has("dec")) { - this->target.ra_hms = params.get(std::string("ra"),std::string("")); - this->target.dec_dms = params.get(std::string("dec"),std::string("")); - } - return move_to_target(); - }, - command.params - }); + if (!current_group) return fail(command, "command outside group"); + + Operation &op = current_group->operations.emplace_back(); + + if (name == "move_to_target") { + op.thr = THR_MOVE_TO_TARGET; + op.func = [this,params=command.params]() { + if (params.has("ra") && params.has("dec")) { + this->target.ra_hms = params.get(std::string("ra"),std::string("")); + this->target.dec_dms = params.get(std::string("dec"),std::string("")); + } + return move_to_target(); + }; + op.params = command.params; } else - if (command.name == "slit_set") { - group.operations.emplace_back( Operation { - THR_SLIT_SET, - [this,params=command.params]() { - size_t mode = params.get("mode", VSM_DATABASE); - return slit_set(static_cast(mode)); - }, - command.params - }); + if (name == "slit_set") { + op.thr = THR_SLIT_SET; + op.func = [this,params=command.params]() { + size_t mode = params.get("mode", VSM_DATABASE); + return slit_set(static_cast(mode)); + }; + op.params = command.params; } - else - if (command.name == "expose") { - group.operations.emplace_back( Operation { - THR_EXPOSURE, - [this]() { - return do_exposure("placeholder"); - }, - {} - }); + if (name == "expose") { + op.thr = THR_EXPOSURE; + op.func = [this]() { + return do_exposure("placeholder"); + }; + op.params = command.params; } else { this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", "ERROR unknown command '"+command.name+"'"); + continue; } + current_group->operations.emplace_back(std::move(op)); } - if (!group.operations.empty()) sequence_out.push_back(group); return NO_ERROR; } From cf6ae4b7df6bc5f35ea4736afa5536e1c088e72f Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 17 Apr 2026 18:26:51 -0700 Subject: [PATCH 17/37] replaces if/else in build_sequence with an operation builder map (just a start, needs to be completed) --- sequencerd/CMakeLists.txt | 1 + sequencerd/operation_builders.cpp | 79 +++++++++++++++++++++++++++++++ sequencerd/sequence.h | 6 +++ sequencerd/sequence_builder.cpp | 72 +++++++++++++++++++++------- 4 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 sequencerd/operation_builders.cpp diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index a365d772..7e50812c 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -40,6 +40,7 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequence_acquisition.cpp ${SEQUENCER_DIR}/sequence_wait.cpp ${SEQUENCER_DIR}/sequence_builder.cpp + ${SEQUENCER_DIR}/operation_builders.cpp ${SEQUENCER_DIR}/sequence_operations.cpp ${SEQUENCER_DIR}/sequence.cpp ${MYSQL_INCLUDES} diff --git a/sequencerd/operation_builders.cpp b/sequencerd/operation_builders.cpp new file mode 100644 index 00000000..d5c09880 --- /dev/null +++ b/sequencerd/operation_builders.cpp @@ -0,0 +1,79 @@ +/** + * @file operation_builders.cpp + * @brief implementation for building operations from command names + * @author David Hale + * + */ + +#include "sequence.h" + +namespace Sequencer { + + /***** Sequencer::Sequence::init_operation_builders ************************/ + /** + * @brief initializes registration between name and operation + * @details Maps command names to function that populate an Operation + * from a ParsedCommand. + * + */ + void Sequence::init_operation_builders() { + + // ---------- FOCUS_SET -------------------------------------------------- + + op_builders["focus_set"] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_FOCUS_SET; + op.func = [this]() { + return focus_set(); + }; + op.params = cmd.params; + }; + + // ---------- MOVE_TO_TARGET --------------------------------------------- + + op_builders["move_to_target"] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_MOVE_TO_TARGET; + op.func = [this,params=cmd.params]() { + if (params.has("ra") && params.has("dec")) { + this->target.ra_hms = params.get(std::string("ra"),std::string("")); + this->target.dec_dms = params.get(std::string("dec"),std::string("")); + } + return move_to_target(); + }; + op.params = cmd.params; + }; + + // ---------- SLIT_SET --------------------------------------------------- + + op_builders["slit_set"] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_SLIT_SET; + op.func = [this,params=cmd.params]() { + size_t mode = params.get("mode", VSM_DATABASE); + return slit_set(static_cast(mode)); + }; + op.params = cmd.params; + }; + + // ---------- EXPOSE ----------------------------------------------------- + + op_builders["expose"] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_EXPOSURE; + op.func = [this]() { + return do_exposure("operation_builder"); + }; + op.params = cmd.params; + }; + + // ---------- FOCUS_SET -------------------------------------------------- + + op_builders["focus_set"] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_FOCUS_SET; + op.func = [this]() { + return focus_set(); + }; + op.params = cmd.params; + }; + + } + /***** Sequencer::Sequence::init_operation_builders ************************/ + +} diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 319641a3..b25608f5 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -393,6 +393,12 @@ namespace Sequencer { Operation tcs_init(); }; + using OperationBuilder = std::function; + + std::unordered_map op_builders; + + void init_operation_builders(); + /** @brief safely runs function in a detached thread using lambda to catch exceptions */ void safe_thread(long (Sequence::*method)(), std::string function) { diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 7eeab2a2..1d4b143c 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -12,8 +12,15 @@ namespace Sequencer { /***** Sequencer::Sequence::build_sequence *********************************/ /** * @brief build a sequence from parsed commands + * @details This accepts a vector of ParsedCommands and returns by reference + * a sequence, which is one or more OperationGroups. Accepts special + * ".dsl" commands for the Sequencer Domain Specific Language. + * Start a group with "parallel:" or "serial:", finish group + * with "end". Include "on_error stop|continue" to set behavior + * of that group when an error occurs in an operation in that + * group. * @param[in] commands vector of ParsedCommands - * @param[out] sequence_out the operation group to execute + * @param[out] sequence_out vector of Operation Groups (sequence) to execute * @return ERROR|NO_ERROR|ABORT * */ @@ -27,29 +34,22 @@ namespace Sequencer { return ERROR; }; - // group of operations currently being processed + // create temporay group of operations currently being processed std::optional current_group; + // empty the sequence sequence_out.clear(); + // loop through vector of commands + // for (const auto &command : commands) { const std::string &name = command.name; - if (name == "on_error") { - if (!current_group) return fail(command, "'on_error' out of a group"); - if (!command.params.has("action")) return fail(command, "on_error requires action=stop|continue"); - - const std::string val = command.params.get("action",""); - - if (val == "stop") current_group->on_error = OnError::STOP; - else - if (val == "continue") current_group->on_error = OnError::CONTINUE; - else { - return fail(command, "on_error requires action=stop|continue"); - } - } + // ---------- SPECIAL COMMAND { parallel: | serial: } ------------------ + // start a group + // if (name == "parallel:" || name == "serial:") { if (current_group) return fail(command, "group already open"); @@ -63,18 +63,47 @@ namespace Sequencer { continue; } + // ---------- SPECIAL COMMAND { end } ---------------------------------- + + // finish a group + // if (name == "end") { if (!current_group) return fail(command, "unexpected 'end' outside group"); - // end of group, add it to the sequence now + // add it to the sequence now sequence_out.push_back(std::move(*current_group)); current_group.reset(); continue; } + // ---------- SPECIAL COMMAND { on_error } ----------------------------- + + // set how does the group behave on error + // + if (name == "on_error") { + // must be in a group for on_error to mean anything + if (!current_group) return fail(command, "'on_error' out of a group"); + if (!command.params.has("action")) return fail(command, "on_error requires action=stop|continue"); + + const std::string val = command.params.get("action",""); + + if (val == "stop") current_group->on_error = OnError::STOP; + else + if (val == "continue") current_group->on_error = OnError::CONTINUE; + else { + return fail(command, "on_error requires action=stop|continue"); + } + continue; + } + + // anything other than { parallel | serial | end | on_error } is a + // command to parse, so a group must have been started first. + // if (!current_group) return fail(command, "command outside group"); + // ---------- MAKE OPERATION FOR GIVEN COMMAND ------------------------- + Operation &op = current_group->operations.emplace_back(); if (name == "move_to_target") { @@ -102,8 +131,17 @@ namespace Sequencer { if (name == "expose") { op.thr = THR_EXPOSURE; + op.func = [this,function]() { + return do_exposure(function); + }; + op.params = command.params; + } + else + + if (name == "focus_set") { + op.thr = THR_FOCUS_SET; op.func = [this]() { - return do_exposure("placeholder"); + return focus_set(); }; op.params = command.params; } From 866c324333fb6db2cf8bb8f24606abe1f17f47cd Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 20 Apr 2026 15:35:51 -0700 Subject: [PATCH 18/37] expand sequencerd DSL ops and validation * adds subsystem command coverage in sequencerd_command and operation_builders * write Ops factories and sequence build/parse flow for daemon-backed operations * preserve positional args for passthrough operations * adds functionality to validate_sequence with safety checks for parallel/serial * adds engineering mode to relax checks --- common/sequencerd_commands.h | 8 + sequencerd/command_rules.h | 466 +++++++++++++++++++++++++++-- sequencerd/operation_builders.cpp | 270 ++++++++++++++--- sequencerd/sequence.cpp | 377 ++++++++++++++++++++++- sequencerd/sequence.h | 27 +- sequencerd/sequence_builder.cpp | 55 +--- sequencerd/sequence_operations.cpp | 194 +++++++++++- sequencerd/sequencer_server.cpp | 11 + 8 files changed, 1293 insertions(+), 115 deletions(-) diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index adce01ee..3a6504e2 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -10,6 +10,7 @@ const std::string SEQUENCERD_ABORT = "abort"; const std::string SEQUENCERD_CONFIG = "config"; const std::string SEQUENCERD_DOTYPE = "do"; +const std::string SEQUENCERD_ENGINEERING = "engineering"; const std::string SEQUENCERD_EXIT = "exit"; const std::string SEQUENCERD_GETONETARGET = "getone"; const std::string SEQUENCERD_GUIDE = "guide"; @@ -36,7 +37,10 @@ const std::string SEQUENCERD_ACAM = "acam"; const std::string SEQUENCERD_CALIB = "calib"; const std::string SEQUENCERD_CAMERA = "camera"; const std::string SEQUENCERD_FILTER = "filter"; +const std::string SEQUENCERD_FLEXURE = "flexure"; +const std::string SEQUENCERD_FOCUS = "focus"; const std::string SEQUENCERD_POWER = "power"; +const std::string SEQUENCERD_SLICECAM = "slicecam"; const std::string SEQUENCERD_SLIT = "slit"; const std::string SEQUENCERD_TCS = "tcs"; const std::vector SEQUENCERD_SYNTAX = { @@ -44,13 +48,17 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_CALIB+" ...", SEQUENCERD_CAMERA+" ...", SEQUENCERD_FILTER+" ...", + SEQUENCERD_FLEXURE+" ...", + SEQUENCERD_FOCUS+" ...", SEQUENCERD_POWER+" ...", + SEQUENCERD_SLICECAM+" ...", SEQUENCERD_SLIT+" ...", SEQUENCERD_TCS+" ...", "", SEQUENCERD_ABORT, SEQUENCERD_CONFIG, SEQUENCERD_DOTYPE+" [ one | all ]", + SEQUENCERD_ENGINEERING+" [ true | false ]", SEQUENCERD_EXIT, SEQUENCERD_GETONETARGET, SEQUENCERD_GUIDE, diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h index 5ec60136..2cb086a1 100644 --- a/sequencerd/command_rules.h +++ b/sequencerd/command_rules.h @@ -1,37 +1,461 @@ +/** + * @file command_rules.h + * @brief rules for validating command parameters and transitioning states + * @details Each daemon has an enum of states, min/max number of parameters + * and a list of from-to states for each command. The daemon must + * be in the "from" state to accept the command, which then takes it + * to the "to" state. + * @author David Hale + * + */ #pragma once - #include "command.h" - #include namespace Sequencer { + // ---------- CAMERAD -------------------------------------------------------- + enum class CameraState { - IDLE, - READY, - EXPOSING, - READING - }; + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + EXPOSING, ///< exposure in progress + READING, ///< readout in progress + PAUSED ///< exposure paused + }; const CommandSpecMap camerad_specs = { - { CAMERAD_ACTIVATE, {0, 4} }, - { CAMERAD_DEACTIVATE, {1, 4} }, - { CAMERAD_OPEN, {0, 1} }, - { CAMERAD_CLOSE, {0, 0} }, - { CAMERAD_EXPTIME, {0, 1} }, - { CAMERAD_EXPOSE, {0, 0} }, - { CAMERAD_READOUT, {0, 2} } + { CAMERAD_ABORT, {0, 0} }, + { CAMERAD_ACTIVATE, {0, 4} }, ///< 0 or more of: [ { U G R I } ] + { CAMERAD_BIN, {1, 2} }, ///< [ ] + { CAMERAD_BOI, {1,21} }, ///< | [full| [ [...]]]] + { CAMERAD_CLOSE, {0, 0} }, + { CAMERAD_DEACTIVATE, {1, 4} }, ///< 1 or more of: { U G R I } + { CAMERAD_EXPTIME, {0, 1} }, ///< [ ] + { CAMERAD_EXPOSE, {0, 0} }, + { CAMERAD_FITSNAME, {0, 1} }, + { CAMERAD_FITSNAMING, {0, 1} }, ///< [ time | number] + { CAMERAD_FRAMETRANSFER, {1, 2} }, ///< | | all [ yes | no ] + { CAMERAD_GEOMETRY, {1, 3} }, ///< | [ | ] + { CAMERAD_IMDIR, {0, 1} }, ///< [ ] + { CAMERAD_IMNUM, {0, 1} }, ///< [ ] + { CAMERAD_IMSIZE, {1, 5} }, ///< | [ ] + { CAMERAD_KEY, {1, 2} }, ///< list | KEYWORD=VALUE//COMMENT + { CAMERAD_LOAD, {0, 2} }, ///< [ ] + { CAMERAD_MODEXPTIME, {1, 1} }, ///< + { CAMERAD_OPEN, {0, 4} }, ///< 0 or more of: [ { 0 1 2 3 } ] + { CAMERAD_PAUSE, {0, 0} }, + { CAMERAD_READOUT, {0, 2} }, + { CAMERAD_RESUME, {0, 0} }, + { CAMERAD_SHUTTER, {0, 1} }, ///< [ enable | 1 | disable | 0 ] + { CAMERAD_STOP, {0, 0} } }; const std::vector> camerad_transitions = { - { CameraState::IDLE, CAMERAD_OPEN, CameraState::READY }, - { CameraState::READY, CAMERAD_ACTIVATE, CameraState::READY }, - { CameraState::READY, CAMERAD_DEACTIVATE, CameraState::READY }, - { CameraState::READY, CAMERAD_EXPTIME, CameraState::READY }, - { CameraState::READY, CAMERAD_EXPOSE, CameraState::EXPOSING }, - { CameraState::EXPOSING, CAMERAD_READOUT, CameraState::READING }, - { CameraState::READING, CAMERAD_READOUT, CameraState::READY } + { CameraState::EXPOSING, CAMERAD_ABORT, CameraState::READY }, + { CameraState::READING, CAMERAD_ABORT, CameraState::READY }, + { CameraState::PAUSED, CAMERAD_ABORT, CameraState::READY }, + { CameraState::READY, CAMERAD_ACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_BIN, CameraState::READY }, + { CameraState::READY, CAMERAD_BOI, CameraState::READY }, + { CameraState::READY, CAMERAD_CLOSE, CameraState::IDLE }, + { CameraState::READY, CAMERAD_DEACTIVATE, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPTIME, CameraState::READY }, + { CameraState::READY, CAMERAD_EXPOSE, CameraState::EXPOSING }, + { CameraState::READY, CAMERAD_FITSNAME, CameraState::READY }, + { CameraState::READY, CAMERAD_FITSNAMING, CameraState::READY }, + { CameraState::READY, CAMERAD_FRAMETRANSFER, CameraState::READY }, + { CameraState::READY, CAMERAD_GEOMETRY, CameraState::READY }, + { CameraState::READY, CAMERAD_IMDIR, CameraState::READY }, + { CameraState::READY, CAMERAD_IMNUM, CameraState::READY }, + { CameraState::READY, CAMERAD_IMSIZE, CameraState::READY }, + { CameraState::READY, CAMERAD_KEY, CameraState::READY }, + { CameraState::READY, CAMERAD_LOAD, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_MODEXPTIME, CameraState::EXPOSING }, + { CameraState::IDLE, CAMERAD_OPEN, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_PAUSE, CameraState::PAUSED }, + { CameraState::EXPOSING, CAMERAD_READOUT, CameraState::READING }, + { CameraState::READING, CAMERAD_READOUT, CameraState::READY }, + { CameraState::PAUSED, CAMERAD_RESUME, CameraState::EXPOSING }, + { CameraState::READY, CAMERAD_SHUTTER, CameraState::READY }, + { CameraState::EXPOSING, CAMERAD_STOP, CameraState::READY } + }; + + + // ---------- ACAMD ---------------------------------------------------------- + + enum class AcamState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + ACQUIRING, ///< target acquisition in progress + GUIDING ///< guiding in progress + }; + + const CommandSpecMap acamd_specs = { + { ACAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] + { ACAMD_CLOSE, {0, 0} }, + { ACAMD_INIT, {0, 0} }, + { ACAMD_ACQUIRE, {0, 4} }, ///< [ | target | guide | stop ] + { ACAMD_COORDS, {0, 3} }, ///< [ ] + { ACAMD_EXPTIME, {0, 1} }, ///< [ ] + { ACAMD_BIN, {0, 2} }, ///< [ ] + { ACAMD_GAIN, {0, 1} }, ///< [ ] + { ACAMD_TEMP, {0, 1} }, ///< [ ] + { ACAMD_FRAMEGRAB, {1, 3} }, ///< start | stop | one [ ] | status + { ACAMD_HOME, {0, 1} }, ///< [ ] + { ACAMD_FILTER, {0, 1} }, ///< [ | home | ishome ] + { ACAMD_COVER, {0, 1} }, ///< [ open | close | home | ishome ] + { ACAMD_ISOPEN, {0, 1} }, ///< [ motion | camera ] + { ACAMD_ISHOME, {0, 1} }, ///< [ ] + { ACAMD_ISACQUIRED, {0, 0} }, + { ACAMD_PUTONSLIT, {0, 2} }, ///< [ ] + { ACAMD_SOLVE, {0, 1} }, ///< [ ] [ = ... ] -- simplified + { ACAMD_SHUTDOWN, {0, 0} }, + { ACAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] + { ACAMD_OFFSETGOAL, {0, 2} }, ///< [ ] + { ACAMD_OFFSETPERIOD,{0, 1} } ///< [ ] + }; + + const std::vector> acamd_transitions = { + { AcamState::IDLE, ACAMD_OPEN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_CLOSE, AcamState::IDLE }, // TODO: verify + { AcamState::READY, ACAMD_INIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ACQUIRE, AcamState::ACQUIRING }, // TODO: verify + { AcamState::READY, ACAMD_COORDS, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_EXPTIME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_BIN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_GAIN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_TEMP, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_FRAMEGRAB, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_HOME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_FILTER, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_COVER, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_PUTONSLIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_SOLVE, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_OFFSETGOAL, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_OFFSETPERIOD,AcamState::READY }, // TODO: verify + { AcamState::ACQUIRING, ACAMD_ACQUIRE, AcamState::READY }, // TODO: verify (stop) + { AcamState::ACQUIRING, ACAMD_ISACQUIRED, AcamState::ACQUIRING }, // TODO: verify + { AcamState::READY, ACAMD_TCSINIT, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_SHUTDOWN, AcamState::IDLE }, // TODO: verify + { AcamState::READY, ACAMD_ISOPEN, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ISHOME, AcamState::READY }, // TODO: verify + { AcamState::READY, ACAMD_ISACQUIRED, AcamState::READY } // TODO: verify + }; + + + // ---------- CALIBD --------------------------------------------------------- + + enum class CalibState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING ///< actuator motion in progress + }; + + const CommandSpecMap calibd_specs = { + { CALIBD_OPEN, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_CLOSE, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_ISOPEN, {0, 1} }, ///< [ motion | lampmod ] + { CALIBD_GET, {0, 1} }, ///< [ ] + { CALIBD_HOME, {0, 0} }, + { CALIBD_ISHOME, {0, 0} }, + { CALIBD_SET, {1, 8} }, ///< =open|close ... (variable) + { CALIBD_LAMPMOD, {1, 4} }, ///< open | close | reconnect | default | ... + { CALIBD_NATIVE, {2, 2} } ///< + }; + + const std::vector> calibd_transitions = { + { CalibState::IDLE, CALIBD_OPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_CLOSE, CalibState::IDLE }, // TODO: verify + { CalibState::READY, CALIBD_ISOPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_GET, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_HOME, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_ISHOME, CalibState::MOVING }, // TODO: verify + { CalibState::READY, CALIBD_ISHOME, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_SET, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_SET, CalibState::READY }, // TODO: verify (completion) + { CalibState::READY, CALIBD_LAMPMOD, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_NATIVE, CalibState::READY } // TODO: verify + }; + + + // ---------- SLITD ---------------------------------------------------------- + + enum class SlitState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< slit motion in progress + HOMED ///< slit has been homed + }; + + const CommandSpecMap slitd_specs = { + { SLITD_OPEN, {0, 0} }, + { SLITD_CLOSE, {0, 0} }, + { SLITD_ISOPEN, {0, 0} }, + { SLITD_GET, {0, 1} }, ///< [ mm ] + { SLITD_HOME, {0, 0} }, + { SLITD_ISHOME, {0, 0} }, + { SLITD_OFFSET, {1, 1} }, ///< + { SLITD_SET, {1, 2} }, ///< [ ] + { SLITD_NATIVE, {2, 4} } ///< [ ] + }; + + const std::vector> slitd_transitions = { + { SlitState::IDLE, SLITD_OPEN, SlitState::READY }, // TODO: verify + { SlitState::READY, SLITD_CLOSE, SlitState::IDLE }, // TODO: verify + { SlitState::HOMED, SLITD_CLOSE, SlitState::IDLE }, // TODO: verify + { SlitState::READY, SLITD_ISOPEN, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_ISOPEN, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_GET, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_GET, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_HOME, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_ISHOME, SlitState::MOVING }, // TODO: verify (poll) + { SlitState::MOVING, SLITD_HOME, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::READY, SLITD_ISHOME, SlitState::READY }, // TODO: verify + { SlitState::HOMED, SLITD_ISHOME, SlitState::HOMED }, // TODO: verify + { SlitState::HOMED, SLITD_SET, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_SET, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::HOMED, SLITD_OFFSET, SlitState::MOVING }, // TODO: verify + { SlitState::MOVING, SLITD_OFFSET, SlitState::HOMED }, // TODO: verify (completion) + { SlitState::HOMED, SLITD_NATIVE, SlitState::HOMED }, // TODO: verify + { SlitState::READY, SLITD_NATIVE, SlitState::READY } // TODO: verify + }; + + + // ---------- TCSD ----------------------------------------------------------- + + enum class TcsState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + SLEWING, ///< telescope slew in progress + TRACKING, ///< telescope tracking on target + OFFSETTING ///< telescope offset in progress + }; + + const CommandSpecMap tcsd_specs = { + { TCSD_OPEN, {1, 1} }, ///< + { TCSD_CLOSE, {0, 0} }, + { TCSD_ISOPEN, {0, 0} }, + { TCSD_COORDS, {5, 7} }, ///< [] [""] + { TCSD_GET_COORDS, {0, 0} }, + { TCSD_GET_CASS, {0, 0} }, + { TCSD_GET_DOME, {0, 0} }, + { TCSD_GET_FOCUS, {0, 0} }, + { TCSD_GET_MOTION, {0, 0} }, + { TCSD_GET_OFFSETS, {0, 0} }, + { TCSD_GET_PA, {0, 0} }, + { TCSD_GET_NAME, {0, 0} }, + { TCSD_RINGGO, {1, 1} }, ///< + { TCSD_PTOFFSET, {2, 2} }, ///< + { TCSD_RETOFFSETS, {0, 0} }, + { TCSD_ZERO_OFFSETS, {0, 0} }, + { TCSD_OFFSETRATE, {0, 2} }, ///< [ ] + { TCSD_SET_FOCUS, {1, 1} }, ///< + { TCSD_NATIVE, {1, 1} }, ///< + { TCSD_LIST, {0, 0} }, + { TCSD_LLIST, {0, 0} }, + { TCSD_WEATHER_COORDS, {0, 0} } + }; + + const std::vector> tcsd_transitions = { + { TcsState::IDLE, TCSD_OPEN, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_CLOSE, TcsState::IDLE }, // TODO: verify + { TcsState::TRACKING, TCSD_CLOSE, TcsState::IDLE }, // TODO: verify + { TcsState::READY, TCSD_ISOPEN, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_ISOPEN, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_COORDS, TcsState::SLEWING }, // TODO: verify + { TcsState::TRACKING, TCSD_COORDS, TcsState::SLEWING }, // TODO: verify + { TcsState::SLEWING, TCSD_GET_MOTION, TcsState::SLEWING }, // TODO: verify (poll) + { TcsState::SLEWING, TCSD_GET_COORDS, TcsState::TRACKING }, // TODO: verify (completion) + { TcsState::READY, TCSD_GET_COORDS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_COORDS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_CASS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_CASS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_DOME, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_DOME, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_FOCUS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_FOCUS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_MOTION, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_GET_OFFSETS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_OFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_PA, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_PA, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_GET_NAME, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_GET_NAME, TcsState::TRACKING }, // TODO: verify + { TcsState::TRACKING, TCSD_RINGGO, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_RINGGO, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_PTOFFSET, TcsState::OFFSETTING }, // TODO: verify + { TcsState::OFFSETTING, TCSD_GET_MOTION, TcsState::OFFSETTING }, // TODO: verify (poll) + { TcsState::OFFSETTING, TCSD_RETOFFSETS, TcsState::TRACKING }, // TODO: verify (completion) + { TcsState::TRACKING, TCSD_RETOFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_ZERO_OFFSETS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_ZERO_OFFSETS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_OFFSETRATE, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_OFFSETRATE, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_SET_FOCUS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_SET_FOCUS, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_NATIVE, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_NATIVE, TcsState::TRACKING }, // TODO: verify + { TcsState::READY, TCSD_LIST, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_LLIST, TcsState::READY }, // TODO: verify + { TcsState::READY, TCSD_WEATHER_COORDS, TcsState::READY }, // TODO: verify + { TcsState::TRACKING, TCSD_WEATHER_COORDS, TcsState::TRACKING } // TODO: verify + }; + + + // ---------- FOCUSD --------------------------------------------------------- + + enum class FocusState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING ///< focus motion in progress + }; + + const CommandSpecMap focusd_specs = { + { FOCUSD_OPEN, {0, 0} }, + { FOCUSD_CLOSE, {0, 0} }, + { FOCUSD_ISOPEN, {0, 0} }, + { FOCUSD_GET, {1, 1} }, ///< + { FOCUSD_HOME, {0, 1} }, ///< [ ] + { FOCUSD_ISHOME, {0, 0} }, + { FOCUSD_SET, {2, 2} }, ///< { | nominal } + { FOCUSD_DEFAULTPOS, {0, 0} }, + { FOCUSD_NATIVE, {2, 2} }, ///< + { FOCUSD_TEST, {1, 4} } ///< ... + }; + + const std::vector> focusd_transitions = { + { FocusState::IDLE, FOCUSD_OPEN, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_CLOSE, FocusState::IDLE }, // TODO: verify + { FocusState::READY, FOCUSD_ISOPEN, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_GET, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_HOME, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_HOME, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_ISHOME, FocusState::READY }, // TODO: verify + { FocusState::MOVING, FOCUSD_ISHOME, FocusState::MOVING }, // TODO: verify (poll) + { FocusState::READY, FOCUSD_SET, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_SET, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_DEFAULTPOS, FocusState::MOVING }, // TODO: verify + { FocusState::MOVING, FOCUSD_DEFAULTPOS, FocusState::READY }, // TODO: verify (completion) + { FocusState::READY, FOCUSD_NATIVE, FocusState::READY }, // TODO: verify + { FocusState::READY, FOCUSD_TEST, FocusState::READY } // TODO: verify + }; + + + // ---------- FLEXURED ------------------------------------------------------- + + enum class FlexureState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING ///< flexure compensation motion in progress + }; + + const CommandSpecMap flexured_specs = { + { FLEXURED_OPEN, {0, 0} }, + { FLEXURED_CLOSE, {0, 0} }, + { FLEXURED_ISOPEN, {0, 0} }, + { FLEXURED_GET, {2, 2} }, ///< + { FLEXURED_SET, {3, 3} }, ///< + { FLEXURED_COMPENSATE, {0, 1} }, ///< [ dryrun ] + { FLEXURED_DEFAULTPOS, {0, 0} }, + { FLEXURED_NATIVE, {2, 2} }, ///< + { FLEXURED_TEST, {1, 4} } ///< ... + }; + + const std::vector> flexured_transitions = { + { FlexureState::IDLE, FLEXURED_OPEN, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_CLOSE, FlexureState::IDLE }, // TODO: verify + { FlexureState::READY, FLEXURED_ISOPEN, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_GET, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_SET, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_SET, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_COMPENSATE, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_COMPENSATE, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_DEFAULTPOS, FlexureState::MOVING }, // TODO: verify + { FlexureState::MOVING, FLEXURED_DEFAULTPOS, FlexureState::READY }, // TODO: verify (completion) + { FlexureState::READY, FLEXURED_NATIVE, FlexureState::READY }, // TODO: verify + { FlexureState::READY, FLEXURED_TEST, FlexureState::READY } // TODO: verify + }; + + + // ---------- POWERD --------------------------------------------------------- + + enum class PowerState { + IDLE, ///< not yet opened + READY ///< open and ready to accept commands + }; + + const CommandSpecMap powerd_specs = { + { POWERD_OPEN, {0, 0} }, + { POWERD_CLOSE, {0, 0} }, + { POWERD_ISOPEN, {0, 0} }, + { POWERD_STATUS, {0, 1} }, ///< [ ? ] + { POWERD_REOPEN, {0, 0} }, + { POWERD_LIST, {0, 0} } + // NOTE: plug switching commands ( ON|OFF|BOOT or ON|OFF|BOOT) + // do not have a fixed first-word command name and so cannot be registered here by name. + // TODO: decide how plug switching is exposed via SEQUENCERD_OP. + }; + + const std::vector> powerd_transitions = { + { PowerState::IDLE, POWERD_OPEN, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_CLOSE, PowerState::IDLE }, // TODO: verify + { PowerState::READY, POWERD_ISOPEN, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_STATUS, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_REOPEN, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_LIST, PowerState::READY } // TODO: verify + }; + + + // ---------- SLICECAMD ------------------------------------------------------ + + enum class SlicecamState { + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + ACQUIRING, ///< fine acquisition in progress + GUIDING ///< guiding in progress + }; + + const CommandSpecMap slicecamd_specs = { + { SLICECAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] + { SLICECAMD_CLOSE, {0, 0} }, + { SLICECAMD_ISOPEN, {0, 1} }, ///< [ motion | camera ] + { SLICECAMD_INIT, {0, 0} }, + { SLICECAMD_EXPTIME, {0, 1} }, ///< [ ] + { SLICECAMD_BIN, {0, 3} }, ///< [ ] + { SLICECAMD_GAIN, {0, 1} }, ///< [ ] + { SLICECAMD_TEMP, {0, 1} }, ///< [ ] + { SLICECAMD_FRAMEGRAB, {1, 3} }, ///< start | stop | one [ ] | status + { SLICECAMD_FINEACQUIRE, {1, 2} }, ///< status | stop | start { L | R } + { SLICECAMD_PUTONSLIT, {0, 4} }, ///< [ ] + { SLICECAMD_SAVEFRAMES, {0, 2} }, ///< [ ] + { SLICECAMD_SHUTTER, {1, 1} }, ///< open | close | auto + { SLICECAMD_SHUTDOWN, {0, 0} }, + { SLICECAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] + { SLICECAMD_IMFLIP, {0, 3} }, ///< [ ] + { SLICECAMD_IMROT, {0, 2} } ///< [ ] + }; + + const std::vector> slicecamd_transitions = { + { SlicecamState::IDLE, SLICECAMD_OPEN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_CLOSE, SlicecamState::IDLE }, // TODO: verify + { SlicecamState::READY, SLICECAMD_ISOPEN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_INIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_EXPTIME, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_BIN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_GAIN, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_TEMP, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_FRAMEGRAB, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_FINEACQUIRE, SlicecamState::ACQUIRING }, // TODO: verify + { SlicecamState::ACQUIRING, SLICECAMD_FINEACQUIRE, SlicecamState::READY }, // TODO: verify (stop) + { SlicecamState::READY, SLICECAMD_PUTONSLIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SAVEFRAMES, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SHUTTER, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_SHUTDOWN, SlicecamState::IDLE }, // TODO: verify + { SlicecamState::READY, SLICECAMD_TCSINIT, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_IMFLIP, SlicecamState::READY }, // TODO: verify + { SlicecamState::READY, SLICECAMD_IMROT, SlicecamState::READY } // TODO: verify }; } diff --git a/sequencerd/operation_builders.cpp b/sequencerd/operation_builders.cpp index d5c09880..38d32f1a 100644 --- a/sequencerd/operation_builders.cpp +++ b/sequencerd/operation_builders.cpp @@ -3,76 +3,274 @@ * @brief implementation for building operations from command names * @author David Hale * + * This file registers the mapping from a parsed sequencer command name to an + * OperationBuilder lambda. Builders fall into two categories: + * + * 1. Internal : sequencer-level orchestration (database-driven, may span + * multiple daemons). These delegate to Ops:: factory methods + * which wrap Sequence:: member functions. Parameters are + * carried as key=value pairs on cmd.params and are exposed to + * the member function via Sequence::current_op_params (set by + * Sequence::run() immediately before op.func() is invoked). + * + * 2. Passthrough : per-daemon validated passthrough (e.g. "camera exptime 30000" + * or "slit set 2.0 0.0"). These are built inline here (no Ops:: + * method) and forward the positional args (cmd.args) to the + * target daemon. The first arg is the daemon subcommand; the + * remainder are forwarded as-is in daemon native syntax. + * + * NOTE: Passthrough commands currently forward via Common::DaemonClient::send() as a + * PLACEHOLDER. This will be replaced by CommandClient::send() + * later, at which point per-command state-machine validation and + * arg-count checks will be performed at execution time. + * */ #include "sequence.h" +#include "sequencerd_commands.h" namespace Sequencer { + namespace { + + /***** Sequencer::daemon_passthrough *************************************/ + /** + * @brief TEMPORARY daemon passthrough helper + * @param[in] function calling function name for logging + * @param[in] daemon DaemonClient to forward to + * @param[in] args positional args from ParsedCommand (first is subcommand) + * @return ERROR|NO_ERROR + * @details Builds a daemon-native command string from args[0..N-1] and + * forwards via Common::DaemonClient::send(). Self-identifies in + * log as a placeholder so the pending replacement is unambiguous. + * + * TEMPORARY -- REMOVE ONCE CommandClient IS WIRED IN (Steps 2+3). + * Each Passthrough op_builder lambda will then call + * CommandClient::send() directly, performing per-command + * state-machine validation and arg-count checks at execution + * time, and this helper becomes unnecessary. + * + */ + long daemon_passthrough( const std::string &function, + Common::DaemonClient &daemon, + const std::vector &args ) { + if (args.empty()) { + logwrite(function, "ERROR no subcommand provided to "+daemon.name); + return ERROR; + } + std::string cmd_str = args[0]; + for (size_t i=1; i "+daemon.name+ + "): "+cmd_str); + long error = daemon.send(cmd_str, reply); + if (error != NO_ERROR) { + logwrite(function, "ERROR forwarding \""+cmd_str+"\" to "+daemon.name+": "+reply); + } + return error; + } + /***** Sequencer::daemon_passthrough *************************************/ + + } // anonymous namespace + + /***** Sequencer::Sequence::init_operation_builders ************************/ /** * @brief initializes registration between name and operation - * @details Maps command names to function that populate an Operation + * @details Maps command names to functions that populate an Operation * from a ParsedCommand. * */ void Sequence::init_operation_builders() { - // ---------- FOCUS_SET -------------------------------------------------- + // ---------- INTERNAL ------------------------------------------------------ - op_builders["focus_set"] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_FOCUS_SET; - op.func = [this]() { - return focus_set(); - }; + // sequencer-level orchestration (Ops factory methods) + // Command names come from the THR_ -> thread_names map in sequence.h. + // I probably should rename these from "thread" to something else (operation?) + + op_builders[thread_names.at(THR_ACAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.acam_init(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_CALIB_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.calib_init(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_CALIB_SET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.calib_set(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_CAMERA_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.camera_init(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_CAMERA_SET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.camera_set(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_expose(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_FLEXURE_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.flexure_init(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_FLEXURE_SET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.flexure_set(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_FOCUS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.focus_init(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_FOCUS_SET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.focus_set(); + op.params = cmd.params; + }; + + op_builders[thread_names.at(THR_MOVE_TO_TARGET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.move_to_target(); + op.params = cmd.params; // may carry optional ra= / dec= overrides + }; + + op_builders[thread_names.at(THR_POWER_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.power_init(); op.params = cmd.params; }; - // ---------- MOVE_TO_TARGET --------------------------------------------- + op_builders[thread_names.at(THR_REPEAT_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.repeat_exposure(); + op.params = cmd.params; + }; - op_builders["move_to_target"] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_MOVE_TO_TARGET; - op.func = [this,params=cmd.params]() { - if (params.has("ra") && params.has("dec")) { - this->target.ra_hms = params.get(std::string("ra"),std::string("")); - this->target.dec_dms = params.get(std::string("dec"),std::string("")); - } - return move_to_target(); - }; + op_builders[thread_names.at(THR_SHUTDOWN)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_shutdown(); op.params = cmd.params; }; - // ---------- SLIT_SET --------------------------------------------------- + op_builders[thread_names.at(THR_SLICECAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slicecam_init(); + op.params = cmd.params; + }; - op_builders["slit_set"] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_SLIT_SET; - op.func = [this,params=cmd.params]() { - size_t mode = params.get("mode", VSM_DATABASE); - return slit_set(static_cast(mode)); - }; + op_builders[thread_names.at(THR_SLIT_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slit_init(); op.params = cmd.params; }; - // ---------- EXPOSE ----------------------------------------------------- + op_builders[thread_names.at(THR_SLIT_SET)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slit_set(); + op.params = cmd.params; // may carry optional mode= override + }; - op_builders["expose"] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_EXPOSURE; - op.func = [this]() { - return do_exposure("operation_builder"); - }; + op_builders[thread_names.at(THR_STARTUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.do_startup(); op.params = cmd.params; }; - // ---------- FOCUS_SET -------------------------------------------------- + op_builders[thread_names.at(THR_TCS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.tcs_init(); + op.params = cmd.params; + }; - op_builders["focus_set"] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_FOCUS_SET; - op.func = [this]() { - return focus_set(); - }; + // target_offset shares THR_MOVE_TO_TARGET with move_to_target, so it needs a + // distinct CLI/DSL name. Use its Sequence:: member-function name "target_offset" + // (there is no separate entry in thread_names for it). + op_builders["target_offset"] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.target_offset(); op.params = cmd.params; }; + // ---------- PASSTHROUGH --------------------------------------------------- + + // per-daemon validated passthrough (inline, no Ops method). + // Command names come from SEQUENCERD_* constants in common/sequencerd_commands.h. + // PLACEHOLDER: forwards via Common::DaemonClient::send() until Steps 2+3 + // replace with CommandClient::send() (state-machine validated). + + op_builders[SEQUENCERD_ACAM] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_ACAM_INIT; // passthrough uses nearest daemon THR_ identity + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_ACAM+"]"); + return daemon_passthrough(function, this->acamd, args); + }; + }; + + op_builders[SEQUENCERD_CALIB] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_CALIB_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CALIB+"]"); + return daemon_passthrough(function, this->calibd, args); + }; + }; + + op_builders[SEQUENCERD_CAMERA] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_CAMERA_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CAMERA+"]"); + return daemon_passthrough(function, this->camerad, args); + }; + }; + + op_builders[SEQUENCERD_FLEXURE] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_FLEXURE_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FLEXURE+"]"); + return daemon_passthrough(function, this->flexured, args); + }; + }; + + op_builders[SEQUENCERD_FOCUS] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_FOCUS_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FOCUS+"]"); + return daemon_passthrough(function, this->focusd, args); + }; + }; + + op_builders[SEQUENCERD_POWER] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_POWER_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_POWER+"]"); + return daemon_passthrough(function, this->powerd, args); + }; + }; + + op_builders[SEQUENCERD_SLICECAM] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_SLICECAM_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLICECAM+"]"); + return daemon_passthrough(function, this->slicecamd, args); + }; + }; + + op_builders[SEQUENCERD_SLIT] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_SLIT_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLIT+"]"); + return daemon_passthrough(function, this->slitd, args); + }; + }; + + op_builders[SEQUENCERD_TCS] = [this](Operation &op, const ParsedCommand &cmd) { + op.thr = THR_TCS_INIT; + op.func = [this, args=cmd.args]() { + const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_TCS+"]"); + return daemon_passthrough(function, this->tcsd, args); + }; + }; + } /***** Sequencer::Sequence::init_operation_builders ************************/ diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 67617b69..d854ad9d 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -18,6 +18,97 @@ namespace Sequencer { constexpr long CAMERA_PROLOG_TIMEOUT = 6000; ///< timeout msec to send camera prolog command + namespace { + // ------------------------------------------------------------------------ + // Maps each Operation::thr to the DaemonBit it depends on. Operations + // that manage daemon state themselves (startup, shutdown, power_init, + // power_shutdown) are intentionally absent from this map. + // Used exclusively by validate_sequence(). + // ------------------------------------------------------------------------ + const std::unordered_map thr_to_daemon = { + { THR_EXPOSURE, DAEMON_CAMERA }, + { THR_CAMERA_SET, DAEMON_CAMERA }, + { THR_CAMERA_INIT, DAEMON_CAMERA }, + { THR_CAMERA_SHUTDOWN, DAEMON_CAMERA }, + { THR_SLIT_SET, DAEMON_SLIT }, + { THR_SLIT_INIT, DAEMON_SLIT }, + { THR_SLIT_SHUTDOWN, DAEMON_SLIT }, + { THR_MOVE_TO_TARGET, DAEMON_TCS }, + { THR_TCS_INIT, DAEMON_TCS }, + { THR_TCS_SHUTDOWN, DAEMON_TCS }, + { THR_FLEXURE_SET, DAEMON_FLEXURE }, + { THR_FLEXURE_INIT, DAEMON_FLEXURE }, + { THR_FLEXURE_SHUTDOWN, DAEMON_FLEXURE }, + { THR_FOCUS_SET, DAEMON_FOCUS }, + { THR_FOCUS_INIT, DAEMON_FOCUS }, + { THR_FOCUS_SHUTDOWN, DAEMON_FOCUS }, + { THR_CALIB_SET, DAEMON_CALIB }, + { THR_CALIBRATOR_SET, DAEMON_CALIB }, + { THR_CALIB_INIT, DAEMON_CALIB }, + { THR_CALIB_SHUTDOWN, DAEMON_CALIB }, + { THR_ACAM_INIT, DAEMON_ACAM }, + { THR_ACAM_SHUTDOWN, DAEMON_ACAM }, + { THR_SLICECAM_INIT, DAEMON_SLICECAM }, + { THR_SLICECAM_SHUTDOWN,DAEMON_SLICECAM } + }; + + // ------------------------------------------------------------------------ + // Returns true if the two Operation::thr values are unsafe to run in the + // same PARALLEL group. Serial ordering of any such pair is fine. + // ------------------------------------------------------------------------ + inline bool is_parallel_unsafe_pair(ThreadStatusBits a, ThreadStatusBits b) { + // normalize ordering so each pair is checked once + if (a > b) std::swap(a, b); + + // global sequence ops conflict with anything big + if (a == THR_STARTUP && b == THR_SHUTDOWN) return true; + if (a == THR_STARTUP && b == THR_EXPOSURE) return true; + + // startup runs all inits internally; parallelling any init with startup is a double-init + auto is_init = [](ThreadStatusBits t) { + return t == THR_ACAM_INIT || t == THR_CALIB_INIT || + t == THR_CAMERA_INIT || t == THR_FLEXURE_INIT|| + t == THR_FOCUS_INIT || t == THR_POWER_INIT || + t == THR_SLICECAM_INIT || t == THR_SLIT_INIT || + t == THR_TCS_INIT; + }; + auto is_shutdown = [](ThreadStatusBits t) { + return t == THR_ACAM_SHUTDOWN || t == THR_CALIB_SHUTDOWN || + t == THR_CAMERA_SHUTDOWN || t == THR_FLEXURE_SHUTDOWN|| + t == THR_FOCUS_SHUTDOWN || t == THR_POWER_SHUTDOWN || + t == THR_SLICECAM_SHUTDOWN || t == THR_SLIT_SHUTDOWN || + t == THR_TCS_SHUTDOWN; + }; + + if (a == THR_STARTUP && is_init(b)) return true; + if (a == THR_SHUTDOWN && is_init(b)) return true; + + // power_init vs power_shutdown + if (a == THR_POWER_INIT && b == THR_POWER_SHUTDOWN) return true; + + // matching init/shutdown pair for the same subsystem + if (is_init(a) && is_shutdown(b)) { + auto subsystem_of = [](ThreadStatusBits t) -> int { + switch (t) { + case THR_ACAM_INIT: case THR_ACAM_SHUTDOWN: return 1; + case THR_CALIB_INIT: case THR_CALIB_SHUTDOWN: return 2; + case THR_CAMERA_INIT: case THR_CAMERA_SHUTDOWN: return 3; + case THR_FLEXURE_INIT: case THR_FLEXURE_SHUTDOWN: return 4; + case THR_FOCUS_INIT: case THR_FOCUS_SHUTDOWN: return 5; + case THR_POWER_INIT: case THR_POWER_SHUTDOWN: return 6; + case THR_SLICECAM_INIT: case THR_SLICECAM_SHUTDOWN: return 7; + case THR_SLIT_INIT: case THR_SLIT_SHUTDOWN: return 8; + case THR_TCS_INIT: case THR_TCS_SHUTDOWN: return 9; + default: return 0; + } + }; + if (subsystem_of(a) == subsystem_of(b)) return true; + } + + return false; + } + } // anonymous namespace + /***** Sequencer::Sequence::operation_sleep ********************************/ /** * @brief interruptable sleep for operations @@ -72,6 +163,7 @@ namespace Sequencer { while (!is_cancelled()) { try { + this->current_op_params = op.params; // expose params to op member functions error = op.func(); if (error==NO_ERROR) return NO_ERROR; if (error==ABORT || @@ -402,14 +494,21 @@ namespace Sequencer { ParsedCommand command; command.name = word; - // any additional words are parameters, expected to be key=val pairs + // Additional words are either key=val params (INTERNAL) or positional + // args (PASSTHROUGH). Both collections are populated; each op_builder + // chooses which to consume. while (iss >> word) { + // key=value parameters auto eq = word.find('='); if (eq != std::string::npos) { std::string key = word.substr(0, eq); std::string val = word.substr(eq+1); command.params.map[key] = val; } + // positional arguments + else { + command.args.push_back(word); + } } return command; @@ -425,21 +524,204 @@ namespace Sequencer { * */ long Sequence::validate_sequence(const std::vector &sequence) { + const std::string function("Sequencer::Sequence::validate_sequence"); + long error = NO_ERROR; + + // ---------- RULE ------------------------------------------------------- + // Sequencer must not already be running, stopping, starting, or paused. + // Only READY or NOTREADY states allow a new sequence. + + if ( seq_state_manager.is_set(Sequencer::SEQ_RUNNING) ) { + this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer already running"); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_STOPPING) ) { + this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is stopping"); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_STARTING) ) { + this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is starting"); + return ERROR; + } + + if ( seq_state_manager.is_set(Sequencer::SEQ_PAUSED) ) { + this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is paused"); + return ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Sequence must not be empty. + + if ( sequence.empty() ) { + this->async.enqueue_and_log(function, "ERROR sequence is empty"); + return ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Every group must contain at least one operation. - // sequence is a vector of OperationGroups for (const auto &group : sequence) { + if ( group.operations.empty() ) { + this->async.enqueue_and_log(function, "ERROR sequence contains an empty operation group"); + return ERROR; + } + } - // group is a vector of Operations + // ---------- RULE ------------------------------------------------------- + // Collect presence flags + counts by Operation::thr identifier. + // Used by subsequent rules. + + bool has_expose = false; + bool has_camera_set = false; + bool has_slit_set = false; + int expose_count = 0; + + for (const auto &group : sequence) { for (const auto &op : group.operations) { + switch (op.thr) { + case THR_EXPOSURE: has_expose = true; ++expose_count; break; + case THR_CAMERA_SET: has_camera_set = true; break; + case THR_SLIT_SET: has_slit_set = true; break; + default: break; + } + } + } + + // ---------- RULE ------------------------------------------------------- + // If expose is present, camera_set must also be present. + + if ( has_expose && !has_camera_set ) { + this->async.enqueue_and_log(function, + "ERROR sequence contains 'expose' without 'camera_set'"); + error = ERROR; + } + + // ---------- RULE ------------------------------------------------------- + // Daemon readiness. + // Science mode (engineering_mode == false): + // If the sequence contains any op that depends on a daemon, SEQ_READY + // must be set (meaning all daemons are up, established by startup()). + // Engineering mode (engineering_mode == true): + // Only the specific daemons needed by the operations in the sequence + // are required. All failures are reported together. - if (op.name() == "expose") { + { + // collect the set of daemons this sequence depends on + std::bitset required; + for (const auto &group : sequence) { + for (const auto &op : group.operations) { + auto it = thr_to_daemon.find(op.thr); + if ( it != thr_to_daemon.end() ) required.set(it->second); + } + } + + if ( required.any() ) { + // for engineering mode it is sufficient if only the required subsystem + // is ready. + if ( this->engineering_mode.load() ) { + for (std::size_t bit = 0; bit < NUM_DAEMONS; ++bit) { + if ( required.test(bit) && + !daemon_manager.is_set(static_cast(bit)) ) { + std::ostringstream oss; + oss << "ERROR sequence requires daemon '" + << daemon_name.at(static_cast(bit)) + << "' but it is not ready"; + this->async.enqueue_and_log(function, oss.str()); + error = ERROR; + } } - else - if (op.name() == "slit_set") { + } + else + // for science mode (non-engineering) all subsystems must be ready + if ( !seq_state_manager.is_set(Sequencer::SEQ_READY) ) { + this->async.enqueue_and_log(function, + "ERROR sequence requires SEQ_READY (all daemons ready) in science mode"); + error = ERROR; + } + } + } + + // ---------- RULE ------------------------------------------------------- + // Parallel-unsafe pairs. + // Always (either mode): + // No PARALLEL group may contain a parallel-unsafe pair of operations. + // Science mode only: + // A parallel-unsafe pair is also rejected if it appears anywhere in + // the sequence (across groups), because scripted science ops should + // not mix startup/shutdown/expose even in serial form. + // Engineering mode: + // SERIAL placement of any parallel-unsafe pair is allowed; ordering + // is explicit and on_error STOP handles failures. + + { + // intra-group check: no PARALLEL group may contain a parallel-unsafe pair + for (const auto &group : sequence) { + if ( group.type != OperationType::PARALLEL ) continue; + for (std::size_t i = 0; i < group.operations.size(); ++i) { + for (std::size_t j = i+1; j < group.operations.size(); ++j) { + if ( is_parallel_unsafe_pair(group.operations[i].thr, + group.operations[j].thr) ) { + std::ostringstream oss; + oss << "ERROR parallel group contains unsafe pair: '" + << thread_names.at(group.operations[i].thr) << "' and '" + << thread_names.at(group.operations[j].thr) << "'"; + this->async.enqueue_and_log(function, oss.str()); + error = ERROR; + } } } } - return NO_ERROR; + + // science-mode cross-group check: reject parallel-unsafe pairs anywhere + if ( !this->engineering_mode.load() ) { + std::vector all_thrs; + all_thrs.reserve(NUM_THREAD_STATES); + for (const auto &group : sequence) { + for (const auto &op : group.operations) all_thrs.push_back(op.thr); + } + for (std::size_t i = 0; i < all_thrs.size(); ++i) { + for (std::size_t j = i+1; j < all_thrs.size(); ++j) { + if ( is_parallel_unsafe_pair(all_thrs[i], all_thrs[j]) ) { + std::ostringstream oss; + oss << "ERROR sequence contains unsafe pair in science mode: '" + << thread_names.at(all_thrs[i]) << "' and '" + << thread_names.at(all_thrs[j]) + << "' (use 'engineering true' to allow in SERIAL groups)"; + this->async.enqueue_and_log(function, oss.str()); + error = ERROR; + } + } + } + } + } + + // ---------- RULE ------------------------------------------------------- + // TBD should I allow expose to not appear more than once across all groups? + + if ( expose_count > 1 ) { + this->async.enqueue_and_log(function, + "NOTICE sequence contains multiple 'expose' operations -- placeholder check"); + // not a hard error yet; pending clarification of multi-expose DSL usage + } + + // ---------- RULE ------------------------------------------------------- + // PLACEHOLDER: parallel contention check. + // Parallel groups must not contain operations that share a subsystem wait + // state, since they would contend on the same daemon. + // + // When Operation gains a wait_bit field (WaitStateBits), check here that + // no two operations in the same PARALLEL group share the same wait_bit. + // For now this is a no-op stub that compiles and runs cleanly. + + if (false) { + // stub -- replace with real contention check + this->async.enqueue_and_log(function, + "PLACEHOLDER parallel contention check not yet implemented"); + } + + return error; } /***** Sequencer::Sequence::validate_sequence ******************************/ @@ -2265,6 +2547,38 @@ namespace Sequencer { ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); + // Apply optional CLI/DSL coordinate overrides carried in current_op_params + // (e.g. "move_to_target ra=01:23:45 dec=+45:00:00"). Overrides are written + // into this->target for the duration of this call and restored on exit so + // the database-backed target fields are not permanently mutated. + // + struct TargetCoordGuard { + TargetInfo &t; + mysqlx::string saved_ra; + mysqlx::string saved_dec; + bool ra_overridden; + bool dec_overridden; + TargetCoordGuard(TargetInfo &t_, bool ra_ov, bool dec_ov) + : t(t_), + saved_ra(t_.ra_hms), + saved_dec(t_.dec_dms), + ra_overridden(ra_ov), + dec_overridden(dec_ov) {} + ~TargetCoordGuard() { + if (ra_overridden) t.ra_hms = saved_ra; + if (dec_overridden) t.dec_dms = saved_dec; + } + }; + const bool ra_override = this->current_op_params.has("ra"); + const bool dec_override = this->current_op_params.has("dec"); + TargetCoordGuard coord_guard( this->target, ra_override, dec_override ); + if (ra_override) { + this->target.ra_hms = this->current_op_params.get("ra", std::string(this->target.ra_hms)); + } + if (dec_override) { + this->target.dec_dms = this->current_op_params.get("dec", std::string(this->target.dec_dms)); + } + // If RA and DEC fields are both empty then no telescope move // if ( this->target.ra_hms.empty() && this->target.dec_dms.empty() ) { @@ -3274,6 +3588,55 @@ namespace Sequencer { /***** Sequencer::Sequence::dotype ******************************************/ + /***** Sequencer::Sequence::engineering *************************************/ + /** + * @brief set or get the engineering-mode flag + * @param[in] args empty (query) or { "true"|"false"|"1"|"0"|"enable"|"disable" } + * @param[out] retstring returns "true" or "false" + * @return NO_ERROR | ERROR + * + * When engineering_mode is true, validate_sequence() applies per-daemon + * readiness checks instead of requiring SEQ_READY, and it allows + * parallel-unsafe operation pairs when they are in SERIAL groups. + * + * Intended for single-subsystem / bench / engineering operation. Should not + * be used for science operations. + * + */ + long Sequence::engineering( std::string args, std::string &retstring ) { + const std::string function("Sequencer::Sequence::engineering"); + std::stringstream message; + long error = NO_ERROR; + + if ( not args.empty() ) { + std::transform( args.begin(), args.end(), args.begin(), ::tolower ); + + if ( args == "true" || args == "1" || args == "enable" ) { + this->engineering_mode.store( true ); + logwrite( function, "WARNING engineering mode ENABLED: validate_sequence will bypass full-instrument checks" ); + } + else + if ( args == "false" || args == "0" || args == "disable" ) { + this->engineering_mode.store( false ); + logwrite( function, "engineering mode DISABLED: validate_sequence will require SEQ_READY for full-instrument operations" ); + } + else { + message.str(""); message << "ERROR unrecognized argument " << args << ": expected {true|false|1|0|enable|disable}"; + logwrite( function, message.str() ); + error = ERROR; + } + } + + retstring = ( this->engineering_mode.load() ? "true" : "false" ); + + message.str(""); message << "ENGINEERING: " << retstring; + this->async.enqueue( message.str() ); + + return( error ); + } + /***** Sequencer::Sequence::engineering *************************************/ + + /***** Sequencer::Sequence::get_dome_position *******************************/ /** * @brief read the dome and telescope positions diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index b25608f5..a89c7793 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -289,6 +289,7 @@ namespace Sequencer { * @brief the Sequence class defines the "sequences", i.e. functions of the sequencer */ class Sequence { + friend struct SequenceTestAccess; ///< test-only friend (see sequencerd/test_sequence.cpp) private: zmqpp::context context; bool ready_to_start; ///< set on nightly startup success, used to return seqstate to READY after an abort @@ -301,6 +302,7 @@ namespace Sequencer { std::atomic is_usercontinue{false}; ///< remotely set by the user to continue std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? std::atomic is_acam_guiding{false}; ///< is acam guiding? + std::atomic engineering_mode{false}; ///< when true, validate_sequence applies per-daemon checks instead of SEQ_READY ThreadPool pool; @@ -372,10 +374,18 @@ namespace Sequencer { */ struct ParsedCommand { std::string name; - OperationParams params; + OperationParams params; ///< key=value pairs (for Internal Operations) + std::vector args; ///< positional args (for Passthrough Operations) int linenum{0}; }; + /** @brief key=value params of the currently executing operation + * @details set by run() just before op.func() is invoked and consumed + * by Sequence:: member functions that accept optional overrides + * (e.g. move_to_target, slit_set) + */ + OperationParams current_op_params; + class Ops { private: Sequence* seq; @@ -384,12 +394,23 @@ namespace Sequencer { Operation acam_init(); Operation calib_init(); + Operation calib_set(); Operation camera_init(); + Operation camera_set(); + Operation do_expose(); + Operation do_shutdown(); + Operation do_startup(); Operation flexure_init(); + Operation flexure_set(); Operation focus_init(); + Operation focus_set(); + Operation move_to_target(); Operation power_init(); + Operation repeat_exposure(); Operation slicecam_init(); Operation slit_init(); + Operation slit_set(); + Operation target_offset(); Operation tcs_init(); }; @@ -397,6 +418,8 @@ namespace Sequencer { std::unordered_map op_builders; + Ops ops{ this }; ///< factory used by op_builders to build Internal Operations + void init_operation_builders(); /** @brief safely runs function in a detached thread using lambda to catch exceptions @@ -442,6 +465,7 @@ namespace Sequencer { is_subscriber_thread_running(false), should_subscriber_thread_run(false) { + init_operation_builders(); seq_state_manager.set_callback([this](const std::bitset& states) { broadcast_seqstate(); }); wait_state_manager.set_callback([this](const std::bitset& states) { broadcast_waitstate(); }); thread_state_manager.set_callback([this](const std::bitset& states) { publish_threadstate(); }); @@ -674,6 +698,7 @@ namespace Sequencer { std::string seqstate_string( uint32_t state ); ///< returns string form of states set in state word long dotype( std::string args ); ///< set do type (one/all) long dotype( std::string args, std::string &retstring ); ///< set or get do type (one/all) + long engineering( std::string args, std::string &retstring ); ///< set or get engineering mode (true/false) long poll_dome_position( double &domeazi, double &telazi ); long get_dome_position( double &domeazi, double &telazi ); long get_dome_position( bool poll, double &domeazi, double &telazi ); diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 1d4b143c..8706c0bb 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -97,61 +97,22 @@ namespace Sequencer { continue; } + // ---------- MAKE OPERATION FOR GIVEN COMMAND ------------------------- + // anything other than { parallel | serial | end | on_error } is a // command to parse, so a group must have been started first. // if (!current_group) return fail(command, "command outside group"); - // ---------- MAKE OPERATION FOR GIVEN COMMAND ------------------------- + // find the command in the operation builders map + // + auto it = op_builders.find(name); - Operation &op = current_group->operations.emplace_back(); + if (it == op_builders.end()) return fail(command, "unrecognized command"); - if (name == "move_to_target") { - op.thr = THR_MOVE_TO_TARGET; - op.func = [this,params=command.params]() { - if (params.has("ra") && params.has("dec")) { - this->target.ra_hms = params.get(std::string("ra"),std::string("")); - this->target.dec_dms = params.get(std::string("dec"),std::string("")); - } - return move_to_target(); - }; - op.params = command.params; - } - else - - if (name == "slit_set") { - op.thr = THR_SLIT_SET; - op.func = [this,params=command.params]() { - size_t mode = params.get("mode", VSM_DATABASE); - return slit_set(static_cast(mode)); - }; - op.params = command.params; - } - else - - if (name == "expose") { - op.thr = THR_EXPOSURE; - op.func = [this,function]() { - return do_exposure(function); - }; - op.params = command.params; - } - else - - if (name == "focus_set") { - op.thr = THR_FOCUS_SET; - op.func = [this]() { - return focus_set(); - }; - op.params = command.params; - } + Operation &op = current_group->operations.emplace_back(); - else { - this->async.enqueue_and_log("Sequencer::Sequence::build_sequence", - "ERROR unknown command '"+command.name+"'"); - continue; - } - current_group->operations.emplace_back(std::move(op)); + it->second(op, command); } return NO_ERROR; diff --git a/sequencerd/sequence_operations.cpp b/sequencerd/sequence_operations.cpp index a43608d7..832fa646 100644 --- a/sequencerd/sequence_operations.cpp +++ b/sequencerd/sequence_operations.cpp @@ -3,6 +3,13 @@ * @brief implementation of operations * @author David Hale * + * Each Ops factory method here returns a populated Sequence::Operation that + * wraps a Sequence:: member function and identifies itself via its THR_ bit. + * These are the "Internal" sequencer-level operations (database-driven, + * may coordinate multiple daemons). Per-daemon "Passthrough" operations + * (e.g. "camera exptime 30000") are built inline by init_operation_builders() + * and therefore do not appear here. + * */ #include "sequence.h" @@ -27,7 +34,7 @@ namespace Sequencer { op.on_retry = [this]() { long error=NO_ERROR; error |= seq->set_power_switch(OFF, POWER_ACAM_CAM, std::chrono::seconds(5)); - error |= seq->daemon_restart(seq->acamd); + error |= seq->daemon_restart(seq->acamd); return error; }; return op; @@ -50,6 +57,21 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::calib_init ********************************/ + /***** Sequencer::Sequence::Ops::calib_set *********************************/ + /** + * @brief defines the calib_set operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::calib_set() { + Sequence::Operation op; + op.thr = THR_CALIB_SET; + op.func = [this]() { return seq->calib_set(); }; + return op; + } + /***** Sequencer::Sequence::Ops::calib_set *********************************/ + + /***** Sequencer::Sequence::Ops::camera_init *******************************/ /** * @brief defines the camera_init operation @@ -65,6 +87,69 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::camera_init *******************************/ + /***** Sequencer::Sequence::Ops::camera_set ********************************/ + /** + * @brief defines the camera_set operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::camera_set() { + Sequence::Operation op; + op.thr = THR_CAMERA_SET; + op.func = [this]() { return seq->camera_set(); }; + return op; + } + /***** Sequencer::Sequence::Ops::camera_set ********************************/ + + + /***** Sequencer::Sequence::Ops::do_expose *********************************/ + /** + * @brief defines the expose operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_expose() { + Sequence::Operation op; + op.thr = THR_EXPOSURE; + op.func = [this]() { + const std::string caller("Sequencer::Sequence::Ops::do_expose"); + return seq->do_exposure(caller); + }; + return op; + } + /***** Sequencer::Sequence::Ops::do_expose *********************************/ + + + /***** Sequencer::Sequence::Ops::do_shutdown *******************************/ + /** + * @brief defines the shutdown operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_shutdown() { + Sequence::Operation op; + op.thr = THR_SHUTDOWN; + op.func = [this]() { return seq->shutdown(); }; + return op; + } + /***** Sequencer::Sequence::Ops::do_shutdown *******************************/ + + + /***** Sequencer::Sequence::Ops::do_startup ********************************/ + /** + * @brief defines the startup operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::do_startup() { + Sequence::Operation op; + op.thr = THR_STARTUP; + op.func = [this]() { return seq->startup(); }; + return op; + } + /***** Sequencer::Sequence::Ops::do_startup ********************************/ + + /***** Sequencer::Sequence::Ops::flexure_init ******************************/ /** * @brief defines the flexure_init operation @@ -80,6 +165,21 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::flexure_init ******************************/ + /***** Sequencer::Sequence::Ops::flexure_set *******************************/ + /** + * @brief defines the flexure_set operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::flexure_set() { + Sequence::Operation op; + op.thr = THR_FLEXURE_SET; + op.func = [this]() { return seq->flexure_set(); }; + return op; + } + /***** Sequencer::Sequence::Ops::flexure_set *******************************/ + + /***** Sequencer::Sequence::Ops::focus_init ********************************/ /** * @brief defines the focus_init operation @@ -95,6 +195,42 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::focus_init ********************************/ + /***** Sequencer::Sequence::Ops::focus_set *********************************/ + /** + * @brief defines the focus_set operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::focus_set() { + Sequence::Operation op; + op.thr = THR_FOCUS_SET; + op.func = [this]() { return seq->focus_set(); }; + return op; + } + /***** Sequencer::Sequence::Ops::focus_set *********************************/ + + + /***** Sequencer::Sequence::Ops::move_to_target ****************************/ + /** + * @brief defines the move_to_target operation + * @details Optional ra= and dec= params on the ParsedCommand override the + * target coordinates for this invocation only; the overrides are + * read from seq->current_op_params inside Sequence::move_to_target() + * (populated by Sequence::run() just before op.func() is called) + * and restored on return so the database-backed target is not + * permanently mutated. + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::move_to_target() { + Sequence::Operation op; + op.thr = THR_MOVE_TO_TARGET; + op.func = [this]() { return seq->move_to_target(); }; + return op; + } + /***** Sequencer::Sequence::Ops::move_to_target ****************************/ + + /***** Sequencer::Sequence::Ops::power_init ********************************/ /** * @brief defines the power_init operation @@ -110,6 +246,21 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::power_init ********************************/ + /***** Sequencer::Sequence::Ops::repeat_exposure ***************************/ + /** + * @brief defines the repeat_exposure operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::repeat_exposure() { + Sequence::Operation op; + op.thr = THR_REPEAT_EXPOSURE; + op.func = [this]() { return seq->repeat_exposure(); }; + return op; + } + /***** Sequencer::Sequence::Ops::repeat_exposure ***************************/ + + /***** Sequencer::Sequence::Ops::slicecam_init *****************************/ /** * @brief defines the slicecam_init operation @@ -118,7 +269,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::slicecam_init() { Sequence::Operation op; - op.thr = THR_ACAM_INIT; + op.thr = THR_SLICECAM_INIT; op.func = [this]() { return seq->slicecam_init(); }; op.params = { }; op.max_attempts = 3; @@ -126,7 +277,7 @@ namespace Sequencer { op.on_retry = [this]() { long error=NO_ERROR; error |= seq->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(5)); - error |= seq->daemon_restart(seq->slicecamd); + error |= seq->daemon_restart(seq->slicecamd); return error; }; return op; @@ -149,6 +300,43 @@ namespace Sequencer { /***** Sequencer::Sequence::Ops::slit_init *********************************/ + /***** Sequencer::Sequence::Ops::slit_set **********************************/ + /** + * @brief defines the slit_set operation + * @details Reads optional mode= param from seq->current_op_params. When + * absent the default VSM_DATABASE mode is used (drives slit from + * database target entry). + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::slit_set() { + Sequence::Operation op; + op.thr = THR_SLIT_SET; + op.func = [this]() { + const size_t mode = seq->current_op_params.get("mode", + static_cast(VSM_DATABASE)); + return seq->slit_set(static_cast(mode)); + }; + return op; + } + /***** Sequencer::Sequence::Ops::slit_set **********************************/ + + + /***** Sequencer::Sequence::Ops::target_offset *****************************/ + /** + * @brief defines the target_offset operation + * @return Operation + * + */ + Sequence::Operation Sequence::Ops::target_offset() { + Sequence::Operation op; + op.thr = THR_MOVE_TO_TARGET; // no dedicated THR_ for target_offset; reuse move_to_target identity + op.func = [this]() { return seq->target_offset(); }; + return op; + } + /***** Sequencer::Sequence::Ops::target_offset *****************************/ + + /***** Sequencer::Sequence::Ops::tcs_init **********************************/ /** * @brief defines the tcs_init operation diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index e0ed8961..7206e859 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1484,6 +1484,17 @@ namespace Sequencer { } else + // Set/Get Engineering mode. + // When enabled, validate_sequence allows single-subsystem operation + // (per-daemon checks instead of SEQ_READY) and permits parallel-unsafe + // operation pairs in SERIAL groups. + // + if ( cmd == SEQUENCERD_ENGINEERING ) { + ret = this->sequence.engineering( args, retstring ); + retstring.append( " " ); + } + else + // Report the Sequencer State, // which will be returned, logged, and written to the async message port. // From 188300b297c4a7e59e7e12ca0134f648d96e28bf Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 24 Apr 2026 15:01:48 -0700 Subject: [PATCH 19/37] * starts migrating sequencer off UDP broadcast, replaced with ZMQ * splits ACQUIRE wait state into MOVETO, ACAM_ACQUIRE, SLICECAM_FINEACQUIRE * fixes minor logging typos in focus_server and tcs_server --- common/message_keys.h | 40 +++- sequencerd/sequence.cpp | 277 ++++++++++++++++------------ sequencerd/sequence.h | 45 +++-- sequencerd/sequence_acquisition.cpp | 22 +-- sequencerd/sequence_wait.cpp | 18 +- 5 files changed, 248 insertions(+), 154 deletions(-) diff --git a/common/message_keys.h b/common/message_keys.h index 13a4d7ff..12ef41c7 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -8,13 +8,39 @@ #include +namespace Daemon { + inline const std::string ACAMD = "acamd"; + inline const std::string CALIBD = "calibd"; + inline const std::string CAMERAD = "camerad"; + inline const std::string FLEXURED = "flexured"; + inline const std::string FOCUSD = "focusd"; + inline const std::string POWERD = "powerd"; + inline const std::string SEQUENCER = "sequencerd"; + inline const std::string SLICECAMD = "slicecamd"; + inline const std::string SLITD = "slitd"; + inline const std::string TCSD = "tcsd"; + inline const std::string THERMALD = "thermald"; +} + +namespace Severity { + inline const std::string NOTICE = "NOTICE"; + inline const std::string WARNING = "WARNING"; + inline const std::string ERROR = "ERROR"; +} + namespace Topic { inline const std::string SNAPSHOT = "_snapshot"; inline const std::string TARGETINFO = "targetinfo"; + inline const std::string BROADCAST = "broadcast"; inline const std::string TCSD = "tcsd"; inline const std::string SLITD = "slitd"; inline const std::string CAMERAD = "camerad"; inline const std::string ACAMD = "acamd"; + inline const std::string CALIBD = "calibd"; + inline const std::string FLEXURED = "flexured"; + inline const std::string FOCUSD = "focusd"; + inline const std::string POWERD = "powerd"; + inline const std::string THERMALD = "thermald"; inline const std::string SEQ_DAEMONSTATE = "seq_daemonstate"; inline const std::string SEQ_SEQSTATE = "seq_seqstate"; inline const std::string SEQ_THREADSTATE = "seq_threadstate"; @@ -26,13 +52,23 @@ namespace Key { inline const std::string SOURCE = "source"; + namespace Broadcast { + inline const std::string SEVERITY = "severity"; + inline const std::string MESSAGE = "message"; + } + namespace Sequencer { inline const std::string SEQSTATE = "seqstate"; } namespace Camerad { - inline const std::string READY = "ready"; - inline const std::string SHUTTERTIME = "shuttime_sec"; + inline const std::string READY = "ready"; + inline const std::string SHUTTERTIME = "shuttime_sec"; + inline const std::string EXPTIME = "exptime"; + inline const std::string IMNUM = "imnum"; + inline const std::string IMNAME = "imname"; + inline const std::string FRAMECOUNT = "framecount"; + inline const std::string FRAMETRANSFER = "frametransfer"; } namespace Acamd { diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index d854ad9d..35599995 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -532,22 +532,22 @@ namespace Sequencer { // Only READY or NOTREADY states allow a new sequence. if ( seq_state_manager.is_set(Sequencer::SEQ_RUNNING) ) { - this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer already running"); + this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer already running" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_STOPPING) ) { - this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is stopping"); + this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is stopping" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_STARTING) ) { - this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is starting"); + this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is starting" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_PAUSED) ) { - this->async.enqueue_and_log(function, "ERROR sequence rejected: sequencer is paused"); + this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is paused" ); return ERROR; } @@ -555,7 +555,7 @@ namespace Sequencer { // Sequence must not be empty. if ( sequence.empty() ) { - this->async.enqueue_and_log(function, "ERROR sequence is empty"); + this->broadcast( function, Severity::ERROR, "sequence is empty" ); return ERROR; } @@ -564,7 +564,7 @@ namespace Sequencer { for (const auto &group : sequence) { if ( group.operations.empty() ) { - this->async.enqueue_and_log(function, "ERROR sequence contains an empty operation group"); + this->broadcast( function, Severity::ERROR, "sequence contains an empty operation group" ); return ERROR; } } @@ -593,8 +593,7 @@ namespace Sequencer { // If expose is present, camera_set must also be present. if ( has_expose && !has_camera_set ) { - this->async.enqueue_and_log(function, - "ERROR sequence contains 'expose' without 'camera_set'"); + this->broadcast( function, Severity::ERROR, "sequence contains 'expose' without 'camera_set'" ); error = ERROR; } @@ -625,10 +624,10 @@ namespace Sequencer { if ( required.test(bit) && !daemon_manager.is_set(static_cast(bit)) ) { std::ostringstream oss; - oss << "ERROR sequence requires daemon '" + oss << "sequence requires daemon '" << daemon_name.at(static_cast(bit)) << "' but it is not ready"; - this->async.enqueue_and_log(function, oss.str()); + this->broadcast(function, Severity::ERROR, oss.str()); error = ERROR; } } @@ -636,8 +635,7 @@ namespace Sequencer { else // for science mode (non-engineering) all subsystems must be ready if ( !seq_state_manager.is_set(Sequencer::SEQ_READY) ) { - this->async.enqueue_and_log(function, - "ERROR sequence requires SEQ_READY (all daemons ready) in science mode"); + this->broadcast( function, Severity::ERROR, "sequence requires SEQ_READY (all daemons ready) in science mode" ); error = ERROR; } } @@ -664,10 +662,10 @@ namespace Sequencer { if ( is_parallel_unsafe_pair(group.operations[i].thr, group.operations[j].thr) ) { std::ostringstream oss; - oss << "ERROR parallel group contains unsafe pair: '" + oss << "parallel group contains unsafe pair: '" << thread_names.at(group.operations[i].thr) << "' and '" << thread_names.at(group.operations[j].thr) << "'"; - this->async.enqueue_and_log(function, oss.str()); + this->broadcast(function, Severity::ERROR, oss.str()); error = ERROR; } } @@ -685,11 +683,11 @@ namespace Sequencer { for (std::size_t j = i+1; j < all_thrs.size(); ++j) { if ( is_parallel_unsafe_pair(all_thrs[i], all_thrs[j]) ) { std::ostringstream oss; - oss << "ERROR sequence contains unsafe pair in science mode: '" + oss << "sequence contains unsafe pair in science mode: '" << thread_names.at(all_thrs[i]) << "' and '" << thread_names.at(all_thrs[j]) << "' (use 'engineering true' to allow in SERIAL groups)"; - this->async.enqueue_and_log(function, oss.str()); + this->broadcast(function, Severity::ERROR, oss.str()); error = ERROR; } } @@ -701,8 +699,7 @@ namespace Sequencer { // TBD should I allow expose to not appear more than once across all groups? if ( expose_count > 1 ) { - this->async.enqueue_and_log(function, - "NOTICE sequence contains multiple 'expose' operations -- placeholder check"); + this->broadcast( function, Severity::NOTICE, "sequence contains multiple 'expose' operations -- placeholder check" ); // not a hard error yet; pending clarification of multi-expose DSL usage } @@ -717,7 +714,7 @@ namespace Sequencer { if (false) { // stub -- replace with real contention check - this->async.enqueue_and_log(function, + this->broadcast(function, Severity::NOTICE, "PLACEHOLDER parallel contention check not yet implemented"); } @@ -1049,31 +1046,58 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_seqstate ******************************/ /** - * @brief writes string of seq_state to the async port - * @details This broadcasts the seqstate as a string with the "SEQSTATE:" - * message tag. + * @brief publishes seq_state via PUB-SUB + * @details The legacy "SEQSTATE:" UDP async string has been removed; + * seqstate is published exclusively on the PUB-SUB topic now. * */ void Sequence::broadcast_seqstate() { this->publish_seqstate(); - this->async.enqueue_and_log( "Sequencer::Sequence::broadcast_seqstate", - "SEQSTATE: "+seq_state_manager.get_set_states() ); this->cv.notify_all(); } /***** Sequencer::Sequence::broadcast_seqstate ******************************/ + /***** Sequencer::Sequence::broadcast ***************************************/ + /** + * @brief logs a narrative message and publishes it on Topic::BROADCAST + * @param[in] function name of caller function (for log) + * @param[in] severity one of Severity::NOTICE, Severity::WARNING, Severity::ERROR + * @param[in] message the operator-facing narrative text + * @details This replaces the legacy pattern of enqueuing narrative + * strings onto the UDP async queue. Messages are routed through + * the PUB-SUB broadcast topic instead, where the GUI message + * area subscribes. Logging behavior is preserved. + */ + void Sequence::broadcast( const std::string &function, + const std::string &severity, + const std::string &message ) { + logwrite( function, severity+": "+message ); + if ( this->publisher ) { + nlohmann::json jmessage; + jmessage[Key::Broadcast::SEVERITY] = severity; + jmessage[Key::Broadcast::MESSAGE] = message; + jmessage[Key::SOURCE] = Daemon::SEQUENCER; + try { + this->publisher->publish( jmessage, Topic::BROADCAST ); + } + catch ( const std::exception &e ) { + logwrite( function, "ERROR publishing broadcast: "+std::string(e.what()) ); + } + } + } + /***** Sequencer::Sequence::broadcast ***************************************/ + + /***** Sequencer::Sequence::broadcast_waitstate *****************************/ /** - * @brief writes string of all set wait_state bits to the asyn port - * @details This broadcasts the seqstate as a string with the "WAITSTATE:" - * message tag. + * @brief publishes wait_state via PUB-SUB + * @details The legacy "WAITSTATE:" UDP async string has been removed; + * waitstate is published exclusively on the PUB-SUB topic now. * */ void Sequence::broadcast_waitstate() { this->publish_waitstate(); - this->async.enqueue_and_log( "Sequencer::Sequence::broadcast_waitstate", - "WAITSTATE: "+wait_state_manager.get_set_states() ); this->cv.notify_all(); } /***** Sequencer::Sequence::broadcast_waitstate *****************************/ @@ -1215,14 +1239,14 @@ namespace Sequencer { // The Sequencer can only be started once // if ( thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { - this->async.enqueue_and_log( function, "ERROR sequencer already running" ); + this->broadcast( function, Severity::ERROR, "sequencer already running" ); return; } // The Sequencer can only be started when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->async.enqueue_and_log( function, "ERROR cannot start: system not ready" ); + this->broadcast( function, Severity::ERROR, "cannot start: system not ready" ); return; } @@ -1313,7 +1337,7 @@ namespace Sequencer { } else if ( targetstate == TargetInfo::TARGET_ERROR ) { // request stop on error - this->async.enqueue_and_log( function, "ERROR getting next target. stopping" ); + this->broadcast( function, Severity::ERROR, "getting next target. stopping" ); break; } @@ -1327,7 +1351,7 @@ namespace Sequencer { } if (is_cancelled()) { - this->async.enqueue_and_log(function, "NOTICE: sequence cancelled"); + this->broadcast( function, Severity::NOTICE, "sequence cancelled" ); break; } @@ -1423,14 +1447,14 @@ namespace Sequencer { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); /*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } if (!deactivechans.str().empty()) { std::string cmd = CAMERAD_DEACTIVATE + deactivechans.str(); if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR sending \""+cmd+"\": "+reply); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } @@ -1443,7 +1467,7 @@ namespace Sequencer { long exptime_msec = (long)( this->target.exptime_req * 1000 ); camcmd.str(""); camcmd << CAMERAD_EXPTIME << " " << exptime_msec; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1451,12 +1475,12 @@ namespace Sequencer { // camcmd.str(""); camcmd << CAMERAD_BIN << " spat " << this->target.binspat; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } camcmd.str(""); camcmd << CAMERAD_BIN << " spec " << this->target.binspect; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1517,7 +1541,7 @@ namespace Sequencer { logwrite( function, "moving slit to "+slitcmd.str()+" for "+modestr+"position" ); if ( this->slitd.command_timeout( slitcmd.str(), reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR setting slit" ); + this->broadcast( function, Severity::ERROR, "setting slit" ); this->thread_error_manager.set( THR_SLIT_SET ); throw std::runtime_error("slit returned: "+reply); } @@ -1543,7 +1567,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_POWER ); // powerd not ready if ( this->reopen_hardware(this->powerd, POWERD_REOPEN, 10000 ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing power control" ); + this->broadcast( function, Severity::ERROR, "initializing power control" ); throw std::runtime_error("could not initialize power control"); } @@ -1595,13 +1619,13 @@ namespace Sequencer { this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success if ( this->set_power_switch(ON, POWER_SLIT, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering slit hardware" ); + this->broadcast( function, Severity::ERROR, "powering slit hardware" ); throw std::runtime_error("could not power slit hardware"); } bool was_opened=false; if ( this->open_hardware(this->slitd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to slit" ); + this->broadcast( function, Severity::ERROR, "connecting to slit" ); throw std::runtime_error("could not open connection to slit hardware"); } @@ -1610,7 +1634,7 @@ namespace Sequencer { bool ishomed=false; std::string reply; if ( this->slitd.command( SLITD_ISHOME, reply ) ) { - this->async.enqueue_and_log( function, "ERROR communicating with slit hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); throw std::runtime_error("could not communicate with slit hardware: "+reply); } this->parse_state( function, reply, ishomed ); @@ -1620,7 +1644,7 @@ namespace Sequencer { if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->slitd.command_timeout( SLITD_HOME, reply, SLITD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with slit hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); throw std::runtime_error("could not home slit hardware: "+reply); } } @@ -1630,7 +1654,7 @@ namespace Sequencer { if ( was_opened && !this->config_init["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_init["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to slit" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); throw std::runtime_error("slit "+cmd+" returned: "+reply); } } @@ -1684,7 +1708,7 @@ namespace Sequencer { if (error==NO_ERROR && !this->config_shutdown["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_shutdown["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to slit" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); throw std::runtime_error(cmd+" returned: "+reply); } } @@ -1694,7 +1718,7 @@ namespace Sequencer { logwrite( function, "closing slit hardware" ); error = this->slitd.command( SLITD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR closing connection to slit hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to slit hardware" ); throw std::runtime_error("closing slit connection returned: "+reply); } @@ -1731,14 +1755,14 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_SLICECAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing slicecam control" ); + this->broadcast( function, Severity::ERROR, "initializing slicecam control" ); throw std::runtime_error("could not power slicecam hardware"); } // open connection is all that is needed, slicecamd takes care of everything // if ( this->open_hardware(this->slicecamd, SLICECAMD_OPEN, SLICECAMD_OPEN_TIMEOUT) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR starting slicecam" ); + this->broadcast( function, Severity::ERROR, "starting slicecam" ); throw SlicecamException("could not start slicecam"); } @@ -1771,7 +1795,7 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_ACAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering acam hardware" ); + this->broadcast( function, Severity::ERROR, "powering acam hardware" ); throw std::runtime_error("could not power acam hardware"); } @@ -1779,7 +1803,7 @@ namespace Sequencer { // bool was_opened=false; if ( this->open_hardware(this->acamd, ACAMD_OPEN, ACAMD_OPEN_TIMEOUT, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening acam camera" ); + this->broadcast( function, Severity::ERROR, "opening acam camera" ); throw AcamException(ErrorCode::ERROR_ACAM_CAMERA, "could not open acam camera"); } @@ -1790,14 +1814,14 @@ namespace Sequencer { if ( ! this->config_init["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_init["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_init["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_init["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1846,14 +1870,14 @@ namespace Sequencer { } if ( (error=this->connect_to_daemon(this->slicecamd)) != NO_ERROR ) { - this->async.enqueue_and_log(function, "ERROR connecting to slicecamd"); + this->broadcast( function, Severity::ERROR, "connecting to slicecamd" ); } // close connections between slicecamd and the hardware with which it communicates // logwrite( function, "closing slicecam hardware" ); if ( (error=this->slicecamd.command_timeout( SLICECAMD_SHUTDOWN, reply, SLICECAMD_SHUTDOWN_TIMEOUT )) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR closing connection to slicecam hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to slicecam hardware" ); } // disconnect me from slicecamd, irrespective of any previous error @@ -1864,7 +1888,7 @@ namespace Sequencer { // Turn off power to slicecam hardware. // if ( this->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off slicecam" ); + this->broadcast( function, Severity::ERROR, "switching off slicecam" ); throw std::runtime_error("could not power off slicecam hardware"); } @@ -1904,14 +1928,14 @@ namespace Sequencer { if ( ! this->config_shutdown["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_shutdown["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_shutdown["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_shutdown["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1924,7 +1948,7 @@ namespace Sequencer { if ( error==NO_ERROR ) { logwrite( function, "closing acam hardware" ); error = this->acamd.command_timeout( ACAMD_SHUTDOWN, ACAMD_SHUTDOWN_TIMEOUT ); - if ( error != NO_ERROR ) this->async.enqueue_and_log( function, "ERROR shutting down acam" ); + if ( error != NO_ERROR ) this->broadcast( function, Severity::ERROR, "shutting down acam" ); } // disconnect me from acamd, irrespective of any previous error @@ -1935,7 +1959,7 @@ namespace Sequencer { // Turn off power to acam hardware. // if ( this->set_power_switch(OFF, POWER_ACAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off acam" ); + this->broadcast( function, Severity::ERROR, "switching off acam" ); throw std::runtime_error("could not switch off acam"); } @@ -1965,14 +1989,14 @@ namespace Sequencer { // make sure calib hardware is powered if ( this->set_power_switch(ON, POWER_CALIB, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering focus control" ); + this->broadcast( function, Severity::ERROR, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to calibd bool was_opened=false; if ( this->open_hardware(this->calibd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing calib control" ); + this->broadcast( function, Severity::ERROR, "initializing calib control" ); throw std::runtime_error("could not power calib control"); } @@ -1984,14 +2008,14 @@ namespace Sequencer { std::string reply; long error = this->calibd.command( CALIBD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with calib hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } // home calib actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->calibd.command_timeout( CALIBD_HOME, reply, CALIBD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with calib hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } } @@ -2004,7 +2028,7 @@ namespace Sequencer { if ( !this->config_init["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_init["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("could not move calib door and/or cover"); } } @@ -2041,7 +2065,7 @@ namespace Sequencer { // bool poweron=false; if ( check_power_switch(ON, POWER_CALIB, poweron ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR checking calib power switch" ); + this->broadcast( function, Severity::ERROR, "checking calib power switch" ); throw std::runtime_error("checking calib power switch"); } @@ -2059,7 +2083,7 @@ namespace Sequencer { if ( !this->config_shutdown["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_shutdown["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } } @@ -2083,14 +2107,14 @@ namespace Sequencer { // Turn off power to calib hardware. // if ( this->set_power_switch(OFF, POWER_CALIB, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off calib hardware" ); + this->broadcast( function, Severity::ERROR, "switching off calib hardware" ); error=ERROR; } // always turn off power to lamps // if ( this->set_power_switch(OFF, POWER_LAMP, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering off lamps" ); + this->broadcast( function, Severity::ERROR, "powering off lamps" ); error=ERROR; } @@ -2124,7 +2148,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_TCS ); // tcsd not ready if ( this->open_hardware(this->tcsd) != NO_ERROR ) { - this->async.enqueue_and_log( "Sequencer::Sequence::tcs_init", "ERROR initializing TCS" ); + this->broadcast( "Sequencer::Sequence::tcs_init", Severity::ERROR, "initializing TCS" ); this->thread_error_manager.set( THR_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -2168,7 +2192,7 @@ namespace Sequencer { std::string reply; error = this->tcsd.send( TCSD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR: closing connection to TCS" ); + this->broadcast( function, Severity::ERROR, "closing connection to TCS" ); throw std::runtime_error("closing TCS connection: "+reply); } } @@ -2202,13 +2226,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_FLEXURE, std::chrono::seconds(21)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering flexure control" ); + this->broadcast( function, Severity::ERROR, "powering flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not power flexure control"); } if ( this->open_hardware(this->flexured) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing flexure control" ); + this->broadcast( function, Severity::ERROR, "initializing flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -2255,7 +2279,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->flexured) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to flexure hardware" ); + this->broadcast( function, Severity::ERROR, "connecting to flexure hardware" ); error=ERROR; } @@ -2264,7 +2288,7 @@ namespace Sequencer { // logwrite( function, "closing flexure hardware" ); if (error==NO_ERROR && (error=this->flexured.command( FLEXURED_CLOSE, reply )) != NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR closing connection to flexure hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to flexure hardware" ); } // disconnect me from flexured, irrespective of any previous error @@ -2275,7 +2299,7 @@ namespace Sequencer { // Turn off power to flexure hardware. // if ( this->set_power_switch(OFF, POWER_FLEXURE, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off flexure" ); + this->broadcast( function, Severity::ERROR, "switching off flexure" ); throw std::runtime_error("switching off flexure hardware"); } @@ -2302,14 +2326,14 @@ namespace Sequencer { this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success if ( this->set_power_switch(ON, POWER_FOCUS, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering focus control" ); + this->broadcast( function, Severity::ERROR, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to focusd bool was_opened=false; if ( this->open_hardware(this->focusd, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing focus control" ); + this->broadcast( function, Severity::ERROR, "initializing focus control" ); throw std::runtime_error("could not open focus hardware"); } @@ -2321,14 +2345,14 @@ namespace Sequencer { std::string reply; long error = this->focusd.command( FOCUSD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with focus hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_ISHOME+" returned: "+reply); } // home focus actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->focusd.command_timeout( FOCUSD_HOME, reply, FOCUSD_HOME_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR communicating with focus hardware" ); + this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_HOME+" returned: "+reply); } } @@ -2338,7 +2362,7 @@ namespace Sequencer { for ( const auto &chan : chans ) { std::string command = "set " + chan + " nominal"; if ( this->focusd.command_timeout( command, reply, FOCUSD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR setting focus "+chan ); + this->broadcast( function, Severity::ERROR, "setting focus "+chan ); throw std::runtime_error("focus "+command+" returned: "+reply); } } @@ -2384,7 +2408,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->focusd) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to focus hardware" ); + this->broadcast( function, Severity::ERROR, "connecting to focus hardware" ); error=ERROR; } @@ -2393,7 +2417,7 @@ namespace Sequencer { // logwrite( function, "closing focus hardware" ); if (error==NO_ERROR && (error=this->focusd.command( FOCUSD_CLOSE, reply )) != NO_ERROR) { - this->async.enqueue_and_log( function, "ERROR closing connection to focus hardware" ); + this->broadcast( function, Severity::ERROR, "closing connection to focus hardware" ); } // disconnect me from focusd, irrespective of any previous error @@ -2404,7 +2428,7 @@ namespace Sequencer { // Turn off power to focus hardware. // if ( this->set_power_switch(OFF, POWER_FOCUS, std::chrono::seconds(0)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR switching off focus" ); + this->broadcast( function, Severity::ERROR, "switching off focus" ); throw std::runtime_error("switching off focus hardware"); } @@ -2432,13 +2456,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering camera" ); + this->broadcast( function, Severity::ERROR, "powering camera" ); throw std::runtime_error("switching on camera"); } bool was_opened=false; if ( this->open_hardware(this->camerad, "open", 12000, was_opened) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR initializing camera" ); + this->broadcast( function, Severity::ERROR, "initializing camera" ); throw std::runtime_error("initializing camera"); } @@ -2448,7 +2472,7 @@ namespace Sequencer { if ( was_opened) { for ( const auto &cmd : this->camera_prologue ) { if ( this->camerad.command_timeout( cmd, reply, CAMERA_PROLOG_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending \""+cmd+"\" to camera" ); + this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to camera" ); throw std::runtime_error("sending \""+cmd+"\" to camera"); } } @@ -2519,7 +2543,7 @@ namespace Sequencer { // turn off power to camera hardware // if ( this->set_power_switch(OFF, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR powering off camera" ); + this->broadcast( function, Severity::ERROR, "powering off camera" ); throw std::runtime_error("switching off camera"); } @@ -2546,6 +2570,7 @@ namespace Sequencer { ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); + ScopedState wait_moveto( wait_state_manager, Sequencer::SEQ_WAIT_MOVETO ); // Apply optional CLI/DSL coordinate overrides carried in current_op_params // (e.g. "move_to_target ra=01:23:45 dec=+45:00:00"). Overrides are written @@ -2590,7 +2615,7 @@ namespace Sequencer { // if ( this->target.ra_hms == this->last_ra_hms && this->target.dec_dms == this->last_dec_dms ) { - this->async.enqueue_and_log( function, "NOTICE: no move required for repeat target" ); + this->broadcast( function, Severity::NOTICE, "no move required for repeat target" ); return NO_ERROR; } @@ -2614,7 +2639,7 @@ namespace Sequencer { if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } message << " to decimal"; - this->async.enqueue_and_log( function, "ERROR "+message.str() ); + this->broadcast( function, Severity::ERROR, ""+message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2686,14 +2711,14 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - this->async.enqueue_and_log( function, "NOTICE: waiting for TCS operator to send \"ontarget\" signal" ); + this->broadcast( function, Severity::NOTICE, "waiting for TCS operator to send \"ontarget\" signal" ); while ( !is_cancelled() && !this->is_ontarget.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || is_cancelled() ); } ); } - this->async.enqueue_and_log( function, "NOTICE: received " + this->broadcast( function, Severity::NOTICE, "received " +(is_cancelled() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); } @@ -2765,7 +2790,7 @@ namespace Sequencer { // if ( std::isnan( radec_to_decimal( seq.target.ra_hms, ra_hms ) ) || std::isnan( radec_to_decimal( seq.target.dec_dms, dec_dms ) ) ) { - seq.async.enqueue_and_log( function, "ERROR: can't handle NaN value for RA, DEC" ); + seq.broadcast( function, Severity::ERROR, "can't handle NaN value for RA, DEC" ); seq.thread_error_manager.set( THR_NOTIFY_TCS ); return; } @@ -2851,7 +2876,7 @@ namespace Sequencer { // const auto &calinfo = this->caltarget.get_info(calname); - this->async.enqueue_and_log(function, "NOTICE: configuring calibrator for "+calname); + this->broadcast( function, Severity::NOTICE, "configuring calibrator for "+calname ); // set the calib door and cover // @@ -2863,7 +2888,7 @@ namespace Sequencer { logwrite( function, "calib: "+cmd.str() ); if ( !is_cancelled() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR moving calib door and/or cover" ); + this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } @@ -2876,7 +2901,7 @@ namespace Sequencer { logwrite( function, message.str() ); std::string reply; if ( this->powerd.send( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR "+message.str() ); + this->broadcast( function, Severity::ERROR, ""+message.str() ); throw std::runtime_error("setting lamp "+message.str()); } } @@ -2900,13 +2925,13 @@ namespace Sequencer { if ( is_cancelled() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); + this->broadcast( function, Severity::ERROR, ""+cmd.str() ); throw std::runtime_error("setting lamp modulator "+cmd.str()); } } if ( is_cancelled() ) { - this->async.enqueue_and_log( function, "NOTICE: abort may have left calib system partially set" ); + this->broadcast( function, Severity::NOTICE, "abort may have left calib system partially set" ); } this->thread_error_manager.clear( THR_CALIBRATOR_SET ); // success @@ -2925,6 +2950,24 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); + // Determine post-abort seqstate based on current seqstate, before we + // enter SEQ_ABORTING. Aborting during RUNNING or PAUSED returns to READY; + // aborting during STARTING or STOPPING is unsafe and transitions to FAILED. + // Otherwise, leave seqstate unchanged (abort bit only; no destruct bit). + const bool abort_during_run = seq_state_manager.are_any_set( Sequencer::SEQ_RUNNING, + Sequencer::SEQ_PAUSED ); + const bool abort_during_lifecycle = seq_state_manager.are_any_set( Sequencer::SEQ_STARTING, + Sequencer::SEQ_STOPPING ); + + // Scoped SEQ_ABORTING bit: set on entry, cleared on exit (RAII). + ScopedState seq_state( this->seq_state_manager, Sequencer::SEQ_ABORTING ); + if ( abort_during_run ) { + seq_state.destruct_set( Sequencer::SEQ_READY ); + } + else if ( abort_during_lifecycle ) { + seq_state.destruct_set( Sequencer::SEQ_FAILED ); + } + clear_cancel_flag(); // stop any exposure that may be in progress @@ -2945,7 +2988,7 @@ namespace Sequencer { // this->do_once.store(true); - this->async.enqueue_and_log( function, "NOTICE: cancel signal sent" ); + this->broadcast( function, Severity::NOTICE, "cancel signal sent" ); } /***** Sequencer::Sequence::stop_exposure *********************************/ @@ -2962,7 +3005,7 @@ namespace Sequencer { // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->async.enqueue_and_log( function, "NOTICE: not currently exposing" ); + this->broadcast( function, Severity::NOTICE, "not currently exposing" ); return; } @@ -2979,12 +3022,12 @@ namespace Sequencer { else if ( error == NOTHING ) { // if not exposing, this is a way to ensure WAIT_EXPOSE bit can be cleared - this->async.enqueue_and_log( function, "NOTICE: not exposing" ); + this->broadcast( function, Severity::NOTICE, "not exposing" ); this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); } else if ( error == BUSY ) { - this->async.enqueue_and_log( function, "NOTICE: too late to stop exposure" ); + this->broadcast( function, Severity::NOTICE, "too late to stop exposure" ); // can't stop in the last 5 sec so wait that long and it should stop on its own std::this_thread::sleep_for(std::chrono::seconds(5)); } @@ -3009,7 +3052,7 @@ namespace Sequencer { // can only repeat when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->async.enqueue_and_log( function, "ERROR cannot repeat: system not ready" ); + this->broadcast( function, Severity::ERROR, "cannot repeat: system not ready" ); return ERROR; } @@ -3088,7 +3131,7 @@ namespace Sequencer { // if ( this->camerad.async( message.str() ) != NO_ERROR ) { // if ( this->camerad.send( message.str(), reply ) != NO_ERROR ) { if ( this->camerad.command_timeout( message.str(), reply, 30000 ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending camera "+message.str() ); + this->broadcast( function, Severity::ERROR, "sending camera "+message.str() ); this->thread_error_manager.set( THR_TRIGGER_EXPOSURE ); // tell the world this thread had an error this->target.update_state( Sequencer::TARGET_PENDING ); // return the target state to pending this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); // clear EXPOSE bit @@ -3150,7 +3193,7 @@ namespace Sequencer { // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->async.enqueue_and_log( function, "ERROR cannot update exposure time when not currently exposing" ); + this->broadcast( function, Severity::ERROR, "cannot update exposure time when not currently exposing" ); error = ERROR; } @@ -3196,7 +3239,9 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::startup"); std::stringstream message; - if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, Sequencer::SEQ_NOTREADY ) ) { + if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, + Sequencer::SEQ_NOTREADY, + Sequencer::SEQ_FAILED ) ) { message << "ERROR cannot perform system startup while " << seq_state_manager.get_set_states(); this->async.enqueue_and_log( function, message.str() ); @@ -3244,12 +3289,12 @@ namespace Sequencer { long error = run_sequence(sequence, function); - // change state to READY if all daemons ready w/o error + // change state to READY if all daemons ready w/o error; otherwise FAILED if ( error==NO_ERROR && daemon_manager.are_all_set() ) { seq_state_manager.set_only( {Sequencer::SEQ_READY} ); } else { - seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + seq_state_manager.set_only( {Sequencer::SEQ_FAILED} ); } return error; @@ -3271,11 +3316,9 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running - // set only STOPPING (and clear everything else) + // set only STOPPING (and clear everything else, including any prior FAILED) ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STOPPING, true ); // state=STOPPING (only) - seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); // set state=NOTREADY on exit - // stop everything // this->abort_process(); @@ -3296,7 +3339,7 @@ namespace Sequencer { error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR from power control. Will try to continue (but don't hold your breath)" ); + this->broadcast( function, Severity::ERROR, "from power control. Will try to continue (but don't hold your breath)" ); } // container of shutdown threads to launch, @@ -3316,9 +3359,11 @@ namespace Sequencer { std::ostringstream message; if (error==NO_ERROR) { // TODO need granularity here message << "NOTICE: instrument is shut down"; + seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); // clean shutdown -> NOTREADY } else { message << "ERROR occurred during shutdown and may not have completed"; + seq_state.destruct_set( Sequencer::SEQ_FAILED ); // bad shutdown -> FAILED } this->async.enqueue_and_log( function, message.str() ); @@ -4104,7 +4149,7 @@ namespace Sequencer { } // connection failed too many times if (attempt > maxattempts) { - async.enqueue_and_log(function, "ERROR exceeded max attempts connecting to " + daemon.name); + broadcast( function, Severity::ERROR, "exceeded max attempts connecting to " + daemon.name ); return ERROR; } @@ -4113,7 +4158,7 @@ namespace Sequencer { error |= daemon.send( "isopen", reply ); error |= this->parse_state( function, reply, isopen ); if ( error != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening "+daemon.name+" hardware" ); + this->broadcast( function, Severity::ERROR, "opening "+daemon.name+" hardware" ); return ERROR; } @@ -4123,7 +4168,7 @@ namespace Sequencer { logwrite( function, "opening "+daemon.name+" hardware connections with " +std::to_string(opentimeout)+" ms timeout" ); if ( daemon.command_timeout( opencmd, reply, opentimeout ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR opening connection to "+daemon.name+" hardware" ); + this->broadcast( function, Severity::ERROR, "opening connection to "+daemon.name+" hardware" ); return ERROR; } was_opened=true; @@ -4152,7 +4197,7 @@ namespace Sequencer { if ( !daemon.socket.isconnected() ) { logwrite( function, "connecting to "+daemon.name+" daemon" ); if ( daemon.connect() != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR connecting to "+daemon.name ); + this->broadcast( function, Severity::ERROR, "connecting to "+daemon.name ); return ERROR; } } diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index a89c7793..656f8861 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -132,6 +132,8 @@ namespace Sequencer { SEQ_STOPPING, ///< set when sequencer is shutting down SEQ_PAUSED, ///< set when sequencer is paused SEQ_STARTING, ///< set when sequencer is starting up + SEQ_FAILED, ///< set when sequencer has failed in an unsafe/indeterminate way + SEQ_ABORTING, ///< transitory state while aborting; cleared via RAII NUM_SEQ_STATES }; @@ -141,7 +143,9 @@ namespace Sequencer { {SEQ_RUNNING, "RUNNING"}, {SEQ_STOPPING, "STOPPING"}, {SEQ_PAUSED, "PAUSED"}, - {SEQ_STARTING, "STARTING"} + {SEQ_STARTING, "STARTING"}, + {SEQ_FAILED, "FAILED"}, + {SEQ_ABORTING, "ABORTING"} }; /** @@ -160,7 +164,9 @@ namespace Sequencer { SEQ_WAIT_SLIT, ///< set when waiting for slit SEQ_WAIT_TCS, ///< set when waiting for tcs // states - SEQ_WAIT_ACQUIRE, ///< set when waiting for acquire + SEQ_WAIT_MOVETO, ///< set when waiting for move-to-target + SEQ_WAIT_ACAM_ACQUIRE, ///< set when waiting for ACAM acquire + SEQ_WAIT_FINEACQUIRE, ///< set when waiting for slicecam fineacquire SEQ_WAIT_EXPOSE, ///< set when waiting for camera exposure SEQ_WAIT_READOUT, ///< set when waiting for camera readout SEQ_WAIT_TCSOP, ///< set when waiting specifically for tcs operator @@ -170,21 +176,23 @@ namespace Sequencer { const std::map wait_state_names = { // daemons - {SEQ_WAIT_ACAM, "ACAM"}, - {SEQ_WAIT_CALIB, "CALIB"}, - {SEQ_WAIT_CAMERA, "CAMERA"}, - {SEQ_WAIT_FLEXURE, "FLEXURE"}, - {SEQ_WAIT_FOCUS, "FOCUS"}, - {SEQ_WAIT_POWER, "POWER"}, - {SEQ_WAIT_SLICECAM, "SLICECAM"}, - {SEQ_WAIT_SLIT, "SLIT"}, - {SEQ_WAIT_TCS, "TCS"}, + {SEQ_WAIT_ACAM, "ACAM"}, + {SEQ_WAIT_CALIB, "CALIB"}, + {SEQ_WAIT_CAMERA, "CAMERA"}, + {SEQ_WAIT_FLEXURE, "FLEXURE"}, + {SEQ_WAIT_FOCUS, "FOCUS"}, + {SEQ_WAIT_POWER, "POWER"}, + {SEQ_WAIT_SLICECAM, "SLICECAM"}, + {SEQ_WAIT_SLIT, "SLIT"}, + {SEQ_WAIT_TCS, "TCS"}, // states - {SEQ_WAIT_ACQUIRE, "ACQUIRE"}, - {SEQ_WAIT_EXPOSE, "EXPOSE"}, - {SEQ_WAIT_READOUT, "READOUT"}, - {SEQ_WAIT_TCSOP, "TCSOP"}, - {SEQ_WAIT_USER, "USER"} + {SEQ_WAIT_MOVETO, "MOVETO"}, + {SEQ_WAIT_ACAM_ACQUIRE, "ACAM_ACQUIRE"}, + {SEQ_WAIT_FINEACQUIRE, "FINEACQUIRE"}, + {SEQ_WAIT_EXPOSE, "EXPOSE"}, + {SEQ_WAIT_READOUT, "READOUT"}, + {SEQ_WAIT_TCSOP, "TCSOP"}, + {SEQ_WAIT_USER, "USER"} }; /** @@ -650,6 +658,11 @@ namespace Sequencer { void publish_daemonstate(); void publish_threadstate(); + /** @brief publishes a narrative operator message on Topic::BROADCAST and logs it */ + void broadcast( const std::string &function, + const std::string &severity, + const std::string &message ); + std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint std::string publisher_topic; ///< my default topic for publishing diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 7da563c8..e794bd77 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -20,7 +20,7 @@ namespace Sequencer { std::string reply; ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM_ACQUIRE ); // form and send the ACQUIRE command to ACAM // @@ -29,17 +29,17 @@ namespace Sequencer { double angle_in = this->target.slitangle; if ( std::isnan(ra_in) || std::isnan(dec_in) ) { - this->async.enqueue_and_log( function, "ERROR converting target coordinates to decimal" ); + this->broadcast( function, Severity::ERROR, "converting target coordinates to decimal" ); return ERROR; } std::ostringstream cmd; cmd << ACAMD_ACQUIRE << " " << ra_in << " " << dec_in << " " << angle_in; - this->async.enqueue_and_log( function, "NOTICE: starting ACAM acquisition" ); + this->broadcast( function, Severity::NOTICE, "starting ACAM acquisition" ); if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { - this->async.enqueue_and_log( function, "ERROR sending acquire command to acamd" ); + this->broadcast( function, Severity::ERROR, "sending acquire command to acamd" ); return ERROR; } @@ -58,7 +58,7 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_acam_guiding.load()) { - this->async.enqueue_and_log(function, "ERROR ACAM acquisition timed out!"); + this->broadcast( function, Severity::ERROR, "ACAM acquisition timed out!" ); return TIMEOUT; } @@ -77,12 +77,12 @@ namespace Sequencer { long Sequence::do_slicecam_fineacquire() { const std::string function("Sequencer::Sequence::do_slicecam_fineacquire"); - ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_ACQUIRE); + ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_FINEACQUIRE); // TODO don't hard-code the arguments here: std::string reply; if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start L", reply ) != NO_ERROR) { - this->async.enqueue_and_log(function, "ERROR starting slicecam fine acquisition"); + this->broadcast( function, Severity::ERROR, "starting slicecam fine acquisition" ); return ERROR; } @@ -91,7 +91,7 @@ namespace Sequencer { return ERROR; } - this->async.enqueue_and_log(function, "NOTICE: slicecam fine acquisition started"); + this->broadcast( function, Severity::NOTICE, "slicecam fine acquisition started" ); const bool use_timeout = ( this->acquisition_timeout > 0 ); const auto timeout_time = std::chrono::steady_clock::now() @@ -108,7 +108,7 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_fineacquire_locked.load()) { - this->async.enqueue_and_log(function, "ERROR slicecam fine acquisition timed out!"); + this->broadcast( function, Severity::ERROR, "slicecam fine acquisition timed out!" ); return TIMEOUT; } @@ -132,7 +132,7 @@ namespace Sequencer { // ---------- ACAM acquire ----------------------------- // if ( this->do_acam_acquire() != NO_ERROR ) { - this->async.enqueue_and_log(caller, "WARNING acam acquisition failed"); + this->broadcast( caller, Severity::WARNING, "acam acquisition failed" ); // on ACAM acquisition failure wait for user to continue or cancel if ( this->wait_for_user(caller) == ABORT ) return ABORT; @@ -143,7 +143,7 @@ namespace Sequencer { // ---------- SLICECAM fineacquire --------------------- // if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->async.enqueue_and_log(caller, "WARNING slicecam fine acquisition failed"); + this->broadcast( caller, Severity::WARNING, "slicecam fine acquisition failed" ); } return NO_ERROR; diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp index 711151fc..a9f0a459 100644 --- a/sequencerd/sequence_wait.cpp +++ b/sequencerd/sequence_wait.cpp @@ -21,7 +21,7 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - this->async.enqueue_and_log(caller, "NOTICE: waiting for TCS operator to send 'ontarget' signal"); + this->broadcast( caller, Severity::NOTICE, "waiting for TCS operator to send 'ontarget' signal" ); while ( !this->cancel_flag.load() && !this->is_ontarget.load() ) { @@ -31,7 +31,7 @@ namespace Sequencer { this->cancel_flag.load() ); } ); } - this->async.enqueue_and_log(caller, "NOTICE: received " + this->broadcast( caller, Severity::NOTICE, "received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); @@ -56,20 +56,20 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - this->async.enqueue_and_log( caller, "NOTICE: waiting for USER to send 'continue' signal" ); + this->broadcast( caller, Severity::NOTICE, "waiting for USER to send 'continue' signal" ); while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); } - this->async.enqueue_and_log( caller, "NOTICE: received " + this->broadcast( caller, Severity::NOTICE, "received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) +" signal!" ); } // end scope for wait_state = WAIT_USER if ( this->cancel_flag.load() ) { - this->async.enqueue_and_log( caller, "NOTICE: sequence cancelled" ); + this->broadcast( caller, Severity::NOTICE, "sequence cancelled" ); return ABORT; } @@ -97,7 +97,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: exposure cancelled"); + this->broadcast( caller, Severity::NOTICE, "exposure cancelled" ); return ABORT; } @@ -128,7 +128,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for readout cancelled"); + this->broadcast( caller, Severity::NOTICE, "wait for readout cancelled" ); return ABORT; } @@ -146,7 +146,7 @@ namespace Sequencer { */ long Sequence::wait_for_canexpose(std::string caller) { - this->async.enqueue_and_log(caller, "NOTICE: waiting for camera to be ready to expose"); + this->broadcast( caller, Severity::NOTICE, "waiting for camera to be ready to expose" ); while ( !this->cancel_flag.load() && !this->can_expose.load() ) { @@ -157,7 +157,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->async.enqueue_and_log(caller, "NOTICE: wait for can_expose cancelled"); + this->broadcast( caller, Severity::NOTICE, "wait for can_expose cancelled" ); return ABORT; } From 1750162994de782de54eb311d93fad72876b025a Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 9 Apr 2026 09:18:55 -0700 Subject: [PATCH 20/37] fixes typo in slicecam for fine acquisition (#405) --- slicecamd/slicecam_interface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index fca6d53f..7f26c795 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -754,7 +754,7 @@ namespace Slicecam { applied++; } else - if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT=" ) { + if ( config.param[entry] == "FINE_ACQUIRE_AIMPOINT" ) { std::string which; double x,y; std::istringstream iss(config.arg[entry]); @@ -766,7 +766,7 @@ namespace Slicecam { this->fineacquire_state.aimpoint = { x, y }; } else - if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND=" ) { + if ( config.param[entry] == "FINE_ACQUIRE_BACKGROUND" ) { long x1, x2, y1, y2; std::istringstream iss(config.arg[entry]); if (!(iss >> x1 >> x2 >> y1 >> y2)) { From 10c5f28cbbb63acc79e7577c6e09743d2d607e12 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 29 Apr 2026 19:15:53 -0700 Subject: [PATCH 21/37] Integrates new pubsub model taking place in parallel, which replaces UDP broadcast with a ZMQ publisher and uses PUB-SUB to send/get status automatically instead of having to ask for it. These changes are not pertinent to this feature branch but are being included so that this branch keeps up with other progress. --- Config/sequencerd.cfg.in | 10 -- acamd/acam_interface.cpp | 24 +-- common/common.cpp | 31 ++++ common/common.h | 51 +++++++ common/message_keys.h | 12 +- flexured/flexure_interface.cpp | 4 +- sequencerd/sequence.cpp | 221 ++++++++++++---------------- sequencerd/sequence.h | 8 +- sequencerd/sequence_acquisition.cpp | 18 +-- sequencerd/sequence_wait.cpp | 18 +-- sequencerd/sequencer_server.cpp | 25 ---- slicecamd/slicecam_interface.cpp | 10 +- tcsd/tcs_interface.cpp | 81 ++++++---- tcsd/tcs_interface.h | 7 +- tcsd/tcsd.cpp | 4 + 15 files changed, 285 insertions(+), 239 deletions(-) diff --git a/Config/sequencerd.cfg.in b/Config/sequencerd.cfg.in index 61d81707..636b86d1 100644 --- a/Config/sequencerd.cfg.in +++ b/Config/sequencerd.cfg.in @@ -190,16 +190,6 @@ CAL_TARGET=(SCIENCE close open on on on on off off off off off off # UPDATE_TIME_REMAINING=10 # block updates to active target with less than this exptime (sec) remaining -# ----------------------------------------------------------------------------- -# TELEM_PROVIDER=( ) -# -# This is a list of telemetry providers where is the daemon name, -# and is the port on which to send the telemetry request. -# Provide one per line. -# -TELEM_PROVIDER=(slitd @SLITD_NB_PORT@) -TELEM_PROVIDER=(tcsd @TCSD_NB_PORT@) -TELEM_PROVIDER=(camerad @CAMERAD_NB_PORT@) # SUBSCRIBE_TO=(slitd "tcp://127.0.0.1:@SLITD_PUB_PORT@") SUBSCRIBE_TO=(tcsd "tcp://127.0.0.1:@TCSD_PUB_PORT@") diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index f19fbfcf..86df30be 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -1605,18 +1605,18 @@ namespace Acam { } // extract and store values in the class // - Common::extract_telemetry_value( jmessage, "TCSNAME", telem.tcsname ); - Common::extract_telemetry_value( jmessage, "ISOPEN", telem.is_tcs_open ); - Common::extract_telemetry_value( jmessage, "CASANGLE", telem.angle_scope ); - Common::extract_telemetry_value( jmessage, "TELRA", telem.ra_scope_hms ); - Common::extract_telemetry_value( jmessage, "TELDEC", telem.dec_scope_dms ); - Common::extract_telemetry_value( jmessage, "RA", telem.ra_scope_h ); - Common::extract_telemetry_value( jmessage, "DEC", telem.dec_scope_d ); - Common::extract_telemetry_value( jmessage, "RAOFFSET", telem.offsetra ); - Common::extract_telemetry_value( jmessage, "DECLOFFS", telem.offsetdec ); - Common::extract_telemetry_value( jmessage, "AZ", telem.az ); - Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); - Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); + Common::extract_telemetry_value( jmessage, "TCSNAME", telem.tcsname ); + Common::extract_telemetry_value( jmessage, "ISOPEN", telem.is_tcs_open ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::CASANGLE, telem.angle_scope ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, telem.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELDEC, telem.dec_scope_dms ); + Common::extract_telemetry_value( jmessage, "RA", telem.ra_scope_h ); + Common::extract_telemetry_value( jmessage, "DEC", telem.dec_scope_d ); + Common::extract_telemetry_value( jmessage, "RAOFFSET", telem.offsetra ); + Common::extract_telemetry_value( jmessage, "DECLOFFS", telem.offsetdec ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AZ, telem.az ); + Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AIRMASS, telem.airmass ); // save them to the database // diff --git a/common/common.cpp b/common/common.cpp index 3fc176eb..464fd76d 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -9,6 +9,37 @@ namespace Common { + /***** Common::Broadcaster::emit ********************************************/ + /** + * @brief logs a narrative message and publishes it on Topic::BROADCAST + * @param[in] function name of caller (used for log line) + * @param[in] severity one of Severity::NOTICE, Severity::WARNING, Severity::ERROR + * @param[in] message operator-facing narrative text + * @details Logs message via logwrite, then publishes a JSON payload + * on Topic::BROADCAST if the publisher has been initialized. + * + */ + void Broadcaster::emit( const std::string &function, + const std::string &severity, + const std::string &message ) { + logwrite( function, severity+": "+message ); + + if ( ! this->publisher ) return; + nlohmann::json jmessage; + jmessage[Key::SOURCE] = this->source; + jmessage[Key::Broadcast::SEVERITY] = severity; + jmessage[Key::Broadcast::MESSAGE] = message; + + try { + this->publisher->publish( jmessage, Topic::BROADCAST ); + } + catch ( const std::exception &e ) { + logwrite( function, "ERROR publishing broadcast: "+std::string(e.what()) ); + } + } + /***** Common::Broadcaster::emit ********************************************/ + + /***** Common::collect_telemetry ********************************************/ /** * @brief send the TELEMREQUEST command to daemon to get telemetry diff --git a/common/common.h b/common/common.h index d540dbd3..b8c768f9 100644 --- a/common/common.h +++ b/common/common.h @@ -22,6 +22,7 @@ #include "logentry.h" #include "network.h" +#include "message_keys.h" const long NOTHING = -1; const long NO_ERROR = 0; @@ -369,6 +370,56 @@ namespace Common { }; + /**************** Common::Broadcaster ***************************************/ + /** + * @class Broadcaster + * @brief logs a narrative message and publishes it on Topic::BROADCAST + * @details Captures a reference to a publisher and the source daemon name + * at construction time so that call sites need only supply the + * caller function name and message (and severity, for emit). + * The publisher reference is to the daemon's Common::PubSub, which + * may be null at the time this Broadcaster is constructed and gets + * populated later by init_pubsub. + * + */ + class Broadcaster { + private: + const std::unique_ptr &publisher; ///< reference to owner's publisher + std::string source; ///< source daemon name + + public: + Broadcaster( const std::unique_ptr &publisher, + std::string source ) + : publisher(publisher), source(std::move(source)) { } + + /** + * @brief publish a NOTICE severity broadcast + */ + inline void notice( const std::string &function, const std::string &message ) { + this->emit( function, Severity::NOTICE, message ); + } + + /** + * @brief publish a WARNING severity broadcast + */ + inline void warning( const std::string &function, const std::string &message ) { + this->emit( function, Severity::WARNING, message ); + } + + /** + * @brief publish an ERROR severity broadcast + */ + inline void error( const std::string &function, const std::string &message ) { + this->emit( function, Severity::ERROR, message ); + } + + void emit( const std::string &function, + const std::string &severity, + const std::string &message ); + }; + /**************** Common::Broadcaster ***************************************/ + + void collect_telemetry(const std::pair &provider, std::string &retstring); /***** Common::extract_telemetry_value **************************************/ diff --git a/common/message_keys.h b/common/message_keys.h index 12ef41c7..ef738a7e 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -98,12 +98,12 @@ namespace Key { } namespace Tcsd { - inline const std::string TELRA = "telra"; - inline const std::string TELDEC = "teldec"; - inline const std::string ALT = "alt"; - inline const std::string AZ = "az"; - inline const std::string AIRMASS = "airmass"; - inline const std::string CASANGLE = "casangle"; + inline const std::string TELRA = "TELRA"; + inline const std::string TELDEC = "TELDEC"; + inline const std::string ALT = "ALT"; + inline const std::string AZ = "AZ"; + inline const std::string AIRMASS = "AIRMASS"; + inline const std::string CASANGLE = "CASANGLE"; } } diff --git a/flexured/flexure_interface.cpp b/flexured/flexure_interface.cpp index 55288f71..626eaa9c 100644 --- a/flexured/flexure_interface.cpp +++ b/flexured/flexure_interface.cpp @@ -520,8 +520,8 @@ namespace Flexure { else if ( messagetype == "tcsinfo" ) { double casangle=NAN, alt=NAN; - Common::extract_telemetry_value( message_in, "CASANGLE", casangle ); - Common::extract_telemetry_value( message_in, "ALT", alt ); + Common::extract_telemetry_value( message_in, Key::Tcsd::CASANGLE, casangle ); + Common::extract_telemetry_value( message_in, Key::Tcsd::ALT, alt ); message.str(""); message << "casangle=" << casangle << " alt=" << alt; logwrite( function, message.str() ); } diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 35599995..d0b164fd 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -532,22 +532,22 @@ namespace Sequencer { // Only READY or NOTREADY states allow a new sequence. if ( seq_state_manager.is_set(Sequencer::SEQ_RUNNING) ) { - this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer already running" ); + this->broadcast.error( function, "sequence rejected: sequencer already running" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_STOPPING) ) { - this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is stopping" ); + this->broadcast.error( function, "sequence rejected: sequencer is stopping" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_STARTING) ) { - this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is starting" ); + this->broadcast.error( function, "sequence rejected: sequencer is starting" ); return ERROR; } if ( seq_state_manager.is_set(Sequencer::SEQ_PAUSED) ) { - this->broadcast( function, Severity::ERROR, "sequence rejected: sequencer is paused" ); + this->broadcast.error( function, "sequence rejected: sequencer is paused" ); return ERROR; } @@ -555,7 +555,7 @@ namespace Sequencer { // Sequence must not be empty. if ( sequence.empty() ) { - this->broadcast( function, Severity::ERROR, "sequence is empty" ); + this->broadcast.error( function, "sequence is empty" ); return ERROR; } @@ -564,7 +564,7 @@ namespace Sequencer { for (const auto &group : sequence) { if ( group.operations.empty() ) { - this->broadcast( function, Severity::ERROR, "sequence contains an empty operation group" ); + this->broadcast.error( function, "sequence contains an empty operation group" ); return ERROR; } } @@ -593,7 +593,7 @@ namespace Sequencer { // If expose is present, camera_set must also be present. if ( has_expose && !has_camera_set ) { - this->broadcast( function, Severity::ERROR, "sequence contains 'expose' without 'camera_set'" ); + this->broadcast.error( function, "sequence contains 'expose' without 'camera_set'" ); error = ERROR; } @@ -627,7 +627,7 @@ namespace Sequencer { oss << "sequence requires daemon '" << daemon_name.at(static_cast(bit)) << "' but it is not ready"; - this->broadcast(function, Severity::ERROR, oss.str()); + this->broadcast.error(function, oss.str()); error = ERROR; } } @@ -635,7 +635,7 @@ namespace Sequencer { else // for science mode (non-engineering) all subsystems must be ready if ( !seq_state_manager.is_set(Sequencer::SEQ_READY) ) { - this->broadcast( function, Severity::ERROR, "sequence requires SEQ_READY (all daemons ready) in science mode" ); + this->broadcast.error( function, "sequence requires SEQ_READY (all daemons ready) in science mode" ); error = ERROR; } } @@ -665,7 +665,7 @@ namespace Sequencer { oss << "parallel group contains unsafe pair: '" << thread_names.at(group.operations[i].thr) << "' and '" << thread_names.at(group.operations[j].thr) << "'"; - this->broadcast(function, Severity::ERROR, oss.str()); + this->broadcast.error(function, oss.str()); error = ERROR; } } @@ -687,7 +687,7 @@ namespace Sequencer { << thread_names.at(all_thrs[i]) << "' and '" << thread_names.at(all_thrs[j]) << "' (use 'engineering true' to allow in SERIAL groups)"; - this->broadcast(function, Severity::ERROR, oss.str()); + this->broadcast.error(function, oss.str()); error = ERROR; } } @@ -699,7 +699,7 @@ namespace Sequencer { // TBD should I allow expose to not appear more than once across all groups? if ( expose_count > 1 ) { - this->broadcast( function, Severity::NOTICE, "sequence contains multiple 'expose' operations -- placeholder check" ); + this->broadcast.notice( function, "sequence contains multiple 'expose' operations -- placeholder check" ); // not a hard error yet; pending clarification of multi-expose DSL usage } @@ -714,7 +714,7 @@ namespace Sequencer { if (false) { // stub -- replace with real contention check - this->broadcast(function, Severity::NOTICE, + this->broadcast.notice(function, "PLACEHOLDER parallel contention check not yet implemented"); } @@ -1058,37 +1058,6 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_seqstate ******************************/ - /***** Sequencer::Sequence::broadcast ***************************************/ - /** - * @brief logs a narrative message and publishes it on Topic::BROADCAST - * @param[in] function name of caller function (for log) - * @param[in] severity one of Severity::NOTICE, Severity::WARNING, Severity::ERROR - * @param[in] message the operator-facing narrative text - * @details This replaces the legacy pattern of enqueuing narrative - * strings onto the UDP async queue. Messages are routed through - * the PUB-SUB broadcast topic instead, where the GUI message - * area subscribes. Logging behavior is preserved. - */ - void Sequence::broadcast( const std::string &function, - const std::string &severity, - const std::string &message ) { - logwrite( function, severity+": "+message ); - if ( this->publisher ) { - nlohmann::json jmessage; - jmessage[Key::Broadcast::SEVERITY] = severity; - jmessage[Key::Broadcast::MESSAGE] = message; - jmessage[Key::SOURCE] = Daemon::SEQUENCER; - try { - this->publisher->publish( jmessage, Topic::BROADCAST ); - } - catch ( const std::exception &e ) { - logwrite( function, "ERROR publishing broadcast: "+std::string(e.what()) ); - } - } - } - /***** Sequencer::Sequence::broadcast ***************************************/ - - /***** Sequencer::Sequence::broadcast_waitstate *****************************/ /** * @brief publishes wait_state via PUB-SUB @@ -1239,14 +1208,14 @@ namespace Sequencer { // The Sequencer can only be started once // if ( thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { - this->broadcast( function, Severity::ERROR, "sequencer already running" ); + this->broadcast.error( function, "sequencer already running" ); return; } // The Sequencer can only be started when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->broadcast( function, Severity::ERROR, "cannot start: system not ready" ); + this->broadcast.error( function, "cannot start: system not ready" ); return; } @@ -1337,7 +1306,7 @@ namespace Sequencer { } else if ( targetstate == TargetInfo::TARGET_ERROR ) { // request stop on error - this->broadcast( function, Severity::ERROR, "getting next target. stopping" ); + this->broadcast.error( function, "getting next target. stopping" ); break; } @@ -1351,12 +1320,10 @@ namespace Sequencer { } if (is_cancelled()) { - this->broadcast( function, Severity::NOTICE, "sequence cancelled" ); + this->broadcast.notice( function, "sequence cancelled" ); break; } -// this->request_status(tcsd); // force tcsd to publish his status TODO WORK-IN-PROGRESS - // Update this target's state in the database // if (error==NO_ERROR) error = this->target.update_state( Sequencer::TARGET_COMPLETE ); @@ -1447,14 +1414,14 @@ namespace Sequencer { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); /*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\": "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } if (!deactivechans.str().empty()) { std::string cmd = CAMERAD_DEACTIVATE + deactivechans.str(); if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\": "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } @@ -1467,7 +1434,7 @@ namespace Sequencer { long exptime_msec = (long)( this->target.exptime_req * 1000 ); camcmd.str(""); camcmd << CAMERAD_EXPTIME << " " << exptime_msec; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); + this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1475,12 +1442,12 @@ namespace Sequencer { // camcmd.str(""); camcmd << CAMERAD_BIN << " spat " << this->target.binspat; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); + this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } camcmd.str(""); camcmd << CAMERAD_BIN << " spec " << this->target.binspect; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast( function, Severity::ERROR, "sending \""+camcmd.str()+"\": "+reply ); + this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1541,7 +1508,7 @@ namespace Sequencer { logwrite( function, "moving slit to "+slitcmd.str()+" for "+modestr+"position" ); if ( this->slitd.command_timeout( slitcmd.str(), reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "setting slit" ); + this->broadcast.error( function, "setting slit" ); this->thread_error_manager.set( THR_SLIT_SET ); throw std::runtime_error("slit returned: "+reply); } @@ -1567,7 +1534,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_POWER ); // powerd not ready if ( this->reopen_hardware(this->powerd, POWERD_REOPEN, 10000 ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing power control" ); + this->broadcast.error( function, "initializing power control" ); throw std::runtime_error("could not initialize power control"); } @@ -1619,13 +1586,13 @@ namespace Sequencer { this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success if ( this->set_power_switch(ON, POWER_SLIT, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering slit hardware" ); + this->broadcast.error( function, "powering slit hardware" ); throw std::runtime_error("could not power slit hardware"); } bool was_opened=false; if ( this->open_hardware(this->slitd, was_opened) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "connecting to slit" ); + this->broadcast.error( function, "connecting to slit" ); throw std::runtime_error("could not open connection to slit hardware"); } @@ -1634,7 +1601,7 @@ namespace Sequencer { bool ishomed=false; std::string reply; if ( this->slitd.command( SLITD_ISHOME, reply ) ) { - this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); + this->broadcast.error( function, "communicating with slit hardware" ); throw std::runtime_error("could not communicate with slit hardware: "+reply); } this->parse_state( function, reply, ishomed ); @@ -1644,7 +1611,7 @@ namespace Sequencer { if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->slitd.command_timeout( SLITD_HOME, reply, SLITD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "communicating with slit hardware" ); + this->broadcast.error( function, "communicating with slit hardware" ); throw std::runtime_error("could not home slit hardware: "+reply); } } @@ -1654,7 +1621,7 @@ namespace Sequencer { if ( was_opened && !this->config_init["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_init["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); + this->broadcast.error( function, "sending \""+cmd+"\" to slit" ); throw std::runtime_error("slit "+cmd+" returned: "+reply); } } @@ -1708,7 +1675,7 @@ namespace Sequencer { if (error==NO_ERROR && !this->config_shutdown["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_shutdown["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to slit" ); + this->broadcast.error( function, "sending \""+cmd+"\" to slit" ); throw std::runtime_error(cmd+" returned: "+reply); } } @@ -1718,7 +1685,7 @@ namespace Sequencer { logwrite( function, "closing slit hardware" ); error = this->slitd.command( SLITD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "closing connection to slit hardware" ); + this->broadcast.error( function, "closing connection to slit hardware" ); throw std::runtime_error("closing slit connection returned: "+reply); } @@ -1755,14 +1722,14 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_SLICECAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing slicecam control" ); + this->broadcast.error( function, "initializing slicecam control" ); throw std::runtime_error("could not power slicecam hardware"); } // open connection is all that is needed, slicecamd takes care of everything // if ( this->open_hardware(this->slicecamd, SLICECAMD_OPEN, SLICECAMD_OPEN_TIMEOUT) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "starting slicecam" ); + this->broadcast.error( function, "starting slicecam" ); throw SlicecamException("could not start slicecam"); } @@ -1795,7 +1762,7 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_ACAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering acam hardware" ); + this->broadcast.error( function, "powering acam hardware" ); throw std::runtime_error("could not power acam hardware"); } @@ -1803,7 +1770,7 @@ namespace Sequencer { // bool was_opened=false; if ( this->open_hardware(this->acamd, ACAMD_OPEN, ACAMD_OPEN_TIMEOUT, was_opened) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "opening acam camera" ); + this->broadcast.error( function, "opening acam camera" ); throw AcamException(ErrorCode::ERROR_ACAM_CAMERA, "could not open acam camera"); } @@ -1814,14 +1781,14 @@ namespace Sequencer { if ( ! this->config_init["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_init["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_init["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_init["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1870,14 +1837,14 @@ namespace Sequencer { } if ( (error=this->connect_to_daemon(this->slicecamd)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "connecting to slicecamd" ); + this->broadcast.error( function, "connecting to slicecamd" ); } // close connections between slicecamd and the hardware with which it communicates // logwrite( function, "closing slicecam hardware" ); if ( (error=this->slicecamd.command_timeout( SLICECAMD_SHUTDOWN, reply, SLICECAMD_SHUTDOWN_TIMEOUT )) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "closing connection to slicecam hardware" ); + this->broadcast.error( function, "closing connection to slicecam hardware" ); } // disconnect me from slicecamd, irrespective of any previous error @@ -1888,7 +1855,7 @@ namespace Sequencer { // Turn off power to slicecam hardware. // if ( this->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "switching off slicecam" ); + this->broadcast.error( function, "switching off slicecam" ); throw std::runtime_error("could not power off slicecam hardware"); } @@ -1928,14 +1895,14 @@ namespace Sequencer { if ( ! this->config_shutdown["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_shutdown["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_shutdown["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_shutdown["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to acamd: "+reply ); + this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1948,7 +1915,7 @@ namespace Sequencer { if ( error==NO_ERROR ) { logwrite( function, "closing acam hardware" ); error = this->acamd.command_timeout( ACAMD_SHUTDOWN, ACAMD_SHUTDOWN_TIMEOUT ); - if ( error != NO_ERROR ) this->broadcast( function, Severity::ERROR, "shutting down acam" ); + if ( error != NO_ERROR ) this->broadcast.error( function, "shutting down acam" ); } // disconnect me from acamd, irrespective of any previous error @@ -1959,7 +1926,7 @@ namespace Sequencer { // Turn off power to acam hardware. // if ( this->set_power_switch(OFF, POWER_ACAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "switching off acam" ); + this->broadcast.error( function, "switching off acam" ); throw std::runtime_error("could not switch off acam"); } @@ -1989,14 +1956,14 @@ namespace Sequencer { // make sure calib hardware is powered if ( this->set_power_switch(ON, POWER_CALIB, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering focus control" ); + this->broadcast.error( function, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to calibd bool was_opened=false; if ( this->open_hardware(this->calibd, was_opened) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing calib control" ); + this->broadcast.error( function, "initializing calib control" ); throw std::runtime_error("could not power calib control"); } @@ -2008,14 +1975,14 @@ namespace Sequencer { std::string reply; long error = this->calibd.command( CALIBD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); + this->broadcast.error( function, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } // home calib actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->calibd.command_timeout( CALIBD_HOME, reply, CALIBD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "communicating with calib hardware" ); + this->broadcast.error( function, "communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } } @@ -2028,7 +1995,7 @@ namespace Sequencer { if ( !this->config_init["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_init["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); + this->broadcast.error( function, "moving calib door and/or cover" ); throw std::runtime_error("could not move calib door and/or cover"); } } @@ -2065,7 +2032,7 @@ namespace Sequencer { // bool poweron=false; if ( check_power_switch(ON, POWER_CALIB, poweron ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "checking calib power switch" ); + this->broadcast.error( function, "checking calib power switch" ); throw std::runtime_error("checking calib power switch"); } @@ -2083,7 +2050,7 @@ namespace Sequencer { if ( !this->config_shutdown["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_shutdown["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); + this->broadcast.error( function, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } } @@ -2107,14 +2074,14 @@ namespace Sequencer { // Turn off power to calib hardware. // if ( this->set_power_switch(OFF, POWER_CALIB, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "switching off calib hardware" ); + this->broadcast.error( function, "switching off calib hardware" ); error=ERROR; } // always turn off power to lamps // if ( this->set_power_switch(OFF, POWER_LAMP, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering off lamps" ); + this->broadcast.error( function, "powering off lamps" ); error=ERROR; } @@ -2148,7 +2115,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_TCS ); // tcsd not ready if ( this->open_hardware(this->tcsd) != NO_ERROR ) { - this->broadcast( "Sequencer::Sequence::tcs_init", Severity::ERROR, "initializing TCS" ); + this->broadcast.error( "Sequencer::Sequence::tcs_init", "initializing TCS" ); this->thread_error_manager.set( THR_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -2192,7 +2159,7 @@ namespace Sequencer { std::string reply; error = this->tcsd.send( TCSD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "closing connection to TCS" ); + this->broadcast.error( function, "closing connection to TCS" ); throw std::runtime_error("closing TCS connection: "+reply); } } @@ -2226,13 +2193,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_FLEXURE, std::chrono::seconds(21)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering flexure control" ); + this->broadcast.error( function, "powering flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not power flexure control"); } if ( this->open_hardware(this->flexured) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing flexure control" ); + this->broadcast.error( function, "initializing flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -2279,7 +2246,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->flexured) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "connecting to flexure hardware" ); + this->broadcast.error( function, "connecting to flexure hardware" ); error=ERROR; } @@ -2288,7 +2255,7 @@ namespace Sequencer { // logwrite( function, "closing flexure hardware" ); if (error==NO_ERROR && (error=this->flexured.command( FLEXURED_CLOSE, reply )) != NO_ERROR) { - this->broadcast( function, Severity::ERROR, "closing connection to flexure hardware" ); + this->broadcast.error( function, "closing connection to flexure hardware" ); } // disconnect me from flexured, irrespective of any previous error @@ -2299,7 +2266,7 @@ namespace Sequencer { // Turn off power to flexure hardware. // if ( this->set_power_switch(OFF, POWER_FLEXURE, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "switching off flexure" ); + this->broadcast.error( function, "switching off flexure" ); throw std::runtime_error("switching off flexure hardware"); } @@ -2326,14 +2293,14 @@ namespace Sequencer { this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success if ( this->set_power_switch(ON, POWER_FOCUS, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering focus control" ); + this->broadcast.error( function, "powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to focusd bool was_opened=false; if ( this->open_hardware(this->focusd, was_opened) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing focus control" ); + this->broadcast.error( function, "initializing focus control" ); throw std::runtime_error("could not open focus hardware"); } @@ -2345,14 +2312,14 @@ namespace Sequencer { std::string reply; long error = this->focusd.command( FOCUSD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); + this->broadcast.error( function, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_ISHOME+" returned: "+reply); } // home focus actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->focusd.command_timeout( FOCUSD_HOME, reply, FOCUSD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "communicating with focus hardware" ); + this->broadcast.error( function, "communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_HOME+" returned: "+reply); } } @@ -2362,7 +2329,7 @@ namespace Sequencer { for ( const auto &chan : chans ) { std::string command = "set " + chan + " nominal"; if ( this->focusd.command_timeout( command, reply, FOCUSD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "setting focus "+chan ); + this->broadcast.error( function, "setting focus "+chan ); throw std::runtime_error("focus "+command+" returned: "+reply); } } @@ -2408,7 +2375,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->focusd) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "connecting to focus hardware" ); + this->broadcast.error( function, "connecting to focus hardware" ); error=ERROR; } @@ -2417,7 +2384,7 @@ namespace Sequencer { // logwrite( function, "closing focus hardware" ); if (error==NO_ERROR && (error=this->focusd.command( FOCUSD_CLOSE, reply )) != NO_ERROR) { - this->broadcast( function, Severity::ERROR, "closing connection to focus hardware" ); + this->broadcast.error( function, "closing connection to focus hardware" ); } // disconnect me from focusd, irrespective of any previous error @@ -2428,7 +2395,7 @@ namespace Sequencer { // Turn off power to focus hardware. // if ( this->set_power_switch(OFF, POWER_FOCUS, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "switching off focus" ); + this->broadcast.error( function, "switching off focus" ); throw std::runtime_error("switching off focus hardware"); } @@ -2456,13 +2423,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering camera" ); + this->broadcast.error( function, "powering camera" ); throw std::runtime_error("switching on camera"); } bool was_opened=false; if ( this->open_hardware(this->camerad, "open", 12000, was_opened) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "initializing camera" ); + this->broadcast.error( function, "initializing camera" ); throw std::runtime_error("initializing camera"); } @@ -2472,7 +2439,7 @@ namespace Sequencer { if ( was_opened) { for ( const auto &cmd : this->camera_prologue ) { if ( this->camerad.command_timeout( cmd, reply, CAMERA_PROLOG_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending \""+cmd+"\" to camera" ); + this->broadcast.error( function, "sending \""+cmd+"\" to camera" ); throw std::runtime_error("sending \""+cmd+"\" to camera"); } } @@ -2543,7 +2510,7 @@ namespace Sequencer { // turn off power to camera hardware // if ( this->set_power_switch(OFF, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "powering off camera" ); + this->broadcast.error( function, "powering off camera" ); throw std::runtime_error("switching off camera"); } @@ -2615,7 +2582,7 @@ namespace Sequencer { // if ( this->target.ra_hms == this->last_ra_hms && this->target.dec_dms == this->last_dec_dms ) { - this->broadcast( function, Severity::NOTICE, "no move required for repeat target" ); + this->broadcast.notice( function, "no move required for repeat target" ); return NO_ERROR; } @@ -2639,7 +2606,7 @@ namespace Sequencer { if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } message << " to decimal"; - this->broadcast( function, Severity::ERROR, ""+message.str() ); + this->broadcast.error( function, ""+message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2711,14 +2678,14 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - this->broadcast( function, Severity::NOTICE, "waiting for TCS operator to send \"ontarget\" signal" ); + this->broadcast.notice( function, "waiting for TCS operator to send \"ontarget\" signal" ); while ( !is_cancelled() && !this->is_ontarget.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_ontarget.load() || is_cancelled() ); } ); } - this->broadcast( function, Severity::NOTICE, "received " + this->broadcast.notice( function, "received " +(is_cancelled() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); } @@ -2790,7 +2757,7 @@ namespace Sequencer { // if ( std::isnan( radec_to_decimal( seq.target.ra_hms, ra_hms ) ) || std::isnan( radec_to_decimal( seq.target.dec_dms, dec_dms ) ) ) { - seq.broadcast( function, Severity::ERROR, "can't handle NaN value for RA, DEC" ); + seq.broadcast.error( function, "can't handle NaN value for RA, DEC" ); seq.thread_error_manager.set( THR_NOTIFY_TCS ); return; } @@ -2876,7 +2843,7 @@ namespace Sequencer { // const auto &calinfo = this->caltarget.get_info(calname); - this->broadcast( function, Severity::NOTICE, "configuring calibrator for "+calname ); + this->broadcast.notice( function, "configuring calibrator for "+calname ); // set the calib door and cover // @@ -2888,7 +2855,7 @@ namespace Sequencer { logwrite( function, "calib: "+cmd.str() ); if ( !is_cancelled() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "moving calib door and/or cover" ); + this->broadcast.error( function, "moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } @@ -2901,7 +2868,7 @@ namespace Sequencer { logwrite( function, message.str() ); std::string reply; if ( this->powerd.send( cmd.str(), reply ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, ""+message.str() ); + this->broadcast.error( function, ""+message.str() ); throw std::runtime_error("setting lamp "+message.str()); } } @@ -2925,13 +2892,13 @@ namespace Sequencer { if ( is_cancelled() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, ""+cmd.str() ); + this->broadcast.error( function, ""+cmd.str() ); throw std::runtime_error("setting lamp modulator "+cmd.str()); } } if ( is_cancelled() ) { - this->broadcast( function, Severity::NOTICE, "abort may have left calib system partially set" ); + this->broadcast.notice( function, "abort may have left calib system partially set" ); } this->thread_error_manager.clear( THR_CALIBRATOR_SET ); // success @@ -2988,7 +2955,7 @@ namespace Sequencer { // this->do_once.store(true); - this->broadcast( function, Severity::NOTICE, "cancel signal sent" ); + this->broadcast.notice( function, "cancel signal sent" ); } /***** Sequencer::Sequence::stop_exposure *********************************/ @@ -3005,7 +2972,7 @@ namespace Sequencer { // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->broadcast( function, Severity::NOTICE, "not currently exposing" ); + this->broadcast.notice( function, "not currently exposing" ); return; } @@ -3022,12 +2989,12 @@ namespace Sequencer { else if ( error == NOTHING ) { // if not exposing, this is a way to ensure WAIT_EXPOSE bit can be cleared - this->broadcast( function, Severity::NOTICE, "not exposing" ); + this->broadcast.notice( function, "not exposing" ); this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); } else if ( error == BUSY ) { - this->broadcast( function, Severity::NOTICE, "too late to stop exposure" ); + this->broadcast.notice( function, "too late to stop exposure" ); // can't stop in the last 5 sec so wait that long and it should stop on its own std::this_thread::sleep_for(std::chrono::seconds(5)); } @@ -3052,7 +3019,7 @@ namespace Sequencer { // can only repeat when state is READY // if ( ! seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->broadcast( function, Severity::ERROR, "cannot repeat: system not ready" ); + this->broadcast.error( function, "cannot repeat: system not ready" ); return ERROR; } @@ -3131,7 +3098,7 @@ namespace Sequencer { // if ( this->camerad.async( message.str() ) != NO_ERROR ) { // if ( this->camerad.send( message.str(), reply ) != NO_ERROR ) { if ( this->camerad.command_timeout( message.str(), reply, 30000 ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending camera "+message.str() ); + this->broadcast.error( function, "sending camera "+message.str() ); this->thread_error_manager.set( THR_TRIGGER_EXPOSURE ); // tell the world this thread had an error this->target.update_state( Sequencer::TARGET_PENDING ); // return the target state to pending this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); // clear EXPOSE bit @@ -3193,7 +3160,7 @@ namespace Sequencer { // This function is only used while exposing // if ( ! this->wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->broadcast( function, Severity::ERROR, "cannot update exposure time when not currently exposing" ); + this->broadcast.error( function, "cannot update exposure time when not currently exposing" ); error = ERROR; } @@ -3339,7 +3306,7 @@ namespace Sequencer { error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); if ( error != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "from power control. Will try to continue (but don't hold your breath)" ); + this->broadcast.error( function, "from power control. Will try to continue (but don't hold your breath)" ); } // container of shutdown threads to launch, @@ -4149,7 +4116,7 @@ namespace Sequencer { } // connection failed too many times if (attempt > maxattempts) { - broadcast( function, Severity::ERROR, "exceeded max attempts connecting to " + daemon.name ); + this->broadcast.error( function, "exceeded max attempts connecting to " + daemon.name ); return ERROR; } @@ -4158,7 +4125,7 @@ namespace Sequencer { error |= daemon.send( "isopen", reply ); error |= this->parse_state( function, reply, isopen ); if ( error != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "opening "+daemon.name+" hardware" ); + this->broadcast.error( function, "opening "+daemon.name+" hardware" ); return ERROR; } @@ -4168,7 +4135,7 @@ namespace Sequencer { logwrite( function, "opening "+daemon.name+" hardware connections with " +std::to_string(opentimeout)+" ms timeout" ); if ( daemon.command_timeout( opencmd, reply, opentimeout ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "opening connection to "+daemon.name+" hardware" ); + this->broadcast.error( function, "opening connection to "+daemon.name+" hardware" ); return ERROR; } was_opened=true; @@ -4197,7 +4164,7 @@ namespace Sequencer { if ( !daemon.socket.isconnected() ) { logwrite( function, "connecting to "+daemon.name+" daemon" ); if ( daemon.connect() != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "connecting to "+daemon.name ); + this->broadcast.error( function, "connecting to "+daemon.name ); return ERROR; } } diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 656f8861..928ae9e0 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -527,8 +527,6 @@ namespace Sequencer { this->cv.notify_all(); } - std::map telemetry_providers; ///< map of port[daemon_name] for external telemetry providers - double acquisition_timeout; ///< timeout for target acquisition (in sec) set by configuration parameter ACAM_ACQUIRE_TIMEOUT int acquisition_max_retrys; ///< max number of acquisition loop attempts double tcs_offsetrate_ra; ///< TCS offset rate RA ("MRATE") in arcsec per second @@ -658,12 +656,8 @@ namespace Sequencer { void publish_daemonstate(); void publish_threadstate(); - /** @brief publishes a narrative operator message on Topic::BROADCAST and logs it */ - void broadcast( const std::string &function, - const std::string &severity, - const std::string &message ); - std::unique_ptr publisher; ///< publisher object + Common::Broadcaster broadcast { this->publisher, Sequencer::DAEMON_NAME }; ///< logs and publishes a narrative message on Topic::BROADCAST std::string publisher_address; ///< publish socket endpoint std::string publisher_topic; ///< my default topic for publishing std::unique_ptr subscriber; ///< subscriber object diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index e794bd77..66f99c7e 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -29,17 +29,17 @@ namespace Sequencer { double angle_in = this->target.slitangle; if ( std::isnan(ra_in) || std::isnan(dec_in) ) { - this->broadcast( function, Severity::ERROR, "converting target coordinates to decimal" ); + this->broadcast.error( function, "converting target coordinates to decimal" ); return ERROR; } std::ostringstream cmd; cmd << ACAMD_ACQUIRE << " " << ra_in << " " << dec_in << " " << angle_in; - this->broadcast( function, Severity::NOTICE, "starting ACAM acquisition" ); + this->broadcast.notice( function, "starting ACAM acquisition" ); if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { - this->broadcast( function, Severity::ERROR, "sending acquire command to acamd" ); + this->broadcast.error( function, "sending acquire command to acamd" ); return ERROR; } @@ -58,7 +58,7 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_acam_guiding.load()) { - this->broadcast( function, Severity::ERROR, "ACAM acquisition timed out!" ); + this->broadcast.error( function, "ACAM acquisition timed out!" ); return TIMEOUT; } @@ -82,7 +82,7 @@ namespace Sequencer { // TODO don't hard-code the arguments here: std::string reply; if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start L", reply ) != NO_ERROR) { - this->broadcast( function, Severity::ERROR, "starting slicecam fine acquisition" ); + this->broadcast.error( function, "starting slicecam fine acquisition" ); return ERROR; } @@ -91,7 +91,7 @@ namespace Sequencer { return ERROR; } - this->broadcast( function, Severity::NOTICE, "slicecam fine acquisition started" ); + this->broadcast.notice( function, "slicecam fine acquisition started" ); const bool use_timeout = ( this->acquisition_timeout > 0 ); const auto timeout_time = std::chrono::steady_clock::now() @@ -108,7 +108,7 @@ namespace Sequencer { if (this->cancel_flag.load()) return ABORT; if (use_timeout && !this->is_fineacquire_locked.load()) { - this->broadcast( function, Severity::ERROR, "slicecam fine acquisition timed out!" ); + this->broadcast.error( function, "slicecam fine acquisition timed out!" ); return TIMEOUT; } @@ -132,7 +132,7 @@ namespace Sequencer { // ---------- ACAM acquire ----------------------------- // if ( this->do_acam_acquire() != NO_ERROR ) { - this->broadcast( caller, Severity::WARNING, "acam acquisition failed" ); + this->broadcast.warning( caller, "acam acquisition failed" ); // on ACAM acquisition failure wait for user to continue or cancel if ( this->wait_for_user(caller) == ABORT ) return ABORT; @@ -143,7 +143,7 @@ namespace Sequencer { // ---------- SLICECAM fineacquire --------------------- // if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->broadcast( caller, Severity::WARNING, "slicecam fine acquisition failed" ); + this->broadcast.warning( caller, "slicecam fine acquisition failed" ); } return NO_ERROR; diff --git a/sequencerd/sequence_wait.cpp b/sequencerd/sequence_wait.cpp index a9f0a459..feba92fc 100644 --- a/sequencerd/sequence_wait.cpp +++ b/sequencerd/sequence_wait.cpp @@ -21,7 +21,7 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCSOP ); - this->broadcast( caller, Severity::NOTICE, "waiting for TCS operator to send 'ontarget' signal" ); + this->broadcast.notice( caller, "waiting for TCS operator to send 'ontarget' signal" ); while ( !this->cancel_flag.load() && !this->is_ontarget.load() ) { @@ -31,7 +31,7 @@ namespace Sequencer { this->cancel_flag.load() ); } ); } - this->broadcast( caller, Severity::NOTICE, "received " + this->broadcast.notice( caller, "received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("ontarget")) +" signal!" ); @@ -56,20 +56,20 @@ namespace Sequencer { { ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_USER ); - this->broadcast( caller, Severity::NOTICE, "waiting for USER to send 'continue' signal" ); + this->broadcast.notice( caller, "waiting for USER to send 'continue' signal" ); while ( !this->cancel_flag.load() && !this->is_usercontinue.load() ) { std::unique_lock lock(cv_mutex); this->cv.wait( lock, [this]() { return( this->is_usercontinue.load() || this->cancel_flag.load() ); } ); } - this->broadcast( caller, Severity::NOTICE, "received " + this->broadcast.notice( caller, "received " +(this->cancel_flag.load() ? std::string("cancel") : std::string("continue")) +" signal!" ); } // end scope for wait_state = WAIT_USER if ( this->cancel_flag.load() ) { - this->broadcast( caller, Severity::NOTICE, "sequence cancelled" ); + this->broadcast.notice( caller, "sequence cancelled" ); return ABORT; } @@ -97,7 +97,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->broadcast( caller, Severity::NOTICE, "exposure cancelled" ); + this->broadcast.notice( caller, "exposure cancelled" ); return ABORT; } @@ -128,7 +128,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->broadcast( caller, Severity::NOTICE, "wait for readout cancelled" ); + this->broadcast.notice( caller, "wait for readout cancelled" ); return ABORT; } @@ -146,7 +146,7 @@ namespace Sequencer { */ long Sequence::wait_for_canexpose(std::string caller) { - this->broadcast( caller, Severity::NOTICE, "waiting for camera to be ready to expose" ); + this->broadcast.notice( caller, "waiting for camera to be ready to expose" ); while ( !this->cancel_flag.load() && !this->can_expose.load() ) { @@ -157,7 +157,7 @@ namespace Sequencer { } if (this->cancel_flag.load()) { - this->broadcast( caller, Severity::NOTICE, "wait for can_expose cancelled" ); + this->broadcast.notice( caller, "wait for can_expose cancelled" ); return ABORT; } diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 7206e859..20be3f59 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -867,31 +867,6 @@ namespace Sequencer { } } - // TELEM_PROVIDER : contains daemon name and port to contact for header telemetry info - // - if ( config.param[entry] == "TELEM_PROVIDER" ) { - std::vector tokens; - Tokenize( config.arg[entry], tokens, " " ); - try { - if ( tokens.size() == 2 ) { - this->sequence.telemetry_providers[tokens.at(0)] = std::stod(tokens.at(1)); - } - else { - message.str(""); message << "ERROR bad format TELEM_PROVIDER=\"" << config.arg[entry] << "\": expected "; - logwrite( function, message.str() ); - return ERROR; - } - } - catch ( const std::exception &e ) { - message.str(""); message << "ERROR parsing TELEM_PROVIDER from " << config.arg[entry] << ": " << e.what(); - logwrite( function, message.str() ); - return ERROR; - } - message.str(""); message << "config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( "SEQUENCERD", function, message.str() ); - applied++; - } - } // end loop through the entries in the configuration file message.str(""); diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 7f26c795..e6e86bd6 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -462,16 +462,16 @@ namespace Slicecam { // Common::extract_telemetry_value( jmessage, "TCSNAME", telem.tcsname ); Common::extract_telemetry_value( jmessage, "ISOPEN", telem.is_tcs_open ); - Common::extract_telemetry_value( jmessage, "CASANGLE", telem.angle_scope ); - Common::extract_telemetry_value( jmessage, "TELRA", telem.ra_scope_hms ); - Common::extract_telemetry_value( jmessage, "TELDEC", telem.dec_scope_dms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::CASANGLE, telem.angle_scope ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, telem.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELDEC, telem.dec_scope_dms ); Common::extract_telemetry_value( jmessage, "RA", telem.ra_scope_h ); Common::extract_telemetry_value( jmessage, "DEC", telem.dec_scope_d ); Common::extract_telemetry_value( jmessage, "RAOFFSET", telem.offsetra ); Common::extract_telemetry_value( jmessage, "DECLOFFS", telem.offsetdec ); - Common::extract_telemetry_value( jmessage, "AZ", telem.az ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AZ, telem.az ); Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); - Common::extract_telemetry_value( jmessage, "AIRMASS", telem.airmass ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AIRMASS, telem.airmass ); } /***** Slicecam::Interface::handletopic_tcsd ********************************/ diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index ed59ae1f..576ba9b8 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -42,28 +42,32 @@ namespace TCS { this->get_tcs_info(); nlohmann::json jmessage_out; - jmessage_out["source"] = "tcsd"; - - jmessage_out["ISOPEN"] = this->tcs_info.isopen; - jmessage_out["TCSNAME"] = this->tcs_info.tcsname; - - jmessage_out["PA"] = this->tcs_info.pa; // double - jmessage_out["CASANGLE"] = this->tcs_info.cassangle; // double - jmessage_out["HA"] = this->tcs_info.ha; // string - jmessage_out["RAOFFSET"] = this->tcs_info.offsetra; // double - jmessage_out["DECLOFFS"] = this->tcs_info.offsetdec; // double - jmessage_out["TELRA"] = this->tcs_info.ra_hms; // string "hh:mm:ss.s" - jmessage_out["TELDEC"] = this->tcs_info.dec_dms; // string "dd:mm:ss.s" - jmessage_out["RA"] = radec_to_decimal( this->tcs_info.ra_hms ); - jmessage_out["DEC"] = radec_to_decimal( this->tcs_info.dec_dms ); - jmessage_out["AZ"] = this->tcs_info.azimuth; - jmessage_out["ALT"] = 90. - this->tcs_info.zenithangle; - jmessage_out["ZENANGLE"] = this->tcs_info.zenithangle; - jmessage_out["DOMEAZ"] = this->tcs_info.domeazimuth; - jmessage_out["DOMESHUT"] = this->tcs_info.domeshutters==1?"open":"closed"; - jmessage_out["TELFOCUS"] = this->tcs_info.focus; - jmessage_out["AIRMASS"] = this->tcs_info.airmass; - jmessage_out["MOTION"] = this->tcs_info.motion; + jmessage_out[Key::SOURCE] = Daemon::TCSD; + + { + std::lock_guard lock(tcs_info_mtx); + + jmessage_out["ISOPEN"] = this->tcs_info.isopen; + jmessage_out["TCSNAME"] = this->tcs_info.tcsname; + + jmessage_out["PA"] = this->tcs_info.pa; // double + jmessage_out[Key::Tcsd::CASANGLE] = this->tcs_info.cassangle; // double + jmessage_out["HA"] = this->tcs_info.ha; // string + jmessage_out["RAOFFSET"] = this->tcs_info.offsetra; // double + jmessage_out["DECLOFFS"] = this->tcs_info.offsetdec; // double + jmessage_out[Key::Tcsd::TELRA] = this->tcs_info.ra_hms; // string "hh:mm:ss.s" + jmessage_out[Key::Tcsd::TELDEC] = this->tcs_info.dec_dms; // string "dd:mm:ss.s" + jmessage_out["RA"] = radec_to_decimal( this->tcs_info.ra_hms ); + jmessage_out["DEC"] = radec_to_decimal( this->tcs_info.dec_dms ); + jmessage_out[Key::Tcsd::AZ] = this->tcs_info.azimuth; + jmessage_out[Key::Tcsd::ALT] = 90. - this->tcs_info.zenithangle; + jmessage_out["ZENANGLE"] = this->tcs_info.zenithangle; + jmessage_out["DOMEAZ"] = this->tcs_info.domeazimuth; + jmessage_out["DOMESHUT"] = this->tcs_info.domeshutters==1?"open":"closed"; + jmessage_out["TELFOCUS"] = this->tcs_info.focus; + jmessage_out[Key::Tcsd::AIRMASS] = this->tcs_info.airmass; + jmessage_out["MOTION"] = this->tcs_info.motion; + } // for backwards compatibility jmessage_out["messagetype"] = "tcsinfo"; @@ -82,6 +86,27 @@ namespace TCS { /***** TCS::Interface::publish_snapshot *************************************/ + /***** TCS::Interface::do_continuous_snapshot *******************************/ + /** + * @brief publish snapshot at 1 Hz when connected + * + */ + void Interface::do_continuous_snapshot() { + auto next = std::chrono::steady_clock::now(); + while (should_publish.load()) { + bool isopen = false; + { + std::lock_guard lock(tcs_info_mtx); + isopen = this->tcs_info.isopen; + } + if (isopen) publish_snapshot(); + next += std::chrono::seconds(1); + std::this_thread::sleep_until(next); + } + } + /***** TCS::Interface::do_continuous_snapshot *******************************/ + + /***** TCS::Interface::get_tcs_info *****************************************/ /** * @brief fills the tcs_info class @@ -92,6 +117,8 @@ namespace TCS { long error = NO_ERROR; std::string retstring; + std::lock_guard lock(tcs_info_mtx); + // erase the class because it's all or nothing. If something fails partway // through, we don't want to mix values from a command now with values from // an earlier command. E.G. if reqpos fails here but reqstat and weather @@ -371,10 +398,12 @@ namespace TCS { } } + { + std::lock_guard lock(tcs_info_mtx); this->tcs_info.isopen = ( ! _name.empty() ? true : false ); this->tcs_info.tcsname = _name; - retstring = ( this->tcs_info.isopen ? "true" : "false" ); // return string is the state + } asyncmsg.str(""); asyncmsg << "TCSD:open:" << retstring; this->async.enqueue( asyncmsg.str() ); // broadcast the state @@ -1137,10 +1166,12 @@ namespace TCS { // parse the reply which stores it in the TcsInfo class // - this->tcs_info.parse_pa(tcsreply); - std::ostringstream oss; + this->tcs_info.parse_pa(tcsreply); + { + std::lock_guard lock(tcs_info_mtx); oss << this->tcs_info.pa; + } retstring = oss.str(); if ( !retstring.empty() && !silent ) logwrite( function, retstring ); diff --git a/tcsd/tcs_interface.h b/tcsd/tcs_interface.h index 9c76b03f..e451f6a1 100644 --- a/tcsd/tcs_interface.h +++ b/tcsd/tcs_interface.h @@ -13,6 +13,7 @@ #include "common.h" #include "tcs_constants.h" #include "tcsd_commands.h" +#include "message_keys.h" #include #include #include @@ -427,6 +428,7 @@ logwrite(function,message.str()); private: zmqpp::context context; std::string default_tcs; ///< default TCS to use specified in .cfg + std::mutex tcs_info_mtx; ///< protects tcs_info public: inline void set_default_tcs(const std::string &which) { this->default_tcs=which; } @@ -445,13 +447,13 @@ logwrite(function,message.str()); std::condition_variable publish_condition; std::condition_variable collect_condition; - std::atomic publish_enable; + std::atomic should_publish; std::atomic collect_enable; Interface() : context(), offsetrate(0), - publish_enable(false), + should_publish(true), collect_enable(false), subscriber(std::make_unique(context, Common::PubSub::Mode::SUB)), is_subscriber_thread_running(false), @@ -487,6 +489,7 @@ logwrite(function,message.str()); void publish_snapshot(); void publish_snapshot(std::string &retstring); + void do_continuous_snapshot(); /** * These are the functions for communicating with the TCS diff --git a/tcsd/tcsd.cpp b/tcsd/tcsd.cpp index e7d19000..c21c20b5 100644 --- a/tcsd/tcsd.cpp +++ b/tcsd/tcsd.cpp @@ -132,6 +132,10 @@ int main(int argc, char **argv) { // publish my snapshot so the world knows I'm online tcsd.interface.publish_snapshot(); + // thread to publish snapshot when connected + std::thread( &TCS::Interface::do_continuous_snapshot, + std::ref(tcsd.interface) ).detach(); + // This will pre-thread N_THREADS threads, a little differently from other // daemons. There will be N_THREADS-1 non-blocking threads as before then // loop forever on Accept to dynamically spawn a new thread for each blocking From c6ce5b39793d188fbca4aef09b903e5859516159 Mon Sep 17 00:00:00 2001 From: David Hale Date: Mon, 11 May 2026 23:54:54 -0700 Subject: [PATCH 22/37] Adds more work done in parallel on the fineacquire-update-sequencer-pub branch in support of fine acquisition and general pub-sub improvement, and other fixes along the way. These changes are not pertinent to the work being done for the sequencer-scripting branch but must be included so that this branch keeps up with other progress while awaiting PR approval. --- Config/slicecamd.cfg.in | 34 ++ acamd/acam_interface.cpp | 126 +++---- acamd/acam_interface.h | 10 +- common/common.cpp | 1 + common/common.h | 13 +- common/fits_header_defs.h | 129 ++++++++ common/message_keys.h | 70 +++- common/sequencerd_commands.h | 2 + common/tcsd_commands.h | 2 + sequencerd/sequence.cpp | 134 ++++++-- sequencerd/sequence.h | 27 +- sequencerd/sequence_acquisition.cpp | 190 +++++++++-- sequencerd/sequencer_server.cpp | 4 + sequencerd/sequencerd.cpp | 8 +- slicecamd/slicecam_interface.cpp | 266 ++++++++++++--- slicecamd/slicecam_interface.h | 47 ++- tcsd/tcs_interface.cpp | 114 +++---- tcsd/tcs_interface.h | 3 + tcsd/tcs_server.cpp | 7 + utils/logentry.cpp | 2 +- utils/logentry.h | 2 +- utils/network.cpp | 4 +- utils/seqmon.cpp | 492 ++++++++++++++++++++++++++++ utils/utilities.h | 7 + 24 files changed, 1447 insertions(+), 247 deletions(-) create mode 100644 common/fits_header_defs.h create mode 100644 utils/seqmon.cpp diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index e3abb098..46c35ca1 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -77,6 +77,40 @@ FINE_ACQUIRE_AIMPOINT=(L 150.0 115.5) # FINE_ACQUIRE_BACKGROUND=(80 165 30 210) +# FINE_ACQUIRE_MIN_SAMPLES= +# Minimum centroid samples before scatter-gated early exit is evaluated. +# Once this many samples are collected and the MAD scatter per axis is below +# the precision threshold, a correction is issued without waiting for MAX_SAMPLES. +# +FINE_ACQUIRE_MIN_SAMPLES=3 + +# FINE_ACQUIRE_SETTLE_FRAMES= +# frames discarded after each telescope move to allow settling before +# centroid measurements resume. +# +FINE_ACQUIRE_SETTLE_FRAMES=2 + +# FINE_ACQUIRE_GAIN= +# Proportional gain = {0..1} applied to the commanded offset when the residual +# is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec. +# +FINE_ACQUIRE_GAIN=0.7 + +# FINE_ACQUIRE_GAIN_LARGE= +# Proportional gain applied when the residual exceeds FINE_ACQUIRE_GAIN_THRESHOLD. +# Set to 1.0 for a full-correction step on large offsets to reduce +# the number of correction cycles needed. Set equal to FINE_ACQUIRE_GAIN to +# disable gain scheduling. +# +FINE_ACQUIRE_GAIN_LARGE=1.0 + +# FINE_ACQUIRE_GAIN_THRESHOLD= +# Offset magnitude (arcsec) above which FINE_ACQUIRE_GAIN_LARGE is used instead +# of FINE_ACQUIRE_GAIN. Set to 0 to disable gain scheduling (GAIN is used for +# all corrections. +# +FINE_ACQUIRE_GAIN_THRESHOLD=2.0 + # SkySimulator options: # SKYSIM_IMAGE_SIZE= where is integer # Sets the keyword argument "IMAGE_SIZE=" diff --git a/acamd/acam_interface.cpp b/acamd/acam_interface.cpp index 86df30be..796719e7 100644 --- a/acamd/acam_interface.cpp +++ b/acamd/acam_interface.cpp @@ -9,6 +9,7 @@ */ #include "acam_interface.h" +#include "fits_header_defs.h" namespace Acam { @@ -1492,6 +1493,7 @@ namespace Acam { jmessage_out[Key::Acamd::ATTEMPTS] = this->target.attempts; jmessage_out[Key::Acamd::SEEING] = this->astrometry.get_seeing(); jmessage_out[Key::Acamd::BACKGROUND] = this->astrometry.get_background(); + jmessage_out[Key::PUBTIME] = get_time_us(); try { this->publisher->publish( jmessage_out, Topic::ACAMD ); @@ -1507,7 +1509,7 @@ namespace Acam { /***** Acam::Interface::request_snapshot ************************************/ /** - * @brief publises request for snapshot + * @brief [obsolete] publises request for snapshot * @details publishing Topic::SNAPSHOT induces subscribers to publish a * snapshot of their telemetry * @@ -1534,7 +1536,7 @@ namespace Acam { /***** Acam::Interface::wait_for_snapshots **********************************/ /** - * @brief wait for everyone to publish their snaphots + * @brief [obsolete] wait for everyone to publish their snaphots * @details When forcing subscribers to publish their telemetry, * this waits until they have done so. * @@ -1599,33 +1601,32 @@ namespace Acam { * */ void Interface::handletopic_tcsd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::TCSD]=true; - } + + std::lock_guard lock(tcsdata_mtx); + // extract and store values in the class // - Common::extract_telemetry_value( jmessage, "TCSNAME", telem.tcsname ); - Common::extract_telemetry_value( jmessage, "ISOPEN", telem.is_tcs_open ); - Common::extract_telemetry_value( jmessage, Key::Tcsd::CASANGLE, telem.angle_scope ); - Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, telem.ra_scope_hms ); - Common::extract_telemetry_value( jmessage, Key::Tcsd::TELDEC, telem.dec_scope_dms ); - Common::extract_telemetry_value( jmessage, "RA", telem.ra_scope_h ); - Common::extract_telemetry_value( jmessage, "DEC", telem.dec_scope_d ); - Common::extract_telemetry_value( jmessage, "RAOFFSET", telem.offsetra ); - Common::extract_telemetry_value( jmessage, "DECLOFFS", telem.offsetdec ); - Common::extract_telemetry_value( jmessage, Key::Tcsd::AZ, telem.az ); - Common::extract_telemetry_value( jmessage, "TELFOCUS", telem.telfocus ); - Common::extract_telemetry_value( jmessage, Key::Tcsd::AIRMASS, telem.airmass ); + Common::extract_telemetry_value( jmessage, "TCSNAME", tcsdata.tcsname ); + Common::extract_telemetry_value( jmessage, "ISOPEN", tcsdata.is_tcs_open ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::CASANGLE, tcsdata.angle_scope ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELRA, tcsdata.ra_scope_hms ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::TELDEC, tcsdata.dec_scope_dms ); + Common::extract_telemetry_value( jmessage, "RA", tcsdata.ra_scope_h ); + Common::extract_telemetry_value( jmessage, "DEC", tcsdata.dec_scope_d ); + Common::extract_telemetry_value( jmessage, "RAOFFSET", tcsdata.offsetra ); + Common::extract_telemetry_value( jmessage, "DECLOFFS", tcsdata.offsetdec ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AZ, tcsdata.az ); + Common::extract_telemetry_value( jmessage, "TELFOCUS", tcsdata.telfocus ); + Common::extract_telemetry_value( jmessage, Key::Tcsd::AIRMASS, tcsdata.airmass ); // save them to the database // - this->database.add_key_val( "CASANGLE", telem.angle_scope ); - this->database.add_key_val( "RAtel", telem.ra_scope_h ); - this->database.add_key_val( "DECLtel", telem.dec_scope_d ); - this->database.add_key_val( "AZ", telem.az ); - this->database.add_key_val( "focus", telem.telfocus ); - this->database.add_key_val( "AIRMASS", telem.airmass ); + this->database.add_key_val( "CASANGLE", tcsdata.angle_scope ); + this->database.add_key_val( "RAtel", tcsdata.ra_scope_h ); + this->database.add_key_val( "DECLtel", tcsdata.dec_scope_d ); + this->database.add_key_val( "AZ", tcsdata.az ); + this->database.add_key_val( "focus", tcsdata.telfocus ); + this->database.add_key_val( "AIRMASS", tcsdata.airmass ); } /***** Acam::Interface::handletopic_tcsd ************************************/ @@ -1638,10 +1639,6 @@ namespace Acam { * */ void Interface::handletopic_targetinfo( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::TARGETINFO]=true; - } this->database.add_from_json( jmessage, "OBS_ID" ); this->database.add_from_json( jmessage, "NAME" ); this->database.add_from_json( jmessage, "POINTMODE" ); @@ -1659,12 +1656,10 @@ namespace Acam { * */ void Interface::handletopic_slitd( const nlohmann::json &jmessage ) { - { - std::lock_guard lock(snapshot_mtx); - snapshot_status[Topic::SLITD]=true; + for ( const auto &keyinfo : FitsHeaderKeys::SlitdTelemKeys ) { + telemkeys.add_json_key(jmessage, keyinfo.jkey, keyinfo.keyword, + keyinfo.comment, keyinfo.type, false); } - this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); - this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); } /***** Acam::Interface::handletopic_slitd ***********************************/ @@ -2706,7 +2701,7 @@ namespace Acam { do { if ( iface.camera.andor.camera_info.exptime == 0 ) continue; // wait for non-zero exposure time - if ( iface.collect_header_info() == ERROR ) { // collect header information + if ( iface.assemble_header_info() == ERROR ) { // assemble header keyword information logwrite(function,"ERROR collecting header info"); continue; } @@ -3377,7 +3372,11 @@ logwrite( function, message.str() ); this->acquire_mode = requested_mode; - iface->publish_status(); + // acam needs to publish status on every acquire command to initialize + // a freshness timer in the sequencer. The extra pub noise is a small + // price to pay for the simplicity of not adding another mechanism. + // + iface->publish_status(true); return NO_ERROR; } @@ -4083,6 +4082,10 @@ logwrite( function, message.str() ); // if ( this->motion.is_open() ) error |= this->motion.cover( "close", dontcare ); + // diable target acquisition + // + error |= this->acquire( "stop", dontcare); + // stop the framegrab thread // error |= this->framegrab( "stop", dontcare ); @@ -4375,7 +4378,7 @@ logwrite( function, message.str() ); do { logwrite( function, std::to_string(nacquires) ); error |= this->camera.andor.acquire_one(); // acquire a single image - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4420,7 +4423,7 @@ logwrite( function, message.str() ); do { logwrite( function, std::to_string(nacquires) ); error |= this->camera.andor.get_recent(3000); // - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4454,7 +4457,7 @@ logwrite( function, message.str() ); // if ( tokens[1]=="getrecent" ) { error = this->camera.andor.get_recent(3000); - error |= this->collect_header_info(); // collect header information + error |= this->assemble_header_info(); // assemble header keyword information error |= this->camera.write_frame( "", this->imagename, this->tcs_online.load() ); // write to FITS file @@ -4583,7 +4586,7 @@ logwrite( function, message.str() ); retstring.append( " Gather information and add it to the internal keyword database.\n" ); return HELP; } - else error = this->collect_header_info(); // collect header information + else error = this->assemble_header_info(); // assemble header keyword information } else // -------------------------------- @@ -5005,7 +5008,7 @@ logwrite( function, message.str() ); /***** Acam::Interface::solve ***********************************************/ - /***** Acam::Interface::collect_header_info *********************************/ + /***** Acam::Interface::assemble_header_info ********************************/ /** * @brief gather information and add it to the internal keyword database * @details Some of the keys are fixed, some come from the Andor::Information @@ -5016,24 +5019,24 @@ logwrite( function, message.str() ); * @return ERROR or NO_ERROR * */ - long Interface::collect_header_info() { - // force subscribers to publish now, then wait - // esults in struct telem. - this->request_snapshot(); - this->wait_for_snapshots(); - - bool _tcs = telem.is_tcs_open; - std::string tcsname = ( _tcs ? telem.tcsname : "offline" ); + long Interface::assemble_header_info() { double angle_acam=NAN, ra_acam=NAN, dec_acam=NAN; // outputs from fpoffsets - if ( _tcs ) this->target.save_casangle( telem.angle_scope ); // store in the Target class, required for acquisition + // ---------- scope lock tcsdata -------------- + { + std::lock_guard lock(tcsdata_mtx); + + bool _tcs = tcsdata.is_tcs_open; + std::string tcsname = ( _tcs ? tcsdata.tcsname : "offline" ); + + if ( _tcs ) this->target.save_casangle( tcsdata.angle_scope ); // store in the Target class, required for acquisition // Compute FP offsets from TCS coordinates (SCOPE) to ACAM coodinates. // compute_offset() always wants degrees and get_coords() returns RA hours. // Results in degrees. // - if ( _tcs ) this->fpoffsets.compute_offset( "SCOPE", "ACAM", (telem.ra_scope_h*TO_DEGREES), telem.dec_scope_d, telem.angle_scope, + if ( _tcs ) this->fpoffsets.compute_offset( "SCOPE", "ACAM", (tcsdata.ra_scope_h*TO_DEGREES), tcsdata.dec_scope_d, tcsdata.angle_scope, ra_acam, dec_acam, angle_acam ); // Get some info from the Andor::Information class, @@ -5054,11 +5057,21 @@ logwrite( function, message.str() ); this->camera.fitsinfo.fitskeys.addkey( "TCS", tcsname, "" ); + this->camera.fitsinfo.fitskeys.addkey( "TELFOCUS", tcsdata.telfocus, "telescope focus (mm)" ); + this->camera.fitsinfo.fitskeys.addkey( "AIRMASS", tcsdata.airmass, "" ); + this->camera.fitsinfo.fitskeys.addkey( "RA", tcsdata.ra_scope_hms, "Telecscope Right Ascension" ); + this->camera.fitsinfo.fitskeys.addkey( "DEC", tcsdata.dec_scope_dms, "Telescope Declination" ); + this->camera.fitsinfo.fitskeys.addkey( "TELRA", tcsdata.ra_scope_h, "Telecscope Right Ascension hours" ); + this->camera.fitsinfo.fitskeys.addkey( "TELDEC", tcsdata.dec_scope_d, "Telescope Declination degrees" ); + this->camera.fitsinfo.fitskeys.addkey( "RAOFFS", tcsdata.offsetra, "Telescope RA offset" ); + this->camera.fitsinfo.fitskeys.addkey( "DECLOFFS", tcsdata.offsetdec, "Telescope DEC offset" ); + this->camera.fitsinfo.fitskeys.addkey( "CASANGLE", tcsdata.angle_scope, "Cassegrain ring angle" ); + } + // ---------- end scope lock tcsdata ---------- + this->camera.fitsinfo.fitskeys.addkey( "CREATOR", "acamd", "file creator" ); this->camera.fitsinfo.fitskeys.addkey( "INSTRUME", "NGPS", "name of instrument" ); this->camera.fitsinfo.fitskeys.addkey( "TELESCOP", "P200", "name of telescope" ); - this->camera.fitsinfo.fitskeys.addkey( "TELFOCUS", telem.telfocus, "telescope focus (mm)" ); - this->camera.fitsinfo.fitskeys.addkey( "AIRMASS", telem.airmass, "" ); // get parameters from FPOffsets, results are stored in the class // @@ -5109,13 +5122,6 @@ logwrite( function, message.str() ); this->camera.fitsinfo.fitskeys.addkey( "POSANG", angle_acam, "" ); this->camera.fitsinfo.fitskeys.addkey( "TARGET", this->target.get_name(), "target name" ); - this->camera.fitsinfo.fitskeys.addkey( "RA", telem.ra_scope_hms, "Telecscope Right Ascension" ); - this->camera.fitsinfo.fitskeys.addkey( "DEC", telem.dec_scope_dms, "Telescope Declination" ); - this->camera.fitsinfo.fitskeys.addkey( "TELRA", telem.ra_scope_h, "Telecscope Right Ascension hours" ); - this->camera.fitsinfo.fitskeys.addkey( "TELDEC", telem.dec_scope_d, "Telescope Declination degrees" ); - this->camera.fitsinfo.fitskeys.addkey( "RAOFFS", telem.offsetra, "Telescope RA offset" ); - this->camera.fitsinfo.fitskeys.addkey( "DECLOFFS", telem.offsetdec, "Telescope DEC offset" ); - this->camera.fitsinfo.fitskeys.addkey( "CASANGLE", telem.angle_scope, "Cassegrain ring angle" ); this->camera.fitsinfo.fitskeys.addkey( "WCSAXES", 2, "" ); this->camera.fitsinfo.fitskeys.addkey( "RADESYSA", "ICRS", "" ); this->camera.fitsinfo.fitskeys.addkey( "CTYPE1", "RA---TAN", "" ); @@ -5137,7 +5143,7 @@ logwrite( function, message.str() ); return NO_ERROR; } - /***** Acam::Interface::collect_header_info *********************************/ + /***** Acam::Interface::assemble_header_info ********************************/ /***** Acam::Interface::target_coords ***************************************/ diff --git a/acamd/acam_interface.h b/acamd/acam_interface.h index 3cb5d4f6..04e29eb8 100644 --- a/acamd/acam_interface.h +++ b/acamd/acam_interface.h @@ -286,7 +286,7 @@ namespace Acam { * @details the script pushes the indicated file to the Guider GUI display * @param[in] filename fits file to send */ - void push_guider_image( std::string filename ) { + void push_guider_image( std::string_view filename ) { std::string function = "Acam::GuideManager::push_guider_image"; std::stringstream cmd; cmd << push_image << " " @@ -304,7 +304,7 @@ namespace Acam { * @brief calls the push_message script with the supplied message string * @param[in] message message to send */ - void push_guider_message( std::string message ) { + void push_guider_message( std::string_view message ) { std::string function = "Acam::GuideManager::push_guider_message"; std::stringstream cmd; cmd << push_message << " " @@ -555,7 +555,9 @@ namespace Acam { double az; double telfocus; double airmass; - } telem; + } tcsdata; + + std::mutex tcsdata_mtx; std::mutex snapshot_mtx; std::unordered_map snapshot_status; @@ -694,7 +696,7 @@ namespace Acam { long exptime( const std::string args, std::string &retstring ); long fan_mode( std::string args, std::string &retstring ); - long collect_header_info(); + long assemble_header_info(); inline void init_names() { imagename=""; wcsname=""; return; } // TODO still needed? diff --git a/common/common.cpp b/common/common.cpp index 464fd76d..bfd417e8 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -25,6 +25,7 @@ namespace Common { logwrite( function, severity+": "+message ); if ( ! this->publisher ) return; + nlohmann::json jmessage; jmessage[Key::SOURCE] = this->source; jmessage[Key::Broadcast::SEVERITY] = severity; diff --git a/common/common.h b/common/common.h index b8c768f9..2e5d8f8e 100644 --- a/common/common.h +++ b/common/common.h @@ -475,7 +475,8 @@ namespace Common { } else if constexpr ( std::is_same::value || std::is_same::value || std::is_same::value ) { - if ( jvalue.type() == json::value_t::number_unsigned ) { + if ( jvalue.type() == json::value_t::number_integer || + jvalue.type() == json::value_t::number_unsigned ) { value = jvalue.template get(); } } @@ -968,9 +969,13 @@ namespace Common { * @brief template class adds key,value,comment to indicated keydb from json message * @details This extracts the value from a JSON message and uses add_key to add the * keyword to the indicated database map. - * @param[in] type reference to FitsKeys database object - * @param[in] jmessage JSON message is the source of the value - * @param[in] comment comment string for header keyword + * @param[in] jmessage JSON message is the source of the value + * @param[in] jkey key to index jmessage + * @param[in] keyword FITS header keyword + * @param[in] comment comment string for header keyword + * @param[in] type type of key can be optionally specified + * @param[in] use_extension extension or primary + * @param[in] chan channel selects keyword db * */ void add_json_key( const json &jmessage, diff --git a/common/fits_header_defs.h b/common/fits_header_defs.h new file mode 100644 index 00000000..a439ef3d --- /dev/null +++ b/common/fits_header_defs.h @@ -0,0 +1,129 @@ +/** + * @file fits_header_defs.h + * @brief shared FITS header keyword metadata for add_json_key consumers + * @author David Hale + * + */ +#pragma once + +#include "message_keys.h" + +namespace FitsHeaderKeys { + + /** + * @struct Primary + * @brief holds metadata for a primary-header keyword extracted from JSON + */ + struct Primary { + const char* jkey; ///< key to extract from JSON message + const char* keyword; ///< FITS header keyword (uses jkey if empty) + const char* comment; ///< FITS keyword comment + const char* type = ""; ///< optional explicit datatype + }; + + /** + * @struct Extension + * @brief holds metadata for an extension-header keyword extracted from JSON + */ + struct Extension { + const char* chan; ///< extension channel name + const char* jkey; ///< key to extract from JSON message + const char* keyword; ///< FITS header keyword (uses jkey if empty) + const char* comment; ///< FITS keyword comment + const char* type = ""; ///< optional explicit datatype + }; + + const Primary CalibInfoKeys[] = { + { Key::Calibd::MODFEAR.c_str(), "", "FeAr lamp modulator pow dut per" }, + { Key::Calibd::MODTHAR.c_str(), "", "ThAr lamp modulator pow dut per" }, + { Key::Calibd::MODBLCON.c_str(), "", "Blue continuum modulator pow dut per" }, + { Key::Calibd::MODBLBYP.c_str(), "", "Blue bypass modulator pow dut per" }, + { Key::Calibd::MODRDCON.c_str(), "", "Red continuum modulator pow dut per" }, + { Key::Calibd::MODRDBYP.c_str(), "", "Red bypass modulator pow dut per" }, + { Key::Calibd::CALCOVER.c_str(), "", "calib cover state" }, + { Key::Calibd::CALDOOR.c_str(), "", "calib door state" } + }; + + const Extension FlexureInfoKeys[] = { + { "I", Key::Flexured::FLXSPE_I.c_str(), "FLXSPE", "I flexure spectral axis 2 (X) in um", "" }, + { "I", Key::Flexured::FLXSPA_I.c_str(), "FLXSPA", "I flexure spatial axis 3 (Y) in um", "" }, + { "I", Key::Flexured::FLXPIS_I.c_str(), "FLXPIS", "I flexure piston axis 1 (Z) in um", "" }, + { "R", Key::Flexured::FLXSPE_R.c_str(), "FLXSPE", "R flexure spectral axis 2 (X) in um", "" }, + { "R", Key::Flexured::FLXSPA_R.c_str(), "FLXSPA", "R flexure spatial axis 3 (Y) in um", "" }, + { "R", Key::Flexured::FLXPIS_R.c_str(), "FLXPIS", "R flexure piston axis 1 (Z) in um", "" }, + { "G", Key::Flexured::FLXSPE_G.c_str(), "FLXSPE", "G flexure spectral axis 2 (X) in um", "" }, + { "G", Key::Flexured::FLXSPA_G.c_str(), "FLXSPA", "G flexure spatial axis 3 (Y) in um", "" }, + { "G", Key::Flexured::FLXPIS_G.c_str(), "FLXPIS", "G flexure piston axis 1 (Z) in um", "" }, + { "U", Key::Flexured::FLXSPE_U.c_str(), "FLXSPE", "U flexure spectral axis 2 (X) in um", "" }, + { "U", Key::Flexured::FLXSPA_U.c_str(), "FLXSPA", "U flexure spatial axis 3 (Y) in um", "" }, + { "U", Key::Flexured::FLXPIS_U.c_str(), "FLXPIS", "U flexure piston axis 1 (Z) in um", "" } + }; + + const Extension FocusInfoKeys[] = { + { "I", Key::Focusd::FOCUSI.c_str(), "FOCUS", "science camera I focus position in mm", "" }, + { "R", Key::Focusd::FOCUSR.c_str(), "FOCUS", "science camera R focus position in mm", "" }, + { "G", Key::Focusd::FOCUSG.c_str(), "FOCUS", "science camera G focus position in mm", "" }, + { "U", Key::Focusd::FOCUSU.c_str(), "FOCUS", "science camera U focus position in mm", "" } + }; + + const Primary PowerInfoKeys[] = { + { Key::Powerd::LAMPTHAR.c_str(), "", "is ThAr lamp on" }, + { Key::Powerd::LAMPFEAR.c_str(), "", "is FeAr lamp on" }, + { Key::Powerd::LAMPBLUC.c_str(), "", "is blue Xe continuum lamp on" }, + { Key::Powerd::LAMPREDC.c_str(), "", "is red continuum lamp on" }, + { Key::Powerd::LAMPXE.c_str(), "", "is Xe lamp on" }, + { Key::Powerd::LAMPINCA.c_str(), "", "is Incandescent lamp on" } + }; + + const Primary SlitInfoKeys[] = { + { Key::Slitd::SLITW.c_str(), "", "slit width in arcsec" }, + { Key::Slitd::SLITO.c_str(), "", "slit offset in arcsec" }, + { Key::Slitd::SLITPOSA.c_str(), "", "slit actuator A position in mm" }, + { Key::Slitd::SLITPOSB.c_str(), "", "slit actuator B position in mm" } + }; + + const Primary TargetInfoKeys[] = { + { "OBS_ID", "", "Observation ID", "INT" }, + { "NAME", "", "target name", "STRING" }, + { "SLITA", "", "slit angle in deg", "FLOAT" }, + { "POINTMDE", "", "pointing mode", "STRING" }, + { "RA", "", "requested Right Ascension in J2000", "STRING" }, + { "DECL", "", "requested Declination in J2000", "STRING" } + }; + + const Primary TcsInfoKeys[] = { + { Key::Tcsd::CASANGLE.c_str(), "", "TCS reported Cassegrain angle in deg", "FLOAT" }, + { Key::Tcsd::HA.c_str(), "", "hour angle" }, + { Key::Tcsd::RAOFFSET.c_str(), "", "offset Right Ascension" }, + { Key::Tcsd::DECLOFFS.c_str(), "", "offset Declination" }, + { Key::Tcsd::TELRA.c_str(), "", "TCS reported Right Ascension" }, + { Key::Tcsd::TELDEC.c_str(), "", "TCS reported Declination" }, + { Key::Tcsd::AZ.c_str(), "", "TCS reported azimuth" }, + { Key::Tcsd::ZENANGLE.c_str(), "", "TCS reported Zenith angle", "FLOAT" }, + { Key::Tcsd::DOMEAZ.c_str(), "", "TCS reported dome azimuth", "FLOAT" }, + { Key::Tcsd::DOMESHUT.c_str(), "", "dome shutters" }, + { Key::Tcsd::TELFOCUS.c_str(), "", "TCS reported telescope focus position in mm", "FLOAT" } + }; + + const Extension ThermalInfoKeys[] = { + { "I", Key::Thermald::TCCD_I.c_str(), "CCDTEMP", "I CCD temperature in Kelvin", "FLOAT" }, + { "R", Key::Thermald::TCCD_R.c_str(), "CCDTEMP", "R CCD temperature in Kelvin", "FLOAT" }, + { "G", Key::Thermald::TCCD_G.c_str(), "CCDTEMP", "G CCD temperature in Kelvin", "FLOAT" }, + { "U", Key::Thermald::TCCD_U.c_str(), "CCDTEMP", "U CCD temperature in Kelvin", "FLOAT" }, + + { "I", Key::Thermald::TCOLL_I.c_str(), "COLTEMP", "I collimator temp in deg C", "FLOAT" }, + { "R", Key::Thermald::TCOLL_R.c_str(), "COLTEMP", "R collimator temp in deg C", "FLOAT" }, + { "G", Key::Thermald::TCOLL_G.c_str(), "COLTEMP", "G collimator temp in deg C", "FLOAT" }, + + { "I", Key::Thermald::TFOCUS_I.c_str(), "FOCTEMP", "I focus temp in deg C", "FLOAT" }, + { "R", Key::Thermald::TFOCUS_R.c_str(), "FOCTEMP", "R focus temp in deg C", "FLOAT" }, + { "G", Key::Thermald::TFOCUS_G.c_str(), "FOCTEMP", "G focus temp in deg C", "FLOAT" }, + { "U", Key::Thermald::TFOCUS_U.c_str(), "FOCTEMP", "U focus temp in deg C", "FLOAT" } + }; + + const Primary SlitdTelemKeys[] = { + { Key::Slitd::SLITO.c_str(), "SLITO", "slit offset in arcsec", "FLOAT" }, + { Key::Slitd::SLITW.c_str(), "SLITW", "slit width in arcsec", "FLOAT" } + }; + +} diff --git a/common/message_keys.h b/common/message_keys.h index ef738a7e..fa731040 100644 --- a/common/message_keys.h +++ b/common/message_keys.h @@ -50,7 +50,8 @@ namespace Topic { namespace Key { - inline const std::string SOURCE = "source"; + inline const std::string SOURCE = "source"; + inline const std::string PUBTIME = "pubtime"; namespace Broadcast { inline const std::string SEVERITY = "severity"; @@ -58,7 +59,8 @@ namespace Key { } namespace Sequencer { - inline const std::string SEQSTATE = "seqstate"; + inline const std::string SEQSTATE = "seqstate"; + inline const std::string SHOULD_FINEACQUIRE = "should_fineacquire"; } namespace Camerad { @@ -104,6 +106,70 @@ namespace Key { inline const std::string AZ = "AZ"; inline const std::string AIRMASS = "AIRMASS"; inline const std::string CASANGLE = "CASANGLE"; + inline const std::string MOTION = "MOTION"; + inline const std::string HA = "HA"; + inline const std::string RAOFFSET = "RAOFFSET"; + inline const std::string DECLOFFS = "DECLOFFS"; + inline const std::string ZENANGLE = "ZENANGLE"; + inline const std::string DOMEAZ = "DOMEAZ"; + inline const std::string DOMESHUT = "DOMESHUT"; + inline const std::string TELFOCUS = "TELFOCUS"; + } + + namespace Calibd { + inline const std::string MODFEAR = "MODFEAR"; + inline const std::string MODTHAR = "MODTHAR"; + inline const std::string MODBLCON = "MODBLCON"; + inline const std::string MODBLBYP = "MODBLBYP"; + inline const std::string MODRDCON = "MODRDCON"; + inline const std::string MODRDBYP = "MODRDBYP"; + inline const std::string CALCOVER = "CALCOVER"; + inline const std::string CALDOOR = "CALDOOR"; + } + + namespace Flexured { + inline const std::string FLXSPE_I = "FLXSPE_I"; + inline const std::string FLXSPA_I = "FLXSPA_I"; + inline const std::string FLXPIS_I = "FLXPIS_I"; + inline const std::string FLXSPE_R = "FLXSPE_R"; + inline const std::string FLXSPA_R = "FLXSPA_R"; + inline const std::string FLXPIS_R = "FLXPIS_R"; + inline const std::string FLXSPE_G = "FLXSPE_G"; + inline const std::string FLXSPA_G = "FLXSPA_G"; + inline const std::string FLXPIS_G = "FLXPIS_G"; + inline const std::string FLXSPE_U = "FLXSPE_U"; + inline const std::string FLXSPA_U = "FLXSPA_U"; + inline const std::string FLXPIS_U = "FLXPIS_U"; + } + + namespace Focusd { + inline const std::string FOCUSI = "FOCUSI"; + inline const std::string FOCUSR = "FOCUSR"; + inline const std::string FOCUSG = "FOCUSG"; + inline const std::string FOCUSU = "FOCUSU"; + } + + namespace Powerd { + inline const std::string LAMPTHAR = "LAMPTHAR"; + inline const std::string LAMPFEAR = "LAMPFEAR"; + inline const std::string LAMPBLUC = "LAMPBLUC"; + inline const std::string LAMPREDC = "LAMPREDC"; + inline const std::string LAMPXE = "LAMPXE"; + inline const std::string LAMPINCA = "LAMPINCA"; + } + + namespace Thermald { + inline const std::string TCCD_I = "TCCD_I"; + inline const std::string TCCD_R = "TCCD_R"; + inline const std::string TCCD_G = "TCCD_G"; + inline const std::string TCCD_U = "TCCD_U"; + inline const std::string TCOLL_I = "TCOLL_I"; + inline const std::string TCOLL_R = "TCOLL_R"; + inline const std::string TCOLL_G = "TCOLL_G"; + inline const std::string TFOCUS_I = "TFOCUS_I"; + inline const std::string TFOCUS_R = "TFOCUS_R"; + inline const std::string TFOCUS_G = "TFOCUS_G"; + inline const std::string TFOCUS_U = "TFOCUS_U"; } } diff --git a/common/sequencerd_commands.h b/common/sequencerd_commands.h index 3a6504e2..40fcdd14 100644 --- a/common/sequencerd_commands.h +++ b/common/sequencerd_commands.h @@ -12,6 +12,7 @@ const std::string SEQUENCERD_CONFIG = "config"; const std::string SEQUENCERD_DOTYPE = "do"; const std::string SEQUENCERD_ENGINEERING = "engineering"; const std::string SEQUENCERD_EXIT = "exit"; +const std::string SEQUENCERD_FINEACQUIRE= "fineacquire"; const std::string SEQUENCERD_GETONETARGET = "getone"; const std::string SEQUENCERD_GUIDE = "guide"; const std::string SEQUENCERD_MODEXPTIME = "modexptime"; @@ -60,6 +61,7 @@ const std::vector SEQUENCERD_SYNTAX = { SEQUENCERD_DOTYPE+" [ one | all ]", SEQUENCERD_ENGINEERING+" [ true | false ]", SEQUENCERD_EXIT, + SEQUENCERD_FINEACQUIRE+" [ enable | disable ]", SEQUENCERD_GETONETARGET, SEQUENCERD_GUIDE, SEQUENCERD_MODEXPTIME+" ", diff --git a/common/tcsd_commands.h b/common/tcsd_commands.h index e8cffebc..01fa1f85 100644 --- a/common/tcsd_commands.h +++ b/common/tcsd_commands.h @@ -26,6 +26,7 @@ const std::string TCSD_NATIVE = "native"; const std::string TCSD_OFFSETRATE = "offsetrate"; const std::string TCSD_OPEN = "open"; const std::string TCSD_PTOFFSET = "offset"; +const std::string TCSD_PUBLISHSTATE = "publishstate"; const std::string TCSD_RETOFFSETS = "retoffsets"; const std::string TCSD_RINGGO = "ringgo"; const std::string TCSD_SET_FOCUS = "setfocus"; @@ -51,6 +52,7 @@ const std::vector TCSD_SYNTAX = { TCSD_OFFSETRATE+" [ ? | ]", TCSD_OPEN+" ? | ", TCSD_PTOFFSET+" ? | ", + TCSD_PUBLISHSTATE+" ? | on | off", TCSD_RETOFFSETS+" [ ? ]", TCSD_RINGGO+" ? | ", TCSD_SET_FOCUS+" ? | ", diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index d0b164fd..1e5fbfd7 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -832,13 +832,21 @@ namespace Sequencer { * */ void Sequence::handletopic_slicecamd(const nlohmann::json &jmessage) { - // set is_fineacquire_locked flag - bool fineacquirelocked; - Common::extract_telemetry_value( jmessage, Key::Slicecamd::FINEACQUIRE_LOCKED, fineacquirelocked ); - this->is_fineacquire_locked.store(fineacquirelocked, std::memory_order_relaxed); - - std::lock_guard lock(this->fineacquire_mtx); - this->fineacquire_cv.notify_all(); + bool changed = false; + if ( jmessage.contains( Key::Slicecamd::FINEACQUIRE_RUNNING ) ) { + this->is_fineacquire_running.store( + jmessage[Key::Slicecamd::FINEACQUIRE_RUNNING].get(), std::memory_order_relaxed ); + changed = true; + } + if ( jmessage.contains( Key::Slicecamd::FINEACQUIRE_LOCKED ) ) { + this->is_fineacquire_locked.store( + jmessage[Key::Slicecamd::FINEACQUIRE_LOCKED].get(), std::memory_order_relaxed ); + changed = true; + } + if ( changed ) { + std::lock_guard lock(this->fineacquire_mtx); + this->fineacquire_cv.notify_all(); + } } /***** Sequencer::Sequence::handletopic_slicecamd **************************/ @@ -870,11 +878,20 @@ namespace Sequencer { * */ void Sequence::handletopic_acamd(const nlohmann::json &jmessage) { - // set is_acam_guiding flag bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + // track whether acamd is actively trying to acquire (mode == "acquiring") + if ( jmessage.contains( Key::Acamd::ACQUIRE_MODE ) ) { + const std::string mode = jmessage[Key::Acamd::ACQUIRE_MODE].get(); + this->is_acam_acquiring.store( mode == "acquiring", std::memory_order_relaxed ); + } + + int64_t pubtime=0; + Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime ); + this->acam_pubtime.store( pubtime, std::memory_order_relaxed ); + std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } @@ -902,8 +919,10 @@ namespace Sequencer { /***** Sequencer::Sequence::publish_seqstate ********************************/ /** - * @brief publishes sequencer state with topic "seq_seqstate" - * @details seqstate is a single string + * @brief publishes sequencer state under Topic::SEQ_SEQSTATE + * @details The payload carries the sequencer state (Key::Sequencer::SEQSTATE) + * plus Key::Sequencer::SHOULD_FINEACQUIRE, which is a user-settable + * sequencer config and so rides along on this topic. * */ void Sequence::publish_seqstate() { @@ -915,6 +934,9 @@ namespace Sequencer { rtrim( seqstate ); jmessage_out[Key::Sequencer::SEQSTATE] = seqstate; + // user-settable: should automatic fine acquisition run? + jmessage_out[Key::Sequencer::SHOULD_FINEACQUIRE] = this->should_fineacquire.load(); + try { this->publisher->publish( jmessage_out, Topic::SEQ_SEQSTATE ); } @@ -1021,23 +1043,25 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_daemonstate ***************************/ /** * @brief publishes daemonstate and can control seqstate - * @details If not STARTING or STOPPING and not all daemons ready then - * this ensures that the seqstate drops into NOTREADY. + * @details Daemon-readiness changes may only force the sequencer into + * NOTREADY when the current seqstate is itself READY or + * NOTREADY. Lifecycle states (STARTING, STOPPING, RUNNING, + * PAUSED, ABORTING, FAILED) are owned by the lifecycle + * functions and are never overridden here. The one-hot + * seqstate contract is preserved. * */ void Sequence::broadcast_daemonstate() { // always publish daemonstate when called this->publish_daemonstate(); - // If any daemon isn't ready then the sequencer can't be ready, - // but don't override STARTING or STOPPING, unless none are ready. - if ( daemon_manager.are_all_clear() ) { - seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); - } - else - if ( ! seq_state_manager.is_set(SEQ_STARTING) && - ! seq_state_manager.is_set(SEQ_STOPPING) && - ! daemon_manager.are_all_set() ) { + // Only degrade seqstate to NOTREADY when the sequencer is currently + // READY or NOTREADY. Never override an active lifecycle transition + // (STARTING, STOPPING, RUNNING, PAUSED, ABORTING) or FAILED. + // + if ( ! daemon_manager.are_all_set() && + seq_state_manager.are_any_set( Sequencer::SEQ_READY, + Sequencer::SEQ_NOTREADY ) ) { seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); } } @@ -1046,23 +1070,38 @@ namespace Sequencer { /***** Sequencer::Sequence::broadcast_seqstate ******************************/ /** - * @brief publishes seq_state via PUB-SUB - * @details The legacy "SEQSTATE:" UDP async string has been removed; - * seqstate is published exclusively on the PUB-SUB topic now. + * @brief publishes seq_state on the SEQ_SEQSTATE topic + * @details Legacy UDP "SEQSTATE:" async strings have been removed. + * Seqstate is now broadcast only via PUB-SUB. * */ void Sequence::broadcast_seqstate() { + const std::string function("Sequencer::Sequence::broadcast_seqstate"); + + // publish the structured seqstate topic + // this->publish_seqstate(); this->cv.notify_all(); + + // emit a NOTICE on Topic::BROADCAST only when the lifecycle state has + // actually changed, so operators (and logs) get a breadcrumb trail of + // state transitions without noise from repeated identical callbacks. + // + std::string current( this->seq_state_manager.get_set_states() ); + rtrim( current ); + if ( current != this->last_seqstate_str ) { + this->last_seqstate_str = current; + this->broadcast.notice( function, "sequencer state: "+current ); + } } /***** Sequencer::Sequence::broadcast_seqstate ******************************/ /***** Sequencer::Sequence::broadcast_waitstate *****************************/ /** - * @brief publishes wait_state via PUB-SUB - * @details The legacy "WAITSTATE:" UDP async string has been removed; - * waitstate is published exclusively on the PUB-SUB topic now. + * @brief publishes wait_state on the SEQ_WAITSTATE topic + * @details Legacy UDP "WAITSTATE:" async strings have been removed. + * Waitstate is now broadcast only via PUB-SUB. * */ void Sequence::broadcast_waitstate() { @@ -3649,6 +3688,47 @@ namespace Sequencer { /***** Sequencer::Sequence::engineering *************************************/ + /***** Sequencer::Sequence::fine_acquire ************************************/ + /** + * @brief enable or disable automatic fine acquisition step + * @param[in] args enable|disable + * @param[out] retstring state {enabled|disabled} + * @return ERROR|NO_ERROR|HELP + * + */ + long Sequence::fine_acquire(std::string args, std::string &retstring) { + if (args=="help"||args=="?") { + retstring = SLICECAMD_FINEACQUIRE; + retstring.append( " [ enable | disable ]\n" ); + retstring.append( " enables or disables the automatic fine acquisition step\n" ); + retstring.append( " no arg returns state only\n" ); + return HELP; + } + + const bool prev = this->should_fineacquire.load(); + + if (args=="enable") this->should_fineacquire.store(true); + else + if (args=="disable") this->should_fineacquire.store(false); + else + if (!args.empty()) { + logwrite("Sequencer::Sequence::fineacquire", + "ERROR invalid '"+args+"' expected enable|disable"); + return ERROR; + } + + retstring = this->should_fineacquire.load() ? "enabled" : "disabled"; + + // publish on change + if ( this->should_fineacquire.load() != prev ) { + this->publish_seqstate(); + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::fine_acquire ************************************/ + + /***** Sequencer::Sequence::get_dome_position *******************************/ /** * @brief read the dome and telescope positions diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 928ae9e0..64be1376 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -132,8 +132,8 @@ namespace Sequencer { SEQ_STOPPING, ///< set when sequencer is shutting down SEQ_PAUSED, ///< set when sequencer is paused SEQ_STARTING, ///< set when sequencer is starting up - SEQ_FAILED, ///< set when sequencer has failed in an unsafe/indeterminate way - SEQ_ABORTING, ///< transitory state while aborting; cleared via RAII + SEQ_FAILED, ///< set on a fatal/indeterminate failure; cleared only by startup or shutdown + SEQ_ABORTING, ///< transitory; set/cleared via RAII in abort_process() NUM_SEQ_STATES }; @@ -308,8 +308,20 @@ namespace Sequencer { std::atomic cancel_flag{false}; std::atomic is_ontarget{false}; ///< remotely set by the TCS operator to indicate that the target is ready std::atomic is_usercontinue{false}; ///< remotely set by the user to continue + std::atomic should_fineacquire{true}; ///< should I use fineacquire? (user-switchable) std::atomic is_fineacquire_locked{false}; ///< is slicecam fine acquisition locked? - std::atomic is_acam_guiding{false}; ///< is acam guiding? + std::atomic is_fineacquire_running{false}; ///< is slicecam fine acquisition running? + std::atomic is_acam_guiding{false}; ///< is acam guiding (IS_ACQUIRED)? + std::atomic is_acam_acquiring{false}; ///< is acam in an acquire mode? + std::atomic acam_pubtime{0}; ///< publish time (us) of latest received acamd status + + /** @brief guard-band (us) subtracted from the acquire-command send time + * when computing the freshness boundary in do_acam_acquire. Tolerates + * jitter and the race between the command send and ACAM's forced publish. + * Adjust here to tune. + */ + static constexpr int64_t ACAM_FRESHNESS_GUARD_US = 500'000; + std::atomic engineering_mode{false}; ///< when true, validate_sequence applies per-daemon checks instead of SEQ_READY ThreadPool pool; @@ -681,8 +693,10 @@ namespace Sequencer { /// void set_seqstate_bit( uint32_t mb ); ///< set the specified masked bit in the seqstate word void broadcast_daemonstate(); ///< void broadcast_threadstate(); ///< - void broadcast_seqstate(); ///< writes the seqstate string to the async port - void broadcast_waitstate(); ///< writes the waitstate string to the async port + void broadcast_seqstate(); ///< publishes the seqstate on the seq_seqstate topic + void broadcast_waitstate(); ///< publishes the waitstate on the seq_waitstate topic + + std::string last_seqstate_str; ///< last seqstate string announced via broadcast_seqstate() (for change detection) uint32_t get_reqstate(); ///< get the reqstate word @@ -699,6 +713,7 @@ namespace Sequencer { long parse_calibration_target(); long parse_state( std::string whoami, std::string reply, bool &state ); ///< parse true|false state from reply string void dothread_test_fpoffset(); ///< for testing, calls Python function from thread + long fine_acquire( std::string args, std::string &retstring ); ///< enable|disable fineacquisition step long test( std::string args, std::string &retstring ); ///< handles test commands long extract_tcs_value( std::string reply, int &value ); ///< extract value returned by the TCS via tcsd long parse_tcs_generic( int value ); ///< parse generic TCS reply @@ -761,7 +776,9 @@ namespace Sequencer { * these are in sequence_acquisition.cpp */ long do_acam_acquire(); + long do_acam_stop(); long do_slicecam_fineacquire(); + long do_slicecam_stop(); long do_target_acquisition(std::string caller); long do_target_virtualslit(VirtualSlitMode mode); diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 66f99c7e..affbf926 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -29,7 +29,7 @@ namespace Sequencer { double angle_in = this->target.slitangle; if ( std::isnan(ra_in) || std::isnan(dec_in) ) { - this->broadcast.error( function, "converting target coordinates to decimal" ); + logwrite( function, "ERROR converting target coordinates to decimal" ); return ERROR; } @@ -38,8 +38,15 @@ namespace Sequencer { this->broadcast.notice( function, "starting ACAM acquisition" ); + // Freshness boundary: any ACAMD status publish strictly newer than this + // timestamp is considered fresh for this acquire cycle. The guard-band + // absorbs jitter and the timing race between this command send and + // ACAM's forced publish triggered by the command. + // + const int64_t freshness_boundary_us = get_time_us() - ACAM_FRESHNESS_GUARD_US; + if ( this->acamd.command( cmd.str(), reply ) != NO_ERROR ) { - this->broadcast.error( function, "sending acquire command to acamd" ); + logwrite( function, "ERROR sending acquire command to acamd" ); return ERROR; } @@ -47,27 +54,102 @@ namespace Sequencer { const auto timeout_time = std::chrono::steady_clock::now() + std::chrono::duration( this->acquisition_timeout ); - // wait for is_acam_guiding (I subscribe to this) - // or cancel, or timeout + // wait for is_acam_guiding (I subscribe to this) with a fresh ACAMD publish + // since the acquire command was sent, or acamd stopped without acquiring + // (failure), or cancel, or timeout. + // was_acquiring latches true once acamd enters "acquiring" mode, so that + // !is_acam_acquiring only signals failure after the attempt began — not + // before acamd's first telemetry publish arrives. // + bool was_acquiring = false; std::unique_lock lock(this->acam_mtx); this->acam_cv.wait(lock, [&]() { - return this->is_acam_guiding.load() || this->cancel_flag.load() || - (use_timeout && std::chrono::steady_clock::now() > timeout_time); + if ( this->is_acam_acquiring.load() ) was_acquiring = true; + const bool fresh = this->acam_pubtime.load() > freshness_boundary_us; + return ( fresh && this->is_acam_guiding.load() ) || this->cancel_flag.load() || + ( was_acquiring && !this->is_acam_acquiring.load() ) || + ( use_timeout && std::chrono::steady_clock::now() > timeout_time ); }); if (this->cancel_flag.load()) return ABORT; - if (use_timeout && !this->is_acam_guiding.load()) { - this->broadcast.error( function, "ACAM acquisition timed out!" ); - return TIMEOUT; + + // Determine outcome by reading state. If not guiding, then this is a + // failure of some kind; distinguish a true timeout from acamd stopping + // without acquiring by consulting the clock. This ordering ensures that + // a failure publish arriving at or near the timeout boundary is still + // reported as a failure (not mis-labeled as a timeout). + // + if (!this->is_acam_guiding.load()) { + if (use_timeout && std::chrono::steady_clock::now() >= timeout_time) { + logwrite( function, "ERROR ACAM acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR ACAM acquisition stopped without acquiring target" ); + return ERROR; } - this->async.enqueue_and_log(function, "ACAM target acquired"); + this->broadcast.notice( function, "ACAM target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_acam_acquire **********************************/ + /***** Sequencer::Sequence::do_acam_stop *************************************/ + /** + * @brief stops ACAM guiding + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_acam_stop() { + const std::string function("Sequencer::Sequence::do_acam_stop"); + + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); + + // any acamd status newer than this timestamp is considered fresh + const int64_t freshness_boundary_us = get_time_us() - ACAM_FRESHNESS_GUARD_US; + + // send ACQUIRE STOP command to ACAM + std::string reply; + if ( this->acamd.command( ACAMD_ACQUIRE+" stop", reply ) != NO_ERROR ) { + logwrite( function, "ERROR stopping guiding" ); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + logwrite( function, "ERROR acam: "+reply ); + return ERROR; + } + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for a new is_acam_guiding, or cancel, or timeout + std::unique_lock lock(this->acam_mtx); + this->acam_cv.wait(lock, [&]() { + const bool fresh = this->acam_pubtime.load() > freshness_boundary_us; + return ( fresh && !this->is_acam_guiding.load() ) || this->cancel_flag.load() || + ( use_timeout && std::chrono::steady_clock::now() > timeout_time ); + }); + + // success + if (!this->is_acam_guiding.load()) { + this->broadcast.notice( function, "guiding stopped" ); + return NO_ERROR; + } + + // failure + if (this->cancel_flag.load()) return ABORT; + if (use_timeout) { + logwrite( function, "ERROR timeout stopping guiding" ); + return TIMEOUT; + } + logwrite( function, "ERROR stopping guiding" ); + return ERROR; + } + /***** Sequencer::Sequence::do_acam_stop *************************************/ + + /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ /** * @brief trigger SLICECAM fine acquisition and wait until locked @@ -79,15 +161,14 @@ namespace Sequencer { ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_FINEACQUIRE); - // TODO don't hard-code the arguments here: std::string reply; - if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start L", reply ) != NO_ERROR) { - this->broadcast.error( function, "starting slicecam fine acquisition" ); + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start", reply ) != NO_ERROR) { + logwrite( function, "ERROR starting slicecam fine acquisition" ); return ERROR; } if ( reply.find("ERROR") != std::string::npos ) { - this->async.enqueue_and_log(function, "slicecam fine acquisition mode: "+reply); + logwrite( function, "ERROR slicecam fine acquisition mode: "+reply ); return ERROR; } @@ -98,26 +179,96 @@ namespace Sequencer { + std::chrono::duration( this->acquisition_timeout ); // wait for is_fineacquire_locked (I subscribe to this) - // or cancel, or timeout + // or slicecam stopped without locking (failure), or cancel, or timeout. + // was_running latches true once slicecamd confirms it started, so that + // !is_fineacquire_running only signals failure after the run began — not + // before slicecamd's first telemetry publish arrives. // + bool was_running = false; std::unique_lock lock(this->fineacquire_mtx); this->fineacquire_cv.wait(lock, [&]() { + if ( this->is_fineacquire_running.load() ) was_running = true; return this->is_fineacquire_locked.load() || this->cancel_flag.load() || + ( was_running && !this->is_fineacquire_running.load() ) || (use_timeout && std::chrono::steady_clock::now() > timeout_time); }); if (this->cancel_flag.load()) return ABORT; - if (use_timeout && !this->is_fineacquire_locked.load()) { - this->broadcast.error( function, "slicecam fine acquisition timed out!" ); - return TIMEOUT; + + // Determine outcome by reading state. If not locked, then this is a + // failure of some kind; distinguish a true timeout from slicecamd + // stopping without locking by consulting the clock. This ordering + // ensures that a failure publish arriving at or near the timeout + // boundary is still reported as a failure (not mis-labeled as a + // timeout). + // + if (!this->is_fineacquire_locked.load()) { + if (use_timeout && std::chrono::steady_clock::now() >= timeout_time) { + logwrite( function, "ERROR slicecam fine acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR slicecam fine acquisition stopped without locking" ); + return ERROR; } - this->async.enqueue_and_log(function, "slicecam fine acquisition target acquired"); + this->broadcast.notice( function, "slicecam fine acquisition target acquired" ); return NO_ERROR; } /***** Sequencer::Sequence::do_slicecam_fineacquire **************************/ + /***** Sequencer::Sequence::do_slicecam_stop **********************************/ + /** + * @brief stops slicecam fineacquire + * @return NO_ERROR | ERROR | TIMEOUT + * + */ + long Sequence::do_slicecam_stop() { + const std::string function("Sequencer::Sequence::do_slicecam_stop"); + + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); + + // send STOP command to SLICECAM + std::string reply; + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" stop", reply ) != NO_ERROR) { + logwrite( function, "ERROR stopping fine acquisition" ); + return ERROR; + } + + if ( reply.find("ERROR") != std::string::npos ) { + logwrite( function, "ERROR slicecam fine acquisition mode: "+reply ); + return ERROR; + } + + const bool use_timeout = ( this->acquisition_timeout > 0 ); + const auto timeout_time = std::chrono::steady_clock::now() + + std::chrono::duration( this->acquisition_timeout ); + + // wait for !is_fineacquire_locked or cancel, or timeout + std::unique_lock lock(this->fineacquire_mtx); + this->fineacquire_cv.wait(lock, [&]() { + return !this->is_fineacquire_locked.load() || this->cancel_flag.load() || + (use_timeout && std::chrono::steady_clock::now() > timeout_time); + }); + + // success + if (!this->is_fineacquire_locked.load()) { + this->broadcast.notice( function, "slicecam fine acquisition stopped" ); + return NO_ERROR; + } + + // failure + if (this->cancel_flag.load()) return ABORT; + if (use_timeout) { + logwrite( function, "ERROR slicecam fine acquisition timed out!" ); + return TIMEOUT; + } + logwrite( function, "ERROR stopping fine acquisition" ); + return ERROR; + } + /***** Sequencer::Sequence::do_slicecam_stop *********************************/ + + /***** Sequencer::Sequence::do_target_acquisition ****************************/ /** * @brief performs target acquisition @@ -163,4 +314,5 @@ namespace Sequencer { return this->slit_set(mode); } /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + } diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 20be3f59..5920b3ce 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1161,6 +1161,10 @@ namespace Sequencer { this->exit_cleanly(); // shutdown the sequencer } else + if ( cmd == SEQUENCERD_FINEACQUIRE ) { + this->sequence.fine_acquire(args, retstring); + } + else // These commands go to acamd // diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 26f43523..f7c76c3e 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -131,15 +131,15 @@ int main(int argc, char **argv) { // if ( sequencerd.sequence.init_pubsub( { Topic::CAMERAD, Topic::ACAMD, - Topic::TCSD, + Topic::SLICECAMD, Topic::SLITD, - Topic::SLICECAMD } ) == ERROR ) { + Topic::TCSD } ) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); sequencerd.exit_cleanly(); } - sequencerd.sequence.seq_state_manager.set(Sequencer::SEQ_NOTREADY); + sequencerd.sequence.seq_state_manager.set_only({Sequencer::SEQ_NOTREADY}); - std::this_thread::sleep_for( std::chrono::milliseconds(100) ); + std::this_thread::sleep_for( std::chrono::milliseconds(200) ); sequencerd.sequence.publish_snapshot(); // This will pre-thread N_THREADS threads. diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index e6e86bd6..1c57175c 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -10,6 +10,7 @@ #include "slicecam_interface.h" #include "slicecam_math.h" +#include "fits_header_defs.h" namespace Slicecam { @@ -54,21 +55,18 @@ namespace Slicecam { // stop fine acquisition if (action=="stop") { - if (!this->is_fineacquire_running.load(std::memory_order_acquire)) { - logwrite(function, "stopped"); - } - else { - this->is_fineacquire_running.store(false); - this->publish_status(); - logwrite(function, "stop requested"); - } + const bool was_running = this->is_fineacquire_running.load(std::memory_order_acquire); + this->is_fineacquire_locked.store(false, std::memory_order_release); + this->is_fineacquire_running.store(false, std::memory_order_release); + this->publish_status(); + logwrite(function, was_running ? "stop requested" : "stopped"); retstring=this->is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; return NO_ERROR; } else // not empty, stop or start is an error - if (action != "start" || tokens.size() < 2) { + if (action != "start") { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; @@ -90,28 +88,57 @@ namespace Slicecam { return ERROR; } - // ACAM must be guiding - if (!this->is_acam_guiding.load(std::memory_order_acquire)) { - logwrite(function, "ERROR ACAM is not guiding"); - retstring="stopped"; + // Wait briefly for ACAM status to be fresh and guiding. This absorbs the + // pub/sub propagation lag where sequencerd has seen the ACAM acquire + // publish but slicecamd's subscriber thread has not yet delivered it. + { + const auto wait_duration = std::chrono::microseconds( ACAM_WAIT_TIMEOUT_US ); + std::unique_lock lock(this->acam_mtx); + const bool ready = this->acam_cv.wait_for( lock, wait_duration, [this]() { + return this->is_acam_status_fresh() && + this->is_acam_guiding.load(std::memory_order_relaxed); + }); + + if ( !ready ) { + if ( !this->is_acam_status_fresh() ) { + const int64_t age_ms = ( get_time_us() - this->last_acam_pubtime.load() ) / 1000; + message.str(""); message << "ERROR timed out waiting for fresh ACAM status (age=" + << age_ms << " ms)"; + } + else { + message.str(""); message << "ERROR timed out waiting for ACAM to guide"; + } + logwrite( function, message.str() ); + retstring = "stopped"; return ERROR; } + } // are optional but if specified then require all three - if (tokens.size() > 1 && tokens.size() != 4) { - logwrite(function, "ERROR ACAM is not guiding"); - retstring="stopped"; + if ( tokens.size() != 1 && tokens.size() != 4 ) { + logwrite(function, "ERROR expected stop | start [ { L | R } ]"); + retstring="invalid_argument"; return ERROR; } - else - // override the default camera and aimpoint if provided - if (tokens.size() > 1 && tokens.size() == 4) { + + if ( tokens.size() == 1 ) { + // no which/aimpoint args so use defaults from config file + if ( this->default_which.empty() || !this->default_aimpoint.is_valid() ) { + logwrite(function, "ERROR fineacquire defaults not configured"); + retstring="not_configured"; + return ERROR; + } + this->fineacquire_state.which = this->default_which; + this->fineacquire_state.aimpoint = this->default_aimpoint; + } + else { + // use the passed-in values try { const std::string which = tokens.at(1); - double x = std::stod(tokens.at(2)); - double y = std::stod(tokens.at(3)); + const double x = std::stod(tokens.at(2)); + const double y = std::stod(tokens.at(3)); if ( (which != "L" && which != "R") || - std::isnan(x) || std::isnan(y) || x<0 || y<0) { + std::isnan(x) || std::isnan(y) || x<0 || y<0 ) { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); retstring="invalid_argument"; return ERROR; @@ -126,6 +153,12 @@ namespace Slicecam { } } + // log the effective camera and aimpoint for this invocation + message.str(""); message << "using camera=" << this->fineacquire_state.which + << " aimpoint=(" << this->fineacquire_state.aimpoint.x + << ", " << this->fineacquire_state.aimpoint.y << ")"; + logwrite(function, message.str()); + // start the state machine this->fineacquire_state.reset(); this->is_fineacquire_locked.store(false, std::memory_order_release); @@ -134,10 +167,10 @@ namespace Slicecam { // publishes status on change only this->publish_status(); - logwrite(function, "fine target acquisition enabled"); - retstring=is_fineacquire_running.load(std::memory_order_acquire)?"running":"stopped"; + logwrite(function, "fine target acquisition "+retstring); + return NO_ERROR; } /***** Slicecam::Interface::fineacquire *************************************/ @@ -158,8 +191,8 @@ namespace Slicecam { // skip frames if we are waiting for the telescope to settle after a move // - if (this->fineacquire_state.skip_frames > 0) { - this->fineacquire_state.skip_frames--; + if (this->fineacquire_state.settle_frames > 0) { + this->fineacquire_state.settle_frames--; return; } @@ -197,9 +230,17 @@ namespace Slicecam { this->fineacquire_state.bg_region, this->fineacquire_state.aimpoint, centroid) != NO_ERROR ) { - logwrite(function, "WARNING: failed to find centroid, skipping frame"); + const int max_failures = 3 * this->fineacquire_state.max_samples; + if ( ++this->fineacquire_state.consecutive_centroid_failures >= max_failures ) { + logwrite(function, "ERROR: too many consecutive centroid failures, stopping fine acquisition"); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->publish_status(); + } else { + logwrite(function, "WARNING: failed to find centroid, skipping frame"); + } return; } + this->fineacquire_state.consecutive_centroid_failures = 0; // convert centroid pixel -> sky using WCS from FITS header // @@ -236,23 +277,44 @@ namespace Slicecam { this->fineacquire_state.dra_samp.push_back( offsets.first ); this->fineacquire_state.ddec_samp.push_back( offsets.second ); - // wait until there are enough samples to evaluate a move - // + const int n = static_cast( this->fineacquire_state.dra_samp.size() ); const int max_samples = this->fineacquire_state.max_samples; - if ( static_cast(this->fineacquire_state.dra_samp.size()) < max_samples ) { - return; - } + const int min_samples = this->fineacquire_state.min_samples; + + // wait for the minimum number of samples before evaluating anything + // + if ( n < min_samples ) return; - // calculate median from accumulated samples + // compute running median // - std::vector dra_sorted = this->fineacquire_state.dra_samp; + std::vector dra_sorted = this->fineacquire_state.dra_samp; std::vector ddec_sorted = this->fineacquire_state.ddec_samp; std::sort( dra_sorted.begin(), dra_sorted.end() ); std::sort( ddec_sorted.begin(), ddec_sorted.end() ); - const double med_dra = dra_sorted[ max_samples / 2 ]; - const double med_ddec = ddec_sorted[ max_samples / 2 ]; + const double med_dra = dra_sorted[ n / 2 ]; + const double med_ddec = ddec_sorted[ n / 2 ]; + + // compute MAD-derived scatter per axis to gate early exit + // + std::vector abs_dra( n ), abs_ddec( n ); + for ( int i = 0; i < n; i++ ) { + abs_dra[i] = std::abs( this->fineacquire_state.dra_samp[i] - med_dra ); + abs_ddec[i] = std::abs( this->fineacquire_state.ddec_samp[i] - med_ddec ); + } + std::sort( abs_dra.begin(), abs_dra.end() ); + std::sort( abs_ddec.begin(), abs_ddec.end() ); + + const double sig_dra = 1.4826 * abs_dra[ n / 2 ] * 3600.0; // arcsec + const double sig_ddec = 1.4826 * abs_ddec[ n / 2 ] * 3600.0; // arcsec + + const double prec = this->fineacquire_state.prec_arcsec; + const bool scatter_ok = ( sig_dra <= prec && sig_ddec <= prec ); + + // keep accumulating if scatter is still too high and max not reached + // + if ( !scatter_ok && n < max_samples ) return; // convert to arcsec only for the threshold comparison and logging // @@ -269,17 +331,26 @@ namespace Slicecam { return; } + // select gain: use gain_large when offset is well above the goal threshold + // + const double effective_gain = ( offset_arcsec > this->fineacquire_state.gain_threshold_arcsec ) + ? this->fineacquire_state.gain_large + : this->fineacquire_state.gain; + // send gain-weighted offsets to acam // std::ostringstream oss; oss << "offset dRA=" << med_dra * 3600.0 << " dDEC=" << med_ddec * 3600.0 << " arcsec (r=" << offset_arcsec - << " arcsec) -- applying correction"; + << " arcsec, n=" << n + << " scatter=(" << sig_dra << "," << sig_ddec << ") arcsec)" + << " gain=" << effective_gain + << " -- applying correction"; logwrite( function, oss.str() ); - const double cmd_dra = this->fineacquire_state.gain * med_dra; - const double cmd_ddec = this->fineacquire_state.gain * med_ddec; + const double cmd_dra = effective_gain * med_dra; + const double cmd_ddec = effective_gain * med_ddec; if ( this->offset_acam_goal( { cmd_dra, cmd_ddec }, true ) != NO_ERROR ) { logwrite( function, "ERROR failed to send offset to ACAM" ); @@ -288,9 +359,9 @@ namespace Slicecam { return; } - // reset samples and skip a couple of frames for telescope settling + // reset samples and discard settle_count frames for telescope settling this->fineacquire_state.reset(); - this->fineacquire_state.skip_frames = 2; + this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count; } /***** Slicecam::Interface::do_fineacquire **********************************/ @@ -421,10 +492,34 @@ namespace Slicecam { bool acquired; Common::extract_telemetry_value( jmessage, Key::Acamd::IS_ACQUIRED, acquired ); this->is_acam_guiding.store(acquired, std::memory_order_relaxed); + + // acam's publish time + int64_t pubtime=0; + Common::extract_telemetry_value( jmessage, Key::PUBTIME, pubtime ); + this->last_acam_pubtime.store( pubtime, std::memory_order_relaxed ); + + // wake any thread waiting on ACAM state (e.g. fineacquire) + std::lock_guard lock(this->acam_mtx); + this->acam_cv.notify_all(); } /***** Slicecam::Interface::handletopic_acamd *******************************/ + /***** Slicecam::Interface::is_acam_status_fresh ****************************/ + /** + * @brief check whether cached ACAM status is fresh enough to trust + * @return true if cached status has been received at least once and is + * within ACAM_STATUS_MAX_AGE_US of the current time + * + */ + bool Interface::is_acam_status_fresh() const { + const int64_t last = this->last_acam_pubtime.load(); + if ( last == 0 ) return false; + return ( get_time_us() - last ) <= ACAM_STATUS_MAX_AGE_US; + } + /***** Slicecam::Interface::is_acam_status_fresh ****************************/ + + /***** Slicecam::Interface::handletopic_slitd *******************************/ /** * @brief what to do when the topic is Topic::SLITD @@ -437,11 +532,13 @@ namespace Slicecam { std::lock_guard lock(snapshot_mtx); snapshot_status[Topic::SLITD]=true; } - Common::extract_telemetry_value( jmessage, "SLITO", telem.slitoffset ); - Common::extract_telemetry_value( jmessage, "SLITW", telem.slitwidth ); + Common::extract_telemetry_value( jmessage, Key::Slitd::SLITO, telem.slitoffset ); + Common::extract_telemetry_value( jmessage, Key::Slitd::SLITW, telem.slitwidth ); - this->telemkeys.add_json_key(jmessage, "SLITO", "SLITO", "slit offset in arcsec", "FLOAT", false); - this->telemkeys.add_json_key(jmessage, "SLITW", "SLITW", "slit width in arcsec", "FLOAT", false); + for ( const auto &keyinfo : FitsHeaderKeys::SlitdTelemKeys ) { + this->telemkeys.add_json_key(jmessage, keyinfo.jkey, keyinfo.keyword, + keyinfo.comment, keyinfo.type, false); + } } /***** Slicecam::Interface::handletopic_slitd *******************************/ @@ -762,6 +859,13 @@ namespace Slicecam { logwrite(function, "ERROR invalid FINE_ACQUIRE_AIMPOINT='"+config.arg[entry]+"' expected "); return ERROR; } + if ( which != "L" && which != "R" ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_AIMPOINT which=\"" << which + << "\" : expected { L R }"; + logwrite( function, message.str() ); + return ERROR; + } + // store in the class for validation this->fineacquire_state.which = which; this->fineacquire_state.aimpoint = { x, y }; } @@ -775,12 +879,82 @@ namespace Slicecam { } this->fineacquire_state.bg_region = { x1, x2, y1, y2 }; } + else + if ( config.param[entry] == "FINE_ACQUIRE_MIN_SAMPLES" ) { + try { this->fineacquire_state.min_samples = std::stoi( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_MIN_SAMPLES " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_SETTLE_FRAMES" ) { + try { this->fineacquire_state.settle_count = std::stoi( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_SETTLE_FRAMES " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN" ) { + try { this->fineacquire_state.gain = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN_LARGE" ) { + try { this->fineacquire_state.gain_large = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN_LARGE " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_GAIN_THRESHOLD" ) { + try { this->fineacquire_state.gain_threshold_arcsec = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_GAIN_THRESHOLD " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } } // FINE_ACQUIRE parameters must have been configured properly // - if (!this->fineacquire_state.is_valid()) { + if (this->fineacquire_state.is_valid()) { + // if so then save these as the defaults + this->default_which = this->fineacquire_state.which; + this->default_aimpoint = this->fineacquire_state.aimpoint; + } + else { logwrite(function, "ERROR bad or missing FINE_ACQUIRE configuration"); return ERROR; } diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 0ca7625e..0785aa84 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -76,21 +76,28 @@ namespace Slicecam { * goal_arcsec - convergence threshold; loop stops when median offset * magnitude falls below this value * gain - fraction of the median offset commanded each cycle (0..1) - * skip_frames - counts down frames to discard while telescope settles - * after a commanded move + * settle_count - configured frames to discard after each commanded move + * settle_frames - runtime countdown; decremented to zero before resuming */ struct FineAcqState { std::string which; - Point aimpoint; ///< 1-based pixel aim point + Point aimpoint { NAN, NAN }; ///< 1-based pixel aim point; NAN until configured Rect bg_region; ///< background ROI (1-based) std::vector dra_samp; ///< dRA*cos(dec) samples, degrees std::vector ddec_samp; ///< dDEC samples, degrees int max_samples = 10; ///< samples before evaluating a move + int min_samples = 3; ///< minimum samples before scatter-gated early exit + double prec_arcsec = 0.1; ///< MAD scatter threshold per axis for early exit (arcsec) double goal_arcsec = 0.3; ///< convergence threshold, arcsec - double gain = 0.7; ///< gain applied to commanded offset - int skip_frames = 0; ///< frames to skip after a telescope move - - void reset() { dra_samp.clear(); ddec_samp.clear(); skip_frames = 0; } + double gain = 0.7; ///< gain applied when offset <= gain_threshold_arcsec + double gain_large = 1.0; ///< gain applied when offset > gain_threshold_arcsec + double gain_threshold_arcsec = 2.0; ///< offset above which gain_large is used + int settle_frames = 0; ///< countdown of frames to discard while telescope settles + int settle_count = 2; ///< configured: frames to discard after each move + int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures + + void reset() { dra_samp.clear(); ddec_samp.clear(); settle_frames = 0; + consecutive_centroid_failures = 0; } bool is_valid() const noexcept { return !which.empty() && aimpoint.is_valid() && bg_region.is_valid(); } @@ -119,8 +126,14 @@ namespace Slicecam { std::mutex framegrab_mtx; std::condition_variable cv; + std::mutex acam_mtx; ///< guards waiters on acam_cv + std::condition_variable acam_cv; ///< notified when cached ACAM state updates + FineAcqState fineacquire_state; + std::string default_which; ///< configured default camera for fineacquire + Point default_aimpoint { NAN, NAN }; ///< configured default aimpoint for fineacquire + public: std::unique_ptr publisher; ///< publisher object std::string publisher_address; ///< publish socket endpoint @@ -140,6 +153,16 @@ namespace Slicecam { std::atomic is_fineacquire_locked; ///< set when fine acquire target acquired std::atomic is_acam_guiding; ///< is acam guiding? + std::atomic last_acam_pubtime{0}; ///< pubtime (us) of latest received acamd status + + /// Max acceptable age (us) for cached ACAM status used by fineacquire. + static constexpr int64_t ACAM_STATUS_MAX_AGE_US = 10'000'000; + + /// Max time (us) fineacquire() will wait for a fresh, guiding ACAM status before failing. + static constexpr int64_t ACAM_WAIT_TIMEOUT_US = 2'000'000; + + bool is_acam_status_fresh() const; + /** these are set by Interface::saveframes() */ std::atomic nsave_preserve_frames; ///< number of frames to preserve (normally overwritten) @@ -207,14 +230,8 @@ namespace Slicecam { inline std::string get_imagename() { return this->imagename; } inline std::string get_wcsname() { return this->wcsname; } - inline void set_imagename( std::string name_in ) { - this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : std::move(name_in) ); - return; - } - inline void set_wcsname( std::string name_in ) { - this->wcsname = std::move(name_in); - return; - } + inline void set_imagename( std::string name_in ) { this->imagename = ( name_in.empty() ? DEFAULT_IMAGENAME : name_in ); return; } + inline void set_wcsname( std::string name_in ) { this->wcsname = name_in; return; } Slicecam::FitsInfo fitsinfo; diff --git a/tcsd/tcs_interface.cpp b/tcsd/tcs_interface.cpp index 576ba9b8..deee79b0 100644 --- a/tcsd/tcs_interface.cpp +++ b/tcsd/tcs_interface.cpp @@ -44,9 +44,12 @@ namespace TCS { nlohmann::json jmessage_out; jmessage_out[Key::SOURCE] = Daemon::TCSD; + std::string motion; { std::lock_guard lock(tcs_info_mtx); + motion = this->tcs_info.motion; + jmessage_out[Key::Tcsd::MOTION] = motion; jmessage_out["ISOPEN"] = this->tcs_info.isopen; jmessage_out["TCSNAME"] = this->tcs_info.tcsname; @@ -66,7 +69,13 @@ namespace TCS { jmessage_out["DOMESHUT"] = this->tcs_info.domeshutters==1?"open":"closed"; jmessage_out["TELFOCUS"] = this->tcs_info.focus; jmessage_out[Key::Tcsd::AIRMASS] = this->tcs_info.airmass; - jmessage_out["MOTION"] = this->tcs_info.motion; + } + + // broadcast motion status if it changed + if (!motion.empty() && + motion != this->last_published_motion) { + this->broadcast.notice("TCS::Interface::publish_snapshot", "telescope "+motion); + this->last_published_motion = motion; } // for backwards compatibility @@ -107,6 +116,37 @@ namespace TCS { /***** TCS::Interface::do_continuous_snapshot *******************************/ + /***** TCS::Interface::publish_state ****************************************/ + /** + * @brief set | get snapshot publish state + * @param[in] arg on|off + * @param[out] retstring reference to string to contain the state + * @return NO_ERROR | HELP + * + */ + long Interface::publish_state( const std::string &arg, std::string &retstring ) { + const std::string function = "TCS::Interface::publish_state"; + + // help + if ( arg == "?" || arg == "help" ) { + retstring = TCSD_PUBLISHSTATE; + retstring.append( " on | off\n" ); + retstring.append( " set | get continuous snapshot publish state\n" ); + return HELP; + } + // on + else if ( arg == "on" ) should_publish.store(true); + + // off + else if ( arg == "off" ) should_publish.store(false); + + retstring = should_publish.load() ? "on" : "off"; + + return NO_ERROR; + } + /***** TCS::Interface::publish_state ****************************************/ + + /***** TCS::Interface::get_tcs_info *****************************************/ /** * @brief fills the tcs_info class @@ -203,7 +243,7 @@ namespace TCS { */ long Interface::llist( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::llist"; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -226,9 +266,6 @@ namespace TCS { retstring = message.str(); - asyncmsg << "TCSD:llist:" << retstring; - this->async.enqueue( asyncmsg.str() ); - return NO_ERROR; } /***** TCS::Interface::llist ************************************************/ @@ -243,7 +280,7 @@ namespace TCS { */ long Interface::open( std::string arg, std::string &retstring ) { const std::string function("TCS::Interface::open"); - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -378,7 +415,7 @@ namespace TCS { */ long Interface::isopen( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::isopen"; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -405,12 +442,6 @@ namespace TCS { retstring = ( this->tcs_info.isopen ? "true" : "false" ); // return string is the state } - asyncmsg.str(""); asyncmsg << "TCSD:open:" << retstring; - this->async.enqueue( asyncmsg.str() ); // broadcast the state - - asyncmsg.str(""); asyncmsg << "TCSD:name:" << ( ! _name.empty() ? _name : "offline" ); - this->async.enqueue( asyncmsg.str() ); // broadcast the name - return NO_ERROR; } /***** TCS::Interface::isopen ***********************************************/ @@ -424,7 +455,7 @@ namespace TCS { */ long Interface::close( ) { std::string function = "TCS::Interface::close"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; for ( const auto &[key,tcs] : this->tcsmap ) { @@ -474,14 +505,11 @@ namespace TCS { logwrite( function, "connection open to "+tcs->getname() ); } retstring = tcs->getname(); // Found the connected TCS - this->async.enqueue( "TCSD:name:"+retstring ); return NO_ERROR; } } retstring="offline"; - this->async.enqueue( "TCSD:name:offline" ); - return NO_ERROR; } /***** TCS::Interface::get_name *********************************************/ @@ -498,7 +526,7 @@ namespace TCS { */ long Interface::get_weather_coords( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_weather_coords"; - std::stringstream message, asyncmsg; + std::stringstream message; std::string weather; long error = NO_ERROR; @@ -555,9 +583,6 @@ namespace TCS { retstring = message.str(); - asyncmsg << "TCSD:weathercoords:" << ( error==NO_ERROR ? message.str() : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_weather_coords ***********************************/ @@ -575,7 +600,7 @@ namespace TCS { */ long Interface::get_coords( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_coords"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -650,9 +675,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:coords:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_coords *******************************************/ @@ -670,7 +692,7 @@ namespace TCS { */ long Interface::get_cass( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_cass"; - std::stringstream message, asyncmsg; + std::stringstream message; std::stringstream reply; long error = NO_ERROR; @@ -747,9 +769,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:cassangle:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_cass *********************************************/ @@ -767,7 +786,7 @@ namespace TCS { */ long Interface::get_dome( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_dome"; - std::stringstream message, asyncmsg; + std::stringstream message; std::string weather; long error = NO_ERROR; @@ -819,9 +838,6 @@ namespace TCS { if ( !retstring.empty() ) logwrite( function, retstring ); - asyncmsg << "TCSD:dome:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_dome *********************************************/ @@ -838,7 +854,7 @@ namespace TCS { */ long Interface::set_focus( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::set_focus"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -852,7 +868,7 @@ namespace TCS { if ( arg.empty() ) { retstring="missing_argument"; return ERROR; } - double value; + double value=NAN; try { value = std::stod( arg ); @@ -918,7 +934,7 @@ namespace TCS { */ long Interface::get_focus( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_focus"; - std::stringstream message, asyncmsg; + std::stringstream message; std::stringstream reply; long error = NO_ERROR; @@ -1003,9 +1019,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - asyncmsg << "TCSD:focus:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_focus ********************************************/ @@ -1043,9 +1056,6 @@ namespace TCS { logwrite( function, retstring ); } - message.str(""); message << "TCSD:offsets:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( message.str() ); - return NO_ERROR; } /***** TCS::Interface::get_offsets ******************************************/ @@ -1176,10 +1186,6 @@ namespace TCS { if ( !retstring.empty() && !silent ) logwrite( function, retstring ); - std::stringstream asyncmsg; - asyncmsg << "TCSD:parallactic:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return NO_ERROR; } /***** TCS::Interface::get_pa ***********************************************/ @@ -1196,7 +1202,7 @@ namespace TCS { */ long Interface::pt_offsetrate( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::pt_offsetrate"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -1264,7 +1270,7 @@ namespace TCS { */ long Interface::get_motion( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::get_motion"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = NO_ERROR; // Help @@ -1283,9 +1289,6 @@ namespace TCS { error = ERROR; } - asyncmsg << "TCSD:motion:" << ( !retstring.empty() ? retstring : "ERROR" ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::get_motion *******************************************/ @@ -1302,7 +1305,7 @@ namespace TCS { */ long Interface::ringgo( const std::string &arg, std::string &retstring ) { std::string function = "TCS::Interface::ringgo"; - std::stringstream message, asyncmsg; + std::stringstream message; long error = ERROR; double angle = NAN; @@ -1385,7 +1388,7 @@ namespace TCS { long Interface::coords( std::string args, std::string &retstring ) { std::string function = "TCS::Interface::coords"; std::string retcode; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // @@ -1414,9 +1417,6 @@ namespace TCS { long error = this->send_command( cmd.str(), retstring, TCS::FAST_RESPONSE ); - asyncmsg << "TCSD:coords:" << ( error == ERROR ? "ERROR" : retstring ); - this->async.enqueue( asyncmsg.str() ); - return error; } /***** TCS::Interface::coords ***********************************************/ @@ -1524,7 +1524,7 @@ namespace TCS { long Interface::zero_offsets( const std::string args, std::string &retstring ) { std::string function = "TCS::Interface::zero_offsets"; std::string retcode; - std::stringstream message, asyncmsg; + std::stringstream message; // Help // diff --git a/tcsd/tcs_interface.h b/tcsd/tcs_interface.h index e451f6a1..6e73e6bd 100644 --- a/tcsd/tcs_interface.h +++ b/tcsd/tcs_interface.h @@ -429,6 +429,7 @@ logwrite(function,message.str()); zmqpp::context context; std::string default_tcs; ///< default TCS to use specified in .cfg std::mutex tcs_info_mtx; ///< protects tcs_info + std::string last_published_motion; ///< last published motion (publish on change) public: inline void set_default_tcs(const std::string &which) { this->default_tcs=which; } @@ -502,6 +503,7 @@ logwrite(function,message.str()); long isopen( std::string &retstring ); long isopen( const std::string &arg, std::string &retstring ); long close(); + long publish_state( const std::string &arg, std::string &retstring ); long get_name( const std::string &arg, std::string &retstring ); long get_weather_coords( const std::string &arg, std::string &retstring ); long get_coords( const std::string &arg, std::string &retstring ); @@ -530,6 +532,7 @@ logwrite(function,message.str()); long get_tcs_info(); ///< fills the tcs_info class Common::Queue async; ///< asynchronous message queue object + Common::Broadcaster broadcast { this->publisher, Daemon::TCSD }; }; /***** TCS::Interface *******************************************************/ diff --git a/tcsd/tcs_server.cpp b/tcsd/tcs_server.cpp index 6139981e..96882f93 100644 --- a/tcsd/tcs_server.cpp +++ b/tcsd/tcs_server.cpp @@ -668,6 +668,13 @@ void doit(TcsIO &tcs_io, const std::string &client_cmd, bool is_slow_command) { } else + // publishstate + // + if ( cmd.compare( TCSD_PUBLISHSTATE ) == 0 ) { + ret = this->interface.publish_state( args, retstring ); + } + else + // llist // if ( cmd.compare( TCSD_LLIST ) == 0 ) { diff --git a/utils/logentry.cpp b/utils/logentry.cpp index b4acb65f..a384ee9e 100644 --- a/utils/logentry.cpp +++ b/utils/logentry.cpp @@ -175,7 +175,7 @@ void close_log() { * log filestream isn't open. * */ -void logwrite( std::string function, std::string message ) { +void logwrite( const std::string &function, std::string message ) { std::ostringstream logmsg; std::lock_guard lock(loglock); // lock mutex to protect from multiple access diff --git a/utils/logentry.h b/utils/logentry.h index a5c464e3..ddf95890 100644 --- a/utils/logentry.h +++ b/utils/logentry.h @@ -21,6 +21,6 @@ extern unsigned int nextday; /// number of seconds long init_log( std::string logpath, std::string name ); /// initialize the logging system long init_log( std::string logpath, std::string name, bool stderr_in ); /// initialize the logging system void close_log(); /// close the log file stream -void logwrite(std::string function, std::string message); /// create a time-stamped log entry "message" from "function" +void logwrite(const std::string &function, std::string message); /// create a time-stamped log entry "message" from "function" #endif diff --git a/utils/network.cpp b/utils/network.cpp index 6ad3e8eb..5b6a2402 100644 --- a/utils/network.cpp +++ b/utils/network.cpp @@ -176,12 +176,12 @@ namespace Network { */ int UdpSocket::Send(std::string message) { std::string function = "Network::UdpSocket::Send"; - std::ostringstream errstm; + std::stringstream errstm; ssize_t nbytes; if ( !this->is_running() ) return 0; // silently do nothing if the UDP multicast socket isn't running - if ( ( nbytes = sendto( this->fd, std::string(message).c_str(), (size_t)message.length(), 0, + if ( ( nbytes = sendto( this->fd, message.c_str(), (size_t)message.length(), 0, (struct sockaddr*) &this->addr, (socklen_t)sizeof(this->addr) ) ) < 0 ) { errstm << "error " << errno << " calling sendto: " << strerror(errno); logwrite(function, errstm.str()); diff --git a/utils/seqmon.cpp b/utils/seqmon.cpp new file mode 100644 index 00000000..8220cd7f --- /dev/null +++ b/utils/seqmon.cpp @@ -0,0 +1,492 @@ +/** + * @file seqmon.cpp + * @brief terminal-based subscriber that mimics the sequencer operator display + * @author David Hale + * + * Subscribes to sequencer topics on the ZMQ broker and renders a simple + * color-coded terminal display: + * + * seq_seqstate lifecycle state (top line, color by value) + * seq_waitstate active wait bits (second line, list of set labels) + * seq_daemonstate daemon online/offline status + * acamd acquisition mode, acquired flag, seeing + * slicecamd fine-acquisition locked/running flags + * broadcast operator messages (scrolling log, color by severity) + * + * On startup a Topic::SNAPSHOT request is published so that sequencerd, + * acamd, and slicecamd immediately re-publish their current telemetry, + * avoiding a stale display until the next natural update. + * + * This is a standalone utility intended as a stand-in for the GUI message + * window during testing. It has no dependencies on the sequencer sources; + * it only shares the message_keys.h contract. + * + */ + +#include +#include +#include "message_keys.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + constexpr const char *BROKER_ENDPOINT = "tcp://localhost:5556"; // subscribers connect here + constexpr const char *BROKER_PUB_ENDPOINT = "tcp://localhost:5555"; // publishers connect here + + // ANSI escape codes + // + constexpr const char *ANSI_RESET = "\033[0m"; + constexpr const char *ANSI_GRAY = "\033[90m"; + constexpr const char *ANSI_RED = "\033[31m"; + constexpr const char *ANSI_GREEN = "\033[32m"; + constexpr const char *ANSI_YELLOW = "\033[33m"; + constexpr const char *ANSI_BLUE = "\033[34m"; + constexpr const char *ANSI_CYAN = "\033[36m"; + constexpr const char *ANSI_BOLDYEL = "\033[1;33m"; + + // cursor control: save/restore, move up 3 lines, clear entire line + // + constexpr const char *CURSOR_SAVE = "\033[s"; + constexpr const char *CURSOR_RESTORE = "\033[u"; + constexpr const char *CURSOR_HOME = "\033[H"; + constexpr const char *CLEAR_SCREEN = "\033[2J"; + constexpr const char *CLEAR_LINE = "\033[2K"; + + // DECSTBM scrolling-region control. The header occupies HEADER_ROWS fixed + // rows at the top of the terminal; broadcast messages scroll within the + // region starting at row HEADER_ROWS+1 and extending to the bottom. Using + // 999 as the bottom bound lets the terminal clamp to its actual height. + // + // NOTE: the "6" in SCROLL_REGION_SET and CURSOR_LOG_START is HEADER_ROWS+1. + // If HEADER_ROWS changes, update these literals to match. + // + constexpr int HEADER_ROWS = 5; + constexpr const char *SCROLL_REGION_SET = "\033[6;999r"; // top = HEADER_ROWS+1 + constexpr const char *SCROLL_REGION_RESET = "\033[r"; // full-screen scrolling + constexpr const char *CURSOR_LOG_START = "\033[6;1H"; // row HEADER_ROWS+1, col 1 + constexpr const char *CURSOR_LOG_END = "\033[999;1H"; // bottom of scroll region + + std::atomic running{true}; + + // Cached last-known state so a redraw after a broadcast message can + // reprint the fixed top lines. + // + std::string last_seqstate = "(unknown)"; + std::string last_waitstate = "(none)"; + std::string last_daemonstate = "(none)"; + std::string last_acq_mode = "?"; // "stopped" | "guiding" | "acquiring" + bool last_is_acquired = false; // Key::Acamd::IS_ACQUIRED + bool last_fa_locked = false; // Key::Slicecamd::FINEACQUIRE_LOCKED + bool last_fa_running = false; // Key::Slicecamd::FINEACQUIRE_RUNNING + double last_seeing = 0.0; // Key::Acamd::SEEING + + /***** color_for_state ******************************************************/ + /** + * @brief map a lifecycle state label to an ANSI color + * @param state state string (e.g. "NOTREADY", "RUNNING") + * @return ANSI escape sequence + */ + const char *color_for_state( const std::string &state ) { + if ( state.find("FAILED") != std::string::npos ) return ANSI_RED; + if ( state.find("ABORTING") != std::string::npos ) return ANSI_BOLDYEL; + if ( state.find("RUNNING") != std::string::npos ) return ANSI_GREEN; + if ( state.find("READY") != std::string::npos ) return ANSI_YELLOW; + if ( state.find("STARTING") != std::string::npos ) return ANSI_BLUE; + if ( state.find("STOPPING") != std::string::npos ) return ANSI_BLUE; + if ( state.find("PAUSED") != std::string::npos ) return ANSI_CYAN; + if ( state.find("NOTREADY") != std::string::npos ) return ANSI_GRAY; + return ANSI_RESET; + } + /***** color_for_state ******************************************************/ + + + /***** color_for_severity ***************************************************/ + /** + * @brief map a broadcast severity label to an ANSI color + * @param severity severity string (NOTICE|WARNING|ERROR) + * @return ANSI escape sequence + */ + const char *color_for_severity( const std::string &severity ) { + if ( severity == Severity::ERROR ) return ANSI_RED; + if ( severity == Severity::WARNING ) return ANSI_YELLOW; + return ANSI_RESET; + } + /***** color_for_severity ***************************************************/ + + + /***** color_for_daemonstate ************************************************/ + /** + * @brief map a broadcast severity label to an ANSI color + * @param severity severity string (NOTICE|WARNING|ERROR) + * @return ANSI escape sequence + */ + const char *color_for_daemonstate( bool state) { + return ( state ? ANSI_GREEN : ANSI_RED ); + } + /***** color_for_daemonstate ************************************************/ + + + /***** color_for_acqmode ****************************************************/ + /** + * @brief map an acquisition mode string to an ANSI color + * @param mode "acquiring" | "guiding" | "stopped" + * @return ANSI escape sequence + */ + const char *color_for_acqmode( const std::string &mode ) { + if ( mode == "acquiring" ) return ANSI_GREEN; + if ( mode == "guiding" ) return ANSI_CYAN; + return ANSI_GRAY; // "stopped" or unknown + } + /***** color_for_acqmode ****************************************************/ + + + /***** timestamp ************************************************************/ + /** + * @brief local time as HH:MM:SS + * @return formatted string + */ + std::string timestamp() { + std::time_t now = std::time(nullptr); + std::tm tm_local{}; + localtime_r( &now, &tm_local ); + char buf[16]; + std::strftime( buf, sizeof(buf), "%H:%M:%S", &tm_local ); + return std::string(buf); + } + /***** timestamp ************************************************************/ + + + /***** redraw_status ********************************************************/ + /** + * @brief reprint the fixed status header (lines 1 and 2) in place + * @details Uses ANSI save/restore so the current scroll position for + * broadcast messages is not disturbed. + */ + void redraw_status() { + std::cout << CURSOR_SAVE + << CURSOR_HOME + << CLEAR_LINE + << "STATE: " << color_for_state(last_seqstate) + << last_seqstate << ANSI_RESET << "\n" + << CLEAR_LINE + << "WAITING: " << last_waitstate << ANSI_RESET << "\n" + << CLEAR_LINE + << "SUBSYSTEMS: " << last_daemonstate << "\n" + << CLEAR_LINE + << "ACQUISITION: " + << "ACAM: " << color_for_acqmode(last_acq_mode) + << last_acq_mode << ANSI_RESET + << " " << ( last_is_acquired ? ANSI_GREEN : ANSI_GRAY ) + << "[acquired]" << ANSI_RESET + << " SLICECAM: " + << ( last_fa_locked ? ANSI_GREEN : ANSI_GRAY ) << "locked" << ANSI_RESET + << " " + << ( last_fa_running ? ANSI_GREEN : ANSI_GRAY ) << "running" << ANSI_RESET + << " SEEING: " << std::fixed << std::setprecision(2) + << ANSI_BLUE << last_seeing << ANSI_RESET + << "\n" + << std::string(60, '-') << "\n" + << CURSOR_RESTORE + << std::flush; + } + /***** redraw_status ********************************************************/ + + + /***** handle_seqstate ******************************************************/ + /** + * @brief parse a seq_seqstate payload and update the top status line + * @param payload JSON payload as string + */ + void handle_seqstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + if ( j.contains(Key::Sequencer::SEQSTATE) ) { + last_seqstate = j[Key::Sequencer::SEQSTATE].get(); + if ( last_seqstate.empty() ) last_seqstate = "(none)"; + redraw_status(); + } + } + catch ( const std::exception &e ) { + std::cerr << "seqstate parse error: " << e.what() << "\n"; + } + } + /***** handle_seqstate ******************************************************/ + + + /***** handle_waitstate *****************************************************/ + /** + * @brief parse a seq_waitstate payload and update the wait-bit line + * @param payload JSON payload as string + * @details Payload contains one boolean per wait-bit label; collect the + * labels whose value is true. + */ + void handle_waitstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + std::ostringstream oss; + bool first = true; + for ( auto it = j.begin(); it != j.end(); ++it ) { + if ( ! it.value().is_boolean() ) continue; + if ( it.value().get() ) { + if ( !first ) oss << " "; + oss << it.key(); + first = false; + } + } + last_waitstate = oss.str(); + if ( last_waitstate.empty() ) last_waitstate = "(idle)"; + redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "waitstate parse error: " << e.what() << "\n"; + } + } + /***** handle_waitstate *****************************************************/ + + + void handle_daemonstate( const std::string &payload ) { + try { + auto j = nlohmann::json::parse(payload); + std::ostringstream oss; + for (auto it = j.begin(); it != j.end(); ++it) { + if (!it.value().is_boolean()) continue; + bool daemonstate = it.value().get(); + oss << color_for_daemonstate(daemonstate) + << it.key() << ANSI_RESET << " "; + } + last_daemonstate = oss.str(); + redraw_status(); + } + catch (const std::exception &e) { + std::cerr << "daemonstate parse error: " << e.what() << "\n"; + } + } + + + /***** handle_acamd *********************************************************/ + /** + * @brief parse an acamd payload and update acquisition mode/status + * @param payload JSON payload as string + * @details Handles partial payloads; only keys present are updated. + */ + void handle_acamd( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + bool changed = false; + if ( j.contains( Key::Acamd::ACQUIRE_MODE ) ) { + last_acq_mode = j[Key::Acamd::ACQUIRE_MODE].get(); + changed = true; + } + if ( j.contains( Key::Acamd::IS_ACQUIRED ) ) { + last_is_acquired = j[Key::Acamd::IS_ACQUIRED].get(); + changed = true; + } + if ( j.contains( Key::Acamd::SEEING ) && !j[Key::Acamd::SEEING].is_null() ) { + last_seeing = j[Key::Acamd::SEEING].get(); + changed = true; + } + if ( changed ) redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "acamd parse error: " << e.what() << "\n"; + } + } + /***** handle_acamd *********************************************************/ + + + /***** handle_slicecamd *****************************************************/ + /** + * @brief parse a slicecamd payload and update fine-acquisition status + * @param payload JSON payload as string + * @details Handles partial payloads; only keys present are updated. + */ + void handle_slicecamd( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + bool changed = false; + if ( j.contains( Key::Slicecamd::FINEACQUIRE_LOCKED ) ) { + last_fa_locked = j[Key::Slicecamd::FINEACQUIRE_LOCKED].get(); + changed = true; + } + if ( j.contains( Key::Slicecamd::FINEACQUIRE_RUNNING ) ) { + last_fa_running = j[Key::Slicecamd::FINEACQUIRE_RUNNING].get(); + changed = true; + } + if ( changed ) redraw_status(); + } + catch ( const std::exception &e ) { + std::cerr << "slicecamd parse error: " << e.what() << "\n"; + } + } + /***** handle_slicecamd *****************************************************/ + + + /***** handle_broadcast *****************************************************/ + /** + * @brief parse a broadcast payload and append a colored line to the log + * @param payload JSON payload as string + */ + void handle_broadcast( const std::string &payload ) { + try { + auto j = nlohmann::json::parse( payload ); + std::string severity = j.value( Key::Broadcast::SEVERITY, std::string("") ); + std::string message = j.value( Key::Broadcast::MESSAGE, std::string("(empty)") ); + std::string source = j.value( Key::SOURCE, std::string("unknown") ); + // Park the cursor at the bottom of the scroll region before emitting the + // newline so the message lands inside the region and the header above + // it is not scrolled. + // + std::cout << CURSOR_LOG_END + << color_for_severity(severity) + << "[" << timestamp() << "] " + << "[" << source << "] " + << (severity != Severity::NOTICE ? severity+": " : "") + << message + << ANSI_RESET << "\n" + << std::flush; + } + catch ( const std::exception &e ) { + std::cerr << "broadcast parse error: " << e.what() << "\n"; + } + } + /***** handle_broadcast *****************************************************/ + + + /***** request_snapshot *****************************************************/ + /** + * @brief publish a Topic::SNAPSHOT request to named daemons + * @details Each daemon whose name appears as a key in the payload will + * respond by re-publishing its current telemetry snapshot, allowing + * seqmon to populate the status header immediately on startup rather + * than waiting for the next natural update from each daemon. + * @param publisher connected ZMQ publish socket + */ + void request_snapshot( zmqpp::socket &publisher ) { + nlohmann::json j; + j[Daemon::SEQUENCER] = true; + j[Daemon::ACAMD] = true; + j[Daemon::SLICECAMD] = true; + zmqpp::message zmsg; + zmsg.add( Topic::SNAPSHOT ); + zmsg.add( j.dump() ); + publisher.send( zmsg ); + } + /***** request_snapshot *****************************************************/ + + + /***** signal_handler *******************************************************/ + /** + * @brief SIGINT handler; triggers clean exit of the main loop + */ + void signal_handler( int /*signum*/ ) { + running.store(false); + } + /***** signal_handler *******************************************************/ + +} // anonymous namespace + + +void usage( const char *exe ) { + std::cout << "usage: " << exe << " [endpoint]\n" + << " endpoint ZMQ broker address (default " << BROKER_ENDPOINT << ")\n"; +} + + +int main( int argc, char *argv[] ) { + + std::string endpoint = BROKER_ENDPOINT; + + if ( argc > 1 ) { + std::string arg = argv[1]; + if ( arg == "-h" || arg == "--help" ) { + usage(argv[0]); + return 0; + } + endpoint = arg; + } + + std::signal( SIGINT, signal_handler ); + std::signal( SIGTERM, signal_handler ); + + // set up ZMQ subscriber and publisher + // + zmqpp::context context; + zmqpp::socket subscriber( context, zmqpp::socket_type::subscribe ); + zmqpp::socket publisher ( context, zmqpp::socket_type::publish ); + + try { + subscriber.connect( endpoint ); + subscriber.subscribe( Topic::BROADCAST ); + subscriber.subscribe( Topic::SEQ_SEQSTATE ); + subscriber.subscribe( Topic::SEQ_WAITSTATE ); + subscriber.subscribe( Topic::SEQ_DAEMONSTATE ); + subscriber.subscribe( Topic::ACAMD ); + subscriber.subscribe( Topic::SLICECAMD ); + + publisher.connect( BROKER_PUB_ENDPOINT ); + } + catch ( const std::exception &e ) { + std::cerr << "ERROR connecting to " << endpoint << ": " << e.what() << "\n"; + return 1; + } + + // initial screen: clear, draw status header, leave room for scrolling log + // + std::cout << CLEAR_SCREEN << CURSOR_HOME + << "STATE: " << last_seqstate << "\n" + << "WAITING: " << last_waitstate << "\n" + << "SUBSYSTEMS: " << last_daemonstate << "\n" + << "ACQUISITION: ACAM: " << last_acq_mode + << " SLICECAM: locked running" + << " SEEING: " << std::fixed << std::setprecision(2) << last_seeing << "\n" + << std::string(60, '-') << "\n" + << SCROLL_REGION_SET + << CURSOR_LOG_START + << "seqmon subscribed to " << endpoint + << " (Ctrl-C to exit)\n" + << std::flush; + + // ZMQ PUB sockets have a slow-joiner problem: messages sent immediately + // after connect are dropped before the broker registers the connection. + // A brief sleep ensures the publisher is ready before we send the snapshot + // request. + // + std::this_thread::sleep_for( std::chrono::milliseconds(200) ); + request_snapshot( publisher ); + + // poll so Ctrl-C can break out promptly + // + zmqpp::poller poller; + poller.add( subscriber, zmqpp::poller::poll_in ); + + while ( running.load() ) { + if ( poller.poll(500) == 0 ) continue; + if ( ! poller.has_input(subscriber) ) continue; + + zmqpp::message zmsg; + subscriber.receive( zmsg ); + if ( zmsg.parts() < 2 ) continue; // guard against malformed/partial messages + std::string topic, payload; + zmsg >> topic >> payload; + + if ( topic == Topic::SEQ_SEQSTATE ) handle_seqstate( payload ); + else if ( topic == Topic::SEQ_WAITSTATE ) handle_waitstate( payload ); + else if ( topic == Topic::SEQ_DAEMONSTATE ) handle_daemonstate( payload ); + else if ( topic == Topic::ACAMD ) handle_acamd( payload ); + else if ( topic == Topic::SLICECAMD ) handle_slicecamd( payload ); + else if ( topic == Topic::BROADCAST ) handle_broadcast( payload ); + // unrecognized topics silently ignored + } + + std::cout << SCROLL_REGION_RESET << "\nseqmon exiting\n" << std::flush; + return 0; +} diff --git a/utils/utilities.h b/utils/utilities.h index f53f10f9..41dcb866 100644 --- a/utils/utilities.h +++ b/utils/utilities.h @@ -43,6 +43,7 @@ #include #include #include +#include #define TO_DEGREES ( 360. / 24. ) #define TO_HOURS ( 24. / 360. ) @@ -77,6 +78,12 @@ std::chrono::system_clock::time_point next_occurrence( int hour, int minute, int long get_time( int &year, int &mon, int &mday, int &hour, int &min, int &sec, int &usec ); long get_time( const std::string &tmzone_in, int &year, int &mon, int &mday, int &hour, int &min, int &sec, int &usec ); +inline int64_t get_time_us() { + struct timespec ts; + clock_gettime( CLOCK_REALTIME, &ts ); + return int64_t(ts.tv_sec) * 1000000 + ts.tv_nsec/1000; +} + std::string timestamp_from( struct timespec &time_n ); /// return time from input timespec struct in formatted string "YYYY-MM-DDTHH:MM:SS.sss" std::string timestamp_from( const std::string &tmzone_in, struct timespec &time_in ); From 8b22e826ea65cba4b76fba5f7035f89c193be9bb Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 12 May 2026 23:59:54 -0700 Subject: [PATCH 23/37] fixes improper CMakeLists.txt files wraps md5.c in extern "C" { } rewrites sendcmd.c -> sendcmd.cpp --- Andor/CMakeLists.txt | 6 +- CMakeLists.txt | 12 +-- PI/CMakeLists.txt | 2 +- acamd/CMakeLists.txt | 15 +-- camerad/CMakeLists.txt | 2 +- common/CMakeLists.txt | 4 + emulator/CMakeLists.txt | 2 +- flexured/CMakeLists.txt | 5 + sequencerd/CMakeLists.txt | 13 ++- slicecamd/CMakeLists.txt | 5 +- tcsd/CMakeLists.txt | 4 +- telemd/CMakeLists.txt | 2 +- thermald/CMakeLists.txt | 2 +- utils/CMakeLists.txt | 21 ++++- utils/md5.c | 7 ++ utils/md5.cpp | 193 -------------------------------------- utils/md5.h | 6 ++ utils/sendcmd.cpp | 139 +++++++++++++++++++++++++++ 18 files changed, 213 insertions(+), 227 deletions(-) delete mode 100644 utils/md5.cpp create mode 100644 utils/sendcmd.cpp diff --git a/Andor/CMakeLists.txt b/Andor/CMakeLists.txt index 973fea2e..1af81c1a 100644 --- a/Andor/CMakeLists.txt +++ b/Andor/CMakeLists.txt @@ -10,7 +10,7 @@ set( ANDOR_DIR ${PROJECT_BASE_DIR}/Andor ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O0 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O0 -Wno-variadic-macros -ggdb ) add_definitions(-D_LP64) include_directories( ${PROJECT_BASE_DIR}/utils ) @@ -30,12 +30,14 @@ find_library( CFITS_LIB cfitsio NAMES libcfitsio PATHS /usr/local/lib ) find_path( PYTHON_DEV "Python.h" PATHS /usr/include/python3.9 ) find_library( PYTHON_LIB python3.9 NAMES libpython3.9 PATHS /usr/lib64 ) include_directories( ${PYTHON_DEV} ) + add_link_options( -L/usr/lib64 -lpython3.9 -lcrypt -lpthread -ldl -lutil -lm -lm ) +include_directories( ${PYTHON_DEV} ) + add_library( andor STATIC ${ANDOR_DIR}/andor.cpp ${ANDOR_DIR}/andor_emulator.cpp - ${PYTHON_DEV} ) target_link_libraries( andor diff --git a/CMakeLists.txt b/CMakeLists.txt index 27ac4ab1..c2f900ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,23 +4,23 @@ # @author David Hale # ---------------------------------------------------------------------------- -add_definitions(-DLOGLEVEL_DEBUG) - cmake_minimum_required( VERSION 3.12 ) +project( NGPS ) + +add_definitions(-DLOGLEVEL_DEBUG) + set( CMAKE_CXX_STANDARD 17 ) set( CMAKE_CXX_STANDARD_REQUIRED ON ) set( CMAKE_CXX_EXTENSIONS OFF ) -project( NGPS ) - -set( PROJECT_BASE_DIR $ENV{PWD}/../ ) +set( PROJECT_BASE_DIR ${CMAKE_SOURCE_DIR}) set( PROJECT_SRC_DIR ${PROJECT_BASE_DIR}/src ) set( PROJECT_INC_DIR ${PROJECT_BASE_DIR}/include ) set( EXECUTABLE_OUTPUT_PATH ${PROJECT_BASE_DIR}/bin ) set( LIBRARY_OUTPUT_PATH ${PROJECT_BASE_DIR}/lib ) -set( CMAKE_C_COMPILER /usr/bin/g++ ) +set( CMAKE_C_COMPILER /usr/bin/gcc ) set( CMAKE_CXX_COMPILER /usr/bin/g++ ) find_package( Threads ) diff --git a/PI/CMakeLists.txt b/PI/CMakeLists.txt index cb6e897d..abb08161 100644 --- a/PI/CMakeLists.txt +++ b/PI/CMakeLists.txt @@ -10,7 +10,7 @@ set( PI_DIR ${PROJECT_BASE_DIR}/PI ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/acamd/CMakeLists.txt b/acamd/CMakeLists.txt index 8c509f63..acea97ce 100644 --- a/acamd/CMakeLists.txt +++ b/acamd/CMakeLists.txt @@ -56,13 +56,15 @@ add_executable(acamd ${ACAMD_DIR}/acam_interface.cpp ${ACAMD_DIR}/acam_fits.cpp ${ACAMD_DIR}/motion_interface.cpp + ) +target_include_directories(acamd PRIVATE ${PYTHON_DEV} ${MYSQL_INCLUDES} ) target_link_libraries(acamd andor - python3.9 + ${PYTHON_LIB} physik_instrumente network common @@ -73,32 +75,31 @@ target_link_libraries(acamd database ${ZMQPP_LIB} ${ZMQ_LIB} - ${PYTHON_LIB} ${CCFITS_LIB} ${CFITS_LIB} ${CMAKE_THREAD_LIBS_INIT} ${MYSQL_LIB} ) -target_compile_options(acamd PRIVATE -g -Wall -O1 -Wno-variadic-macros -ggdb) +target_compile_options(acamd PRIVATE -g -Wall -O1 -Wno-deprecated-declarations -Wno-variadic-macros -ggdb) add_executable( newpytest ${ACAMD_DIR}/newpytest.cpp + ) +target_include_directories( newpytest PRIVATE ${PYTHON_DEV} ) - target_link_libraries( newpytest - python3.9 ${PYTHON_LIB} ) add_executable( pytest ${ACAMD_DIR}/pytest.cpp + ) +target_include_directories( pytest PRIVATE ${PYTHON_DEV} ) - target_link_libraries( pytest - python3.9 ${PYTHON_LIB} ) diff --git a/camerad/CMakeLists.txt b/camerad/CMakeLists.txt index f4fcc0a0..bb14f828 100644 --- a/camerad/CMakeLists.txt +++ b/camerad/CMakeLists.txt @@ -20,7 +20,7 @@ link_directories( ${PROJECT_BASE_DIR}/lib ) message( STATUS "compiling for AstroCam GenIII PCI/PCIe") set( INTERFACE_TARGET astrocam) set( ARCAPI_DIR "${PROJECT_BASE_DIR}/ARC") -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) add_definitions( -DASTROCAM) add_definitions( -DARC66_PCIE) find_path( ARCAPI_BASE "CArcBase.h" PATHS ${ARCAPI_DIR}/CArcBase/inc) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 3fc96925..5cd16789 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -23,10 +23,14 @@ include_directories( ${PYTHON_DEV} ) add_library(common STATIC ${PROJECT_UTILS_DIR}/common.cpp + ) +target_link_libraries(common PRIVATE ${ZMQPP_LIB} ${ZMQ_LIB} ) add_library(skyinfo STATIC ${PROJECT_UTILS_DIR}/skyinfo.cpp + ) +target_link_libraries(skyinfo PRIVATE ${PYTHON_LIB} ) diff --git a/emulator/CMakeLists.txt b/emulator/CMakeLists.txt index 91dd386e..1f1f5c9e 100644 --- a/emulator/CMakeLists.txt +++ b/emulator/CMakeLists.txt @@ -10,7 +10,7 @@ set( EMULATOR_DIR ${PROJECT_BASE_DIR}/emulator ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O0 -Wno-variadic-macros ) +add_definitions( -Wall -O0 -Wno-variadic-macros ) include_directories( ${EMULATOR_DIR} ) include_directories( ${PROJECT_BASE_DIR}/utils ) diff --git a/flexured/CMakeLists.txt b/flexured/CMakeLists.txt index 8522b400..ae161f4a 100644 --- a/flexured/CMakeLists.txt +++ b/flexured/CMakeLists.txt @@ -16,6 +16,9 @@ include_directories( ${PROJECT_BASE_DIR}/common ) link_directories( ${PROJECT_BASE_DIR}/lib ) +find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) +find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) + add_executable(flexured ${FLEXURED_DIR}/flexured.cpp ${FLEXURED_DIR}/flexure_server.cpp @@ -28,6 +31,8 @@ target_link_libraries(flexured common logentry utilities + ${ZMQPP_LIB} + ${ZMQ_LIB} ${CMAKE_THREAD_LIBS_INIT} ) diff --git a/sequencerd/CMakeLists.txt b/sequencerd/CMakeLists.txt index 7e50812c..cc9d8a41 100644 --- a/sequencerd/CMakeLists.txt +++ b/sequencerd/CMakeLists.txt @@ -10,7 +10,7 @@ set( SEQUENCER_DIR ${PROJECT_BASE_DIR}/sequencerd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -Wno-deprecated-declarations -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) @@ -18,10 +18,14 @@ include_directories( ${PROJECT_BASE_DIR}/acamd ) link_directories( ${PROJECT_BASE_DIR}/lib ) +# C-Python API Development libraries and headers +# find_path( PYTHON_DEV "Python.h" PATHS /usr/include/python3.9 ) find_library( PYTHON_LIB python3.9 NAMES libpython3.9 PATHS /usr/lib64 ) include_directories( ${PYTHON_DEV} ) +# ZMQ +# find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) @@ -38,20 +42,21 @@ add_executable(sequencerd ${SEQUENCER_DIR}/sequencer_server.cpp ${SEQUENCER_DIR}/sequencer_interface.cpp ${SEQUENCER_DIR}/sequence_acquisition.cpp + ${SEQUENCER_DIR}/sequence.cpp ${SEQUENCER_DIR}/sequence_wait.cpp ${SEQUENCER_DIR}/sequence_builder.cpp ${SEQUENCER_DIR}/operation_builders.cpp ${SEQUENCER_DIR}/sequence_operations.cpp - ${SEQUENCER_DIR}/sequence.cpp - ${MYSQL_INCLUDES} + ) +target_include_directories(sequencerd PRIVATE ${PYTHON_DEV} ) - target_link_libraries(sequencerd network common logentry utilities + md5 skyinfo database ${CMAKE_THREAD_LIBS_INIT} diff --git a/slicecamd/CMakeLists.txt b/slicecamd/CMakeLists.txt index 42969138..7a6ec2c8 100644 --- a/slicecamd/CMakeLists.txt +++ b/slicecamd/CMakeLists.txt @@ -40,21 +40,20 @@ add_executable(slicecamd ${SLICECAMD_DIR}/slicecam_camera.cpp ${SLICECAMD_DIR}/slicecam_fits.cpp ${SLICECAMD_DIR}/slicecam_math.cpp - ${PYTHON_DEV} ) target_link_libraries(slicecamd andor - python3.9 + ${PYTHON_LIB} network common tcsd_client logentry utilities + md5 skyinfo ${ZMQPP_LIB} ${ZMQ_LIB} - ${PYTHON_LIB} ${CCFITS_LIB} ${CFITS_LIB} ${CMAKE_THREAD_LIBS_INIT} diff --git a/tcsd/CMakeLists.txt b/tcsd/CMakeLists.txt index 5412d5cb..d1008d74 100644 --- a/tcsd/CMakeLists.txt +++ b/tcsd/CMakeLists.txt @@ -10,8 +10,8 @@ set( TCSD_DIR ${PROJECT_BASE_DIR}/tcsd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) -#add_definitions( -Wall -Wextra -Wconversion -Wshadow -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) +#add_definitions( -Wall -Wextra -Wconversion -Wshadow -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/telemd/CMakeLists.txt b/telemd/CMakeLists.txt index fbb93bc7..0d5844a7 100644 --- a/telemd/CMakeLists.txt +++ b/telemd/CMakeLists.txt @@ -10,7 +10,7 @@ set( TELEMD_DIR ${PROJECT_BASE_DIR}/telemd ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/common ) diff --git a/thermald/CMakeLists.txt b/thermald/CMakeLists.txt index f21492b1..c4a0144d 100644 --- a/thermald/CMakeLists.txt +++ b/thermald/CMakeLists.txt @@ -10,7 +10,7 @@ set( THERMALD_DIR ${PROJECT_BASE_DIR}/thermald ) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -ansi -O1 -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -O1 -Wno-deprecated-declarations -Wno-variadic-macros -ggdb ) include_directories( ${PROJECT_BASE_DIR}/utils ) include_directories( ${PROJECT_BASE_DIR}/LKS ) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index baa453ba..fb6c0401 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -10,13 +10,12 @@ set(PROJECT_UTILS_DIR ${PROJECT_BASE_DIR}/utils) set( CMAKE_CXX_STANDARD 17 ) -add_definitions( -Wall -Wextra -Wconversion -Wshadow -ansi -Og -Wno-variadic-macros -ggdb ) +add_definitions( -Wall -Wextra -Wconversion -Wshadow -Og -Wno-variadic-macros -ggdb ) add_library(utilities STATIC ${PROJECT_UTILS_DIR}/utilities.cpp - md5 ) -target_link_libraries(utilities PRIVATE stdc++fs) + target_link_libraries(utilities PUBLIC md5 stdc++fs) add_library(logentry STATIC ${PROJECT_UTILS_DIR}/logentry.cpp @@ -49,11 +48,11 @@ include_directories( ${MYSQL_INCLUDES} ) add_library(database STATIC ${PROJECT_UTILS_DIR}/database.cpp - ${MYSQL_LIB} ) +target_link_libraries(database PRIVATE ${MYSQL_LIB}) add_executable(socksend - ${PROJECT_UTILS_DIR}/sendcmd.c + ${PROJECT_UTILS_DIR}/sendcmd.cpp ) add_executable(listener @@ -66,3 +65,15 @@ target_link_libraries( listener PRIVATE utilities ) find_library( ZMQPP_LIB zmqpp NAMES libzmqpp PATHS /usr/local/lib ) find_library( ZMQ_LIB zmq NAMES libzmq PATHS /usr/local/lib ) +add_executable( seqmon + ${PROJECT_UTILS_DIR}/seqmon.cpp + ) +target_include_directories( seqmon PRIVATE + ${PROJECT_BASE_DIR}/utils + ${PROJECT_BASE_DIR}/common + ) +target_link_libraries( seqmon PRIVATE + ${ZMQPP_LIB} + ${ZMQ_LIB} + ) + diff --git a/utils/md5.c b/utils/md5.c index 565a2e6b..fea8fad8 100644 --- a/utils/md5.c +++ b/utils/md5.c @@ -11,6 +11,10 @@ * obtained from https://github.com/B-Con/crypto-algorithms *********************************************************************/ +#ifdef __cplusplus +extern "C" { +#endif + /*************************** HEADER FILES ***************************/ #include #include @@ -189,3 +193,6 @@ void md5_final(MD5_CTX *ctx, BYTE hash[]) hash[i + 12] = (ctx->state[3] >> (i * 8)) & 0x000000ff; } } +#ifdef __cplusplus +} +#endif diff --git a/utils/md5.cpp b/utils/md5.cpp deleted file mode 100644 index 47feae42..00000000 --- a/utils/md5.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/********************************************************************* -* Filename: md5.cpp -* Author: Brad Conte (brad AT bradconte.com) -* Copyright: -* Disclaimer: This code is presented "as is" without any guarantees. -* Details: Implementation of the MD5 hashing algorithm. -* Algorithm specification can be found here: -* http://tools.ietf.org/html/rfc1321 -* This implementation uses little endian byte order. -* -* obtained from https://github.com/B-Con/crypto-algorithms -* adopted c++ idiomataic practices, David Hale -*********************************************************************/ - -/*************************** HEADER FILES ***************************/ -#include -#include -#include "md5.h" - -/****************************** MACROS ******************************/ -constexpr uint32_t ROTLEFT(uint32_t a, uint32_t b) { return (a << b) | (a >> (32 - b)); } -constexpr uint32_t F(uint32_t x, uint32_t y, uint32_t z) { return (x & y ) | ( ~x & z ); } -constexpr uint32_t G(uint32_t x, uint32_t y, uint32_t z) { return (x & z) | ( y & ~z ); } -constexpr uint32_t H(uint32_t x, uint32_t y, uint32_t z) { return x ^ y ^ z; } -constexpr uint32_t I(uint32_t x, uint32_t y, uint32_t z) { return y ^ ( x | ~z ); } -constexpr void FF(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += F(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void GG(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += G(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void HH(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += H(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} -constexpr void II(uint32_t &a, uint32_t b, uint32_t c, uint32_t d, uint32_t m, uint32_t s, uint32_t t) { - a += I(b, c, d) + m + t; - a = b + ROTLEFT(a, s); -} - -/*********************** FUNCTION DEFINITIONS ***********************/ -void md5_transform(MD5_CTX *ctx, const BYTE data[]) { - WORD a, b, c, d, m[16], i, j; - - // MD5 specifies big endian byte order, but this implementation assumes a little - // endian byte order CPU. Reverse all the bytes upon input, and re-reverse them - // on output (in md5_final()). - for (i = 0, j = 0; i < 16; ++i, j += 4) - m[i] = (data[j]) + (data[j + 1] << 8) + (data[j + 2] << 16) + (data[j + 3] << 24); - - a = ctx->state[0]; - b = ctx->state[1]; - c = ctx->state[2]; - d = ctx->state[3]; - - FF(a, b, c, d, m[0], 7, 0xd76aa478); - FF(d, a, b, c, m[1], 12, 0xe8c7b756); - FF(c, d, a, b, m[2], 17, 0x242070db); - FF(b, c, d, a, m[3], 22, 0xc1bdceee); - FF(a, b, c, d, m[4], 7, 0xf57c0faf); - FF(d, a, b, c, m[5], 12, 0x4787c62a); - FF(c, d, a, b, m[6], 17, 0xa8304613); - FF(b, c, d, a, m[7], 22, 0xfd469501); - FF(a, b, c, d, m[8], 7, 0x698098d8); - FF(d, a, b, c, m[9], 12, 0x8b44f7af); - FF(c, d, a, b, m[10], 17, 0xffff5bb1); - FF(b, c, d, a, m[11], 22, 0x895cd7be); - FF(a, b, c, d, m[12], 7, 0x6b901122); - FF(d, a, b, c, m[13], 12, 0xfd987193); - FF(c, d, a, b, m[14], 17, 0xa679438e); - FF(b, c, d, a, m[15], 22, 0x49b40821); - - GG(a, b, c, d, m[1], 5, 0xf61e2562); - GG(d, a, b, c, m[6], 9, 0xc040b340); - GG(c, d, a, b, m[11], 14, 0x265e5a51); - GG(b, c, d, a, m[0], 20, 0xe9b6c7aa); - GG(a, b, c, d, m[5], 5, 0xd62f105d); - GG(d, a, b, c, m[10], 9, 0x02441453); - GG(c, d, a, b, m[15], 14, 0xd8a1e681); - GG(b, c, d, a, m[4], 20, 0xe7d3fbc8); - GG(a, b, c, d, m[9], 5, 0x21e1cde6); - GG(d, a, b, c, m[14], 9, 0xc33707d6); - GG(c, d, a, b, m[3], 14, 0xf4d50d87); - GG(b, c, d, a, m[8], 20, 0x455a14ed); - GG(a, b, c, d, m[13], 5, 0xa9e3e905); - GG(d, a, b, c, m[2], 9, 0xfcefa3f8); - GG(c, d, a, b, m[7], 14, 0x676f02d9); - GG(b, c, d, a, m[12], 20, 0x8d2a4c8a); - - HH(a, b, c, d, m[5], 4, 0xfffa3942); - HH(d, a, b, c, m[8], 11, 0x8771f681); - HH(c, d, a, b, m[11], 16, 0x6d9d6122); - HH(b, c, d, a, m[14], 23, 0xfde5380c); - HH(a, b, c, d, m[1], 4, 0xa4beea44); - HH(d, a, b, c, m[4], 11, 0x4bdecfa9); - HH(c, d, a, b, m[7], 16, 0xf6bb4b60); - HH(b, c, d, a, m[10], 23, 0xbebfbc70); - HH(a, b, c, d, m[13], 4, 0x289b7ec6); - HH(d, a, b, c, m[0], 11, 0xeaa127fa); - HH(c, d, a, b, m[3], 16, 0xd4ef3085); - HH(b, c, d, a, m[6], 23, 0x04881d05); - HH(a, b, c, d, m[9], 4, 0xd9d4d039); - HH(d, a, b, c, m[12], 11, 0xe6db99e5); - HH(c, d, a, b, m[15], 16, 0x1fa27cf8); - HH(b, c, d, a, m[2], 23, 0xc4ac5665); - - II(a, b, c, d, m[0], 6, 0xf4292244); - II(d, a, b, c, m[7], 10, 0x432aff97); - II(c, d, a, b, m[14], 15, 0xab9423a7); - II(b, c, d, a, m[5], 21, 0xfc93a039); - II(a, b, c, d, m[12], 6, 0x655b59c3); - II(d, a, b, c, m[3], 10, 0x8f0ccc92); - II(c, d, a, b, m[10], 15, 0xffeff47d); - II(b, c, d, a, m[1], 21, 0x85845dd1); - II(a, b, c, d, m[8], 6, 0x6fa87e4f); - II(d, a, b, c, m[15], 10, 0xfe2ce6e0); - II(c, d, a, b, m[6], 15, 0xa3014314); - II(b, c, d, a, m[13], 21, 0x4e0811a1); - II(a, b, c, d, m[4], 6, 0xf7537e82); - II(d, a, b, c, m[11], 10, 0xbd3af235); - II(c, d, a, b, m[2], 15, 0x2ad7d2bb); - II(b, c, d, a, m[9], 21, 0xeb86d391); - - ctx->state[0] += a; - ctx->state[1] += b; - ctx->state[2] += c; - ctx->state[3] += d; -} - -void md5_init(MD5_CTX *ctx) { - ctx->datalen = 0; - ctx->bitlen = 0; - ctx->state[0] = 0x67452301; - ctx->state[1] = 0xEFCDAB89; - ctx->state[2] = 0x98BADCFE; - ctx->state[3] = 0x10325476; -} - -void md5_update(MD5_CTX *ctx, const BYTE data[], size_t len) { - size_t i; - - for (i = 0; i < len; ++i) { - ctx->data[ctx->datalen] = data[i]; - ctx->datalen++; - if (ctx->datalen == 64) { - md5_transform(ctx, ctx->data); - ctx->bitlen += 512; - ctx->datalen = 0; - } - } -} - -void md5_final(MD5_CTX *ctx, BYTE hash[]) { - size_t i; - - i = ctx->datalen; - - // Pad whatever data is left in the buffer. - if (ctx->datalen < 56) { - ctx->data[i++] = 0x80; - while (i < 56) - ctx->data[i++] = 0x00; - } else if (ctx->datalen >= 56) { - ctx->data[i++] = 0x80; - while (i < 64) - ctx->data[i++] = 0x00; - md5_transform(ctx, ctx->data); - memset(ctx->data, 0, 56); - } - - // Append to the padding the total message's length in bits and transform. - ctx->bitlen += ctx->datalen * 8; - ctx->data[56] = ctx->bitlen; - ctx->data[57] = ctx->bitlen >> 8; - ctx->data[58] = ctx->bitlen >> 16; - ctx->data[59] = ctx->bitlen >> 24; - ctx->data[60] = ctx->bitlen >> 32; - ctx->data[61] = ctx->bitlen >> 40; - ctx->data[62] = ctx->bitlen >> 48; - ctx->data[63] = ctx->bitlen >> 56; - md5_transform(ctx, ctx->data); - - // Since this implementation uses little endian byte ordering and MD uses big endian, - // reverse all the bytes when copying the final state to the output hash. - for (i = 0; i < 4; ++i) { - hash[i] = (ctx->state[0] >> (i * 8)) & 0x000000ff; - hash[i + 4] = (ctx->state[1] >> (i * 8)) & 0x000000ff; - hash[i + 8] = (ctx->state[2] >> (i * 8)) & 0x000000ff; - hash[i + 12] = (ctx->state[3] >> (i * 8)) & 0x000000ff; - } -} diff --git a/utils/md5.h b/utils/md5.h index 09e02978..e88f072b 100644 --- a/utils/md5.h +++ b/utils/md5.h @@ -10,6 +10,9 @@ #ifndef MD5_H #define MD5_H +#ifdef __cplusplus +extern "C" { +#endif /*************************** HEADER FILES ***************************/ #include @@ -33,4 +36,7 @@ void md5_init(MD5_CTX *ctx); void md5_update(MD5_CTX *ctx, const BYTE data[], size_t len); void md5_final(MD5_CTX *ctx, BYTE hash[]); +#ifdef __cplusplus +} +#endif #endif // MD5_H diff --git a/utils/sendcmd.cpp b/utils/sendcmd.cpp new file mode 100644 index 00000000..66b95a16 --- /dev/null +++ b/utils/sendcmd.cpp @@ -0,0 +1,139 @@ +/** + * @file utils/sendcmd.cpp + * @brief TCP command sender + * @author David Hale + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static constexpr size_t RESPONSE_BUFSIZE = 8192; + +static void usage(const char* exe) { + std::cout << "usage: " << exe + << " [-h hostname] [-p port] [-t timeout] [-m mode] command\n"; +} + +int main(int argc, char* argv[]) { + std::string hostname; + std::string message; + int port = 0; + int timeout = 10; + int mode = 0; // 0: wait for response, 1: fire and forget + + try { + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.size() == 2 && arg[0] == '-') { + if (i + 1 >= argc) { usage(argv[0]); return 0; } + switch (arg[1]) { + case 'h': hostname = argv[++i]; break; + case 'p': port = std::stoi(argv[++i]); break; + case 't': timeout = std::stoi(argv[++i]); break; + case 'm': mode = std::stoi(argv[++i]); break; + default: usage(argv[0]); return 0; + } + } + else { + message = arg; + } + } + } + catch (const std::exception& ex) { + std::cerr << "ERROR invalid argument: " << ex.what() << "\n"; + return 1; + } + + if (hostname.empty() || port <= 0 || message.empty()) { + usage(argv[0]); + return 1; + } + + struct addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = nullptr; + const int gai_ret = getaddrinfo(hostname.c_str(), std::to_string(port).c_str(), &hints, &res); + if (gai_ret != 0) { + std::cerr << "ERROR getting host: " << gai_strerror(gai_ret) << "\n"; + return 1; + } + + int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock < 0) { + std::cerr << "ERROR " << errno << " creating socket\n"; + freeaddrinfo(res); + return -errno; + } + + if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0) { + std::cerr << "ERROR " << errno << " setting socket\n"; + close(sock); + freeaddrinfo(res); + return -errno; + } + + const auto tstart = std::chrono::steady_clock::now(); + auto elapsed_sec = [&]() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - tstart).count(); + }; + + if (connect(sock, res->ai_addr, res->ai_addrlen) == -1 && errno != EINPROGRESS) { + std::cerr << "ERROR " << errno << " connecting\n"; + close(sock); + freeaddrinfo(res); + return -errno; + } + freeaddrinfo(res); + + message += '\n'; + + ssize_t nwrite; + while ((nwrite = write(sock, message.c_str(), message.size())) < 0) { + if (errno != EAGAIN) { + std::cerr << "ERROR " << errno << " writing message\n"; + close(sock); + return -errno; + } + if (elapsed_sec() >= timeout) { + std::cerr << "ERROR " << ETIME << " TIMEOUT\n"; + close(sock); + return -ETIME; + } + } + + if (mode == 0) { + char response[RESPONSE_BUFSIZE]; + ssize_t nread; + while ((nread = read(sock, response, RESPONSE_BUFSIZE - 1)) < 0) { + if (errno != EAGAIN) { + std::cerr << "(sendcmd) ERROR " << errno << " reading\n"; + close(sock); + return -errno; + } + usleep(10000); + if (elapsed_sec() >= timeout) { + std::cerr << "ERROR " << ETIME << " TIMEOUT\n"; + close(sock); + return -ETIME; + } + } + response[nread] = '\0'; + std::cout << response << "\n"; + } + else { + std::cout << "cmd_sent\n"; + } + + return close(sock); +} From 0b6926ef0dc2d9168dc0f399a67843f4084c19a4 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 13 May 2026 00:07:12 -0700 Subject: [PATCH 24/37] fix: on_error must appear before any operations in a group --- sequencerd/sequence_builder.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 8706c0bb..960ca9a9 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -84,6 +84,11 @@ namespace Sequencer { if (name == "on_error") { // must be in a group for on_error to mean anything if (!current_group) return fail(command, "'on_error' out of a group"); + // on_error must precede any operations in the group so it cannot + // silently mutate the policy of an already-populated group + if (!current_group->operations.empty()) { + return fail(command, "'on_error' must appear before any operations in the group"); + } if (!command.params.has("action")) return fail(command, "on_error requires action=stop|continue"); const std::string val = command.params.get("action",""); From 51ad2d253c7462203877df5e76a353e339e698b9 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 13 May 2026 12:08:54 -0700 Subject: [PATCH 25/37] * adds set|get commands to powerd * fills out sequencer command specs and transitions but doesn't use them yet -- this is the last stage before changing how it works * comment-out some debug logging --- Andor/andor_emulator.cpp | 2 +- common/powerd_commands.h | 10 +- powerd/power_server.cpp | 15 ++- sequencerd/command_rules.h | 229 ++++++++++++++++++++++++++++-------- slicecamd/slicecam_math.cpp | 12 +- 5 files changed, 209 insertions(+), 59 deletions(-) diff --git a/Andor/andor_emulator.cpp b/Andor/andor_emulator.cpp index dd41a1bb..c179278f 100644 --- a/Andor/andor_emulator.cpp +++ b/Andor/andor_emulator.cpp @@ -1218,7 +1218,7 @@ namespace Andor { } #ifdef LOGLEVEL_DEBUG - log_python_arguments(pFunction, pArgs, pKwArgs); +// log_python_arguments(pFunction, pArgs, pKwArgs); #endif // Call the Python function here diff --git a/common/powerd_commands.h b/common/powerd_commands.h index da8ef3fa..f10fc2c3 100644 --- a/common/powerd_commands.h +++ b/common/powerd_commands.h @@ -13,6 +13,8 @@ const std::string POWERD_ISOPEN = "isopen"; const std::string POWERD_LIST = "list"; const std::string POWERD_OPEN = "open"; const std::string POWERD_REOPEN = "reopen"; +const std::string POWERD_GET = "get"; +const std::string POWERD_SET = "set"; const std::string POWERD_STATUS = "status"; const std::vector POWERD_SYNTAX = { POWERD_CLOSE, @@ -20,9 +22,9 @@ const std::vector POWERD_SYNTAX = { POWERD_LIST+" [?]", POWERD_OPEN, POWERD_REOPEN+" [?]", + POWERD_GET+" | ", + POWERD_SET+" { ON | OFF | BOOT }", + POWERD_SET+" { ON | OFF | BOOT }", POWERD_STATUS+" [?]", - TELEMREQUEST+" [?]", - "", - " [ ON | OFF | BOOT ]", - " [ ON | OFF | BOOT ]" + TELEMREQUEST+" [?]" }; diff --git a/powerd/power_server.cpp b/powerd/power_server.cpp index 3ebd3d29..5b77057d 100644 --- a/powerd/power_server.cpp +++ b/powerd/power_server.cpp @@ -603,10 +603,13 @@ namespace Power { ret = JSON; } } + else - // all other commands go to the powerd interface for parsing + // set|get plug state -- strip command and forward the + // args to Interface::command(), which already parses + // [ON|OFF|BOOT] and [ON|OFF|BOOT]. // - else { + if ( cmd==POWERD_SET || cmd==POWERD_GET ) { try { std::transform( buf.begin(), buf.end(), buf.begin(), ::toupper ); // make uppercase } @@ -616,6 +619,14 @@ namespace Power { } ret = this->interface.command( buf, retstring ); // send the command } + else { + + // unknown commands generate an error + // + logwrite( function, "ERROR unknown command '"+cmd+"'"); + ret = ERROR; + } + // If retstring not empty then append "DONE" or "ERROR" depending on value of ret, // and log the reply along with the command number. Write the reply back to the socket. diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h index 2cb086a1..f0254bfc 100644 --- a/sequencerd/command_rules.h +++ b/sequencerd/command_rules.h @@ -22,7 +22,8 @@ namespace Sequencer { READY, ///< open and ready to accept commands EXPOSING, ///< exposure in progress READING, ///< readout in progress - PAUSED ///< exposure paused + PAUSED, ///< exposure paused + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap camerad_specs = { @@ -49,7 +50,27 @@ namespace Sequencer { { CAMERAD_READOUT, {0, 2} }, { CAMERAD_RESUME, {0, 0} }, { CAMERAD_SHUTTER, {0, 1} }, ///< [ enable | 1 | disable | 0 ] - { CAMERAD_STOP, {0, 0} } + { CAMERAD_STOP, {0, 0} }, + // Commands added for structural completeness; arg ranges from camerad_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until the from/to states are verified against camerad behavior. + { CAMERAD_AUTODIR, {0, 1} }, + { CAMERAD_BASENAME, {0, 1} }, + { CAMERAD_BIAS, {0, 4} }, + { CAMERAD_BUFFER, {1, 3} }, ///< ? | | [ | ] + { CAMERAD_CONFIG, {0, 1} }, + { CAMERAD_ECHO, {0, 4} }, + { CAMERAD_INTERFACE, {0, 0} }, + { CAMERAD_ISACTIVE, {0, 0} }, + { CAMERAD_ISOPEN, {0, 1} }, + { CAMERAD_LONGERROR, {0, 0} }, + { CAMERAD_MEX, {0, 4} }, + { CAMERAD_MEXAMPS, {0, 4} }, + { CAMERAD_NATIVE, {1, 6} }, ///< ? | [|] [..] + { CAMERAD_PREEXPOSURES, {0, 1} }, + { CAMERAD_TEST, {1, 8} }, ///< ? | ... + { CAMERAD_USEFRAMES, {0, 1} }, + { CAMERAD_WRITEKEYS, {0, 0} } }; const std::vector> camerad_transitions = { @@ -79,7 +100,27 @@ namespace Sequencer { { CameraState::READING, CAMERAD_READOUT, CameraState::READY }, { CameraState::PAUSED, CAMERAD_RESUME, CameraState::EXPOSING }, { CameraState::READY, CAMERAD_SHUTTER, CameraState::READY }, - { CameraState::EXPOSING, CAMERAD_STOP, CameraState::READY } + { CameraState::EXPOSING, CAMERAD_STOP, CameraState::READY }, + // INVALID->INVALID transitions: these commands cannot be invoked by the + // sequencer until from/to states are verified. The unreachable-from-state + // condition rejects them at runtime via CommandClient::send(). + { CameraState::INVALID, CAMERAD_AUTODIR, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BASENAME, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BIAS, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_BUFFER, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_CONFIG, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ECHO, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_INTERFACE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ISACTIVE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_ISOPEN, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_LONGERROR, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_MEX, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_MEXAMPS, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_NATIVE, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_PREEXPOSURES, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_TEST, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_USEFRAMES, CameraState::INVALID }, // TODO: verify and assign real states + { CameraState::INVALID, CAMERAD_WRITEKEYS, CameraState::INVALID } // TODO: verify and assign real states }; @@ -89,7 +130,8 @@ namespace Sequencer { IDLE, ///< not yet opened READY, ///< open and ready to accept commands ACQUIRING, ///< target acquisition in progress - GUIDING ///< guiding in progress + GUIDING, ///< guiding in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap acamd_specs = { @@ -114,7 +156,31 @@ namespace Sequencer { { ACAMD_SHUTDOWN, {0, 0} }, { ACAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] { ACAMD_OFFSETGOAL, {0, 2} }, ///< [ ] - { ACAMD_OFFSETPERIOD,{0, 1} } ///< [ ] + { ACAMD_OFFSETPERIOD,{0, 1} }, ///< [ ] + // Commands added for structural completeness; arg ranges from acamd_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until from/to states are verified against acamd behavior. + { ACAMD_AVGFRAMES, {0, 1} }, + { ACAMD_CONFIG, {0, 1} }, + { ACAMD_ECHO, {0, 1} }, + { ACAMD_EXIT, {0, 0} }, + { ACAMD_FRAMEGRABFIX, {0, 1} }, + { ACAMD_IMFLIP, {0, 2} }, + { ACAMD_IMROT, {0, 1} }, + { ACAMD_SPEED, {0, 2} }, + { ACAMD_EMULATOR, {0, 1} }, + { ACAMD_FAN, {0, 1} }, + { ACAMD_GUIDESET, {0, 4} }, + { ACAMD_LOADCALIBRATION, {0, 1} }, + { ACAMD_MOTION, {0, 4} }, + { ACAMD_OFFSETCAL, {0, 1} }, + { ACAMD_QUALITY, {0, 1} }, + { ACAMD_SAVEFRAMES, {0, 1} }, + { ACAMD_SKIPFRAMES, {0, 1} }, + { ACAMD_TCSGET, {0, 1} }, + { ACAMD_TCSISCONNECTED, {0, 1} }, + { ACAMD_TCSISOPEN, {0, 1} }, + { ACAMD_TEST, {1, 8} } }; const std::vector> acamd_transitions = { @@ -141,7 +207,29 @@ namespace Sequencer { { AcamState::READY, ACAMD_SHUTDOWN, AcamState::IDLE }, // TODO: verify { AcamState::READY, ACAMD_ISOPEN, AcamState::READY }, // TODO: verify { AcamState::READY, ACAMD_ISHOME, AcamState::READY }, // TODO: verify - { AcamState::READY, ACAMD_ISACQUIRED, AcamState::READY } // TODO: verify + { AcamState::READY, ACAMD_ISACQUIRED, AcamState::READY }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { AcamState::INVALID, ACAMD_AVGFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_CONFIG, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_ECHO, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_EXIT, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_FRAMEGRABFIX, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_IMFLIP, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_IMROT, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SPEED, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_EMULATOR, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_FAN, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_GUIDESET, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_LOADCALIBRATION, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_MOTION, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_OFFSETCAL, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_QUALITY, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SAVEFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_SKIPFRAMES, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSGET, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSISCONNECTED, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TCSISOPEN, AcamState::INVALID }, // TODO: verify and assign real states + { AcamState::INVALID, ACAMD_TEST, AcamState::INVALID } // TODO: verify and assign real states }; @@ -150,7 +238,8 @@ namespace Sequencer { enum class CalibState { IDLE, ///< not yet opened READY, ///< open and ready to accept commands - MOVING ///< actuator motion in progress + MOVING, ///< actuator motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap calibd_specs = { @@ -162,31 +251,35 @@ namespace Sequencer { { CALIBD_ISHOME, {0, 0} }, { CALIBD_SET, {1, 8} }, ///< =open|close ... (variable) { CALIBD_LAMPMOD, {1, 4} }, ///< open | close | reconnect | default | ... - { CALIBD_NATIVE, {2, 2} } ///< + { CALIBD_NATIVE, {2, 2} }, ///< + // INVALID-transition commands: rejected at runtime until from/to verified. + { CALIBD_EXIT, {0, 0} } }; const std::vector> calibd_transitions = { - { CalibState::IDLE, CALIBD_OPEN, CalibState::READY }, // TODO: verify - { CalibState::READY, CALIBD_CLOSE, CalibState::IDLE }, // TODO: verify - { CalibState::READY, CALIBD_ISOPEN, CalibState::READY }, // TODO: verify - { CalibState::READY, CALIBD_GET, CalibState::READY }, // TODO: verify - { CalibState::READY, CALIBD_HOME, CalibState::MOVING }, // TODO: verify - { CalibState::MOVING, CALIBD_ISHOME, CalibState::MOVING }, // TODO: verify - { CalibState::READY, CALIBD_ISHOME, CalibState::READY }, // TODO: verify - { CalibState::READY, CALIBD_SET, CalibState::MOVING }, // TODO: verify - { CalibState::MOVING, CALIBD_SET, CalibState::READY }, // TODO: verify (completion) - { CalibState::READY, CALIBD_LAMPMOD, CalibState::READY }, // TODO: verify - { CalibState::READY, CALIBD_NATIVE, CalibState::READY } // TODO: verify + { CalibState::IDLE, CALIBD_OPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_CLOSE, CalibState::IDLE }, // TODO: verify + { CalibState::READY, CALIBD_ISOPEN, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_GET, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_HOME, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_ISHOME, CalibState::MOVING }, // TODO: verify + { CalibState::READY, CALIBD_ISHOME, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_SET, CalibState::MOVING }, // TODO: verify + { CalibState::MOVING, CALIBD_SET, CalibState::READY }, // TODO: verify (completion) + { CalibState::READY, CALIBD_LAMPMOD, CalibState::READY }, // TODO: verify + { CalibState::READY, CALIBD_NATIVE, CalibState::READY }, // TODO: verify + { CalibState::INVALID, CALIBD_EXIT, CalibState::INVALID } // TODO: verify and assign real states }; // ---------- SLITD ---------------------------------------------------------- enum class SlitState { - IDLE, ///< not yet opened - READY, ///< open and ready to accept commands - MOVING, ///< slit motion in progress - HOMED ///< slit has been homed + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< slit motion in progress + HOMED, ///< slit has been homed + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap slitd_specs = { @@ -226,11 +319,12 @@ namespace Sequencer { // ---------- TCSD ----------------------------------------------------------- enum class TcsState { - IDLE, ///< not yet opened - READY, ///< open and ready to accept commands - SLEWING, ///< telescope slew in progress - TRACKING, ///< telescope tracking on target - OFFSETTING ///< telescope offset in progress + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + SLEWING, ///< telescope slew in progress + TRACKING, ///< telescope tracking on target + OFFSETTING, ///< telescope offset in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap tcsd_specs = { @@ -255,7 +349,10 @@ namespace Sequencer { { TCSD_NATIVE, {1, 1} }, ///< { TCSD_LIST, {0, 0} }, { TCSD_LLIST, {0, 0} }, - { TCSD_WEATHER_COORDS, {0, 0} } + { TCSD_WEATHER_COORDS, {0, 0} }, + // INVALID-transition commands: rejected at runtime until from/to verified. + { TCSD_EXIT, {0, 0} }, + { TCSD_PUBLISHSTATE, {0, 1} } }; const std::vector> tcsd_transitions = { @@ -300,16 +397,20 @@ namespace Sequencer { { TcsState::READY, TCSD_LIST, TcsState::READY }, // TODO: verify { TcsState::READY, TCSD_LLIST, TcsState::READY }, // TODO: verify { TcsState::READY, TCSD_WEATHER_COORDS, TcsState::READY }, // TODO: verify - { TcsState::TRACKING, TCSD_WEATHER_COORDS, TcsState::TRACKING } // TODO: verify + { TcsState::TRACKING, TCSD_WEATHER_COORDS, TcsState::TRACKING }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { TcsState::INVALID, TCSD_EXIT, TcsState::INVALID }, // TODO: verify and assign real states + { TcsState::INVALID, TCSD_PUBLISHSTATE, TcsState::INVALID } // TODO: verify and assign real states }; // ---------- FOCUSD --------------------------------------------------------- enum class FocusState { - IDLE, ///< not yet opened - READY, ///< open and ready to accept commands - MOVING ///< focus motion in progress + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< focus motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap focusd_specs = { @@ -346,9 +447,10 @@ namespace Sequencer { // ---------- FLEXURED ------------------------------------------------------- enum class FlexureState { - IDLE, ///< not yet opened - READY, ///< open and ready to accept commands - MOVING ///< flexure compensation motion in progress + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + MOVING, ///< flexure compensation motion in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap flexured_specs = { @@ -382,8 +484,9 @@ namespace Sequencer { // ---------- POWERD --------------------------------------------------------- enum class PowerState { - IDLE, ///< not yet opened - READY ///< open and ready to accept commands + IDLE, ///< not yet opened + READY, ///< open and ready to accept commands + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap powerd_specs = { @@ -392,10 +495,9 @@ namespace Sequencer { { POWERD_ISOPEN, {0, 0} }, { POWERD_STATUS, {0, 1} }, ///< [ ? ] { POWERD_REOPEN, {0, 0} }, - { POWERD_LIST, {0, 0} } - // NOTE: plug switching commands ( ON|OFF|BOOT or ON|OFF|BOOT) - // do not have a fixed first-word command name and so cannot be registered here by name. - // TODO: decide how plug switching is exposed via SEQUENCERD_OP. + { POWERD_LIST, {0, 0} }, + { POWERD_GET, {1, 2} }, + { POWERD_SET, {1, 3} } ///< [ ON | OFF | BOOT ] or [ ON | OFF | BOOT ] }; const std::vector> powerd_transitions = { @@ -404,7 +506,9 @@ namespace Sequencer { { PowerState::READY, POWERD_ISOPEN, PowerState::READY }, // TODO: verify { PowerState::READY, POWERD_STATUS, PowerState::READY }, // TODO: verify { PowerState::READY, POWERD_REOPEN, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_LIST, PowerState::READY } // TODO: verify + { PowerState::READY, POWERD_LIST, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_GET, PowerState::READY }, // TODO: verify + { PowerState::READY, POWERD_SET, PowerState::READY } // TODO: verify }; @@ -414,7 +518,8 @@ namespace Sequencer { IDLE, ///< not yet opened READY, ///< open and ready to accept commands ACQUIRING, ///< fine acquisition in progress - GUIDING ///< guiding in progress + GUIDING, ///< guiding in progress + INVALID ///< sentinel for unverified commands; no live client ever holds this value }; const CommandSpecMap slicecamd_specs = { @@ -434,7 +539,24 @@ namespace Sequencer { { SLICECAMD_SHUTDOWN, {0, 0} }, { SLICECAMD_TCSINIT, {0, 1} }, ///< [ tcs | sim ] { SLICECAMD_IMFLIP, {0, 3} }, ///< [ ] - { SLICECAMD_IMROT, {0, 2} } ///< [ ] + { SLICECAMD_IMROT, {0, 2} }, ///< [ ] + // Commands added for structural completeness; arg ranges from slicecamd_commands.h syntax. + // Transitions for these are all INVALID->INVALID so the sequencer cannot invoke them + // until from/to states are verified against slicecamd behavior. + { SLICECAMD_AVGFRAMES, {0, 1} }, + { SLICECAMD_CONFIG, {0, 1} }, + { SLICECAMD_ECHO, {0, 1} }, + { SLICECAMD_EXIT, {0, 0} }, + { SLICECAMD_FRAMEGRABFIX, {0, 1} }, + { SLICECAMD_SPEED, {0, 2} }, + { SLICECAMD_EMULATOR, {0, 1} }, + { SLICECAMD_FAN, {0, 2} }, + { SLICECAMD_GUISET, {0, 2} }, + { SLICECAMD_ISACQUIRED, {0, 0} }, + { SLICECAMD_TCSGET, {0, 1} }, + { SLICECAMD_TCSISCONNECTED, {0, 1} }, + { SLICECAMD_TCSISOPEN, {0, 1} }, + { SLICECAMD_TEST, {1, 8} } }; const std::vector> slicecamd_transitions = { @@ -455,7 +577,22 @@ namespace Sequencer { { SlicecamState::READY, SLICECAMD_SHUTDOWN, SlicecamState::IDLE }, // TODO: verify { SlicecamState::READY, SLICECAMD_TCSINIT, SlicecamState::READY }, // TODO: verify { SlicecamState::READY, SLICECAMD_IMFLIP, SlicecamState::READY }, // TODO: verify - { SlicecamState::READY, SLICECAMD_IMROT, SlicecamState::READY } // TODO: verify + { SlicecamState::READY, SLICECAMD_IMROT, SlicecamState::READY }, // TODO: verify + // INVALID->INVALID transitions: rejected at runtime until from/to verified. + { SlicecamState::INVALID, SLICECAMD_AVGFRAMES, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_CONFIG, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_ECHO, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_EXIT, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_FRAMEGRABFIX, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_SPEED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_EMULATOR, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_FAN, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_GUISET, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_ISACQUIRED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSGET, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSISCONNECTED, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TCSISOPEN, SlicecamState::INVALID }, // TODO: verify and assign real states + { SlicecamState::INVALID, SLICECAMD_TEST, SlicecamState::INVALID } // TODO: verify and assign real states }; } diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index a87d9e61..6d73637e 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -309,12 +309,12 @@ namespace Slicecam { } } - { - std::ostringstream oss; - oss << "[DEBUG] bkg=" << bkg << " sigma=" << sigma - << " best_val=" << best_val << " best_x=" << best_x << " best_y=" << best_y; - logwrite("Slicecam::Math::calculate_centroid", oss.str()); - } +// { +// std::ostringstream oss; +// oss << "[DEBUG] bkg=" << bkg << " sigma=" << sigma +// << " best_val=" << best_val << " best_x=" << best_x << " best_y=" << best_y; +// logwrite("Slicecam::Math::calculate_centroid", oss.str()); +// } if ( best_x < 0 ) return ERROR; // no source found From f577429d15747487bcc900e1d5f6301156208f27 Mon Sep 17 00:00:00 2001 From: David Hale Date: Wed, 13 May 2026 12:42:27 -0700 Subject: [PATCH 26/37] fix to support new powerd set|get commands --- powerd/power_server.cpp | 4 ++-- sequencerd/sequence.cpp | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/powerd/power_server.cpp b/powerd/power_server.cpp index 5b77057d..f4f75c25 100644 --- a/powerd/power_server.cpp +++ b/powerd/power_server.cpp @@ -611,13 +611,13 @@ namespace Power { // if ( cmd==POWERD_SET || cmd==POWERD_GET ) { try { - std::transform( buf.begin(), buf.end(), buf.begin(), ::toupper ); // make uppercase + std::transform( args.begin(), args.end(), args.begin(), ::toupper ); // make uppercase } catch( ...) { logwrite( function, "ERROR converting command to uppercase" ); ret = ERROR; } - ret = this->interface.command( buf, retstring ); // send the command + ret = this->interface.command( args, retstring ); // send the command } else { diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 1e5fbfd7..9ebfa20a 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -2902,11 +2902,10 @@ namespace Sequencer { // for ( const auto &[lamp,state] : calinfo.lamp ) { if ( is_cancelled() ) break; - cmd.str(""); cmd << lamp << " " << (state?"on":"off"); - message.str(""); message << "power " << cmd.str(); - logwrite( function, message.str() ); + std::string setpower = POWERD_SET+" "+lamp+" "+(state?"on":"off"); + logwrite( function, "power "+setpower); std::string reply; - if ( this->powerd.send( cmd.str(), reply ) != NO_ERROR ) { + if ( this->powerd.send( setpower, reply ) != NO_ERROR ) { this->broadcast.error( function, ""+message.str() ); throw std::runtime_error("setting lamp "+message.str()); } @@ -4059,7 +4058,8 @@ namespace Sequencer { for ( const auto &plug : this->power_switch[which].plugname ) { std::string reply; - if ( this->powerd.send( plug, reply ) == NO_ERROR ) { + std::string cmd = POWERD_GET+" "+plug; + if ( this->powerd.send( cmd, reply ) == NO_ERROR ) { if ( reply.find(reqstatestr) != std::string::npos ) { is_set=true; break; @@ -4079,9 +4079,8 @@ namespace Sequencer { // for ( const auto &plug : this->power_switch[which].plugname ) { std::string reply; - std::stringstream cmd; - cmd << plug; - error = this->powerd.send( cmd.str(), reply ); + std::string cmd = POWERD_GET+" "+plug; + error = this->powerd.send( cmd, reply ); if ( error != NO_ERROR || reply.find(" DONE") == std::string::npos ) { logwrite( function, "ERROR checking plug: "+plug ); continue; @@ -4100,9 +4099,9 @@ namespace Sequencer { std::string reqstatestr = ( reqstate==ON ? "ON" : "OFF" ); if ( state != reqstate ) { - cmd << " " << reqstatestr; + cmd = POWERD_SET+" "+plug+" "+reqstatestr; logwrite( function, "switching plug "+plug+" "+reqstatestr ); - error = this->powerd.send( cmd.str(), reply ); + error = this->powerd.send( cmd, reply ); if ( error != NO_ERROR || reply.find(" DONE") != std::string::npos ) { logwrite( function, "ERROR switching plug: "+plug+" "+reqstatestr ); continue; From 9174374c5045b95372fcd4e88343b8f2a0e8df4c Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 10:22:38 -0700 Subject: [PATCH 27/37] * wires CommandClient into all DSL passthrough op_builders * replaces daemon_passthrough() placeholder with translate_command + CommandClient::send(), adds per-command arg count and state machine validation at execution time * rewrites parse_command with strict key=val grammar * fixes ltrim bug * fixes move_to_target bug for CAL targets * fixes abort_process for clean state transitions * adds missing request_snapshot to get status on startup --- sequencerd/command.h | 52 +++++- sequencerd/command_rules.h | 90 ++++++++-- sequencerd/operation_builders.cpp | 286 +++++++++++++++++++++++------- sequencerd/sequence.cpp | 181 +++++++++++++++---- sequencerd/sequence.h | 121 +++++++++++-- sequencerd/sequence_builder.cpp | 2 +- sequencerd/sequencerd.cpp | 1 + utils/utilities.h | 2 +- 8 files changed, 600 insertions(+), 135 deletions(-) diff --git a/sequencerd/command.h b/sequencerd/command.h index b9a37c5d..d03dff3e 100644 --- a/sequencerd/command.h +++ b/sequencerd/command.h @@ -73,7 +73,13 @@ namespace Sequencer { const CommandSpecMap &specs; State state; const std::vector> &transitions; + const std::unordered_map &state_names; + /** + * @brief rejects cmd if its name is unknown or its arg count is out of range + * @param[in] cmd command to check + * @throws std::runtime_error on unknown command name or arg count out of range + */ void validate_args( const Command &cmd ) const { auto it = specs.find( cmd.name ); if (it == specs.end()) throw std::runtime_error("unknown command: "+cmd.name); @@ -83,15 +89,40 @@ namespace Sequencer { } } + /** + * @brief rejects cmd if it is not permitted from the current state + * @param[in] cmd command to check + * @throws std::runtime_error + */ void validate_order( const Command &cmd ) const { for (const auto &transition : transitions) { - if (transition.from == state && transition.command == cmd.name) { - return; + if (transition.from == state && transition.command == cmd.name) return; + } + // not valid from current state; collect all valid from-states for the error message + auto name_of = [&](State s) -> std::string { + auto it = state_names.find(s); + return (it != state_names.end()) ? it->second : "?"; + }; + std::string valid; + for (const auto &transition : transitions) { + if (transition.command == cmd.name) { + if (!valid.empty()) valid += ", "; + valid += name_of(transition.from); } } - throw std::runtime_error("invalid command order: "+cmd.name); + if (valid.empty()) valid = "(none registered)"; + throw std::runtime_error( + "invalid command order: '"+cmd.name+"' not valid in state " + +name_of(state)+"; valid from: {"+valid+"}"); } + /** + * @brief advances internal state after a successful send + * @param[in] cmd command that was just forwarded to the daemon + * @details Searches the transition table for a matching (from==state, + * command==cmd.name) entry and sets state to transition.to. + * No-ops silently when no matching transition exists. + */ void advance_state( const Command &cmd ) { for (const auto &transition : transitions) { if (transition.from == state && transition.command == cmd.name) { @@ -102,14 +133,24 @@ namespace Sequencer { } public: + /** + * @brief constructs a CommandClient bound to a daemon connection + * @param[in] client DaemonClient used to forward wire commands + * @param[in] specs per-command arg-count constraints + * @param[in] initial_state starting state of the state machine + * @param[in] transitions valid (from, command, to) state transitions + * @param[in] state_names human-readable names for each State value, used in errors + */ CommandClient( Common::DaemonClient &client, const CommandSpecMap &specs, State initial_state, - const std::vector> &transitions ) + const std::vector> &transitions, + const std::unordered_map &state_names ) : client(client), specs(specs), state(initial_state), - transitions(transitions) { } + transitions(transitions), + state_names(state_names) { } /** * @brief primary interface to sending commands @@ -128,6 +169,7 @@ namespace Sequencer { return ret; } + /** @brief returns the current state-machine state */ State get_state() const { return state; } }; } diff --git a/sequencerd/command_rules.h b/sequencerd/command_rules.h index f0254bfc..f1f1d370 100644 --- a/sequencerd/command_rules.h +++ b/sequencerd/command_rules.h @@ -11,6 +11,7 @@ #pragma once #include "command.h" +#include #include namespace Sequencer { @@ -26,6 +27,15 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map camerad_state_names = { + { CameraState::IDLE, "IDLE" }, + { CameraState::READY, "READY" }, + { CameraState::EXPOSING, "EXPOSING" }, + { CameraState::READING, "READING" }, + { CameraState::PAUSED, "PAUSED" }, + { CameraState::INVALID, "INVALID" }, + }; + const CommandSpecMap camerad_specs = { { CAMERAD_ABORT, {0, 0} }, { CAMERAD_ACTIVATE, {0, 4} }, ///< 0 or more of: [ { U G R I } ] @@ -134,6 +144,14 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map acamd_state_names = { + { AcamState::IDLE, "IDLE" }, + { AcamState::READY, "READY" }, + { AcamState::ACQUIRING, "ACQUIRING" }, + { AcamState::GUIDING, "GUIDING" }, + { AcamState::INVALID, "INVALID" }, + }; + const CommandSpecMap acamd_specs = { { ACAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] { ACAMD_CLOSE, {0, 0} }, @@ -242,6 +260,13 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map calibd_state_names = { + { CalibState::IDLE, "IDLE" }, + { CalibState::READY, "READY" }, + { CalibState::MOVING, "MOVING" }, + { CalibState::INVALID, "INVALID" }, + }; + const CommandSpecMap calibd_specs = { { CALIBD_OPEN, {0, 1} }, ///< [ motion | lampmod ] { CALIBD_CLOSE, {0, 1} }, ///< [ motion | lampmod ] @@ -282,6 +307,14 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map slitd_state_names = { + { SlitState::IDLE, "IDLE" }, + { SlitState::READY, "READY" }, + { SlitState::MOVING, "MOVING" }, + { SlitState::HOMED, "HOMED" }, + { SlitState::INVALID, "INVALID" }, + }; + const CommandSpecMap slitd_specs = { { SLITD_OPEN, {0, 0} }, { SLITD_CLOSE, {0, 0} }, @@ -327,6 +360,15 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map tcsd_state_names = { + { TcsState::IDLE, "IDLE" }, + { TcsState::READY, "READY" }, + { TcsState::SLEWING, "SLEWING" }, + { TcsState::TRACKING, "TRACKING" }, + { TcsState::OFFSETTING, "OFFSETTING" }, + { TcsState::INVALID, "INVALID" }, + }; + const CommandSpecMap tcsd_specs = { { TCSD_OPEN, {1, 1} }, ///< { TCSD_CLOSE, {0, 0} }, @@ -413,6 +455,13 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map focusd_state_names = { + { FocusState::IDLE, "IDLE" }, + { FocusState::READY, "READY" }, + { FocusState::MOVING, "MOVING" }, + { FocusState::INVALID, "INVALID" }, + }; + const CommandSpecMap focusd_specs = { { FOCUSD_OPEN, {0, 0} }, { FOCUSD_CLOSE, {0, 0} }, @@ -453,6 +502,13 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map flexured_state_names = { + { FlexureState::IDLE, "IDLE" }, + { FlexureState::READY, "READY" }, + { FlexureState::MOVING, "MOVING" }, + { FlexureState::INVALID, "INVALID" }, + }; + const CommandSpecMap flexured_specs = { { FLEXURED_OPEN, {0, 0} }, { FLEXURED_CLOSE, {0, 0} }, @@ -483,12 +539,18 @@ namespace Sequencer { // ---------- POWERD --------------------------------------------------------- - enum class PowerState { + enum class PowerdState { IDLE, ///< not yet opened READY, ///< open and ready to accept commands INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map powerd_state_names = { + { PowerdState::IDLE, "IDLE" }, + { PowerdState::READY, "READY" }, + { PowerdState::INVALID, "INVALID" }, + }; + const CommandSpecMap powerd_specs = { { POWERD_OPEN, {0, 0} }, { POWERD_CLOSE, {0, 0} }, @@ -500,15 +562,15 @@ namespace Sequencer { { POWERD_SET, {1, 3} } ///< [ ON | OFF | BOOT ] or [ ON | OFF | BOOT ] }; - const std::vector> powerd_transitions = { - { PowerState::IDLE, POWERD_OPEN, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_CLOSE, PowerState::IDLE }, // TODO: verify - { PowerState::READY, POWERD_ISOPEN, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_STATUS, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_REOPEN, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_LIST, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_GET, PowerState::READY }, // TODO: verify - { PowerState::READY, POWERD_SET, PowerState::READY } // TODO: verify + const std::vector> powerd_transitions = { + { PowerdState::IDLE, POWERD_OPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_CLOSE, PowerdState::IDLE }, // TODO: verify + { PowerdState::READY, POWERD_ISOPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_STATUS, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_REOPEN, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_LIST, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_GET, PowerdState::READY }, // TODO: verify + { PowerdState::READY, POWERD_SET, PowerdState::READY } // TODO: verify }; @@ -522,6 +584,14 @@ namespace Sequencer { INVALID ///< sentinel for unverified commands; no live client ever holds this value }; + const std::unordered_map slicecamd_state_names = { + { SlicecamState::IDLE, "IDLE" }, + { SlicecamState::READY, "READY" }, + { SlicecamState::ACQUIRING, "ACQUIRING" }, + { SlicecamState::GUIDING, "GUIDING" }, + { SlicecamState::INVALID, "INVALID" }, + }; + const CommandSpecMap slicecamd_specs = { { SLICECAMD_OPEN, {0, 2} }, ///< [ motion ] [ camera [] ] { SLICECAMD_CLOSE, {0, 0} }, diff --git a/sequencerd/operation_builders.cpp b/sequencerd/operation_builders.cpp index 38d32f1a..52dc43ed 100644 --- a/sequencerd/operation_builders.cpp +++ b/sequencerd/operation_builders.cpp @@ -13,65 +13,159 @@ * the member function via Sequence::current_op_params (set by * Sequence::run() immediately before op.func() is invoked). * - * 2. Passthrough : per-daemon validated passthrough (e.g. "camera exptime 30000" - * or "slit set 2.0 0.0"). These are built inline here (no Ops:: - * method) and forward the positional args (cmd.args) to the - * target daemon. The first arg is the daemon subcommand; the - * remainder are forwarded as-is in daemon native syntax. - * - * NOTE: Passthrough commands currently forward via Common::DaemonClient::send() as a - * PLACEHOLDER. This will be replaced by CommandClient::send() - * later, at which point per-command state-machine validation and - * arg-count checks will be performed at execution time. + * 2. Passthrough : per-daemon translated passthrough. The sequencer DSL form + * "subsystem [do=verb] [key1=val1 ... keyN=valN]" is + * translated to a daemon-native Command{name, arglist} by + * translate_command(), which dispatches to either a + * daemon-specific builder (e.g. build_powerd_command) or + * build_generic_command(). The translated Command is then + * forwarded via CommandClient::send(), which performs + * per-command arg-count and state-machine validation before + * forwarding to the daemon. * */ +#include +#include +#include +#include + #include "sequence.h" #include "sequencerd_commands.h" +#include "powerd_commands.h" namespace Sequencer { - namespace { - - /***** Sequencer::daemon_passthrough *************************************/ - /** - * @brief TEMPORARY daemon passthrough helper - * @param[in] function calling function name for logging - * @param[in] daemon DaemonClient to forward to - * @param[in] args positional args from ParsedCommand (first is subcommand) - * @return ERROR|NO_ERROR - * @details Builds a daemon-native command string from args[0..N-1] and - * forwards via Common::DaemonClient::send(). Self-identifies in - * log as a placeholder so the pending replacement is unambiguous. - * - * TEMPORARY -- REMOVE ONCE CommandClient IS WIRED IN (Steps 2+3). - * Each Passthrough op_builder lambda will then call - * CommandClient::send() directly, performing per-command - * state-machine validation and arg-count checks at execution - * time, and this helper becomes unnecessary. - * - */ - long daemon_passthrough( const std::string &function, - Common::DaemonClient &daemon, - const std::vector &args ) { - if (args.empty()) { - logwrite(function, "ERROR no subcommand provided to "+daemon.name); - return ERROR; + /***** Sequencer::Sequence::validate_keys **********************************/ + /** + * @brief reject any params key not in the allowed whitelist + * @param[in] cmd ParsedCommand whose params are being checked + * @param[in] allowed initializer list of permitted key names + * @throws std::runtime_error if any key in cmd.params is not in allowed + * @details Called from daemon-specific builders after the verb and + * required keys have been matched. Ensures that DSL lines like + * "power do=set name=A state=ON garbage=oops" are rejected + * rather than silently forwarded. + * + */ + void Sequence::validate_keys( const ParsedCommand &cmd, + std::initializer_list allowed ) { + for (const auto &[key, val] : cmd.params) { + if (std::find(allowed.begin(), allowed.end(), + std::string_view{key}) == allowed.end()) { + throw std::runtime_error( + "subsystem '"+cmd.subsystem+"': unexpected key '"+key+"'"); } - std::string cmd_str = args[0]; - for (size_t i=1; i "+daemon.name+ - "): "+cmd_str); - long error = daemon.send(cmd_str, reply); - if (error != NO_ERROR) { - logwrite(function, "ERROR forwarding \""+cmd_str+"\" to "+daemon.name+": "+reply); + } + } + /***** Sequencer::Sequence::validate_keys **********************************/ + + + /***** Sequencer::Sequence::build_generic_command **************************/ + /** + * @brief default ParsedCommand -> Command translation + * @param[in] cmd ParsedCommand to translate + * @return Command{ name, arglist } ready for CommandClient::send() + * @throws std::runtime_error if cmd does not match one of the two + * supported generic forms + * @details Two cases only: + * 1. verb non-empty, params empty + * -> Command{ verb, {} } + * e.g. "acam do=isopen" -> isopen + * 2. verb empty, params has exactly one key=val + * -> Command{ key, { val } } + * e.g. "camera exptime=30000" -> exptime 30000 + * Any other shape (verb + params, multiple params, etc.) is + * a translation error; add a daemon-specific builder. + * + */ + Command Sequence::build_generic_command( const ParsedCommand &cmd ) { + if (!cmd.verb.empty() && cmd.params.size() == 0) { + return Command{ cmd.verb, {} }; + } + if (cmd.verb.empty() && cmd.params.size() == 1) { + const auto &[key, val] = *cmd.params.begin(); + return Command{ key, { val } }; + } + throw std::runtime_error( + "subsystem '"+cmd.subsystem+"': cannot build command" + " (verb='"+cmd.verb+"', "+std::to_string(cmd.params.size())+" params)" + " -- add a daemon-specific translator"); + } + /***** Sequencer::Sequence::build_generic_command **************************/ + + + /***** Sequencer::Sequence::build_powerd_command ***************************/ + /** + * @brief powerd-specific ParsedCommand -> Command translation + * @param[in] cmd ParsedCommand to translate + * @return Command{ name, arglist } in powerd native positional syntax + * @throws std::runtime_error if required keys are missing or unknown + * keys are present + * @details Powerd accepts the following DSL forms: + * power do=set name= state= + * power do=set unit= plug=

state= + * power do=get name= + * power do=get unit= plug=

+ * All other powerd subcommands (open, close, list, status, + * isopen, reopen) fall through to build_generic_command(). + * + */ + Command Sequence::build_powerd_command( const ParsedCommand &cmd ) { + if (cmd.verb == POWERD_SET) { + if (cmd.params.contains("name") && cmd.params.contains("state")) { + validate_keys(cmd, {"name", "state"}); + return Command{ POWERD_SET, + { cmd.params.at("name"), cmd.params.at("state") } }; + } + if (cmd.params.contains("unit") && + cmd.params.contains("plug") && + cmd.params.contains("state")) { + validate_keys(cmd, {"unit", "plug", "state"}); + return Command{ POWERD_SET, + { cmd.params.at("unit"), + cmd.params.at("plug"), + cmd.params.at("state") } }; + } + throw std::runtime_error( + "powerd "+POWERD_SET+": requires name+state or unit+plug+state"); + } + if (cmd.verb == POWERD_GET) { + if (cmd.params.contains("name")) { + validate_keys(cmd, {"name"}); + return Command{ POWERD_GET, { cmd.params.at("name") } }; + } + if (cmd.params.contains("unit") && cmd.params.contains("plug")) { + validate_keys(cmd, {"unit", "plug"}); + return Command{ POWERD_GET, + { cmd.params.at("unit"), cmd.params.at("plug") } }; } - return error; + throw std::runtime_error( + "powerd "+POWERD_GET+": requires name or unit+plug"); } - /***** Sequencer::daemon_passthrough *************************************/ + return build_generic_command(cmd); + } + /***** Sequencer::Sequence::build_powerd_command ***************************/ + - } // anonymous namespace + /***** Sequencer::Sequence::translate_command ******************************/ + /** + * @brief central ParsedCommand -> Command dispatcher + * @param[in] cmd ParsedCommand from the DSL parser + * @return Command in daemon native syntax + * @throws std::runtime_error from the selected builder + * @details Dispatches to a daemon-specific builder when one exists; + * otherwise falls through to build_generic_command(). Plain + * if/else dispatch -- no registry, no table. + * + */ + Command Sequence::translate_command( const ParsedCommand &cmd ) { + if (cmd.subsystem == SEQUENCERD_POWER) { + return build_powerd_command(cmd); + } + return build_generic_command(cmd); + } + /***** Sequencer::Sequence::translate_command ******************************/ /***** Sequencer::Sequence::init_operation_builders ************************/ @@ -194,80 +288,136 @@ namespace Sequencer { // ---------- PASSTHROUGH --------------------------------------------------- - // per-daemon validated passthrough (inline, no Ops method). + // per-daemon translated passthrough (inline, no Ops method). // Command names come from SEQUENCERD_* constants in common/sequencerd_commands.h. - // PLACEHOLDER: forwards via Common::DaemonClient::send() until Steps 2+3 - // replace with CommandClient::send() (state-machine validated). + // translate_command() converts the DSL ParsedCommand into a daemon-native + // Command{name, arglist}, which is then forwarded via + // CommandClient::send(). The client performs per-command arg-count + // validation and state-machine validation before forwarding to the daemon. op_builders[SEQUENCERD_ACAM] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_ACAM_INIT; // passthrough uses nearest daemon THR_ identity - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_ACAM+"]"); - return daemon_passthrough(function, this->acamd, args); + try { + return this->acamd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_CALIB] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_CALIB_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CALIB+"]"); - return daemon_passthrough(function, this->calibd, args); + try { + return this->calibd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_CAMERA] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_CAMERA_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CAMERA+"]"); - return daemon_passthrough(function, this->camerad, args); + try { + return this->camerad_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_FLEXURE] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_FLEXURE_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FLEXURE+"]"); - return daemon_passthrough(function, this->flexured, args); + try { + return this->flexured_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_FOCUS] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_FOCUS_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FOCUS+"]"); - return daemon_passthrough(function, this->focusd, args); + try { + return this->focusd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_POWER] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_POWER_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_POWER+"]"); - return daemon_passthrough(function, this->powerd, args); + try { + return this->powerd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_SLICECAM] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_SLICECAM_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLICECAM+"]"); - return daemon_passthrough(function, this->slicecamd, args); + try { + return this->slicecamd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_SLIT] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_SLIT_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLIT+"]"); - return daemon_passthrough(function, this->slitd, args); + try { + return this->slitd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; op_builders[SEQUENCERD_TCS] = [this](Operation &op, const ParsedCommand &cmd) { op.thr = THR_TCS_INIT; - op.func = [this, args=cmd.args]() { + op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_TCS+"]"); - return daemon_passthrough(function, this->tcsd, args); + try { + return this->tcsd_cmd.send(translate_command(cmd_copy)); + } + catch (const std::runtime_error &e) { + logwrite(function, "ERROR "+std::string(e.what())); + return ERROR; + } }; }; diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 9ebfa20a..d7b0f871 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -477,38 +477,68 @@ namespace Sequencer { /***** Sequencer::Sequence::parse_command **********************************/ /** * @brief parses a single command line - * @details This parses a command and any parameters as key=val pairs - * from the supplied string and returns a ParsedCommand struct. - * @param[in] args string containing command and any optional arguments - * @return nullptr | ParsedCommand + * @details The DSL grammar accepted by this parser is strictly: + * command [key1=val1 key2=val2 ... keyN=valN] + * The first token is the subsystem (command). All remaining + * tokens must be key=value pairs — bare tokens, empty keys, + * empty values, duplicate keys, and duplicate do= are all + * hard parse errors and cause the line to be rejected. + * The reserved key DSL_KEY_DO ("do") is extracted to + * ParsedCommand::verb and not stored in params; all other + * pairs are stored in params. + * @param[in] args string containing one command line (consumed by reference) + * @return std::nullopt on empty line or parse error; ParsedCommand otherwise * */ std::optional Sequence::parse_command(std::string &args) { + const std::string function("Sequencer::Sequence::parse_command"); std::istringstream iss(args); std::string word; - // first word is the command + // first word is the subsystem (the command); empty line returns nullopt if (!(iss >> word)) return std::nullopt; ParsedCommand command; - command.name = word; + command.subsystem = word; - // Additional words are either key=val params (INTERNAL) or positional - // args (PASSTHROUGH). Both collections are populated; each op_builder - // chooses which to consume. while (iss >> word) { - // key=value parameters auto eq = word.find('='); - if (eq != std::string::npos) { - std::string key = word.substr(0, eq); - std::string val = word.substr(eq+1); - command.params.map[key] = val; + + if (eq == std::string::npos) { + logwrite(function, "ERROR bare token '"+word+"' is invalid" + " (key=value required) in line: "+args); + return std::nullopt; } - // positional arguments - else { - command.args.push_back(word); + + std::string key = word.substr(0, eq); + std::string val = word.substr(eq+1); + + if (key.empty()) { + logwrite(function, "ERROR empty key in token '"+word+"' in line: "+args); + return std::nullopt; } + if (val.empty()) { + logwrite(function, "ERROR empty value for key '"+key+"' in line: "+args); + return std::nullopt; + } + + if (key == std::string(DSL_KEY_DO)) { + if (!command.verb.empty()) { + logwrite(function, "ERROR duplicate "+std::string(DSL_KEY_DO) + +"= key in line: "+args); + return std::nullopt; + } + command.verb = val; + continue; + } + + if (command.params.contains(key)) { + logwrite(function, "ERROR duplicate key '"+key+"' in line: "+args); + return std::nullopt; + } + + command.params.set(key, val); } return command; @@ -745,7 +775,13 @@ namespace Sequencer { logwrite(function, "ERROR parsing '"+args+"'"); return ERROR; } - auto commands = { *parsed }; + // Wrap the single command in an implicit serial group so build_sequence() + // accepts it without requiring an explicit serial:/end block in the input. + ParsedCommand group_open; + group_open.subsystem = "serial:"; + ParsedCommand group_close; + group_close.subsystem = "end"; + std::vector commands = { group_open, *parsed, group_close }; std::vector sequence; @@ -917,6 +953,28 @@ namespace Sequencer { /***** Sequencer::Sequence::publish_snapshot *******************************/ + /***** Sequencer::Sequence::request_snapshot ********************************/ + /** + * @brief asks other daemons to publish their current status + * @details Publishes a Topic::SNAPSHOT message containing the names of all + * daemons this sequencer subscribes to. Each named daemon will + * respond by re-publishing its own snapshot, ensuring the sequencer + * receives current state (e.g. camera readiness) even if it + * started after those daemons published their initial status. + * + */ + void Sequence::request_snapshot() { + nlohmann::json jmessage; + jmessage[Daemon::CAMERAD] = true; + jmessage[Daemon::ACAMD] = true; + jmessage[Daemon::SLICECAMD] = true; + jmessage[Daemon::SLITD] = true; + jmessage[Daemon::TCSD] = true; + this->publisher->publish( jmessage, Topic::SNAPSHOT ); + } + /***** Sequencer::Sequence::request_snapshot ********************************/ + + /***** Sequencer::Sequence::publish_seqstate ********************************/ /** * @brief publishes sequencer state under Topic::SEQ_SEQSTATE @@ -2574,6 +2632,10 @@ namespace Sequencer { std::stringstream message; long error=NO_ERROR; + // no telescope moves for calibration targets + // + if ( this->target.iscal ) return NO_ERROR; + ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); ScopedState wait_moveto( wait_state_manager, Sequencer::SEQ_WAIT_MOVETO ); @@ -2955,25 +3017,23 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); - // Determine post-abort seqstate based on current seqstate, before we - // enter SEQ_ABORTING. Aborting during RUNNING or PAUSED returns to READY; - // aborting during STARTING or STOPPING is unsafe and transitions to FAILED. - // Otherwise, leave seqstate unchanged (abort bit only; no destruct bit). - const bool abort_during_run = seq_state_manager.are_any_set( Sequencer::SEQ_RUNNING, - Sequencer::SEQ_PAUSED ); - const bool abort_during_lifecycle = seq_state_manager.are_any_set( Sequencer::SEQ_STARTING, - Sequencer::SEQ_STOPPING ); - - // Scoped SEQ_ABORTING bit: set on entry, cleared on exit (RAII). - ScopedState seq_state( this->seq_state_manager, Sequencer::SEQ_ABORTING ); - if ( abort_during_run ) { - seq_state.destruct_set( Sequencer::SEQ_READY ); - } - else if ( abort_during_lifecycle ) { - seq_state.destruct_set( Sequencer::SEQ_FAILED ); - } + // Decide post-abort seqstate before entering SEQ_ABORTING. These snapshots + // must be taken before any seqstate mutation below, because set_only() + // clears all other lifecycle bits. + // + const bool abort_during_run = this->seq_state_manager.are_any_set( Sequencer::SEQ_RUNNING, + Sequencer::SEQ_PAUSED ); + const bool abort_during_lifecycle = this->seq_state_manager.are_any_set( Sequencer::SEQ_STARTING, + Sequencer::SEQ_STOPPING ); - clear_cancel_flag(); + // Enter SEQ_ABORTING as a strict one-hot state. + // + this->seq_state_manager.set_only( {Sequencer::SEQ_ABORTING} ); + + // Clear any prior cancel state so stop_exposure and acquisition-stop calls + // below run to completion rather than bailing early on a stale flag. + // + this->cancel_flag.store(false); // stop any exposure that may be in progress // @@ -2985,15 +3045,62 @@ namespace Sequencer { logwrite( function, "ERROR stop_exposure exception: "+std::string(e.what()) ); } + // aborts incomplete acquisitions in progress + // + if (this->wait_state_manager.is_set(Sequencer::SEQ_WAIT_FINEACQUIRE) && + this->do_slicecam_stop() != NO_ERROR ) { + this->broadcast.warning(function, "stopping fine acquisition"); + } + if (this->wait_state_manager.is_set(Sequencer::SEQ_WAIT_ACAM_ACQUIRE) && + this->do_acam_stop() != NO_ERROR ) { + this->broadcast.warning(function, "stopping guiding"); + } + // set the cancel flag to stop any cancel-able tasks // - set_cancel_flag(); + this->cancel_flag.store(true); + this->cv.notify_all(); + // Wake threads blocked on subsystem CVs so they can check cancel_flag. + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } // drop into do-one to prevent auto increment to next target // this->do_once.store(true); this->broadcast.notice( function, "cancel signal sent" ); + + // Wait for sequence_start to fully exit before switching to SEQ_READY. + // Without this, we could have SEQ_READY set while THR_SEQUENCE_START is + // still set. Workers just received cancel_flag + CV notifications above, + // so this loop exits after at most a few iterations. + // + if ( abort_during_run ) { + const auto drain_timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while ( this->thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) && + std::chrono::steady_clock::now() < drain_timeout ) { + std::this_thread::sleep_for( std::chrono::milliseconds(20) ); + } + if ( this->thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { + logwrite( function, "WARNING: sequence_start did not exit within drain timeout" ); + } + } + + // Exit SEQ_ABORTING to a strict one-hot terminal state chosen from the + // snapshot taken at entry. If neither condition applies (e.g. abort + // invoked while READY/NOTREADY/FAILED) we leave the state at NOTREADY + // so callers never see SEQ_ABORTING linger and no prior bit is retained. + // + if ( abort_during_run ) { + this->seq_state_manager.set_only( {Sequencer::SEQ_READY} ); + } + else if ( abort_during_lifecycle ) { + this->seq_state_manager.set_only( {Sequencer::SEQ_FAILED} ); + } + else { + this->seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + } } /***** Sequencer::Sequence::stop_exposure *********************************/ diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 64be1376..31cbc0ec 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -35,10 +35,8 @@ #include "sequencerd_commands.h" #include "message_keys.h" #include "thread_pool.h" -/*** Work-In-Progress - * #include "command.h" - * #include "command_rules.h" - */ +#include "command.h" +#include "command_rules.h" #include "tcs_constants.h" #include "acam_interface_shared.h" @@ -53,6 +51,17 @@ namespace Sequencer { constexpr size_t NTHREADS = 10; ///< number of simultaneous operation threads + /** + * @brief DSL reserved keys + * @details DSL_KEY_DO is globally reserved at parse level. It is extracted + * to ParsedCommand::verb and never stored in params. No daemon + * parameter may use this name. + * DSL_KEY_ACTION is semantic-level only — it stays in params and + * is consumed by build_sequence() in the on_error context. + */ + inline constexpr std::string_view DSL_KEY_DO { "do" }; + inline constexpr std::string_view DSL_KEY_ACTION { "action" }; + /** * @enum ErrorCodes * @brief @@ -342,6 +351,31 @@ namespace Sequencer { return map.find(key) != map.end(); } + /** @brief alias for has() — provided for STL-style readability */ + bool contains(const std::string &key) const { + return has(key); + } + + /** @brief throwing accessor — caller must guarantee key presence */ + const std::string &at(const std::string &key) const { + return map.at(key); + } + + /** @brief number of stored key=value pairs */ + size_t size() const { + return map.size(); + } + + /** @brief iteration — ordering unspecified (unordered_map). + * Use only for single-entry access or explicit key checks. */ + auto begin() const { return map.begin(); } + auto end() const { return map.end(); } + + /** @brief insert or overwrite a key=value pair */ + void set(const std::string &key, const std::string &val) { + map[key] = val; + } + template T get(const std::string &key, const T &default_val) const { auto it = map.find(key); @@ -391,14 +425,35 @@ namespace Sequencer { }; /** @brief associates a sequencer command with its parameters + * @details A DSL line of the form + * command [key1=val1 key2=val2 ... keyN=valN] + * parses to: + * subsystem = command (first token) + * verb = value of the reserved do= key, or "" if absent + * params = the remaining key=value pairs (do= removed) + * Block keywords (parallel:, serial:, end, on_error) are + * intercepted by build_sequence() before op_builder dispatch, + * so subsystem here always names a real subsystem after that + * filter has run. */ struct ParsedCommand { - std::string name; - OperationParams params; ///< key=value pairs (for Internal Operations) - std::vector args; ///< positional args (for Passthrough Operations) + std::string subsystem; + std::string verb; + OperationParams params; int linenum{0}; }; + /** @brief DSL -> daemon Command translation helpers + * @details Implemented in operation_builders.cpp. Static because they + * do not depend on Sequence state; declared as members so + * they have access to the private ParsedCommand type. + */ + static void validate_keys( const ParsedCommand &cmd, + std::initializer_list allowed ); + static Command build_generic_command( const ParsedCommand &cmd ); + static Command build_powerd_command( const ParsedCommand &cmd ); + static Command translate_command( const ParsedCommand &cmd ); + /** @brief key=value params of the currently executing operation * @details set by run() just before op.func() is invoked and consumed * by Sequence:: member functions that accept optional overrides @@ -537,6 +592,10 @@ namespace Sequencer { inline void set_cancel_flag() { this->cancel_flag.store(true, std::memory_order_release); this->cv.notify_all(); + // Wake threads blocked on subsystem CVs so they can observe the cancel_flag. + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } } double acquisition_timeout; ///< timeout for target acquisition (in sec) set by configuration parameter ACAM_ACQUIRE_TIMEOUT @@ -615,12 +674,47 @@ namespace Sequencer { Common::DaemonClient slitd { "slitd" }; Common::DaemonClient tcsd { "tcsd" }; -/**** Work-In-Progress - CommandClient camerad_cmd { camerad, - camerad_specs, - CameraState::IDLE, - camerad_transitions}; -*****/ + // CommandClient wrappers add per-daemon arg-count checks and + // state-machine validation in front of the DaemonClient TCP send. + // Initial state is IDLE for every daemon -- they transition to READY + // after the first successful "open" (or equivalent) command. + // + CommandClient acamd_cmd { acamd, acamd_specs, + AcamState::IDLE, + acamd_transitions, + acamd_state_names }; + CommandClient calibd_cmd { calibd, calibd_specs, + CalibState::IDLE, + calibd_transitions, + calibd_state_names }; + CommandClient camerad_cmd { camerad, camerad_specs, + CameraState::IDLE, + camerad_transitions, + camerad_state_names }; + CommandClient flexured_cmd { flexured, flexured_specs, + FlexureState::IDLE, + flexured_transitions, + flexured_state_names }; + CommandClient focusd_cmd { focusd, focusd_specs, + FocusState::IDLE, + focusd_transitions, + focusd_state_names }; + CommandClient powerd_cmd { powerd, powerd_specs, + PowerdState::IDLE, + powerd_transitions, + powerd_state_names }; + CommandClient slicecamd_cmd { slicecamd, slicecamd_specs, + SlicecamState::IDLE, + slicecamd_transitions, + slicecamd_state_names }; + CommandClient slitd_cmd { slitd, slitd_specs, + SlitState::IDLE, + slitd_transitions, + slitd_state_names }; + CommandClient tcsd_cmd { tcsd, tcsd_specs, + TcsState::IDLE, + tcsd_transitions, + tcsd_state_names }; std::map power_switch; ///< STL map of PowerSwitch objects maps all plugnames to each subsystem @@ -663,6 +757,7 @@ namespace Sequencer { void handletopic_tcsd( const nlohmann::json &jmessage ); void publish_snapshot(); void publish_snapshot(std::string &retstring); + void request_snapshot(); void publish_seqstate(); void publish_waitstate(); void publish_daemonstate(); diff --git a/sequencerd/sequence_builder.cpp b/sequencerd/sequence_builder.cpp index 960ca9a9..8191b1ff 100644 --- a/sequencerd/sequence_builder.cpp +++ b/sequencerd/sequence_builder.cpp @@ -44,7 +44,7 @@ namespace Sequencer { // for (const auto &command : commands) { - const std::string &name = command.name; + const std::string &name = command.subsystem; // ---------- SPECIAL COMMAND { parallel: | serial: } ------------------ diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index f7c76c3e..df721744 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -141,6 +141,7 @@ int main(int argc, char **argv) { std::this_thread::sleep_for( std::chrono::milliseconds(200) ); sequencerd.sequence.publish_snapshot(); + sequencerd.sequence.request_snapshot(); // This will pre-thread N_THREADS threads. // The 0th thread is reserved for the blocking port, and the rest are for the non-blocking port. diff --git a/utils/utilities.h b/utils/utilities.h index 41dcb866..87a21899 100644 --- a/utils/utilities.h +++ b/utils/utilities.h @@ -152,7 +152,7 @@ static inline void rtrim(std::string &s) { /// trim off trailing whites s.erase( std::find_if( s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); } ).base(), s.end() ); } static inline void ltrim(std::string &s) { /// trim off leading white space - s.erase(std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); } ) ); + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); } ) ); } static inline void lrtrim(std::string &s) { rtrim(s); ltrim(s); } From 5c7b79ee8544bdaa2399ea25f92ec17c4c06d989 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 11:36:26 -0700 Subject: [PATCH 28/37] places acam startup in parallel group, while keeping slicecam in a separate serial group following that. Should speed-up startup while maintaining serialization of camera inits --- sequencerd/sequence.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index d7b0f871..b44cc991 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -3389,11 +3389,12 @@ namespace Sequencer { ops.flexure_init(), ops.focus_init(), ops.slit_init(), - ops.tcs_init() } + ops.tcs_init(), + ops.acam_init() } // ACAM camera-open overlaps with parallel group }, + // SLICECAM can't be in parallel with ACAM. separate group enforces this { OperationType::SERIAL, OnError::CONTINUE, - { ops.acam_init(), - ops.slicecam_init() } + { ops.slicecam_init() } } }; From f68f8212353758f6e83286e7a69c01a012705127 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 11:52:23 -0700 Subject: [PATCH 29/37] fixes case where a parallel group could still proceed after error --- sequencerd/sequence.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index b44cc991..d1398333 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -233,8 +233,10 @@ namespace Sequencer { ret = futures[i].get(); oss << ops[i].name(); if (ret==ABORT) { is_abort=true; oss << " cancelled"; } - else - oss << " completed" << ( (ret==NO_ERROR) ? "" : " with error"); + else { + if (ret != NO_ERROR) is_error=true; + oss << " completed" << ( (ret==NO_ERROR) ? "" : " with error"); + } } catch (const std::exception &e) { oss << " received exception: " << e.what(); From 9fcb2f10d001b51d681958634f6c75e0560ee8b6 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 12:29:51 -0700 Subject: [PATCH 30/37] fixes state on shutdown --- sequencerd/sequence.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index d1398333..db8e5290 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -3431,13 +3431,21 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running - // set only STOPPING (and clear everything else, including any prior FAILED) - ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STOPPING, true ); // state=STOPPING (only) + // Enter STOPPING as a strict one-hot state. Explicit set_only is used instead + // of ScopedState because abort_process() below transitions seqstate + // independently + // + this->seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); - // stop everything + // stop everything in progress // this->abort_process(); + // abort_process snapshots abort_during_lifecycle=true (STOPPING was set) and + // ends with set_only({SEQ_FAILED}). Restore STOPPING so shutdown can proceed. + // + this->seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); + // clear stop flags // clear_cancel_flag(); @@ -3472,17 +3480,22 @@ namespace Sequencer { }, function ); std::ostringstream message; - if (error==NO_ERROR) { // TODO need granularity here + if (error==NO_ERROR) { message << "NOTICE: instrument is shut down"; - seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); // clean shutdown -> NOTREADY } else { message << "ERROR occurred during shutdown and may not have completed"; - seq_state.destruct_set( Sequencer::SEQ_FAILED ); // bad shutdown -> FAILED } this->async.enqueue_and_log( function, message.str() ); + // Always end in NOTREADY regardless of worker errors. SEQ_FAILED is + // reserved for startup failures and externally-aborted lifecycle transitions. + // Worker errors during shutdown are logged above but do not prevent + // the instrument from being considered shut down (not ready). + // + this->seq_state_manager.set_only( {Sequencer::SEQ_NOTREADY} ); + return error; } /***** Sequencer::Sequence::shutdown ****************************************/ From 082e822a3d04238839dbad9ef234b3526f8081f7 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 12:35:06 -0700 Subject: [PATCH 31/37] . --- sequencerd/sequence.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index db8e5290..58b813a6 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -3431,20 +3431,20 @@ namespace Sequencer { ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running - // Enter STOPPING as a strict one-hot state. Explicit set_only is used instead - // of ScopedState because abort_process() below transitions seqstate - // independently + // Enter STOPPING as a strict one-hot state. Explicit set_only is used + // instead of ScopedState because final state is set explicitly before return. // this->seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); - // stop everything in progress + // Wake any threads waiting on subsystem CVs so they can exit cleanly. + // Inline here rather than calling abort_process(), which transitions + // seqstate through ABORTING and FAILED — confusing during a normal shutdown. // - this->abort_process(); - - // abort_process snapshots abort_during_lifecycle=true (STOPPING was set) and - // ends with set_only({SEQ_FAILED}). Restore STOPPING so shutdown can proceed. - // - this->seq_state_manager.set_only( {Sequencer::SEQ_STOPPING} ); + this->cancel_flag.store(true); + this->cv.notify_all(); + { std::lock_guard lock(this->acam_mtx); this->acam_cv.notify_all(); } + { std::lock_guard lock(this->fineacquire_mtx); this->fineacquire_cv.notify_all(); } + { std::lock_guard lock(this->camerad_mtx); this->camerad_cv.notify_all(); } // clear stop flags // From e811ab8cce7c661602e948bdccdf5e8cc4ecdc0f Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 13:51:51 -0700 Subject: [PATCH 32/37] fixes bug preventing wait_for_user_continue on fine acquire failure --- sequencerd/sequence.cpp | 2 +- sequencerd/sequence.h | 1 + sequencerd/sequence_acquisition.cpp | 47 ++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 58b813a6..eff8ca49 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -358,7 +358,7 @@ namespace Sequencer { [this,caller]() { return this->do_target_acquisition(caller); } }, { THR_MOVE_TO_TARGET, - [this]() { return this->target_offset(); } }, + [this,caller]() { return this->do_target_offset(caller); } }, { THR_SLIT_SET, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 31cbc0ec..01eb2f42 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -875,6 +875,7 @@ namespace Sequencer { long do_slicecam_fineacquire(); long do_slicecam_stop(); long do_target_acquisition(std::string caller); + long do_target_offset(std::string caller); long do_target_virtualslit(VirtualSlitMode mode); long acam_init(); ///< initializes connection to acamd diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index affbf926..30458fbb 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -272,7 +272,9 @@ namespace Sequencer { /***** Sequencer::Sequence::do_target_acquisition ****************************/ /** * @brief performs target acquisition - * @details First acquire on ACAM, then run slicecam fineacquire + * @details First acquire on ACAM, then run slicecam fineacquire if enabled. + * Waits for user confirmation when fineacquire is disabled or fails. + * @param[in] caller name of calling function for logging * @return NO_ERROR | ABORT * */ @@ -284,18 +286,20 @@ namespace Sequencer { // if ( this->do_acam_acquire() != NO_ERROR ) { this->broadcast.warning( caller, "acam acquisition failed" ); - - // on ACAM acquisition failure wait for user to continue or cancel if ( this->wait_for_user(caller) == ABORT ) return ABORT; - return NO_ERROR; // user chose to continue } // ---------- SLICECAM fineacquire --------------------- // - if ( this->do_slicecam_fineacquire() != NO_ERROR ) { - this->broadcast.warning( caller, "slicecam fine acquisition failed" ); - } + bool dofine = this->should_fineacquire.load(); + long ret = NO_ERROR; + if ( dofine ) ret = this->do_slicecam_fineacquire(); + if ( ret != NO_ERROR ) this->broadcast.warning( caller, "slicecam fine acquisition failed" ); + + // wait for user if fineacquire was disabled or failed + if ( (!dofine || ret != NO_ERROR) && + this->wait_for_user(caller) == ABORT ) return ABORT; return NO_ERROR; } @@ -306,7 +310,7 @@ namespace Sequencer { /** * @brief move to virtual slit position * @param[in] mode VirtualSlitMode - * @return NO_ERROR | NO_ERROR + * @return NO_ERROR * */ long Sequence::do_target_virtualslit(VirtualSlitMode mode) { @@ -315,4 +319,31 @@ namespace Sequencer { } /***** Sequencer::Sequence::do_target_virtualslit ****************************/ + + /***** Sequencer::Sequence::do_target_offset *********************************/ + /** + * @brief send guide offsets to the TCS after successful fine acquisition + * @details Offsets are only sent when fineacquire achieved a lock. + * If the offset fails, the operator is given the opportunity to + * continue or cancel. + * @param[in] caller name of calling function for logging + * @return NO_ERROR | ABORT + * + */ + long Sequence::do_target_offset(std::string caller) { + + if (this->target.iscal) return NO_ERROR; + + // offsets are only meaningful when fineacquire achieved a lock + if ( !this->is_fineacquire_locked.load() ) return NO_ERROR; + + if ( this->target_offset() == ERROR ) { + this->broadcast.warning( caller, "target offset failed" ); + if ( this->wait_for_user(caller) == ABORT ) return ABORT; + } + + return NO_ERROR; + } + /***** Sequencer::Sequence::do_target_offset *********************************/ + } From 620a5874034e48cad6c3eef014dfb599a6e28ef4 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 16:28:23 -0700 Subject: [PATCH 33/37] incorporates bug fixes from PR409-- * fix: camerad was missing topic handler for SNAPSHOT * fix: Sequence::camera_set could not always set wait_state * fix: sequencer PAUSE command checked seq_state_manager for wait state --- camerad/astrocam.cpp | 15 +++++++++++++++ camerad/astrocam.h | 6 ++++++ camerad/simulator-arc.cpp | 30 +++++++++++++++--------------- sequencerd/sequence.cpp | 6 +++--- sequencerd/sequencer_server.cpp | 2 +- sequencerd/sequencerd.cpp | 3 ++- 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index fb2ddea2..4757b159 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -51,6 +51,21 @@ namespace AstroCam { /**** AstroCam::Interface::publish_snapshot *********************************/ + /***** AstroCam::Interface::handletopic_snapshot ****************************/ + /** + * @brief what to do when the topic is Topic::SNAPSHOT + * @details This publishes a JSON message containing a snapshot of my + * telemetry info when the subscriber receives the Topic::SNAPSHOT + * topic and the payload contains my name. + * @param[in] jmessage_in subscribed-received JSON message + * + */ + void Interface::handletopic_snapshot( const nlohmann::json &jmessage_in ) { + if ( jmessage_in.contains( Topic::CAMERAD ) ) this->publish_snapshot(); + } + /***** AstroCam::Interface::handletopic_snapshot ****************************/ + + long NewAstroCam::new_expose( std::string nseq_in ) { logwrite( "NewAstroCam::new_expose", nseq_in ); return( NO_ERROR ); diff --git a/camerad/astrocam.h b/camerad/astrocam.h index 4e60f415..78bde549 100644 --- a/camerad/astrocam.h +++ b/camerad/astrocam.h @@ -636,6 +636,11 @@ namespace AstroCam { can_expose(true), // am I ready for the next exposure? modeselected(false), useframes(true) { + topic_handlers = { + { Topic::SNAPSHOT, std::function( + [this](const nlohmann::json &msg) { handletopic_snapshot(msg); } ) } + }; + this->pFits.resize( NUM_EXPBUF ); // pre-allocate FITS_file object pointers for each exposure buffer this->fitsinfo.resize( NUM_EXPBUF ); // pre-allocate Camera Information object pointers for each exposure buffer this->writes_pending.resize( NUM_EXPBUF ); // pre-allocate writes_pending vector for each exposure buffer @@ -687,6 +692,7 @@ namespace AstroCam { void start_subscriber_thread() { Common::PubSubHandler::start_subscriber_thread(*this); } void stop_subscriber_thread() { Common::PubSubHandler::stop_subscriber_thread(*this); } void publish_snapshot(std::string* retstring=nullptr); + void handletopic_snapshot(const nlohmann::json &jmessage_in); // vector of pointers to Camera Information containers, one for each exposure number // diff --git a/camerad/simulator-arc.cpp b/camerad/simulator-arc.cpp index a85f4a6c..dece6c3b 100644 --- a/camerad/simulator-arc.cpp +++ b/camerad/simulator-arc.cpp @@ -43,7 +43,7 @@ namespace AstroCam { // If no string is given then use vector of configured devices // if ( devices_in.empty() ) { - this->devnums = this->configured_devnums; + this->connected_devnums = this->configured_devnums; } else { // Otherwise, tokenize the device list string and build devnums from the tokens @@ -53,8 +53,8 @@ namespace AstroCam { for ( const auto &n : tokens ) { // For each token in the devices_in string, try { int dev = std::stoi( n ); // convert to int - if ( std::find( this->devnums.begin(), this->devnums.end(), dev ) == this->devnums.end() ) { // If it's not already in the vector, - this->devnums.push_back( dev ); // then push into devnums vector. + if ( std::find( this->connected_devnums.begin(), this->connected_devnums.end(), dev ) == this->connected_devnums.end() ) { // If it's not already in the vector, + this->connected_devnums.push_back( dev ); // then push into devnums vector. } } catch (std::invalid_argument &) { @@ -76,7 +76,7 @@ namespace AstroCam { // For each requested dev in devnums, if there is a matching controller in the config file, // then get the devname and store it in the controller map. // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { if ( this->controller.find( dev ) != this->controller.end() ) { this->controller[ dev ].devname = "sim"+std::to_string(dev); } @@ -84,7 +84,7 @@ namespace AstroCam { // set the controller connected state true // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { this->controller[dev].connected = true; } @@ -110,7 +110,7 @@ namespace AstroCam { // clear the controller connected state // - for ( const auto &dev : this->devnums ) { + for ( const auto &dev : this->connected_devnums ) { this->controller[dev].connected = false; } @@ -150,7 +150,7 @@ namespace AstroCam { std::stringstream lodfilestream; // But only use it if the device is open // - if ( std::find( this->devnums.begin(), this->devnums.end(), fw->first ) != this->devnums.end() ) { + if ( std::find( this->connected_devnums.begin(), this->connected_devnums.end(), fw->first ) != this->connected_devnums.end() ) { lodfilestream << fw->first << " " << fw->second; // Call do_load_firmware with the built up string. @@ -452,14 +452,14 @@ namespace AstroCam { } /***** AstroCam::Interface::native ******************************************/ - - long Interface::_image_size( std::string args, std::string &retstring, const bool save_as_default ) { - std::string function = "AstroCam::Interface::_image_size"; - std::stringstream message; - logwrite( function, "NOP" ); - return( NO_ERROR ); - } - +/* + *long Interface::_image_size( std::string args, std::string &retstring, const bool save_as_default ) { + * std::string function = "AstroCam::Interface::_image_size"; + * std::stringstream message; + * logwrite( function, "NOP" ); + * return( NO_ERROR ); + *} + */ /***** AstroCam::Simulator::dothread_expose *********************************/ /** diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index eff8ca49..478972d0 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -1486,13 +1486,13 @@ namespace Sequencer { std::stringstream camcmd; long error=NO_ERROR; + ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_SET ); + ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); + this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_SET ); - ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); - this->thread_error_manager.set( THR_CAMERA_SET ); // assume the worse, clear on success // Controller activate states stored in Sequencer::CalibrationTarget::calinfo map, diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index 5920b3ce..ca6e32ee 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -1509,7 +1509,7 @@ namespace Sequencer { if ( cmd == SEQUENCERD_PAUSE ) { // Can only pause during an exposure // - if ( ! this->sequence.seq_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { + if ( ! this->sequence.wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { this->sequence.async.enqueue_and_log( function, "ERROR: can only pause during an active exposure" ); ret = ERROR; } diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index df721744..7125b3a6 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -139,8 +139,9 @@ int main(int argc, char **argv) { } sequencerd.sequence.seq_state_manager.set_only({Sequencer::SEQ_NOTREADY}); - std::this_thread::sleep_for( std::chrono::milliseconds(200) ); + std::this_thread::sleep_for( std::chrono::milliseconds(250) ); sequencerd.sequence.publish_snapshot(); + std::this_thread::sleep_for( std::chrono::milliseconds(250) ); sequencerd.sequence.request_snapshot(); // This will pre-thread N_THREADS threads. From 4aa1b3a9a1891c697357b7a9aa456c6a8c6bf0ae Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 16:41:30 -0700 Subject: [PATCH 34/37] another from PR409-- * fixes binning bug * fixes make_directory bug --- camerad/astrocam.cpp | 25 ++++++++++++++++++++----- utils/make_directories.cpp | 18 +++++++++--------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/camerad/astrocam.cpp b/camerad/astrocam.cpp index 4757b159..4e37df34 100644 --- a/camerad/astrocam.cpp +++ b/camerad/astrocam.cpp @@ -5077,6 +5077,21 @@ logwrite(function, message.str()); // if (pcontroller->has_boi()) { skipspat = 0; } + // Capture the unbinned, unskipped detector dimensions before trimming + // so that detrows/detcols remain the full requested size across + // repeated bin commands. These are preserved even when the per-call + // geometry is shortened by the skip-modulo adjustment below. + // + int detrows_new, detcols_new; + pcontroller->logical_to_physical(spat, spec, detrows_new, detcols_new); + + // Capture the original requested overscan in physical (row/col) form + // before any modulo-binning trim below, so that osrows0/oscols0 retain + // the true requested overscan across repeated bin commands. + // + int osrows0_new, oscols0_new; + pcontroller->logical_to_physical(osspat, osspec, osrows0_new, oscols0_new); + // Remove those skipped pixels from the image size spat -= skipspat; spec -= skipspec; @@ -5100,10 +5115,10 @@ logwrite(function, message.str()); // unchanged by binning, so that when reverting to binning=1 from some // binnnig factor, this is the default image size to revert to. // - pcontroller->detrows = rows; - pcontroller->detcols = cols; - pcontroller->osrows0 = osrows; - pcontroller->oscols0 = oscols; + pcontroller->detrows = detrows_new; + pcontroller->detcols = detcols_new; + pcontroller->osrows0 = osrows0_new; + pcontroller->oscols0 = oscols0_new; pcontroller->skipcols = skipcols; pcontroller->skiprows = skiprows; @@ -5852,7 +5867,7 @@ logwrite(function, message.str()); int &spat, int &spec, int &osspat, int &osspec, int &binspat, int &binspec) { if (!pcontroller) return; pcontroller->physical_to_logical( pcontroller->detrows, pcontroller->detcols, spat, spec ); - pcontroller->physical_to_logical( pcontroller->osrows, pcontroller->oscols, osspat, osspec ); + pcontroller->physical_to_logical( pcontroller->osrows0, pcontroller->oscols0, osspat, osspec ); pcontroller->physical_to_logical( pcontroller->info.binning[_ROW_], pcontroller->info.binning[_COL_], binspat, binspec ); } diff --git a/utils/make_directories.cpp b/utils/make_directories.cpp index 99c39aaf..b04fdb83 100644 --- a/utils/make_directories.cpp +++ b/utils/make_directories.cpp @@ -17,23 +17,23 @@ namespace fs = std::filesystem; std::string get_date() { std::stringstream current_date; // String to contain the return value std::time_t t=std::time(nullptr); // Container for system time - struct timespec timenow;; // Time of day container struct tm mytime; // time container - // Get the system time, return a bad datestamp on error - if ( clock_gettime( CLOCK_REALTIME, &timenow ) != 0 ) return( "" ); + // UTC now + if ( gmtime_r( &t, &mytime ) == nullptr ) return( "" ); - // Convert the time of day to local or GMT - t = timenow.tv_sec; - if ( localtime_r( &t, &mytime ) == nullptr ) return( "" ); + // at local noon we want tonight's UTC + mytime.tm_mday += 1; + + // normalize struct handles rollovers + if (timegm(&mytime)==-1) return ""; - current_date.setf(std::ios_base::right); current_date << std::setfill('0') << std::setprecision(0) << std::setw(4) << mytime.tm_year + 1900 << std::setw(2) << mytime.tm_mon + 1 - << std::setw(2) << mytime.tm_mday + 1; // tomorrow! + << std::setw(2) << mytime.tm_mday; - return( current_date.str() ); + return current_date.str(); } int main() { From dfa18f6ce3ae07822377eb94137dac15d2837a33 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 21:17:36 -0700 Subject: [PATCH 35/37] cleaned up broadcast and log messages (had already done this in fineacquire-update-sequencer-pub branch, now repeated here) --- sequencerd/sequence.cpp | 277 ++++++++++++------------------- sequencerd/sequence.h | 2 - sequencerd/sequencer_server.cpp | 279 +++++++++++++------------------- sequencerd/sequencer_server.h | 1 - sequencerd/sequencerd.cpp | 8 +- 5 files changed, 223 insertions(+), 344 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 478972d0..219126e8 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -656,7 +656,7 @@ namespace Sequencer { if ( required.test(bit) && !daemon_manager.is_set(static_cast(bit)) ) { std::ostringstream oss; - oss << "sequence requires daemon '" + oss << "sequence requires '" << daemon_name.at(static_cast(bit)) << "' but it is not ready"; this->broadcast.error(function, oss.str()); @@ -1364,8 +1364,7 @@ namespace Sequencer { this->dotype( "ONE" ); // single-target mode must set dotype=ONE } - message.str(""); message << "NOTICE: " << targetstatus; - this->async.enqueue( message.str() ); // broadcast target status + logwrite(function, "targetstatus: "+targetstatus); if ( targetstate == TargetInfo::TARGET_FOUND ) { // target found, get the threads going @@ -1374,9 +1373,9 @@ namespace Sequencer { // if ( ! this->daemon_manager.is_set( Sequencer::DAEMON_TCS ) ) { if ( ! this->target.ra_hms.empty() || ! this->target.dec_dms.empty() ) { - message.str(""); message << "ERROR cannot move to target " << this->target.name + message.str(""); message << "cannot move to target " << this->target.name << " because TCS is not connected"; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.error( function, message.str() ); this->thread_error_manager.set( THR_SEQUENCE_START ); // report error break; } @@ -1387,20 +1386,10 @@ namespace Sequencer { this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error break; } - - // let the world know of the state change - // - message.str(""); message << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); -#ifdef LOGLEVEL_DEBUG - logwrite( function, "[DEBUG] target found, starting threads" ); -#endif } else // targetstate not TARGET_FOUND if ( targetstate == TargetInfo::TARGET_NOT_FOUND ) { // no target found is an automatic stop - logwrite( function, "NOTICE: no targets found. stopping" ); + this->broadcast.notice( function, "no targets found. stopping" ); break; } else @@ -1425,21 +1414,10 @@ namespace Sequencer { // Update this target's state in the database // - if (error==NO_ERROR) error = this->target.update_state( Sequencer::TARGET_COMPLETE ); - else - if (error==ABORT) error = this->target.update_state( Sequencer::TARGET_UNASSIGNED ); - + error = this->target.update_state( Sequencer::TARGET_COMPLETE ); // update the active target table if (error==NO_ERROR) error = this->target.insert_completed(); // insert into the completed table if (error!=NO_ERROR) this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error - // let the world know of the state change - // - std::ostringstream oss; - oss << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue_and_log(function, oss.str()); - // abort sequence on error // if ( this->thread_error_manager.are_any_set() ) break; @@ -1513,14 +1491,14 @@ namespace Sequencer { std::string cmd = CAMERAD_ACTIVATE + activechans.str(); /*** if ( camerad_cmd.send( { CAMERAD_ACTIVATE, { activechans.str() } } ) != NO_ERROR ) { WIP ***/ if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->broadcast.error( function, "sending \""+cmd+"\": "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } if (!deactivechans.str().empty()) { std::string cmd = CAMERAD_DEACTIVATE + deactivechans.str(); if (this->camerad.send(cmd, reply)!=NO_ERROR) { - this->broadcast.error( function, "sending \""+cmd+"\": "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\": "+reply ); throw std::runtime_error("camera returned "+reply); } } @@ -1533,7 +1511,7 @@ namespace Sequencer { long exptime_msec = (long)( this->target.exptime_req * 1000 ); camcmd.str(""); camcmd << CAMERAD_EXPTIME << " " << exptime_msec; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); + logwrite( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1541,12 +1519,12 @@ namespace Sequencer { // camcmd.str(""); camcmd << CAMERAD_BIN << " spat " << this->target.binspat; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); + logwrite( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } camcmd.str(""); camcmd << CAMERAD_BIN << " spec " << this->target.binspect; if (error==NO_ERROR && (error=this->camerad.send( camcmd.str(), reply ))!=NO_ERROR) { - this->broadcast.error( function, "sending \""+camcmd.str()+"\": "+reply ); + logwrite( function, "ERROR sending \""+camcmd.str()+"\": "+reply ); throw std::runtime_error( "camera returned "+reply ); } @@ -1602,12 +1580,10 @@ namespace Sequencer { break; } - this->async.enqueue( "NOTICE: moving slit to "+modestr+" position" ); - - logwrite( function, "moving slit to "+slitcmd.str()+" for "+modestr+"position" ); + this->broadcast.notice(function, "moving slit to "+modestr+" position"); if ( this->slitd.command_timeout( slitcmd.str(), reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "setting slit" ); + logwrite( function, "ERROR setting slit" ); this->thread_error_manager.set( THR_SLIT_SET ); throw std::runtime_error("slit returned: "+reply); } @@ -1633,7 +1609,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_POWER ); // powerd not ready if ( this->reopen_hardware(this->powerd, POWERD_REOPEN, 10000 ) != NO_ERROR ) { - this->broadcast.error( function, "initializing power control" ); + logwrite( function, "ERROR initializing power control" ); throw std::runtime_error("could not initialize power control"); } @@ -1685,13 +1661,13 @@ namespace Sequencer { this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success if ( this->set_power_switch(ON, POWER_SLIT, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering slit hardware" ); + logwrite( function, "ERROR powering slit hardware" ); throw std::runtime_error("could not power slit hardware"); } bool was_opened=false; if ( this->open_hardware(this->slitd, was_opened) != NO_ERROR ) { - this->broadcast.error( function, "connecting to slit" ); + logwrite( function, "ERROR connecting to slit" ); throw std::runtime_error("could not open connection to slit hardware"); } @@ -1700,7 +1676,7 @@ namespace Sequencer { bool ishomed=false; std::string reply; if ( this->slitd.command( SLITD_ISHOME, reply ) ) { - this->broadcast.error( function, "communicating with slit hardware" ); + logwrite( function, "ERROR communicating with slit hardware" ); throw std::runtime_error("could not communicate with slit hardware: "+reply); } this->parse_state( function, reply, ishomed ); @@ -1710,7 +1686,7 @@ namespace Sequencer { if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->slitd.command_timeout( SLITD_HOME, reply, SLITD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "communicating with slit hardware" ); + logwrite( function, "ERROR communicating with slit hardware" ); throw std::runtime_error("could not home slit hardware: "+reply); } } @@ -1720,7 +1696,7 @@ namespace Sequencer { if ( was_opened && !this->config_init["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_init["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to slit" ); + logwrite( function, "ERROR sending \""+cmd+"\" to slit" ); throw std::runtime_error("slit "+cmd+" returned: "+reply); } } @@ -1774,7 +1750,7 @@ namespace Sequencer { if (error==NO_ERROR && !this->config_shutdown["SLIT"].empty() ) { std::string cmd = SLITD_SET+" "+this->config_shutdown["SLIT"]; if ( this->slitd.command_timeout( cmd, reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to slit" ); + logwrite( function, "ERROR sending \""+cmd+"\" to slit" ); throw std::runtime_error(cmd+" returned: "+reply); } } @@ -1784,7 +1760,7 @@ namespace Sequencer { logwrite( function, "closing slit hardware" ); error = this->slitd.command( SLITD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->broadcast.error( function, "closing connection to slit hardware" ); + logwrite( function, "ERROR closing connection to slit hardware" ); throw std::runtime_error("closing slit connection returned: "+reply); } @@ -1821,14 +1797,14 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_SLICECAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->broadcast.error( function, "initializing slicecam control" ); + logwrite( function, "ERROR initializing slicecam control" ); throw std::runtime_error("could not power slicecam hardware"); } // open connection is all that is needed, slicecamd takes care of everything // if ( this->open_hardware(this->slicecamd, SLICECAMD_OPEN, SLICECAMD_OPEN_TIMEOUT) != NO_ERROR ) { - this->broadcast.error( function, "starting slicecam" ); + logwrite( function, "ERROR starting slicecam" ); throw SlicecamException("could not start slicecam"); } @@ -1861,7 +1837,7 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_ACAM, std::chrono::seconds(10)) != NO_ERROR ) { - this->broadcast.error( function, "powering acam hardware" ); + logwrite( function, "ERROR powering acam hardware" ); throw std::runtime_error("could not power acam hardware"); } @@ -1869,7 +1845,7 @@ namespace Sequencer { // bool was_opened=false; if ( this->open_hardware(this->acamd, ACAMD_OPEN, ACAMD_OPEN_TIMEOUT, was_opened) != NO_ERROR ) { - this->broadcast.error( function, "opening acam camera" ); + logwrite( function, "ERROR opening acam camera" ); throw AcamException(ErrorCode::ERROR_ACAM_CAMERA, "could not open acam camera"); } @@ -1880,14 +1856,14 @@ namespace Sequencer { if ( ! this->config_init["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_init["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_init["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_init["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -1936,14 +1912,14 @@ namespace Sequencer { } if ( (error=this->connect_to_daemon(this->slicecamd)) != NO_ERROR ) { - this->broadcast.error( function, "connecting to slicecamd" ); + logwrite( function, "ERROR connecting to slicecamd" ); } // close connections between slicecamd and the hardware with which it communicates // logwrite( function, "closing slicecam hardware" ); if ( (error=this->slicecamd.command_timeout( SLICECAMD_SHUTDOWN, reply, SLICECAMD_SHUTDOWN_TIMEOUT )) != NO_ERROR ) { - this->broadcast.error( function, "closing connection to slicecam hardware" ); + logwrite( function, "ERROR closing connection to slicecam hardware" ); } // disconnect me from slicecamd, irrespective of any previous error @@ -1954,7 +1930,7 @@ namespace Sequencer { // Turn off power to slicecam hardware. // if ( this->set_power_switch(OFF, POWER_SLICECAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast.error( function, "switching off slicecam" ); + logwrite( function, "ERROR switching off slicecam" ); throw std::runtime_error("could not power off slicecam hardware"); } @@ -1994,14 +1970,14 @@ namespace Sequencer { if ( ! this->config_shutdown["ACAM_FILTER"].empty() ) { cmd = ACAMD_FILTER+" "+this->config_shutdown["ACAM_FILTER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } if ( ! this->config_shutdown["ACAM_COVER"].empty() ) { cmd = ACAMD_COVER+" "+this->config_shutdown["ACAM_COVER"]; if ( this->acamd.command_timeout( cmd, reply, ACAMD_MOVE_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to acamd: "+reply ); + logwrite( function, "ERROR sending \""+cmd+"\" to acamd: "+reply ); throw std::runtime_error("acam "+cmd+" returned: "+reply); } } @@ -2014,7 +1990,7 @@ namespace Sequencer { if ( error==NO_ERROR ) { logwrite( function, "closing acam hardware" ); error = this->acamd.command_timeout( ACAMD_SHUTDOWN, ACAMD_SHUTDOWN_TIMEOUT ); - if ( error != NO_ERROR ) this->broadcast.error( function, "shutting down acam" ); + if ( error != NO_ERROR ) logwrite( function, "ERROR shutting down acam" ); } // disconnect me from acamd, irrespective of any previous error @@ -2025,7 +2001,7 @@ namespace Sequencer { // Turn off power to acam hardware. // if ( this->set_power_switch(OFF, POWER_ACAM, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast.error( function, "switching off acam" ); + logwrite( function, "ERROR switching off acam" ); throw std::runtime_error("could not switch off acam"); } @@ -2055,14 +2031,14 @@ namespace Sequencer { // make sure calib hardware is powered if ( this->set_power_switch(ON, POWER_CALIB, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering focus control" ); + logwrite( function, "ERROR powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to calibd bool was_opened=false; if ( this->open_hardware(this->calibd, was_opened) != NO_ERROR ) { - this->broadcast.error( function, "initializing calib control" ); + logwrite( function, "ERROR initializing calib control" ); throw std::runtime_error("could not power calib control"); } @@ -2074,14 +2050,14 @@ namespace Sequencer { std::string reply; long error = this->calibd.command( CALIBD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->broadcast.error( function, "communicating with calib hardware" ); + logwrite( function, "ERROR communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } // home calib actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->calibd.command_timeout( CALIBD_HOME, reply, CALIBD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "communicating with calib hardware" ); + logwrite( function, "ERROR communicating with calib hardware" ); throw std::runtime_error("could not communicate with calib hardware: "+reply); } } @@ -2094,7 +2070,7 @@ namespace Sequencer { if ( !this->config_init["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_init["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "moving calib door and/or cover" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("could not move calib door and/or cover"); } } @@ -2131,7 +2107,7 @@ namespace Sequencer { // bool poweron=false; if ( check_power_switch(ON, POWER_CALIB, poweron ) != NO_ERROR ) { - this->broadcast.error( function, "checking calib power switch" ); + logwrite( function, "ERROR checking calib power switch" ); throw std::runtime_error("checking calib power switch"); } @@ -2149,7 +2125,7 @@ namespace Sequencer { if ( !this->config_shutdown["CALIB_DOOR"].empty() ) cmd << " door=" << this->config_shutdown["CALIB_DOOR"]; logwrite( function, "calib default: "+cmd.str() ); if ( this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "moving calib door and/or cover" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } } @@ -2162,7 +2138,7 @@ namespace Sequencer { std::string reply; logwrite( function, "closing calib hardware" ); error = this->calibd.send( CALIBD_CLOSE, reply ); - if ( error != NO_ERROR ) this->async.enqueue_and_log( function, "ERROR closing connection to calib hardware" ); + if ( error != NO_ERROR ) logwrite( function, "ERROR closing connection to calib hardware" ); } // disconnect me from calibd, irrespective of any previous error @@ -2173,14 +2149,14 @@ namespace Sequencer { // Turn off power to calib hardware. // if ( this->set_power_switch(OFF, POWER_CALIB, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast.error( function, "switching off calib hardware" ); + logwrite( function, "ERROR switching off calib hardware" ); error=ERROR; } // always turn off power to lamps // if ( this->set_power_switch(OFF, POWER_LAMP, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering off lamps" ); + logwrite( function, "ERROR powering off lamps" ); error=ERROR; } @@ -2214,7 +2190,7 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_TCS ); // tcsd not ready if ( this->open_hardware(this->tcsd) != NO_ERROR ) { - this->broadcast.error( "Sequencer::Sequence::tcs_init", "initializing TCS" ); + logwrite( "Sequencer::Sequence::tcs_init", "ERROR initializing TCS" ); this->thread_error_manager.set( THR_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -2258,7 +2234,7 @@ namespace Sequencer { std::string reply; error = this->tcsd.send( TCSD_CLOSE, reply ); if ( error != NO_ERROR ) { - this->broadcast.error( function, "closing connection to TCS" ); + logwrite( function, "ERROR closing connection to TCS" ); throw std::runtime_error("closing TCS connection: "+reply); } } @@ -2292,13 +2268,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_FLEXURE, std::chrono::seconds(21)) != NO_ERROR ) { - this->broadcast.error( function, "powering flexure control" ); + logwrite( function, "ERROR powering flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not power flexure control"); } if ( this->open_hardware(this->flexured) != NO_ERROR ) { - this->broadcast.error( function, "initializing flexure control" ); + logwrite( function, "ERROR initializing flexure control" ); this->thread_error_manager.set( THR_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -2345,7 +2321,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->flexured) != NO_ERROR ) { - this->broadcast.error( function, "connecting to flexure hardware" ); + logwrite( function, "ERROR connecting to flexure hardware" ); error=ERROR; } @@ -2354,7 +2330,7 @@ namespace Sequencer { // logwrite( function, "closing flexure hardware" ); if (error==NO_ERROR && (error=this->flexured.command( FLEXURED_CLOSE, reply )) != NO_ERROR) { - this->broadcast.error( function, "closing connection to flexure hardware" ); + logwrite( function, "ERROR closing connection to flexure hardware" ); } // disconnect me from flexured, irrespective of any previous error @@ -2365,7 +2341,7 @@ namespace Sequencer { // Turn off power to flexure hardware. // if ( this->set_power_switch(OFF, POWER_FLEXURE, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast.error( function, "switching off flexure" ); + logwrite( function, "ERROR switching off flexure" ); throw std::runtime_error("switching off flexure hardware"); } @@ -2392,14 +2368,14 @@ namespace Sequencer { this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success if ( this->set_power_switch(ON, POWER_FOCUS, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering focus control" ); + logwrite( function, "ERROR powering focus control" ); throw std::runtime_error("could not power focus control"); } // connect to focusd bool was_opened=false; if ( this->open_hardware(this->focusd, was_opened) != NO_ERROR ) { - this->broadcast.error( function, "initializing focus control" ); + logwrite( function, "ERROR initializing focus control" ); throw std::runtime_error("could not open focus hardware"); } @@ -2411,14 +2387,14 @@ namespace Sequencer { std::string reply; long error = this->focusd.command( FOCUSD_ISHOME, reply ); if ( error!=NO_ERROR || this->parse_state( function, reply, ishomed ) != NO_ERROR ) { - this->broadcast.error( function, "communicating with focus hardware" ); + logwrite( function, "ERROR communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_ISHOME+" returned: "+reply); } // home focus actuators if not already homed if ( !ishomed ) { logwrite( function, "sending home command" ); if ( this->focusd.command_timeout( FOCUSD_HOME, reply, FOCUSD_HOME_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "communicating with focus hardware" ); + logwrite( function, "ERROR communicating with focus hardware" ); throw std::runtime_error("focus "+FOCUSD_HOME+" returned: "+reply); } } @@ -2428,7 +2404,7 @@ namespace Sequencer { for ( const auto &chan : chans ) { std::string command = "set " + chan + " nominal"; if ( this->focusd.command_timeout( command, reply, FOCUSD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "setting focus "+chan ); + logwrite( function, "ERROR setting focus "+chan ); throw std::runtime_error("focus "+command+" returned: "+reply); } } @@ -2474,7 +2450,7 @@ namespace Sequencer { } if ( this->connect_to_daemon(this->focusd) != NO_ERROR ) { - this->broadcast.error( function, "connecting to focus hardware" ); + logwrite( function, "ERROR connecting to focus hardware" ); error=ERROR; } @@ -2483,7 +2459,7 @@ namespace Sequencer { // logwrite( function, "closing focus hardware" ); if (error==NO_ERROR && (error=this->focusd.command( FOCUSD_CLOSE, reply )) != NO_ERROR) { - this->broadcast.error( function, "closing connection to focus hardware" ); + logwrite( function, "ERROR closing connection to focus hardware" ); } // disconnect me from focusd, irrespective of any previous error @@ -2494,7 +2470,7 @@ namespace Sequencer { // Turn off power to focus hardware. // if ( this->set_power_switch(OFF, POWER_FOCUS, std::chrono::seconds(0)) != NO_ERROR ) { - this->broadcast.error( function, "switching off focus" ); + logwrite( function, "ERROR switching off focus" ); throw std::runtime_error("switching off focus hardware"); } @@ -2522,13 +2498,13 @@ namespace Sequencer { // make sure hardware is powered on // if ( this->set_power_switch(ON, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering camera" ); + logwrite( function, "ERROR powering camera" ); throw std::runtime_error("switching on camera"); } bool was_opened=false; if ( this->open_hardware(this->camerad, "open", 12000, was_opened) != NO_ERROR ) { - this->broadcast.error( function, "initializing camera" ); + logwrite( function, "ERROR initializing camera" ); throw std::runtime_error("initializing camera"); } @@ -2538,7 +2514,7 @@ namespace Sequencer { if ( was_opened) { for ( const auto &cmd : this->camera_prologue ) { if ( this->camerad.command_timeout( cmd, reply, CAMERA_PROLOG_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "sending \""+cmd+"\" to camera" ); + logwrite( function, "ERROR sending \""+cmd+"\" to camera" ); throw std::runtime_error("sending \""+cmd+"\" to camera"); } } @@ -2609,7 +2585,7 @@ namespace Sequencer { // turn off power to camera hardware // if ( this->set_power_switch(OFF, POWER_CAMERA, std::chrono::seconds(5)) != NO_ERROR ) { - this->broadcast.error( function, "powering off camera" ); + logwrite( function, "ERROR powering off camera" ); throw std::runtime_error("switching off camera"); } @@ -2709,7 +2685,7 @@ namespace Sequencer { if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } message << " to decimal"; - this->broadcast.error( function, ""+message.str() ); + logwrite( function, "ERROR "+message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2727,9 +2703,9 @@ namespace Sequencer { double _solved_angle = ( angle_out < 0 ? angle_out + 360.0 : angle_out ); if ( std::abs(_solved_angle) - std::abs(this->target.casangle) > 0.01 ) { - message.str(""); message << "NOTICE: Calculated angle " << angle_out + message.str(""); message << "Calculated angle " << angle_out << " is not equivalent to casangle " << this->target.casangle; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.notice( function, message.str() ); } // Send coordinates using TCS-native COORDS command. @@ -2759,8 +2735,8 @@ namespace Sequencer { error = this->tcsd.send( coords_cmd.str(), coords_reply ); // send to the TCS // second failure return error if ( error != NO_ERROR || coords_reply.compare( 0, strlen(TCS_SUCCESS_STR), TCS_SUCCESS_STR ) != 0 ) { - message.str(""); message << "ERROR sending COORDS command. TCS reply: " << coords_reply; - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "sending COORDS command. TCS reply: " << coords_reply; + logwrite( function, "ERROR "+message.str() ); this->thread_error_manager.set( THR_MOVE_TO_TARGET ); throw std::runtime_error("sending COORDS to TCS: "+coords_reply); } @@ -2773,7 +2749,7 @@ namespace Sequencer { std::stringstream ringgo_cmd; std::string noreply("DONTWAIT"); // indicates don't wait for reply ringgo_cmd << TCSD_RINGGO << " " << angle_out; // this is calculated cass angle - this->async.enqueue_and_log( function, "sending "+ringgo_cmd.str()+" to TCS" ); + this->broadcast.notice( function, "sending "+ringgo_cmd.str()+" to TCS" ); error = this->tcsd.send( ringgo_cmd.str(), noreply ); } @@ -2958,7 +2934,7 @@ namespace Sequencer { logwrite( function, "calib: "+cmd.str() ); if ( !is_cancelled() && this->calibd.command_timeout( cmd.str(), CALIBD_SET_TIMEOUT ) != NO_ERROR ) { - this->broadcast.error( function, "moving calib door and/or cover" ); + logwrite( function, "ERROR moving calib door and/or cover" ); throw std::runtime_error("moving calib door and/or cover"); } @@ -2970,7 +2946,7 @@ namespace Sequencer { logwrite( function, "power "+setpower); std::string reply; if ( this->powerd.send( setpower, reply ) != NO_ERROR ) { - this->broadcast.error( function, ""+message.str() ); + logwrite( function, "ERROR "+message.str() ); throw std::runtime_error("setting lamp "+message.str()); } } @@ -2983,7 +2959,7 @@ namespace Sequencer { // if ( is_cancelled() ) break; // cmd.str(""); cmd << TCSD_NATIVE << " NPS " << lamp << " " << (state?1:0); // if ( this->tcsd.command( cmd.str() ) != NO_ERROR ) { -// this->async.enqueue_and_log( function, "ERROR "+cmd.str() ); +// logwrite( function, "ERROR "+cmd.str() ); // throw std::runtime_error("setting dome lamp: "+cmd.str()); // } // } @@ -2994,7 +2970,7 @@ namespace Sequencer { if ( is_cancelled() ) break; cmd.str(""); cmd << CALIBD_LAMPMOD << " " << mod << " " << (state?1:0) << " 1000"; if ( this->calibd.command( cmd.str() ) != NO_ERROR ) { - this->broadcast.error( function, ""+cmd.str() ); + logwrite( function, "ERROR "+cmd.str() ); throw std::runtime_error("setting lamp modulator "+cmd.str()); } } @@ -3245,7 +3221,7 @@ namespace Sequencer { // if ( this->camerad.async( message.str() ) != NO_ERROR ) { // if ( this->camerad.send( message.str(), reply ) != NO_ERROR ) { if ( this->camerad.command_timeout( message.str(), reply, 30000 ) != NO_ERROR ) { - this->broadcast.error( function, "sending camera "+message.str() ); + logwrite( function, "ERROR sending camera "+message.str() ); this->thread_error_manager.set( THR_TRIGGER_EXPOSURE ); // tell the world this thread had an error this->target.update_state( Sequencer::TARGET_PENDING ); // return the target state to pending this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); // clear EXPOSE bit @@ -3329,15 +3305,10 @@ namespace Sequencer { if ( error==NO_ERROR ) { this->target.exptime_req = updated_exptime; - message.str(""); message << "NOTICE: updated exptime to " << updated_exptime << " sec"; - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "updated exptime to " << updated_exptime << " sec"; + this->broadcast.notice( function, message.str() ); } - // announce the success or failure in an asynchronous broadcast message - // - message.str(""); message << "MODIFY_EXPTIME: " << this->target.exptime_req << ( error==NO_ERROR ? " DONE" : " ERROR" ); - this->async.enqueue( message.str() ); - return; } /***** Sequencer::Sequence::modify_exptime **********************************/ @@ -3356,9 +3327,9 @@ namespace Sequencer { if ( ! seq_state_manager.are_any_set( Sequencer::SEQ_READY, Sequencer::SEQ_NOTREADY, Sequencer::SEQ_FAILED ) ) { - message << "ERROR cannot perform system startup while " + message << "cannot perform system startup while " << seq_state_manager.get_set_states(); - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.error( function, message.str() ); return ERROR; } @@ -3479,16 +3450,13 @@ namespace Sequencer { { THR_TCS_SHUTDOWN, [this]{ return tcs_shutdown(); }, { } } }, function ); - std::ostringstream message; if (error==NO_ERROR) { - message << "NOTICE: instrument is shut down"; + this->broadcast.notice(function, "instrument is shut down"); } else { - message << "ERROR occurred during shutdown and may not have completed"; + this->broadcast.error(function, "shut down may not be complete"); } - this->async.enqueue_and_log( function, message.str() ); - // Always end in NOTREADY regardless of worker errors. SEQ_FAILED is // reserved for startup failures and externally-aborted lifecycle transitions. // Worker errors during shutdown are logged above but do not prevent @@ -3751,10 +3719,7 @@ namespace Sequencer { retstring = ( this->do_once.load() ? "ONE" : "ALL" ); - // send an async message with the current type - // - message.str(""); message << "DOTYPE: " << retstring; - this->async.enqueue( message.str() ); + // @TODO publish dotype return( error ); } @@ -3802,8 +3767,7 @@ namespace Sequencer { retstring = ( this->engineering_mode.load() ? "true" : "false" ); - message.str(""); message << "ENGINEERING: " << retstring; - this->async.enqueue( message.str() ); + logwrite(function, retstring); return( error ); } @@ -4318,7 +4282,7 @@ namespace Sequencer { } // connection failed too many times if (attempt > maxattempts) { - this->broadcast.error( function, "exceeded max attempts connecting to " + daemon.name ); + logwrite( function, "ERROR exceeded max attempts connecting to " + daemon.name ); return ERROR; } @@ -4327,7 +4291,7 @@ namespace Sequencer { error |= daemon.send( "isopen", reply ); error |= this->parse_state( function, reply, isopen ); if ( error != NO_ERROR ) { - this->broadcast.error( function, "opening "+daemon.name+" hardware" ); + logwrite( function, "ERROR opening "+daemon.name+" hardware" ); return ERROR; } @@ -4337,7 +4301,7 @@ namespace Sequencer { logwrite( function, "opening "+daemon.name+" hardware connections with " +std::to_string(opentimeout)+" ms timeout" ); if ( daemon.command_timeout( opencmd, reply, opentimeout ) != NO_ERROR ) { - this->broadcast.error( function, "opening connection to "+daemon.name+" hardware" ); + logwrite( function, "ERROR opening connection to "+daemon.name+" hardware" ); return ERROR; } was_opened=true; @@ -4366,7 +4330,7 @@ namespace Sequencer { if ( !daemon.socket.isconnected() ) { logwrite( function, "connecting to "+daemon.name+" daemon" ); if ( daemon.connect() != NO_ERROR ) { - this->broadcast.error( function, "connecting to "+daemon.name ); + logwrite( function, "ERROR connecting to "+daemon.name ); return ERROR; } } @@ -4499,34 +4463,6 @@ namespace Sequencer { } else - // ---------------------------------------------------- - // async -- queue an asynchronous message - // ---------------------------------------------------- - // - if ( testname == "async" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test async [ ]\n"; - retstring.append( " Queue and broadcast optional . If not supplied\n" ); - retstring.append( " then broadcast \"test\".\n" ); - return HELP; - } - if ( tokens.size() > 1 ) { - bool first=true; - message.str(""); - for ( const auto &word : tokens ) { - if ( first ) { first=false; continue; } // skip the testname - message << word << " "; - } - logwrite( function, message.str() ); - this->async.enqueue( message.str() ); - } - else { - logwrite( function, "test" ); - this->async.enqueue( "test" ); - } - } - else - // ---------------------------------------------------- // prologue -- show the camera prologue commands // ---------------------------------------------------- @@ -4560,16 +4496,16 @@ namespace Sequencer { // write to the log (textually) which bits are set // retstring.clear(); - message.str(""); message << "NOTICE: daemons ready: " << this->daemon_manager.get_set_states(); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "daemons ready: " << this->daemon_manager.get_set_states(); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); - message.str(""); message << "NOTICE: daemons not ready: " << this->daemon_manager.get_cleared_states(); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "daemons not ready: " << this->daemon_manager.get_cleared_states(); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); retstring.append( "\n" ); - message.str(""); message << "NOTICE: camera ready to expose: " << (this->can_expose.load() ? "yes" : "no"); - this->async.enqueue_and_log( function, message.str() ); + message.str(""); message << "camera ready to expose: " << (this->can_expose.load() ? "yes" : "no"); + this->broadcast.notice( function, message.str() ); retstring.append( message.str() ); error = NO_ERROR; @@ -4649,7 +4585,7 @@ namespace Sequencer { } message.str(""); message << "STATES: " << this->seq_state_manager.get_set_states(); - this->async.enqueue( message.str() ); + this->broadcast.notice( function, message.str() ); logwrite( function, message.str() ); message.str(""); message << "THREADS: " << this->thread_state_manager.get_set_states(); @@ -4756,8 +4692,7 @@ namespace Sequencer { } error = NO_ERROR; - message.str(""); message << "NOTICE: " << targetstatus; - this->async.enqueue( message.str() ); // broadcast target status + this->broadcast.notice(function, targetstatus); if ( ret == TargetInfo::TargetState::TARGET_FOUND ) { rts << "name obsid order ra dec casangle slitangle airmasslim\n"; @@ -4915,10 +4850,10 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + message.str(""); message << this->target.state + << " TARGET:" << this->target.name + << " (" << this->target.obsid << ")"; + this->broadcast.notice( function, message.str() ); } else @@ -4946,10 +4881,10 @@ namespace Sequencer { // let the world know of the state change // - message.str(""); message << "TARGETSTATE:" << this->target.state - << " TARGET:" << this->target.name - << " OBSID:" << this->target.obsid; - this->async.enqueue( message.str() ); + message.str(""); message << this->target.state + << " TARGET:" << this->target.name + << " (" << this->target.obsid << ")"; + this->broadcast.notice( function, message.str() ); } } else @@ -5056,9 +4991,9 @@ namespace Sequencer { else this->test_solver_args.clear(); // clear previous solver args if not specified if ( !this->test_solver_args.empty() ) { - message.str(""); message << "NOTICE: test solver args: " << this->test_solver_args; + message.str(""); message << "test solver args: " << this->test_solver_args; } - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.notice( function, message.str() ); // clear stop flags // @@ -5112,12 +5047,12 @@ namespace Sequencer { bool cas_isnan = std::isnan( angle_in ); if ( ra_isnan || dec_isnan || cas_isnan ) { - message.str(""); message << "ERROR: converting"; + message.str(""); message << "converting"; if ( ra_isnan ) { message << " RA=\"" << this->target.ra_hms << "\""; } if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } if ( cas_isnan ) { message << " CASS=\"" << cass_now << "\""; } message << " to decimal"; - this->async.enqueue_and_log( function, message.str() ); + this->broadcast.error( function, message.str() ); return ERROR; } diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 01eb2f42..280cdfd1 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -659,8 +659,6 @@ namespace Sequencer { std::string daemon_control; ///< daemon control script - Common::Queue async; ///< asynchronous message queue - // Here are all the daemon client objects that the Sequencer connects to. // Common::DaemonClient acamd { "acamd" }; diff --git a/sequencerd/sequencer_server.cpp b/sequencerd/sequencer_server.cpp index ca6e32ee..f56da104 100644 --- a/sequencerd/sequencer_server.cpp +++ b/sequencerd/sequencer_server.cpp @@ -27,13 +27,10 @@ namespace Sequencer { case SIGTERM: case SIGINT: logwrite(function, "received termination signal"); - message << "NOTICE:" << Sequencer::DAEMON_NAME << " exit"; - Server::instance->sequence.async.enqueue( message.str() ); Server::instance->exit_cleanly(); // shutdown the daemon break; case SIGHUP: // TODO reconfigure? - Server::instance->sequence.async.enqueue_and_log( function, - "ERROR: caught unhandled HUP signal" ); + logwrite( function, "ERROR: caught unhandled HUP signal" ); break; case SIGPIPE: logwrite(function, "ignored SIGPIPE"); @@ -41,8 +38,6 @@ namespace Sequencer { default: message << "received unknown signal " << strsignal(signo); logwrite( function, message.str() ); - message.str(""); message << "NOTICE:" << Sequencer::DAEMON_NAME << " exit"; - Server::instance->sequence.async.enqueue( message.str() ); break; } return; @@ -58,6 +53,7 @@ namespace Sequencer { void Server::exit_cleanly(void) { std::string function = "Sequencer::Server::exit_cleanly"; logwrite( function, "exiting" ); + Server::instance->sequence.broadcast.notice(function, "sequencerd terminated"); exit(EXIT_SUCCESS); } @@ -90,7 +86,7 @@ namespace Sequencer { catch (const std::exception &e) { // should be impossible message.str(""); message << "ERROR parsing entry " << entry << " of " << this->config.n_entries << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } @@ -106,11 +102,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing NBPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -121,11 +117,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing BLKPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -136,11 +132,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ASYNCPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -151,11 +147,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing MESSAGEPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -163,7 +159,7 @@ namespace Sequencer { if (config.param[entry] == "MESSAGEGROUP") { this->messagegroup = config.arg[entry]; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -172,7 +168,7 @@ namespace Sequencer { if ( config.param[entry] == "PUB_ENDPOINT" ) { this->sequence.publisher_address = config.arg[entry]; this->sequence.publisher_topic = DAEMON_NAME; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } @@ -180,14 +176,14 @@ namespace Sequencer { // if ( config.param[entry] == "SUB_ENDPOINT" ) { this->sequence.subscriber_address = config.arg[entry]; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } // DAEMON_CONTROL_SCRIPT if (config.param[entry] == "DAEMON_CONTROL_SCRIPT") { this->sequence.daemon_control = config.arg[entry]; - this->sequence.async.enqueue_and_log(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); + logwrite(function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry]); applied++; } @@ -198,11 +194,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ACAMD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -213,11 +209,11 @@ namespace Sequencer { } catch (const std::invalid_argument &e) { message.str(""); message << "ERROR parsing CAMERAD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -228,11 +224,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing CAMERAD_NBPORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -243,11 +239,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FLEXURED_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -258,11 +254,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing POWERD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -273,11 +269,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing SLICECAMD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -288,11 +284,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing SLITD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -303,11 +299,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing TCSD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -318,11 +314,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing CALIBD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -333,11 +329,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FILTERD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -348,11 +344,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing FOCUSD_PORT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -363,11 +359,11 @@ namespace Sequencer { } catch (const std::exception &e) { message.str(""); message << "ERROR parsing ACQUIRE_TIMEOUT: " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return ERROR; } message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -379,27 +375,27 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ACQUIRE_RETRYS: unable to convert " << config.arg[entry] << " to integer. retry limit disabled."; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); this->sequence.acquisition_max_retrys = -1; } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ACQUIRE_RETRYS number out of integer range. retry limit disabled." ); + logwrite( function, "ACQUIRE_RETRYS number out of integer range. retry limit disabled." ); this->sequence.acquisition_max_retrys = -1; } this->sequence.acquisition_max_retrys = rt; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } // TCS_WHICH -- which TCS to connect to, defults to real if not specified if ( config.param[entry] == "TCS_WHICH" ) { if ( config.arg[entry] != "sim" && config.arg[entry] != "real" ) { - this->sequence.async.enqueue_and_log( function, "ERROR TCS_WHICH expected { sim real }" ); + logwrite( function, "ERROR TCS_WHICH expected { sim real }" ); return ERROR; } this->sequence.tcs_which = config.arg[entry]; - this->sequence.async.enqueue_and_log( function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry] ); + logwrite( function, "SEQUENCERD:config:"+config.param[entry]+"="+config.arg[entry] ); applied++; } @@ -410,22 +406,22 @@ namespace Sequencer { mrate = std::stod( config.arg[entry] ); if ( mrate < 0 || mrate > 60 ) { message.str(""); message << "ERROR: TCS_OFFSET_RATE_RA " << mrate << " out of range {0:60}"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return( ERROR ); } } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_OFFSET_RATE_RA: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_OFFSET_RATE_RA number out of double range" ); + logwrite( function, "ERROR: TCS_OFFSET_RATE_RA number out of double range" ); return(ERROR); } this->sequence.tcs_offsetrate_ra = mrate; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -436,22 +432,22 @@ namespace Sequencer { mrate = std::stod( config.arg[entry] ); if ( mrate < 0 || mrate > 60 ) { message.str(""); message << "ERROR: TCS_OFFSET_RATE_DEC " << mrate << " out of range {0:60}"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return( ERROR ); } } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_OFFSET_RATE_DEC: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_OFFSET_RATE_DEC number out of double range" ); + logwrite( function, "ERROR: TCS_OFFSET_RATE_DEC number out of double range" ); return(ERROR); } this->sequence.tcs_offsetrate_dec = mrate; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -463,16 +459,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_SETTLE_TIMEOUT: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_SETTLE_TIMEOUT number out of double range" ); + logwrite( function, "ERROR: TCS_SETTLE_TIMEOUT number out of double range" ); return(ERROR); } this->sequence.tcs_settle_timeout = to; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -484,16 +480,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_SETTLE_STABLE: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_SETTLE_STABLE number out of double range" ); + logwrite( function, "ERROR: TCS_SETTLE_STABLE number out of double range" ); return(ERROR); } this->sequence.tcs_settle_stable = stablet; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -505,16 +501,16 @@ namespace Sequencer { } catch (std::invalid_argument &) { message.str(""); message << "ERROR: bad TCS_DOMEAZI_READY: unable to convert " << config.arg[entry] << " to double"; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_DOMEAZI_READY number out of double range" ); + logwrite( function, "ERROR: TCS_DOMEAZI_READY number out of double range" ); return(ERROR); } this->sequence.tcs_domeazi_ready = domeazi; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -525,16 +521,16 @@ namespace Sequencer { to = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad TCS_PREAUTH_TIME: unable to convert to double" ); + logwrite( function, "ERROR: bad TCS_PREAUTH_TIME: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: TCS_PREAUTH_TIME number out of double range" ); + logwrite( function, "ERROR: TCS_PREAUTH_TIME number out of double range" ); return(ERROR); } this->sequence.tcs_preauth_time = to; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -545,16 +541,16 @@ namespace Sequencer { offset = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_OFFSET_THRESHOLD: unable to convert to double" ); + logwrite( function, "ERROR: bad ACQUIRE_OFFSET_THRESHOLD: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_OFFSET_THRESHOLD number out of double range" ); + logwrite( function, "ERROR: ACQUIRE_OFFSET_THRESHOLD number out of double range" ); return(ERROR); } this->sequence.target.offset_threshold = offset; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -565,16 +561,16 @@ namespace Sequencer { repeat = std::stoi( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_MIN_REPEAT: unable to convert to int" ); + logwrite( function, "ERROR: bad ACQUIRE_MIN_REPEAT: unable to convert to int" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_MIN_REPEAT number out of int range" ); + logwrite( function, "ERROR: ACQUIRE_MIN_REPEAT number out of int range" ); return(ERROR); } this->sequence.target.min_repeat = repeat; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -585,16 +581,16 @@ namespace Sequencer { offset = std::stod( config.arg[entry] ); } catch (std::invalid_argument &) { - this->sequence.async.enqueue_and_log( function, "ERROR: bad ACQUIRE_TCS_MAX_OFFSET: unable to convert to double" ); + logwrite( function, "ERROR: bad ACQUIRE_TCS_MAX_OFFSET: unable to convert to double" ); return(ERROR); } catch (std::out_of_range &) { - this->sequence.async.enqueue_and_log( function, "ERROR: ACQUIRE_TCS_MAX_OFFSET number out of double range" ); + logwrite( function, "ERROR: ACQUIRE_TCS_MAX_OFFSET number out of double range" ); return(ERROR); } this->sequence.target.max_tcs_offset = offset; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); applied++; } @@ -609,7 +605,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_HOST, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -618,7 +614,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_PORT, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -627,7 +623,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_USER, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -636,7 +632,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_PASS, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -645,7 +641,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_SCHEMA, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -654,7 +650,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_ACTIVE, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -663,7 +659,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_COMPLETED, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -672,7 +668,7 @@ namespace Sequencer { if ( this->sequence.target.configure_db( DB_SETS, config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -681,7 +677,7 @@ namespace Sequencer { this->sequence.camera_prologue.push_back( this->config.arg[entry] ); applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // CAMERA_EPILOGUE @@ -689,7 +685,7 @@ namespace Sequencer { this->sequence.camera_epilogue.push_back( this->config.arg[entry] ); applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // *__INIT @@ -701,7 +697,7 @@ namespace Sequencer { this->sequence.config_init[key] = this->config.arg[entry]; applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // *__SHUTDOWN @@ -713,7 +709,7 @@ namespace Sequencer { this->sequence.config_shutdown[key] = this->config.arg[entry]; applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } // VIRTUAL_SLITW_ACQUIRE @@ -755,7 +751,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_LAMP].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -764,7 +760,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_SLIT].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -773,7 +769,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_CAMERA].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -782,7 +778,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_CALIB].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -791,7 +787,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FLEXURE].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -800,7 +796,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FILTER].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -809,7 +805,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_FOCUS].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -818,7 +814,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_TELEM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -827,7 +823,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_THERMAL].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -836,7 +832,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_ACAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -845,7 +841,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_ACAM_CAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -854,7 +850,7 @@ namespace Sequencer { if ( this->sequence.power_switch[POWER_SLICECAM].configure( this->config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -863,7 +859,7 @@ namespace Sequencer { if ( this->sequence.caltarget.configure( config.arg[entry] ) == NO_ERROR ) { applied++; message.str(""); message << "SEQUENCERD:config:" << config.param[entry] << "=" << config.arg[entry]; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } } @@ -993,49 +989,6 @@ namespace Sequencer { /**** Server::gui_main ******************************************************/ - /***** Server::async_main ***************************************************/ - /** - * @brief asynchronous message sending thread - * @param[in] seq reference to Sequencer::Server object - * @param[in] sock Network::udpSocket socket object - * - * Loops forever, when a message arrives in the status message queue it is - * sent out via multi-cast UDP datagram. - * - */ - void Server::async_main( Sequencer::Server &seq, Network::UdpSocket sock ) { - std::string function = "Sequencer::Server::async_main"; - std::stringstream message; - int retval; - - retval = sock.Create(); // create the UDP socket - if (retval < 0) { - logwrite(function, "error creating UDP multicast socket for asynchronous messages"); - seq.exit_cleanly(); // do not continue on error - } - if (retval==1) { // exit this thread but continue with daemon - logwrite(function, "asyncrhonous message port disabled by request"); - } - - while (1) { - auto message = seq.sequence.async.dequeue(); // get the latest message from the queue (blocks) - retval = sock.Send(message); // transmit the message - if (retval < 0) { - std::stringstream errstm; - errstm << "error sending UDP message: " << message; - logwrite(function, errstm.str()); - } - if (message=="exit") { // terminate this thread - sock.Close(); - return; - } - } - - return; - } - /***** Server::async_main ***************************************************/ - - /***** Server::doit *********************************************************/ /** * @brief the workhorse of each thread connetion @@ -1071,11 +1024,11 @@ namespace Sequencer { if ( ( pollret=sock.Poll() ) <= 0 ) { if (pollret==0) { message.str(""); message << "ERROR: Poll timeout on fd " << sock.getfd() << " thread " << sock.id; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } if ( pollret <0 && errno ) { message.str(""); message << "ERROR: Poll error on fd " << sock.getfd() << " thread " << sock.id << ": " << strerror(errno); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } break; // this will close the connection } @@ -1091,11 +1044,11 @@ namespace Sequencer { #endif if ( ret<0 && errno ) { // could be an actual read error message.str(""); message << "ERROR: Read error on fd " << sock.getfd() << ": " << strerror(errno); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } if (ret==-2) { // or a timeout message.str(""); message << "ERROR: timeout reading from fd " << sock.getfd(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); } break; // Breaking out of the while loop will close the connection. // This probably means that the client has terminated abruptly, @@ -1134,12 +1087,12 @@ namespace Sequencer { catch ( std::runtime_error &e ) { std::stringstream errstream; errstream << e.what(); message.str(""); message << "ERROR: parsing arguments: " << errstream.str(); - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); ret = -1; } catch ( ... ) { message.str(""); message << "ERROR: unknown error parsing arguments: " << args; - this->sequence.async.enqueue_and_log( function, message.str() ); + logwrite( function, message.str() ); ret = -1; } @@ -1392,7 +1345,7 @@ namespace Sequencer { // if ( cmd == SEQUENCERD_REPEAT ) { if ( !this->sequence.seq_state_manager.is_set( Sequencer::SEQ_READY ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR cannot start exposure: not ready" ); + this->sequence.broadcast.error( function, "cannot start exposure: not ready" ); ret = ERROR; } else { @@ -1499,7 +1452,7 @@ namespace Sequencer { if ( cmd == SEQUENCERD_TARGETSET ) { ret= this->sequence.target.targetset( args, retstring ); message.str(""); message << "TARGETSET: " << retstring; - this->sequence.async.enqueue( message.str() ); + logwrite(function, message.str()); retstring.append( " " ); } else @@ -1510,14 +1463,14 @@ namespace Sequencer { // Can only pause during an exposure // if ( ! this->sequence.wait_state_manager.is_set( Sequencer::SEQ_WAIT_EXPOSE ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: can only pause during an active exposure" ); + this->sequence.broadcast.error( function, "can only pause during an active exposure" ); ret = ERROR; } else // Can't already be paused // if ( this->sequence.seq_state_manager.is_set( Sequencer::SEQ_PAUSED ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: already paused" ); + this->sequence.broadcast.error( function, "already paused" ); ret = ERROR; } else { @@ -1536,7 +1489,7 @@ namespace Sequencer { // Can only resume when paused // if ( ! this->sequence.seq_state_manager.is_set( Sequencer::SEQ_PAUSED ) ) { - this->sequence.async.enqueue_and_log( function, "ERROR: can only resume when paused" ); + this->sequence.broadcast.error( function, "can only resume when paused" ); ret = ERROR; } else { @@ -1563,7 +1516,7 @@ namespace Sequencer { if ( cmd.compare( SEQUENCERD_MODEXPTIME ) == 0 ) { Tokenize( args, tokens, " " ); if ( tokens.size() != 1 ) { - this->sequence.async.enqueue_and_log( function, "ERROR: expected MODEXPTIME " ); + logwrite( function, "ERROR expected MODEXPTIME " ); ret = ERROR; } else { @@ -1572,13 +1525,13 @@ namespace Sequencer { double exptime_req=0; try { exptime_req = std::stod( tokens.at(0) ); } catch( std::out_of_range &e ) { - message.str(""); message << "ERROR: out of range parsing args " << args << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR out of range parsing args " << args << ": " << e.what(); + logwrite( function, message.str() ); ret = ERROR; } catch( std::invalid_argument &e ) { - message.str(""); message << "ERROR: invalid argument parsing args " << args << ": " << e.what(); - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR invalid argument parsing args " << args << ": " << e.what(); + logwrite( function, message.str() ); ret = ERROR; } @@ -1618,8 +1571,8 @@ namespace Sequencer { // Unknown commands generate an error // else { - message.str(""); message << "ERROR: unknown command: " << cmd; - this->sequence.async.enqueue_and_log( function, message.str() ); + message.str(""); message << "ERROR unknown command: " << cmd; + logwrite( function, message.str() ); ret = ERROR; } diff --git a/sequencerd/sequencer_server.h b/sequencerd/sequencer_server.h index aa3440a8..f24bdc6f 100644 --- a/sequencerd/sequencer_server.h +++ b/sequencerd/sequencer_server.h @@ -145,7 +145,6 @@ namespace Sequencer { static void block_main( Sequencer::Server &server, std::shared_ptr ); ///< main function for blocking connection thread static void thread_main( Sequencer::Server &server, std::shared_ptr sock); ///< main function for all non-blocked threads static void gui_main( Sequencer::Server &server, std::shared_ptr sock ); ///< main function for gui threads - static void async_main( Sequencer::Server &server, Network::UdpSocket sock ); ///< asynchronous message sending thread void handle_signal( int signo ); diff --git a/sequencerd/sequencerd.cpp b/sequencerd/sequencerd.cpp index 7125b3a6..4e13530e 100644 --- a/sequencerd/sequencerd.cpp +++ b/sequencerd/sequencerd.cpp @@ -224,16 +224,10 @@ int main(int argc, char **argv) { std::ref(socklist[thrid]) ).detach(); // spawn a thread to handle requests on this socket *****/ - // Instantiate a multicast UDP object and spawn a thread to send asynchronous messages - // - Network::UdpSocket msg(sequencerd.messageport, sequencerd.messagegroup); - std::thread( std::ref(Sequencer::Server::async_main), - std::ref(sequencerd), - msg ).detach(); - // Create my own asynchronous listener thread. // This thread allows the sequencer to listen for asynchronous messages. // + Network::UdpSocket msg(sequencerd.messageport, sequencerd.messagegroup); std::thread( std::ref( Sequencer::Sequence::dothread_sequencer_async_listener ), std::ref( sequencerd.sequence), msg From 673b3d5c34e16291fc70c500ce847bf936119d16 Mon Sep 17 00:00:00 2001 From: David Hale Date: Thu, 14 May 2026 23:07:52 -0700 Subject: [PATCH 36/37] noisey commit but mostly vocabularly change-- * "threads" to "operations" * "xxxx_set" functions to "xxxx_setup" --- sequencerd/operation_builders.cpp | 82 +++-- sequencerd/sequence.cpp | 521 ++++++++++++---------------- sequencerd/sequence.h | 230 ++++++------ sequencerd/sequence_acquisition.cpp | 4 +- sequencerd/sequence_operations.cpp | 92 ++--- 5 files changed, 435 insertions(+), 494 deletions(-) diff --git a/sequencerd/operation_builders.cpp b/sequencerd/operation_builders.cpp index 52dc43ed..d387cd9f 100644 --- a/sequencerd/operation_builders.cpp +++ b/sequencerd/operation_builders.cpp @@ -180,107 +180,106 @@ namespace Sequencer { // ---------- INTERNAL ------------------------------------------------------ // sequencer-level orchestration (Ops factory methods) - // Command names come from the THR_ -> thread_names map in sequence.h. - // I probably should rename these from "thread" to something else (operation?) + // Command names come from the OP_* -> op_names map in sequence.h. - op_builders[thread_names.at(THR_ACAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_ACAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.acam_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_CALIB_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_CALIB_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.calib_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_CALIB_SET)] = [this](Operation &op, const ParsedCommand &cmd) { - op = ops.calib_set(); + op_builders[op_names.at(OP_CALIB_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.calib_setup(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_CAMERA_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_CAMERA_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.camera_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_CAMERA_SET)] = [this](Operation &op, const ParsedCommand &cmd) { - op = ops.camera_set(); + op_builders[op_names.at(OP_CAMERA_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.camera_setup(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.do_expose(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_FLEXURE_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_FLEXURE_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.flexure_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_FLEXURE_SET)] = [this](Operation &op, const ParsedCommand &cmd) { - op = ops.flexure_set(); + op_builders[op_names.at(OP_FLEXURE_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.flexure_setup(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_FOCUS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_FOCUS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.focus_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_FOCUS_SET)] = [this](Operation &op, const ParsedCommand &cmd) { - op = ops.focus_set(); + op_builders[op_names.at(OP_FOCUS_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.focus_setup(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_MOVE_TO_TARGET)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_MOVE_TO_TARGET)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.move_to_target(); op.params = cmd.params; // may carry optional ra= / dec= overrides }; - op_builders[thread_names.at(THR_POWER_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_POWER_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.power_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_REPEAT_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_REPEAT_EXPOSURE)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.repeat_exposure(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_SHUTDOWN)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_SHUTDOWN)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.do_shutdown(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_SLICECAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_SLICECAM_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.slicecam_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_SLIT_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_SLIT_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.slit_init(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_SLIT_SET)] = [this](Operation &op, const ParsedCommand &cmd) { - op = ops.slit_set(); + op_builders[op_names.at(OP_SLIT_SETUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op = ops.slit_setup(); op.params = cmd.params; // may carry optional mode= override }; - op_builders[thread_names.at(THR_STARTUP)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_STARTUP)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.do_startup(); op.params = cmd.params; }; - op_builders[thread_names.at(THR_TCS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { + op_builders[op_names.at(OP_TCS_INIT)] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.tcs_init(); op.params = cmd.params; }; - // target_offset shares THR_MOVE_TO_TARGET with move_to_target, so it needs a + // target_offset shares OP_MOVE_TO_TARGET with move_to_target, so it needs a // distinct CLI/DSL name. Use its Sequence:: member-function name "target_offset" - // (there is no separate entry in thread_names for it). + // (there is no separate entry in op_names for it). op_builders["target_offset"] = [this](Operation &op, const ParsedCommand &cmd) { op = ops.target_offset(); op.params = cmd.params; @@ -296,7 +295,8 @@ namespace Sequencer { // validation and state-machine validation before forwarding to the daemon. op_builders[SEQUENCERD_ACAM] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_ACAM_INIT; // passthrough uses nearest daemon THR_ identity + op.id = OP_ACAM_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_ACAM+"]"); try { @@ -310,7 +310,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_CALIB] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_CALIB_INIT; + op.id = OP_CALIB_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CALIB+"]"); try { @@ -324,7 +325,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_CAMERA] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_CAMERA_INIT; + op.id = OP_CAMERA_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_CAMERA+"]"); try { @@ -338,7 +340,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_FLEXURE] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_FLEXURE_INIT; + op.id = OP_FLEXURE_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FLEXURE+"]"); try { @@ -352,7 +355,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_FOCUS] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_FOCUS_INIT; + op.id = OP_FOCUS_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_FOCUS+"]"); try { @@ -366,7 +370,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_POWER] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_POWER_INIT; + op.id = OP_POWER_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_POWER+"]"); try { @@ -380,7 +385,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_SLICECAM] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_SLICECAM_INIT; + op.id = OP_SLICECAM_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLICECAM+"]"); try { @@ -394,7 +400,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_SLIT] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_SLIT_INIT; + op.id = OP_SLIT_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_SLIT+"]"); try { @@ -408,7 +415,8 @@ namespace Sequencer { }; op_builders[SEQUENCERD_TCS] = [this](Operation &op, const ParsedCommand &cmd) { - op.thr = THR_TCS_INIT; + op.id = OP_TCS_CMD; + op.params = cmd.params; op.func = [this, cmd_copy=cmd]() { const std::string function("Sequencer::Sequence::op_builders["+SEQUENCERD_TCS+"]"); try { diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 219126e8..873165b5 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -20,85 +20,93 @@ namespace Sequencer { namespace { // ------------------------------------------------------------------------ - // Maps each Operation::thr to the DaemonBit it depends on. Operations + // Maps each Operation::id to the DaemonBit it depends on. Operations // that manage daemon state themselves (startup, shutdown, power_init, // power_shutdown) are intentionally absent from this map. // Used exclusively by validate_sequence(). // ------------------------------------------------------------------------ - const std::unordered_map thr_to_daemon = { - { THR_EXPOSURE, DAEMON_CAMERA }, - { THR_CAMERA_SET, DAEMON_CAMERA }, - { THR_CAMERA_INIT, DAEMON_CAMERA }, - { THR_CAMERA_SHUTDOWN, DAEMON_CAMERA }, - { THR_SLIT_SET, DAEMON_SLIT }, - { THR_SLIT_INIT, DAEMON_SLIT }, - { THR_SLIT_SHUTDOWN, DAEMON_SLIT }, - { THR_MOVE_TO_TARGET, DAEMON_TCS }, - { THR_TCS_INIT, DAEMON_TCS }, - { THR_TCS_SHUTDOWN, DAEMON_TCS }, - { THR_FLEXURE_SET, DAEMON_FLEXURE }, - { THR_FLEXURE_INIT, DAEMON_FLEXURE }, - { THR_FLEXURE_SHUTDOWN, DAEMON_FLEXURE }, - { THR_FOCUS_SET, DAEMON_FOCUS }, - { THR_FOCUS_INIT, DAEMON_FOCUS }, - { THR_FOCUS_SHUTDOWN, DAEMON_FOCUS }, - { THR_CALIB_SET, DAEMON_CALIB }, - { THR_CALIBRATOR_SET, DAEMON_CALIB }, - { THR_CALIB_INIT, DAEMON_CALIB }, - { THR_CALIB_SHUTDOWN, DAEMON_CALIB }, - { THR_ACAM_INIT, DAEMON_ACAM }, - { THR_ACAM_SHUTDOWN, DAEMON_ACAM }, - { THR_SLICECAM_INIT, DAEMON_SLICECAM }, - { THR_SLICECAM_SHUTDOWN,DAEMON_SLICECAM } + const std::unordered_map opid_to_daemon = { + { OP_EXPOSURE, DAEMON_CAMERA }, + { OP_CAMERA_SETUP, DAEMON_CAMERA }, + { OP_CAMERA_INIT, DAEMON_CAMERA }, + { OP_CAMERA_SHUTDOWN, DAEMON_CAMERA }, + { OP_SLIT_SETUP, DAEMON_SLIT }, + { OP_SLIT_INIT, DAEMON_SLIT }, + { OP_SLIT_SHUTDOWN, DAEMON_SLIT }, + { OP_MOVE_TO_TARGET, DAEMON_TCS }, + { OP_TCS_INIT, DAEMON_TCS }, + { OP_TCS_SHUTDOWN, DAEMON_TCS }, + { OP_FLEXURE_SETUP, DAEMON_FLEXURE }, + { OP_FLEXURE_INIT, DAEMON_FLEXURE }, + { OP_FLEXURE_SHUTDOWN, DAEMON_FLEXURE }, + { OP_FOCUS_SETUP, DAEMON_FOCUS }, + { OP_FOCUS_INIT, DAEMON_FOCUS }, + { OP_FOCUS_SHUTDOWN, DAEMON_FOCUS }, + { OP_CALIB_SETUP, DAEMON_CALIB }, + { OP_CALIB_INIT, DAEMON_CALIB }, + { OP_CALIB_SHUTDOWN, DAEMON_CALIB }, + { OP_ACAM_INIT, DAEMON_ACAM }, + { OP_ACAM_SHUTDOWN, DAEMON_ACAM }, + { OP_SLICECAM_INIT, DAEMON_SLICECAM }, + { OP_SLICECAM_SHUTDOWN, DAEMON_SLICECAM }, + { OP_ACAM_CMD, DAEMON_ACAM }, + { OP_CALIB_CMD, DAEMON_CALIB }, + { OP_CAMERA_CMD, DAEMON_CAMERA }, + { OP_FLEXURE_CMD, DAEMON_FLEXURE }, + { OP_FOCUS_CMD, DAEMON_FOCUS }, + { OP_POWER_CMD, DAEMON_POWER }, + { OP_SLICECAM_CMD, DAEMON_SLICECAM }, + { OP_SLIT_CMD, DAEMON_SLIT }, + { OP_TCS_CMD, DAEMON_TCS } }; // ------------------------------------------------------------------------ - // Returns true if the two Operation::thr values are unsafe to run in the + // Returns true if the two Operation::id values are unsafe to run in the // same PARALLEL group. Serial ordering of any such pair is fine. // ------------------------------------------------------------------------ - inline bool is_parallel_unsafe_pair(ThreadStatusBits a, ThreadStatusBits b) { + inline bool is_parallel_unsafe_pair(OperationStatusBits a, OperationStatusBits b) { // normalize ordering so each pair is checked once if (a > b) std::swap(a, b); // global sequence ops conflict with anything big - if (a == THR_STARTUP && b == THR_SHUTDOWN) return true; - if (a == THR_STARTUP && b == THR_EXPOSURE) return true; + if (a == OP_STARTUP && b == OP_SHUTDOWN) return true; + if (a == OP_STARTUP && b == OP_EXPOSURE) return true; // startup runs all inits internally; parallelling any init with startup is a double-init - auto is_init = [](ThreadStatusBits t) { - return t == THR_ACAM_INIT || t == THR_CALIB_INIT || - t == THR_CAMERA_INIT || t == THR_FLEXURE_INIT|| - t == THR_FOCUS_INIT || t == THR_POWER_INIT || - t == THR_SLICECAM_INIT || t == THR_SLIT_INIT || - t == THR_TCS_INIT; + auto is_init = [](OperationStatusBits t) { + return t == OP_ACAM_INIT || t == OP_CALIB_INIT || + t == OP_CAMERA_INIT || t == OP_FLEXURE_INIT|| + t == OP_FOCUS_INIT || t == OP_POWER_INIT || + t == OP_SLICECAM_INIT || t == OP_SLIT_INIT || + t == OP_TCS_INIT; }; - auto is_shutdown = [](ThreadStatusBits t) { - return t == THR_ACAM_SHUTDOWN || t == THR_CALIB_SHUTDOWN || - t == THR_CAMERA_SHUTDOWN || t == THR_FLEXURE_SHUTDOWN|| - t == THR_FOCUS_SHUTDOWN || t == THR_POWER_SHUTDOWN || - t == THR_SLICECAM_SHUTDOWN || t == THR_SLIT_SHUTDOWN || - t == THR_TCS_SHUTDOWN; + auto is_shutdown = [](OperationStatusBits t) { + return t == OP_ACAM_SHUTDOWN || t == OP_CALIB_SHUTDOWN || + t == OP_CAMERA_SHUTDOWN || t == OP_FLEXURE_SHUTDOWN|| + t == OP_FOCUS_SHUTDOWN || t == OP_POWER_SHUTDOWN || + t == OP_SLICECAM_SHUTDOWN || t == OP_SLIT_SHUTDOWN || + t == OP_TCS_SHUTDOWN; }; - if (a == THR_STARTUP && is_init(b)) return true; - if (a == THR_SHUTDOWN && is_init(b)) return true; + if (a == OP_STARTUP && is_init(b)) return true; + if (a == OP_SHUTDOWN && is_init(b)) return true; // power_init vs power_shutdown - if (a == THR_POWER_INIT && b == THR_POWER_SHUTDOWN) return true; + if (a == OP_POWER_INIT && b == OP_POWER_SHUTDOWN) return true; // matching init/shutdown pair for the same subsystem if (is_init(a) && is_shutdown(b)) { - auto subsystem_of = [](ThreadStatusBits t) -> int { + auto subsystem_of = [](OperationStatusBits t) -> int { switch (t) { - case THR_ACAM_INIT: case THR_ACAM_SHUTDOWN: return 1; - case THR_CALIB_INIT: case THR_CALIB_SHUTDOWN: return 2; - case THR_CAMERA_INIT: case THR_CAMERA_SHUTDOWN: return 3; - case THR_FLEXURE_INIT: case THR_FLEXURE_SHUTDOWN: return 4; - case THR_FOCUS_INIT: case THR_FOCUS_SHUTDOWN: return 5; - case THR_POWER_INIT: case THR_POWER_SHUTDOWN: return 6; - case THR_SLICECAM_INIT: case THR_SLICECAM_SHUTDOWN: return 7; - case THR_SLIT_INIT: case THR_SLIT_SHUTDOWN: return 8; - case THR_TCS_INIT: case THR_TCS_SHUTDOWN: return 9; + case OP_ACAM_INIT: case OP_ACAM_SHUTDOWN: return 1; + case OP_CALIB_INIT: case OP_CALIB_SHUTDOWN: return 2; + case OP_CAMERA_INIT: case OP_CAMERA_SHUTDOWN: return 3; + case OP_FLEXURE_INIT: case OP_FLEXURE_SHUTDOWN: return 4; + case OP_FOCUS_INIT: case OP_FOCUS_SHUTDOWN: return 5; + case OP_POWER_INIT: case OP_POWER_SHUTDOWN: return 6; + case OP_SLICECAM_INIT: case OP_SLICECAM_SHUTDOWN: return 7; + case OP_SLIT_INIT: case OP_SLIT_SHUTDOWN: return 8; + case OP_TCS_INIT: case OP_TCS_SHUTDOWN: return 9; default: return 0; } }; @@ -332,7 +340,7 @@ namespace Sequencer { if (this->target.pointmode == Acam::POINTMODE_ACAM) { this->dotype("ONE"); sequence.push_back( { OperationType::PARALLEL, OnError::STOP, - { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); + { { OP_MOVE_TO_TARGET, [this]{ return move_to_target(); } } } } ); } else { this->target.pointmode = Acam::POINTMODE_SLIT; @@ -340,13 +348,13 @@ namespace Sequencer { // these are the default operations prior to exposure, // they can be done in parallel sequence.push_back( { OperationType::PARALLEL, OnError::STOP, - { { THR_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, - { THR_CAMERA_SET, [this]{ return camera_set(); } }, - { THR_FOCUS_SET, [this]{ return focus_set(); } }, - { THR_FLEXURE_SET, [this]{ return flexure_set(); } }, - { THR_CALIB_SET, [this]{ return calib_set(); } }, - { THR_SLIT_SET, [this]{ return slit_set(this->target.iscal ? VSM_DATABASE - : VSM_ACQUIRE); } } + { { OP_MOVE_TO_TARGET, [this]{ return move_to_target(); } }, + { OP_CAMERA_SETUP, [this]{ return camera_setup(); } }, + { OP_FOCUS_SETUP, [this]{ return focus_setup(); } }, + { OP_FLEXURE_SETUP, [this]{ return flexure_setup(); } }, + { OP_CALIB_SETUP, [this]{ return calib_setup(); } }, + { OP_SLIT_SETUP, [this]{ return slit_setup(this->target.iscal ? VSM_DATABASE + : VSM_ACQUIRE); } } } } ); } @@ -354,16 +362,16 @@ namespace Sequencer { if (this->target.pointmode != Acam::POINTMODE_ACAM) { sequence.push_back( { OperationType::SERIAL, OnError::STOP, - { { THR_ACQUISITION, + { { OP_ACQUISITION, [this,caller]() { return this->do_target_acquisition(caller); } }, - { THR_MOVE_TO_TARGET, + { OP_MOVE_TO_TARGET, [this,caller]() { return this->do_target_offset(caller); } }, - { THR_SLIT_SET, + { OP_SLIT_SETUP, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - { THR_EXPOSURE, + { OP_EXPOSURE, [this,caller]() { return this->do_exposure(caller); } } } } ); @@ -602,30 +610,30 @@ namespace Sequencer { } // ---------- RULE ------------------------------------------------------- - // Collect presence flags + counts by Operation::thr identifier. + // Collect presence flags + counts by Operation::id identifier. // Used by subsequent rules. - bool has_expose = false; - bool has_camera_set = false; - bool has_slit_set = false; - int expose_count = 0; + bool has_expose = false; + bool has_camera_setup = false; + bool has_slit_setup = false; + int expose_count = 0; for (const auto &group : sequence) { for (const auto &op : group.operations) { - switch (op.thr) { - case THR_EXPOSURE: has_expose = true; ++expose_count; break; - case THR_CAMERA_SET: has_camera_set = true; break; - case THR_SLIT_SET: has_slit_set = true; break; + switch (op.id) { + case OP_EXPOSURE: has_expose = true; ++expose_count; break; + case OP_CAMERA_SETUP: has_camera_setup = true; break; + case OP_SLIT_SETUP: has_slit_setup = true; break; default: break; } } } // ---------- RULE ------------------------------------------------------- - // If expose is present, camera_set must also be present. + // If expose is present, camera_setup must also be present. - if ( has_expose && !has_camera_set ) { - this->broadcast.error( function, "sequence contains 'expose' without 'camera_set'" ); + if ( has_expose && !has_camera_setup ) { + this->broadcast.error( function, "sequence contains 'expose' without 'camera_setup'" ); error = ERROR; } @@ -643,8 +651,8 @@ namespace Sequencer { std::bitset required; for (const auto &group : sequence) { for (const auto &op : group.operations) { - auto it = thr_to_daemon.find(op.thr); - if ( it != thr_to_daemon.end() ) required.set(it->second); + auto it = opid_to_daemon.find(op.id); + if ( it != opid_to_daemon.end() ) required.set(it->second); } } @@ -691,12 +699,12 @@ namespace Sequencer { if ( group.type != OperationType::PARALLEL ) continue; for (std::size_t i = 0; i < group.operations.size(); ++i) { for (std::size_t j = i+1; j < group.operations.size(); ++j) { - if ( is_parallel_unsafe_pair(group.operations[i].thr, - group.operations[j].thr) ) { + if ( is_parallel_unsafe_pair(group.operations[i].id, + group.operations[j].id) ) { std::ostringstream oss; oss << "parallel group contains unsafe pair: '" - << thread_names.at(group.operations[i].thr) << "' and '" - << thread_names.at(group.operations[j].thr) << "'"; + << op_names.at(group.operations[i].id) << "' and '" + << op_names.at(group.operations[j].id) << "'"; this->broadcast.error(function, oss.str()); error = ERROR; } @@ -706,18 +714,18 @@ namespace Sequencer { // science-mode cross-group check: reject parallel-unsafe pairs anywhere if ( !this->engineering_mode.load() ) { - std::vector all_thrs; - all_thrs.reserve(NUM_THREAD_STATES); + std::vector all_ops; + all_ops.reserve(NUM_OP_STATES); for (const auto &group : sequence) { - for (const auto &op : group.operations) all_thrs.push_back(op.thr); + for (const auto &op : group.operations) all_ops.push_back(op.id); } - for (std::size_t i = 0; i < all_thrs.size(); ++i) { - for (std::size_t j = i+1; j < all_thrs.size(); ++j) { - if ( is_parallel_unsafe_pair(all_thrs[i], all_thrs[j]) ) { + for (std::size_t i = 0; i < all_ops.size(); ++i) { + for (std::size_t j = i+1; j < all_ops.size(); ++j) { + if ( is_parallel_unsafe_pair(all_ops[i], all_ops[j]) ) { std::ostringstream oss; oss << "sequence contains unsafe pair in science mode: '" - << thread_names.at(all_thrs[i]) << "' and '" - << thread_names.at(all_thrs[j]) + << op_names.at(all_ops[i]) << "' and '" + << op_names.at(all_ops[j]) << "' (use 'engineering true' to allow in SERIAL groups)"; this->broadcast.error(function, oss.str()); error = ERROR; @@ -1083,8 +1091,8 @@ namespace Sequencer { // iterate through map of thread state bits, add each as a key in the JSON message, // and set true|false if the bit is set or not - std::string active_states( this->thread_state_manager.get_set_states() ); - for ( const auto &[bit,state] : Sequencer::thread_names ) { + std::string active_states( this->op_state_manager.get_set_states() ); + for ( const auto &[bit,state] : Sequencer::op_names ) { jmessage_out[state] = ( active_states.find(state)!=std::string::npos ? true : false); } @@ -1191,7 +1199,7 @@ namespace Sequencer { void Sequence::dothread_sequencer_async_listener( Sequencer::Sequence &seq, Network::UdpSocket udp ) { const std::string function("Sequencer::Sequence::dothread_sequencer_async_listener"); - ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_SEQUENCER_ASYNC_LISTENER ); + ScopedState thr_state( seq.op_state_manager, Sequencer::OP_SEQUENCER_ASYNC_LISTENER ); int retval = udp.Listener(); @@ -1306,7 +1314,7 @@ namespace Sequencer { // The Sequencer can only be started once // - if ( thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { + if ( op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) ) { this->broadcast.error( function, "sequencer already running" ); return; } @@ -1320,7 +1328,7 @@ namespace Sequencer { // ---------- SEQUENCER IS RUNNING --------------------- // - ScopedState thr_state( thread_state_manager, Sequencer::THR_SEQUENCE_START ); // this thread is running + ScopedState thr_state( op_state_manager, Sequencer::OP_SEQUENCE_START ); // this thread is running ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // state = RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit @@ -1330,7 +1338,7 @@ namespace Sequencer { // clear the thread error state // - this->thread_error_manager.clear_all(); + this->op_error_manager.clear_all(); // clear stop flags // @@ -1376,14 +1384,14 @@ namespace Sequencer { message.str(""); message << "cannot move to target " << this->target.name << " because TCS is not connected"; this->broadcast.error( function, message.str() ); - this->thread_error_manager.set( THR_SEQUENCE_START ); // report error + this->op_error_manager.set( OP_SEQUENCE_START ); // report error break; } } error = this->target.update_state( Sequencer::TARGET_ACTIVE ); // set ACTIVE state in database (this says we are using this target) if (error!=NO_ERROR) { - this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error + this->op_error_manager.set( OP_SEQUENCE_START ); // report any error break; } } @@ -1403,7 +1411,7 @@ namespace Sequencer { error = run_default_sequence(function); if (error != NO_ERROR) { - this->thread_error_manager.set(THR_SEQUENCE_START); + this->op_error_manager.set(OP_SEQUENCE_START); break; } @@ -1416,11 +1424,11 @@ namespace Sequencer { // error = this->target.update_state( Sequencer::TARGET_COMPLETE ); // update the active target table if (error==NO_ERROR) error = this->target.insert_completed(); // insert into the completed table - if (error!=NO_ERROR) this->thread_error_manager.set( THR_SEQUENCE_START ); // report any error + if (error!=NO_ERROR) this->op_error_manager.set( OP_SEQUENCE_START ); // report any error // abort sequence on error // - if ( this->thread_error_manager.are_any_set() ) break; + if ( this->op_error_manager.are_any_set() ) break; // Check the "dotype" -- // If this was "do one" then do_once is set and get out now. @@ -1432,14 +1440,14 @@ namespace Sequencer { } // end while true - if ( this->thread_error_manager.are_any_set() ) { + if ( this->op_error_manager.are_any_set() ) { logwrite(function, "ERROR stopping sequencer due to error in: "+ - this->thread_error_manager.get_set_states()); + this->op_error_manager.get_set_states()); // If this target was flagged as active, then change it to unassigned on error. if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { this->target.update_state( Sequencer::TARGET_UNASSIGNED ); } - this->thread_error_manager.clear_all(); // clear the thread error state + this->op_error_manager.clear_all(); // clear the thread error state this->do_once.store(true); } @@ -1449,7 +1457,7 @@ namespace Sequencer { /***** Sequencer::Sequence::sequence_start **********************************/ - /***** Sequencer::Sequence::camera_set **************************************/ + /***** Sequencer::Sequence::camera_setup **************************************/ /** * @brief sets the camera according to the parameters in the target entry * @return NO_ERROR @@ -1458,20 +1466,20 @@ namespace Sequencer { * At the moment, this is only exposure time. * */ - long Sequence::camera_set() { - const std::string function("Sequencer::Sequence::camera_set"); + long Sequence::camera_setup() { + const std::string function("Sequencer::Sequence::camera_setup"); std::string reply; std::stringstream camcmd; long error=NO_ERROR; - ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CAMERA_SETUP ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); this->wait_for_canexpose(function); logwrite( function, "setting camera parameters"); - this->thread_error_manager.set( THR_CAMERA_SET ); // assume the worse, clear on success + this->op_error_manager.set( OP_CAMERA_SETUP ); // assume the worse, clear on success // Controller activate states stored in Sequencer::CalibrationTarget::calinfo map, // indexed by name. Calibration targets use target.name for the index, or @@ -1528,14 +1536,14 @@ namespace Sequencer { throw std::runtime_error( "camera returned "+reply ); } - this->thread_error_manager.clear( THR_CAMERA_SET ); // success + this->op_error_manager.clear( OP_CAMERA_SETUP ); // success return NO_ERROR; } - /***** Sequencer::Sequence::camera_set **************************************/ + /***** Sequencer::Sequence::camera_setup **************************************/ - /***** Sequencer::Sequence::slit_set ****************************************/ + /***** Sequencer::Sequence::slit_setup ****************************************/ /** * @brief sets the slit width and offset * @details Slit width is always set according to the value in the target @@ -1545,12 +1553,12 @@ namespace Sequencer { * @throws std::runtime_error * */ - long Sequence::slit_set(VirtualSlitMode mode) { - const std::string function("Sequencer::Sequence::slit_set"); + long Sequence::slit_setup(VirtualSlitMode mode) { + const std::string function("Sequencer::Sequence::slit_setup"); std::string reply, modestr; std::stringstream slitcmd, message; - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLIT_SETUP ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); // send the SET command to slitd @@ -1584,13 +1592,13 @@ namespace Sequencer { if ( this->slitd.command_timeout( slitcmd.str(), reply, SLITD_SET_TIMEOUT ) != NO_ERROR ) { logwrite( function, "ERROR setting slit" ); - this->thread_error_manager.set( THR_SLIT_SET ); + this->op_error_manager.set( OP_SLIT_SETUP ); throw std::runtime_error("slit returned: "+reply); } return NO_ERROR; } - /***** Sequencer::Sequence::slit_set ****************************************/ + /***** Sequencer::Sequence::slit_setup ****************************************/ /***** Sequencer::Sequence::power_init **************************************/ @@ -1603,7 +1611,7 @@ namespace Sequencer { long Sequence::power_init() { const std::string function("Sequencer::Sequence::power_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_POWER_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_POWER_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_POWER ); this->daemon_manager.clear( Sequencer::DAEMON_POWER ); // powerd not ready @@ -1630,7 +1638,7 @@ namespace Sequencer { long Sequence::power_shutdown() { const std::string function("Sequencer::Sequence::power_shutdown"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_POWER_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_POWER_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_POWER ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_POWER ); @@ -1653,12 +1661,12 @@ namespace Sequencer { long Sequence::slit_init() { const std::string function("Sequencer::Sequence::slit_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLIT_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLIT_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); this->daemon_manager.clear( Sequencer::DAEMON_SLIT ); // slitd not ready - this->thread_error_manager.set( THR_SLIT_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLIT_INIT ); // assume the worst, clear on success if ( this->set_power_switch(ON, POWER_SLIT, std::chrono::seconds(5)) != NO_ERROR ) { logwrite( function, "ERROR powering slit hardware" ); @@ -1701,7 +1709,7 @@ namespace Sequencer { } } - this->thread_error_manager.clear( THR_SLIT_INIT ); // success + this->op_error_manager.clear( OP_SLIT_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_SLIT ); // slitd ready return NO_ERROR; @@ -1722,11 +1730,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SLIT_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SLIT_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_SLIT ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_SLIT ); - this->thread_error_manager.set( THR_SLIT_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLIT_SHUTDOWN ); // assume the worst, clear on success // already off? // @@ -1769,7 +1777,7 @@ namespace Sequencer { logwrite( function, "disconnecting from slitd" ); this->slitd.disconnect(); - this->thread_error_manager.clear( THR_SLIT_SHUTDOWN ); // success + this->op_error_manager.clear( OP_SLIT_SHUTDOWN ); // success return NO_ERROR; } @@ -1789,10 +1797,10 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_SLICECAM ); // slicecamd not ready - ScopedState thr_state( thread_state_manager, Sequencer::THR_SLICECAM_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_SLICECAM_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); - this->thread_error_manager.set( THR_SLICECAM_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLICECAM_INIT ); // assume the worst, clear on success // make sure hardware is powered on // @@ -1809,7 +1817,7 @@ namespace Sequencer { } this->daemon_manager.set( Sequencer::DAEMON_SLICECAM ); // slicecamd ready - this->thread_error_manager.clear( THR_SLICECAM_INIT ); // success + this->op_error_manager.clear( OP_SLICECAM_INIT ); // success return NO_ERROR; } @@ -1829,10 +1837,10 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_ACAM ); // acamd not ready - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACAM_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_ACAM_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); - this->thread_error_manager.set( THR_ACAM_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_ACAM_INIT ); // assume the worst, clear on success // make sure hardware is powered on // @@ -1869,7 +1877,7 @@ namespace Sequencer { } } - this->thread_error_manager.clear( THR_ACAM_INIT ); // success + this->op_error_manager.clear( OP_ACAM_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_ACAM ); // acamd ready return NO_ERROR; @@ -1890,11 +1898,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SLICECAM_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SLICECAM_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_SLICECAM ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_SLICECAM ); - this->thread_error_manager.set( THR_SLICECAM_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_SLICECAM_SHUTDOWN ); // assume the worst, clear on success // already off? // @@ -1934,7 +1942,7 @@ namespace Sequencer { throw std::runtime_error("could not power off slicecam hardware"); } - this->thread_error_manager.clear( THR_SLICECAM_SHUTDOWN ); // success + this->op_error_manager.clear( OP_SLICECAM_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::slicecam_shutdown *******************************/ @@ -1953,11 +1961,11 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ACAM_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_ACAM_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_ACAM ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_ACAM ); - this->thread_error_manager.set( THR_ACAM_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_ACAM_SHUTDOWN ); // assume the worst, clear on success // ensure a connection to the daemon // @@ -2005,7 +2013,7 @@ namespace Sequencer { throw std::runtime_error("could not switch off acam"); } - this->thread_error_manager.clear( THR_ACAM_SHUTDOWN ); // success + this->op_error_manager.clear( OP_ACAM_SHUTDOWN ); // success return NO_ERROR; } @@ -2024,10 +2032,10 @@ namespace Sequencer { this->daemon_manager.clear( Sequencer::DAEMON_CALIB ); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIB_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CALIB_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); - this->thread_error_manager.set( THR_CALIB_INIT ); // assume the worst, clear on success + this->op_error_manager.set( OP_CALIB_INIT ); // assume the worst, clear on success // make sure calib hardware is powered if ( this->set_power_switch(ON, POWER_CALIB, std::chrono::seconds(5)) != NO_ERROR ) { @@ -2079,7 +2087,7 @@ namespace Sequencer { // calibd is ready this->daemon_manager.set( Sequencer::DAEMON_CALIB ); - this->thread_error_manager.clear( THR_CALIB_INIT ); + this->op_error_manager.clear( OP_CALIB_INIT ); return NO_ERROR; } @@ -2097,11 +2105,11 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::calib_shutdown"); long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CALIB_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_CALIB_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_CALIB ); - this->thread_error_manager.set( THR_CALIB_SHUTDOWN ); // assume the worst, clear on success + this->op_error_manager.set( OP_CALIB_SHUTDOWN ); // assume the worst, clear on success // is calib hardware powered? // @@ -2163,11 +2171,11 @@ namespace Sequencer { // set this thread's error status // if (error!=NO_ERROR) { - this->thread_error_manager.set( THR_CALIB_SHUTDOWN ); + this->op_error_manager.set( OP_CALIB_SHUTDOWN ); throw std::runtime_error("shutting down calib control"); } - this->thread_error_manager.clear( THR_CALIB_SHUTDOWN ); // success + this->op_error_manager.clear( OP_CALIB_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::calib_shutdown **********************************/ @@ -2184,14 +2192,14 @@ namespace Sequencer { * */ long Sequence::tcs_init() { - ScopedState thr_state( thread_state_manager, Sequencer::THR_TCS_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_TCS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); this->daemon_manager.clear( Sequencer::DAEMON_TCS ); // tcsd not ready if ( this->open_hardware(this->tcsd) != NO_ERROR ) { logwrite( "Sequencer::Sequence::tcs_init", "ERROR initializing TCS" ); - this->thread_error_manager.set( THR_TCS_INIT ); + this->op_error_manager.set( OP_TCS_INIT ); throw std::runtime_error("could not initialize TCS"); } @@ -2219,7 +2227,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::tcs_shutdown"); std::stringstream message; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_TCS_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_TCS_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_TCS ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_TCS ); @@ -2260,7 +2268,7 @@ namespace Sequencer { long Sequence::flexure_init() { const std::string function("Sequencer::Sequence::flexure_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FLEXURE_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); this->daemon_manager.clear( Sequencer::DAEMON_FLEXURE ); // flexured not ready @@ -2269,13 +2277,13 @@ namespace Sequencer { // if ( this->set_power_switch(ON, POWER_FLEXURE, std::chrono::seconds(21)) != NO_ERROR ) { logwrite( function, "ERROR powering flexure control" ); - this->thread_error_manager.set( THR_FLEXURE_INIT ); + this->op_error_manager.set( OP_FLEXURE_INIT ); throw std::runtime_error("could not power flexure control"); } if ( this->open_hardware(this->flexured) != NO_ERROR ) { logwrite( function, "ERROR initializing flexure control" ); - this->thread_error_manager.set( THR_FLEXURE_INIT ); + this->op_error_manager.set( OP_FLEXURE_INIT ); throw std::runtime_error("could not initialize flexure control"); } @@ -2301,7 +2309,7 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_FLEXURE_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_FLEXURE_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_FLEXURE ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_FLEXURE ); @@ -2360,12 +2368,12 @@ namespace Sequencer { long Sequence::focus_init() { const std::string function("Sequencer::Sequence::focus_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FOCUS_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); this->daemon_manager.clear( Sequencer::DAEMON_FOCUS ); // focusd not ready - this->thread_error_manager.set( THR_FOCUS_INIT ); // assume failure, clear on success + this->op_error_manager.set( OP_FOCUS_INIT ); // assume failure, clear on success if ( this->set_power_switch(ON, POWER_FOCUS, std::chrono::seconds(5)) != NO_ERROR ) { logwrite( function, "ERROR powering focus control" ); @@ -2410,7 +2418,7 @@ namespace Sequencer { } } - this->thread_error_manager.clear( THR_FOCUS_INIT ); // success + this->op_error_manager.clear( OP_FOCUS_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_FOCUS ); // focusd is ready return NO_ERROR; @@ -2430,7 +2438,7 @@ namespace Sequencer { std::string reply; long error=NO_ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_FOCUS_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_FOCUS_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_FOCUS ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_FOCUS ); @@ -2489,11 +2497,11 @@ namespace Sequencer { long Sequence::camera_init() { const std::string function("Sequencer::Sequence::camera_init"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_CAMERA_INIT ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CAMERA_INIT ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); this->daemon_manager.clear( Sequencer::DAEMON_CAMERA ); // camerad not ready - this->thread_error_manager.set( THR_CAMERA_INIT ); // assume failure, clear on success + this->op_error_manager.set( OP_CAMERA_INIT ); // assume failure, clear on success // make sure hardware is powered on // @@ -2525,7 +2533,7 @@ namespace Sequencer { this->camerad.send( CAMERAD_FRAMETRANSFER+" all", reply ); this->is_science_frame_transfer = ( reply.find("yes") != std::string::npos ); - this->thread_error_manager.clear( THR_CAMERA_INIT ); // success + this->op_error_manager.clear( OP_CAMERA_INIT ); // success this->daemon_manager.set( Sequencer::DAEMON_CAMERA ); // camerad ready return NO_ERROR; @@ -2543,11 +2551,11 @@ namespace Sequencer { long Sequence::camera_shutdown() { const std::string function("Sequencer::Sequence::camera_shutdown"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_CAMERA_SHUTDOWN ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_CAMERA_SHUTDOWN ); ScopedState wait_state( this->wait_state_manager, Sequencer::SEQ_WAIT_CAMERA ); ScopedState daemon_state( this->daemon_manager, Sequencer::DAEMON_CAMERA ); - this->thread_error_manager.set( THR_CAMERA_SHUTDOWN ); // assume failure, clear on success + this->op_error_manager.set( OP_CAMERA_SHUTDOWN ); // assume failure, clear on success // Are any cameras on? // @@ -2589,7 +2597,7 @@ namespace Sequencer { throw std::runtime_error("switching off camera"); } - this->thread_error_manager.clear( THR_CAMERA_SHUTDOWN ); // success + this->op_error_manager.clear( OP_CAMERA_SHUTDOWN ); // success return NO_ERROR; } /***** Sequencer::Sequence::camera_shutdown *********************************/ @@ -2614,7 +2622,7 @@ namespace Sequencer { // if ( this->target.iscal ) return NO_ERROR; - ScopedState thr_state( thread_state_manager, Sequencer::THR_MOVE_TO_TARGET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_MOVE_TO_TARGET ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_TCS ); ScopedState wait_moveto( wait_state_manager, Sequencer::SEQ_WAIT_MOVETO ); @@ -2686,7 +2694,7 @@ namespace Sequencer { if ( dec_isnan ) { message << " DEC=\"" << this->target.dec_dms << "\""; } message << " to decimal"; logwrite( function, "ERROR "+message.str() ); - this->thread_error_manager.set( THR_MOVE_TO_TARGET ); + this->op_error_manager.set( OP_MOVE_TO_TARGET ); throw std::runtime_error(message.str()); } @@ -2737,7 +2745,7 @@ namespace Sequencer { if ( error != NO_ERROR || coords_reply.compare( 0, strlen(TCS_SUCCESS_STR), TCS_SUCCESS_STR ) != 0 ) { message.str(""); message << "sending COORDS command. TCS reply: " << coords_reply; logwrite( function, "ERROR "+message.str() ); - this->thread_error_manager.set( THR_MOVE_TO_TARGET ); + this->op_error_manager.set( OP_MOVE_TO_TARGET ); throw std::runtime_error("sending COORDS to TCS: "+coords_reply); } } @@ -2800,7 +2808,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::dothread_notify_tcs"); std::stringstream message; - ScopedState thr_state( seq.thread_state_manager, Sequencer::THR_NOTIFY_TCS ); + ScopedState thr_state( seq.op_state_manager, Sequencer::OP_NOTIFY_TCS ); // If this target is the same as the last then this section gets skipped; // no need to repoint the telescope and wait for a slew if it's already @@ -2837,7 +2845,7 @@ namespace Sequencer { if ( std::isnan( radec_to_decimal( seq.target.ra_hms, ra_hms ) ) || std::isnan( radec_to_decimal( seq.target.dec_dms, dec_dms ) ) ) { seq.broadcast.error( function, "can't handle NaN value for RA, DEC" ); - seq.thread_error_manager.set( THR_NOTIFY_TCS ); + seq.op_error_manager.set( OP_NOTIFY_TCS ); return; } @@ -2861,59 +2869,59 @@ namespace Sequencer { /***** Sequencer::Sequence::dothread_notify_tcs *****************************/ - /***** Sequencer::Sequence::focus_set ***************************************/ + /***** Sequencer::Sequence::focus_setup ***************************************/ /** * @brief set the focus * @return NO_ERROR * @todo focus not yet implemented * */ - long Sequence::focus_set() { - const std::string function("Sequencer::Sequence::focus_set"); + long Sequence::focus_setup() { + const std::string function("Sequencer::Sequence::focus_setup"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FOCUS_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FOCUS_SETUP ); logwrite( function, "focus not yet implemented." ); return NO_ERROR; } - /***** Sequencer::Sequence::focus_set ***************************************/ + /***** Sequencer::Sequence::focus_setup ***************************************/ - /***** Sequencer::Sequence::flexure_set *************************************/ + /***** Sequencer::Sequence::flexure_setup *************************************/ /** * @brief set the flexure * @return NO_ERROR * @todo flexure not yet implemented * */ - long Sequence::flexure_set() { - const std::string function("Sequencer::Sequence::flexure_set"); + long Sequence::flexure_setup() { + const std::string function("Sequencer::Sequence::flexure_setup"); - ScopedState thr_state( thread_state_manager, Sequencer::THR_FLEXURE_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_FLEXURE_SETUP ); logwrite( function, "flexure not yet implemented." ); return NO_ERROR; } - /***** Sequencer::Sequence::flexure_set *************************************/ + /***** Sequencer::Sequence::flexure_setup *************************************/ - /***** Sequencer::Sequence::calib_set ***************************************/ + /***** Sequencer::Sequence::calib_setup ***************************************/ /** * @brief set the calibrator * @return NO_ERROR * @throws std::runtime_error * */ - long Sequence::calib_set() { - const std::string function("Sequencer::Sequence::calib_set"); + long Sequence::calib_setup() { + const std::string function("Sequencer::Sequence::calib_setup"); std::stringstream message; - ScopedState thr_state( thread_state_manager, Sequencer::THR_CALIBRATOR_SET ); + ScopedState thr_state( op_state_manager, Sequencer::OP_CALIB_SETUP ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_CALIB ); - this->thread_error_manager.set( THR_CALIBRATOR_SET ); // assume the worse, clear on success + this->op_error_manager.set( OP_CALIB_SETUP ); // assume the worse, clear on success const std::string calname = std::string(this->target.iscal ? this->target.name : "SCIENCE"); @@ -2979,10 +2987,10 @@ namespace Sequencer { this->broadcast.notice( function, "abort may have left calib system partially set" ); } - this->thread_error_manager.clear( THR_CALIBRATOR_SET ); // success + this->op_error_manager.clear( OP_CALIB_SETUP ); // success return NO_ERROR; } - /***** Sequencer::Sequence::calib_set ***************************************/ + /***** Sequencer::Sequence::calib_setup ***************************************/ /***** Sequencer::Sequence::abort_process *********************************/ @@ -2993,7 +3001,7 @@ namespace Sequencer { void Sequence::abort_process() { const std::string function("Sequencer::Sequence::abort_process"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_ABORT_PROCESS ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_ABORT_PROCESS ); // Decide post-abort seqstate before entering SEQ_ABORTING. These snapshots // must be taken before any seqstate mutation below, because set_only() @@ -3050,17 +3058,17 @@ namespace Sequencer { this->broadcast.notice( function, "cancel signal sent" ); // Wait for sequence_start to fully exit before switching to SEQ_READY. - // Without this, we could have SEQ_READY set while THR_SEQUENCE_START is + // Without this, we could have SEQ_READY set while OP_SEQUENCE_START is // still set. Workers just received cancel_flag + CV notifications above, // so this loop exits after at most a few iterations. // if ( abort_during_run ) { const auto drain_timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5); - while ( this->thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) && + while ( this->op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) && std::chrono::steady_clock::now() < drain_timeout ) { std::this_thread::sleep_for( std::chrono::milliseconds(20) ); } - if ( this->thread_state_manager.is_set( Sequencer::THR_SEQUENCE_START ) ) { + if ( this->op_state_manager.is_set( Sequencer::OP_SEQUENCE_START ) ) { logwrite( function, "WARNING: sequence_start did not exit within drain timeout" ); } } @@ -3090,7 +3098,7 @@ namespace Sequencer { void Sequence::stop_exposure() { const std::string function("Sequencer::Sequence::stop_exposure"); - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_STOP_EXPOSURE ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_STOP_EXPOSURE ); // This function is only used while exposing // @@ -3146,7 +3154,7 @@ namespace Sequencer { return ERROR; } - ScopedState thr_state( thread_state_manager, Sequencer::THR_REPEAT_EXPOSURE ); + ScopedState thr_state( op_state_manager, Sequencer::OP_REPEAT_EXPOSURE ); ScopedState seq_state( seq_state_manager, Sequencer::SEQ_RUNNING, true ); // set state=RUNNING (only) seq_state.destruct_set( Sequencer::SEQ_READY ); // set state=READY on exit @@ -3163,9 +3171,9 @@ namespace Sequencer { std::vector sequence = { { OperationType::SERIAL, OnError::STOP, { - { THR_SLIT_SET, + { OP_SLIT_SETUP, [this]() { return this->do_target_virtualslit(Sequencer::VSM_EXPOSE); } }, - { THR_EXPOSURE, + { OP_EXPOSURE, [this,function]() { return this->do_exposure(function); } } } } }; @@ -3199,7 +3207,7 @@ namespace Sequencer { this->wait_for_canexpose(function); - ScopedState thr_state( thread_state_manager, Sequencer::THR_TRIGGER_EXPOSURE ); + ScopedState thr_state( op_state_manager, Sequencer::OP_TRIGGER_EXPOSURE ); // Check tcs_preauth_time and set notify_tcs_next_target -- // When the preauth_time is non-zero, set this flag to true in order @@ -3222,7 +3230,7 @@ namespace Sequencer { // if ( this->camerad.send( message.str(), reply ) != NO_ERROR ) { if ( this->camerad.command_timeout( message.str(), reply, 30000 ) != NO_ERROR ) { logwrite( function, "ERROR sending camera "+message.str() ); - this->thread_error_manager.set( THR_TRIGGER_EXPOSURE ); // tell the world this thread had an error + this->op_error_manager.set( OP_TRIGGER_EXPOSURE ); // tell the world this thread had an error this->target.update_state( Sequencer::TARGET_PENDING ); // return the target state to pending this->wait_state_manager.clear( Sequencer::SEQ_WAIT_EXPOSE ); // clear EXPOSE bit this->arm_readout_flag = false; // disarm async_listener from looking for readout @@ -3278,7 +3286,7 @@ namespace Sequencer { long error = NO_ERROR; double updated_exptime=0; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_MODIFY_EXPTIME ); + ScopedState thr_state( this->op_state_manager, Sequencer::OP_MODIFY_EXPTIME ); // This function is only used while exposing // @@ -3333,12 +3341,12 @@ namespace Sequencer { return ERROR; } - ScopedState thread_state( thread_state_manager, Sequencer::THR_STARTUP ); // this thread is running + ScopedState thread_state( op_state_manager, Sequencer::OP_STARTUP ); // this thread is running // set only STARTING (and clear everything else) ScopedState seq_state( seq_state_manager, Sequencer::SEQ_STARTING, true ); // state=STARTING (only) - this->thread_error_manager.clear_all(); // clear the thread error state + this->op_error_manager.clear_all(); // clear the thread error state // clear stop flags // @@ -3400,7 +3408,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::shutdown"); long error=ERROR; - ScopedState thr_state( this->thread_state_manager, Sequencer::THR_SHUTDOWN ); // this thread is running + ScopedState thr_state( this->op_state_manager, Sequencer::OP_SHUTDOWN ); // this thread is running // Enter STOPPING as a strict one-hot state. Explicit set_only is used // instead of ScopedState because final state is set explicitly before return. @@ -3425,12 +3433,12 @@ namespace Sequencer { // clear the thread error state // - this->thread_error_manager.clear_all(); + this->op_error_manager.clear_all(); // Everything (except TCS) needs the power control to be running // so make sure power control is initialized before continuing. // - error = run( { THR_POWER_INIT, [this]{ return power_init(); }, { } }, function ); + error = run( { OP_POWER_INIT, [this]{ return power_init(); }, { } }, function ); if ( error != NO_ERROR ) { this->broadcast.error( function, "from power control. Will try to continue (but don't hold your breath)" ); @@ -3440,14 +3448,14 @@ namespace Sequencer { // pair their ThreadStatusBit with the function to call // error = run_parallel( { - { THR_ACAM_SHUTDOWN, [this]{ return acam_shutdown(); }, { } }, - { THR_CALIB_SHUTDOWN, [this]{ return calib_shutdown(); }, { } }, - { THR_CAMERA_SHUTDOWN, [this]{ return camera_shutdown(); }, { } }, - { THR_FLEXURE_SHUTDOWN, [this]{ return flexure_shutdown(); }, { } }, - { THR_FOCUS_SHUTDOWN, [this]{ return focus_shutdown(); }, { } }, - { THR_SLIT_SHUTDOWN, [this]{ return slit_shutdown(); }, { } }, - { THR_SLICECAM_SHUTDOWN, [this]{ return slicecam_shutdown(); }, { } }, - { THR_TCS_SHUTDOWN, [this]{ return tcs_shutdown(); }, { } } + { OP_ACAM_SHUTDOWN, [this]{ return acam_shutdown(); }, { } }, + { OP_CALIB_SHUTDOWN, [this]{ return calib_shutdown(); }, { } }, + { OP_CAMERA_SHUTDOWN, [this]{ return camera_shutdown(); }, { } }, + { OP_FLEXURE_SHUTDOWN, [this]{ return flexure_shutdown(); }, { } }, + { OP_FOCUS_SHUTDOWN, [this]{ return focus_shutdown(); }, { } }, + { OP_SLIT_SHUTDOWN, [this]{ return slit_shutdown(); }, { } }, + { OP_SLICECAM_SHUTDOWN, [this]{ return slicecam_shutdown(); }, { } }, + { OP_TCS_SHUTDOWN, [this]{ return tcs_shutdown(); }, { } } }, function ); if (error==NO_ERROR) { @@ -4432,8 +4440,6 @@ namespace Sequencer { retstring.append( " addrow ? | \n" ); retstring.append( " async [ ? | ]\n" ); retstring.append( " acquire [ ? ]\n" ); - retstring.append( " calibset [ ? ]\n" ); - retstring.append( " cameraset [ ? ]\n" ); retstring.append( " cancel [ ? ]\n" ); retstring.append( " clearlasttarget\n" ); retstring.append( " completed [ ? ]\n" ); @@ -4512,52 +4518,6 @@ namespace Sequencer { } else - // ---------------------------------------------------- - // calibset -- sets the calibrator according to the parameters in the target entry - // ---------------------------------------------------- - // - if ( testname == "calibset" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test calibset\n"; - retstring.append( " Set only the calib according to the parameters in the target row.\n" ); - return HELP; - } - // launch thread and wait for it to return - auto calibset = std::async(std::launch::async, &Sequence::calib_set, this); - try { - error = calibset.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR calib_set exception: "+std::string(e.what()) ); - return ERROR; - } - return error; - } - else - - // ---------------------------------------------------- - // cameraset -- sets the camera according to the parameters in the target entry - // ---------------------------------------------------- - // - if ( testname == "cameraset" ) { - if ( tokens.size() > 1 && tokens[1] == "?" ) { - retstring = "test cameraset\n"; - retstring.append( " Set only the camera according to the parameters in the target row.\n" ); - return HELP; - } - // launch thread and wait for it to return - auto cameraset = std::async(std::launch::async, &Sequence::camera_set, this); - try { - error = cameraset.get(); - } - catch (const std::exception& e) { - logwrite( function, "ERROR camera_set exception: "+std::string(e.what()) ); - return ERROR; - } - return error; - } - else - // ---------------------------------------------------- // expose -- trigger exposure // ---------------------------------------------------- @@ -4588,7 +4548,7 @@ namespace Sequencer { this->broadcast.notice( function, message.str() ); logwrite( function, message.str() ); - message.str(""); message << "THREADS: " << this->thread_state_manager.get_set_states(); + message.str(""); message << "THREADS: " << this->op_state_manager.get_set_states(); logwrite( function, message.str() ); message.str(""); message << "DAEMONS NOT READY: " << this->daemon_manager.get_cleared_states(); @@ -5138,49 +5098,6 @@ namespace Sequencer { } else - // --------------------------------------------------------- - // slitset -- spawn slit_set with the selected mode - // --------------------------------------------------------- - // - if ( testname == "slitset" ) { - - if ( tokens.size() > 1 && ( tokens[1] == "?" || tokens[1] == "help" || tokens[1] == "-h" ) ) { - retstring = "test slitset \n"; - retstring.append( " Spawn slit_set for = { expose, acquire, database }\n" ); - return HELP; - } - - if ( tokens.size() < 2 ) { - logwrite( function, "ERROR expected slitset " ); - retstring="invalid_argument"; - return ERROR; - } - - Sequencer::VirtualSlitMode mode; - - if ( tokens[1]=="expose" ) { mode = Sequencer::VSM_EXPOSE; } - else - if ( tokens[1]=="acquire" ) { mode = Sequencer::VSM_ACQUIRE; } - else - if ( tokens[1]=="database" ) { mode = Sequencer::VSM_DATABASE; } - else { - logwrite( function, "ERROR invalid mode "+tokens[1]+": expected { expose acquire database }" ); - retstring="invalid_argument"; - return ERROR; - } - - auto slitset = std::async(std::launch::async, &Sequence::slit_set, this, mode); - try { - error = slitset.get(); - } - catch (const std::exception& e) { - retstring="slit_set_exception"; - logwrite( function, "ERROR slit set exception: "+std::string(e.what()) ); - return ERROR; - } - } - else - // --------------------------------------------------------- // startup -- startup a single specified module // --------------------------------------------------------- diff --git a/sequencerd/sequence.h b/sequencerd/sequence.h index 280cdfd1..427b5d2f 100644 --- a/sequencerd/sequence.h +++ b/sequencerd/sequence.h @@ -205,99 +205,115 @@ namespace Sequencer { }; /** - * @enum ThreadStatusBits - * @brief assigns each thread a bit in a threadstate word - * @details bit is set while thread is running + * @enum OperationStatusBits + * @brief assigns each operation a bit in an operation-state word + * @details bit is set while the operation is running */ - enum ThreadStatusBits : size_t { - THR_SEQUENCER_ASYNC_LISTENER=0, - THR_TRIGGER_EXPOSURE, - THR_EXPOSURE, - THR_REPEAT_EXPOSURE, - THR_STOP_EXPOSURE, - THR_ABORT_PROCESS, - THR_SEQUENCE_START, - THR_RUN_SCRIPT, - THR_MONITOR_READY_STATE, - THR_CALIB_SET, - THR_CAMERA_SET, - THR_SLIT_SET, - THR_MOVE_TO_TARGET, - THR_NOTIFY_TCS, - THR_FOCUS_SET, - THR_FLEXURE_SET, - THR_CALIBRATOR_SET, - THR_ACAM_INIT, - THR_ANDOR_INIT, - THR_SLICECAM_INIT, - THR_CALIB_INIT, - THR_TCS_INIT, - THR_SLIT_INIT, - THR_CAMERA_INIT, - THR_FLEXURE_INIT, - THR_FOCUS_INIT, - THR_POWER_INIT, - THR_ACAM_SHUTDOWN, - THR_ANDOR_SHUTDOWN, - THR_SLICECAM_SHUTDOWN, - THR_CALIB_SHUTDOWN, - THR_CAMERA_SHUTDOWN, - THR_FLEXURE_SHUTDOWN, - THR_FOCUS_SHUTDOWN, - THR_SLIT_SHUTDOWN, - THR_TCS_SHUTDOWN, - THR_POWER_SHUTDOWN, - THR_MODIFY_EXPTIME, - THR_ACQUISITION, - THR_GUIDING, - THR_STARTUP, - THR_SHUTDOWN, - NUM_THREAD_STATES + enum OperationStatusBits : size_t { + OP_SEQUENCER_ASYNC_LISTENER=0, + OP_TRIGGER_EXPOSURE, + OP_EXPOSURE, + OP_REPEAT_EXPOSURE, + OP_STOP_EXPOSURE, + OP_ABORT_PROCESS, + OP_SEQUENCE_START, + OP_RUN_SCRIPT, + OP_MONITOR_READY_STATE, + OP_CALIB_SETUP, + OP_CAMERA_SETUP, + OP_SLIT_SETUP, + OP_MOVE_TO_TARGET, + OP_NOTIFY_TCS, + OP_FOCUS_SETUP, + OP_FLEXURE_SETUP, + OP_ACAM_INIT, + OP_ANDOR_INIT, + OP_SLICECAM_INIT, + OP_CALIB_INIT, + OP_TCS_INIT, + OP_SLIT_INIT, + OP_CAMERA_INIT, + OP_FLEXURE_INIT, + OP_FOCUS_INIT, + OP_POWER_INIT, + OP_ACAM_SHUTDOWN, + OP_ANDOR_SHUTDOWN, + OP_SLICECAM_SHUTDOWN, + OP_CALIB_SHUTDOWN, + OP_CAMERA_SHUTDOWN, + OP_FLEXURE_SHUTDOWN, + OP_FOCUS_SHUTDOWN, + OP_SLIT_SHUTDOWN, + OP_TCS_SHUTDOWN, + OP_POWER_SHUTDOWN, + OP_MODIFY_EXPTIME, + OP_ACQUISITION, + OP_GUIDING, + OP_STARTUP, + OP_SHUTDOWN, + OP_ACAM_CMD, + OP_CALIB_CMD, + OP_CAMERA_CMD, + OP_FLEXURE_CMD, + OP_FOCUS_CMD, + OP_POWER_CMD, + OP_SLICECAM_CMD, + OP_SLIT_CMD, + OP_TCS_CMD, + NUM_OP_STATES }; - const std::map thread_names = { - {THR_SEQUENCER_ASYNC_LISTENER, "async_listener"}, - {THR_TRIGGER_EXPOSURE, "trigger_exposure"}, - {THR_EXPOSURE, "exposure"}, - {THR_REPEAT_EXPOSURE, "repeat_exposure"}, - {THR_STOP_EXPOSURE, "stop_exposure"}, - {THR_ABORT_PROCESS, "abort_process"}, - {THR_SEQUENCE_START, "sequence_start"}, - {THR_RUN_SCRIPT, "run_script"}, - {THR_MONITOR_READY_STATE, "monitor_ready_state"}, - {THR_CALIB_SET, "calib_set"}, - {THR_CAMERA_SET, "camera_set"}, - {THR_SLIT_SET, "slit_set"}, - {THR_MOVE_TO_TARGET, "move_to_target"}, - {THR_NOTIFY_TCS, "notify_tcs"}, - {THR_FOCUS_SET, "focus_set"}, - {THR_FLEXURE_SET, "flexure_set"}, - {THR_CALIBRATOR_SET, "calibrator_set"}, - {THR_ACAM_INIT, "acam_init"}, - {THR_ANDOR_INIT, "andor_init"}, - {THR_SLICECAM_INIT, "slicecam_init"}, - {THR_CALIB_INIT, "calib_init"}, - {THR_TCS_INIT, "tcs_init"}, - {THR_SLIT_INIT, "slit_init"}, - {THR_CAMERA_INIT, "camera_init"}, - {THR_FLEXURE_INIT, "flexure_init"}, - {THR_FOCUS_INIT, "focus_init"}, - {THR_POWER_INIT, "power_init"}, - {THR_ACAM_SHUTDOWN, "acam_shutdown"}, - {THR_ANDOR_SHUTDOWN, "andor_shutdown"}, - {THR_SLICECAM_SHUTDOWN, "slicecam_shutdown"}, - {THR_CALIB_SHUTDOWN, "calib_shutdown"}, - {THR_CAMERA_SHUTDOWN, "camera_shutdown"}, - {THR_FLEXURE_SHUTDOWN, "flexure_shutdown"}, - {THR_FOCUS_SHUTDOWN, "focus_shutdown"}, - {THR_SLIT_SHUTDOWN, "slit_shutdown"}, - {THR_TCS_SHUTDOWN, "tcs_shutdown"}, - {THR_POWER_SHUTDOWN, "power_shutdown"}, - {THR_MODIFY_EXPTIME, "modify_exptime"}, - {THR_ACQUISITION, "acquisition"}, - {THR_GUIDING, "guiding"}, - {THR_STARTUP, "startup"}, - {THR_SHUTDOWN, "shutdown"} + const std::map op_names = { + {OP_SEQUENCER_ASYNC_LISTENER, "async_listener"}, + {OP_TRIGGER_EXPOSURE, "trigger_exposure"}, + {OP_EXPOSURE, "exposure"}, + {OP_REPEAT_EXPOSURE, "repeat_exposure"}, + {OP_STOP_EXPOSURE, "stop_exposure"}, + {OP_ABORT_PROCESS, "abort_process"}, + {OP_SEQUENCE_START, "sequence_start"}, + {OP_RUN_SCRIPT, "run_script"}, + {OP_MONITOR_READY_STATE, "monitor_ready_state"}, + {OP_CALIB_SETUP, "calib_setup"}, + {OP_CAMERA_SETUP, "camera_setup"}, + {OP_SLIT_SETUP, "slit_setup"}, + {OP_MOVE_TO_TARGET, "move_to_target"}, + {OP_NOTIFY_TCS, "notify_tcs"}, + {OP_FOCUS_SETUP, "focus_setup"}, + {OP_FLEXURE_SETUP, "flexure_setup"}, + {OP_ACAM_INIT, "acam_init"}, + {OP_ANDOR_INIT, "andor_init"}, + {OP_SLICECAM_INIT, "slicecam_init"}, + {OP_CALIB_INIT, "calib_init"}, + {OP_TCS_INIT, "tcs_init"}, + {OP_SLIT_INIT, "slit_init"}, + {OP_CAMERA_INIT, "camera_init"}, + {OP_FLEXURE_INIT, "flexure_init"}, + {OP_FOCUS_INIT, "focus_init"}, + {OP_POWER_INIT, "power_init"}, + {OP_ACAM_SHUTDOWN, "acam_shutdown"}, + {OP_ANDOR_SHUTDOWN, "andor_shutdown"}, + {OP_SLICECAM_SHUTDOWN, "slicecam_shutdown"}, + {OP_CALIB_SHUTDOWN, "calib_shutdown"}, + {OP_CAMERA_SHUTDOWN, "camera_shutdown"}, + {OP_FLEXURE_SHUTDOWN, "flexure_shutdown"}, + {OP_FOCUS_SHUTDOWN, "focus_shutdown"}, + {OP_SLIT_SHUTDOWN, "slit_shutdown"}, + {OP_TCS_SHUTDOWN, "tcs_shutdown"}, + {OP_POWER_SHUTDOWN, "power_shutdown"}, + {OP_MODIFY_EXPTIME, "modify_exptime"}, + {OP_ACQUISITION, "acquisition"}, + {OP_GUIDING, "guiding"}, + {OP_STARTUP, "startup"}, + {OP_SHUTDOWN, "shutdown"}, + {OP_ACAM_CMD, "acam_cmd"}, + {OP_CALIB_CMD, "calib_cmd"}, + {OP_CAMERA_CMD, "camera_cmd"}, + {OP_FLEXURE_CMD, "flexure_cmd"}, + {OP_FOCUS_CMD, "focus_cmd"}, + {OP_POWER_CMD, "power_cmd"}, + {OP_SLICECAM_CMD, "slicecam_cmd"}, + {OP_SLIT_CMD, "slit_cmd"}, + {OP_TCS_CMD, "tcs_cmd"} }; /***** Sequencer::Sequence **************************************************/ @@ -396,13 +412,13 @@ namespace Sequencer { /** @brief sequencer operation contains status bit, function and params */ struct Operation { - ThreadStatusBits thr; + OperationStatusBits id; std::function func; OperationParams params; const std::string &name() const { static const std::string empty; - auto it = thread_names.find(thr); - return(it==thread_names.end() ? empty : it->second); + auto it = op_names.find(id); + return(it==op_names.end() ? empty : it->second); } int max_attempts=1; int retry_delay=0; @@ -457,7 +473,7 @@ namespace Sequencer { /** @brief key=value params of the currently executing operation * @details set by run() just before op.func() is invoked and consumed * by Sequence:: member functions that accept optional overrides - * (e.g. move_to_target, slit_set) + * (e.g. move_to_target, slit_setup) */ OperationParams current_op_params; @@ -469,22 +485,22 @@ namespace Sequencer { Operation acam_init(); Operation calib_init(); - Operation calib_set(); + Operation calib_setup(); Operation camera_init(); - Operation camera_set(); + Operation camera_setup(); Operation do_expose(); Operation do_shutdown(); Operation do_startup(); Operation flexure_init(); - Operation flexure_set(); + Operation flexure_setup(); Operation focus_init(); - Operation focus_set(); + Operation focus_setup(); Operation move_to_target(); Operation power_init(); Operation repeat_exposure(); Operation slicecam_init(); Operation slit_init(); - Operation slit_set(); + Operation slit_setup(); Operation target_offset(); Operation tcs_init(); }; @@ -543,7 +559,7 @@ namespace Sequencer { init_operation_builders(); seq_state_manager.set_callback([this](const std::bitset& states) { broadcast_seqstate(); }); wait_state_manager.set_callback([this](const std::bitset& states) { broadcast_waitstate(); }); - thread_state_manager.set_callback([this](const std::bitset& states) { publish_threadstate(); }); + op_state_manager.set_callback([this](const std::bitset& states) { publish_threadstate(); }); daemon_manager.set_callback([this](const std::bitset& states) { broadcast_daemonstate(); }); topic_handlers = { @@ -633,10 +649,10 @@ namespace Sequencer { std::mutex seqstate_mtx; std::condition_variable seqstate_cv; - ImprovedStateManager(Sequencer::NUM_THREAD_STATES)> thread_error_manager{ Sequencer::thread_names }; + ImprovedStateManager(Sequencer::NUM_OP_STATES)> op_error_manager{ Sequencer::op_names }; ImprovedStateManager(Sequencer::NUM_SEQ_STATES)> seq_state_manager{Sequencer::seq_state_names}; ImprovedStateManager(Sequencer::NUM_WAIT_STATES)> wait_state_manager{Sequencer::wait_state_names}; - ImprovedStateManager(Sequencer::NUM_THREAD_STATES)> thread_state_manager{ Sequencer::thread_names }; + ImprovedStateManager(Sequencer::NUM_OP_STATES)> op_state_manager{ Sequencer::op_names }; ImprovedStateManager(Sequencer::NUM_DAEMONS)> daemon_manager{ Sequencer::daemon_name }; TargetInfo target; ///< TargetInfo object contains info for a target row and how to read it @@ -857,13 +873,13 @@ namespace Sequencer { long wait_for_canexpose(std::string caller); ///< wait for camera can_expose void sequence_start(std::string obsid_in=""); ///< main sequence start thread. optional obsid_in for single target obs - long calib_set(); ///< sets calib according to target entry params - long camera_set(); ///< sets camera according to target entry params - long slit_set(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode + long calib_setup(); ///< sets calib according to target entry params + long camera_setup(); ///< sets camera according to target entry params + long slit_setup(VirtualSlitMode mode=VSM_DATABASE); ///< sets slit according to target entry params and mode long move_to_target(); ///< sends request to TCS to move to target coords static void dothread_notify_tcs( Sequencer::Sequence &seq ); ///< like move_to_target but for preauth only - long focus_set(); - long flexure_set(); + long focus_setup(); + long flexure_setup(); /** * these are in sequence_acquisition.cpp diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 30458fbb..1b613340 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -19,7 +19,7 @@ namespace Sequencer { const std::string function("Sequencer::Sequence::do_acam_acquire"); std::string reply; - ScopedState thr_state( thread_state_manager, Sequencer::THR_ACQUISITION ); + ScopedState thr_state( op_state_manager, Sequencer::OP_ACQUISITION ); ScopedState wait_state( wait_state_manager, Sequencer::SEQ_WAIT_ACAM_ACQUIRE ); // form and send the ACQUIRE command to ACAM @@ -315,7 +315,7 @@ namespace Sequencer { */ long Sequence::do_target_virtualslit(VirtualSlitMode mode) { if (this->target.iscal) return NO_ERROR; - return this->slit_set(mode); + return this->slit_setup(mode); } /***** Sequencer::Sequence::do_target_virtualslit ****************************/ diff --git a/sequencerd/sequence_operations.cpp b/sequencerd/sequence_operations.cpp index 832fa646..beb2451c 100644 --- a/sequencerd/sequence_operations.cpp +++ b/sequencerd/sequence_operations.cpp @@ -4,7 +4,7 @@ * @author David Hale * * Each Ops factory method here returns a populated Sequence::Operation that - * wraps a Sequence:: member function and identifies itself via its THR_ bit. + * wraps a Sequence:: member function and identifies itself via its OP_ bit. * These are the "Internal" sequencer-level operations (database-driven, * may coordinate multiple daemons). Per-daemon "Passthrough" operations * (e.g. "camera exptime 30000") are built inline by init_operation_builders() @@ -26,7 +26,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::acam_init() { Sequence::Operation op; - op.thr = THR_ACAM_INIT; + op.id = OP_ACAM_INIT; op.func = [this]() { return seq->acam_init(); }; op.params = { }; op.max_attempts = 3; @@ -50,26 +50,26 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::calib_init() { Sequence::Operation op; - op.thr = THR_CALIB_INIT; + op.id = OP_CALIB_INIT; op.func = [this]() { return seq->calib_init(); }; return op; } /***** Sequencer::Sequence::Ops::calib_init ********************************/ - /***** Sequencer::Sequence::Ops::calib_set *********************************/ + /***** Sequencer::Sequence::Ops::calib_setup *********************************/ /** - * @brief defines the calib_set operation + * @brief defines the calib_setup operation * @return Operation * */ - Sequence::Operation Sequence::Ops::calib_set() { + Sequence::Operation Sequence::Ops::calib_setup() { Sequence::Operation op; - op.thr = THR_CALIB_SET; - op.func = [this]() { return seq->calib_set(); }; + op.id = OP_CALIB_SETUP; + op.func = [this]() { return seq->calib_setup(); }; return op; } - /***** Sequencer::Sequence::Ops::calib_set *********************************/ + /***** Sequencer::Sequence::Ops::calib_setup *********************************/ /***** Sequencer::Sequence::Ops::camera_init *******************************/ @@ -80,26 +80,26 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::camera_init() { Sequence::Operation op; - op.thr = THR_CAMERA_INIT; + op.id = OP_CAMERA_INIT; op.func = [this]() { return seq->camera_init(); }; return op; } /***** Sequencer::Sequence::Ops::camera_init *******************************/ - /***** Sequencer::Sequence::Ops::camera_set ********************************/ + /***** Sequencer::Sequence::Ops::camera_setup ********************************/ /** - * @brief defines the camera_set operation + * @brief defines the camera_setup operation * @return Operation * */ - Sequence::Operation Sequence::Ops::camera_set() { + Sequence::Operation Sequence::Ops::camera_setup() { Sequence::Operation op; - op.thr = THR_CAMERA_SET; - op.func = [this]() { return seq->camera_set(); }; + op.id = OP_CAMERA_SETUP; + op.func = [this]() { return seq->camera_setup(); }; return op; } - /***** Sequencer::Sequence::Ops::camera_set ********************************/ + /***** Sequencer::Sequence::Ops::camera_setup ********************************/ /***** Sequencer::Sequence::Ops::do_expose *********************************/ @@ -110,7 +110,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::do_expose() { Sequence::Operation op; - op.thr = THR_EXPOSURE; + op.id = OP_EXPOSURE; op.func = [this]() { const std::string caller("Sequencer::Sequence::Ops::do_expose"); return seq->do_exposure(caller); @@ -128,7 +128,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::do_shutdown() { Sequence::Operation op; - op.thr = THR_SHUTDOWN; + op.id = OP_SHUTDOWN; op.func = [this]() { return seq->shutdown(); }; return op; } @@ -143,7 +143,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::do_startup() { Sequence::Operation op; - op.thr = THR_STARTUP; + op.id = OP_STARTUP; op.func = [this]() { return seq->startup(); }; return op; } @@ -158,26 +158,26 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::flexure_init() { Sequence::Operation op; - op.thr = THR_FLEXURE_INIT; + op.id = OP_FLEXURE_INIT; op.func = [this]() { return seq->flexure_init(); }; return op; } /***** Sequencer::Sequence::Ops::flexure_init ******************************/ - /***** Sequencer::Sequence::Ops::flexure_set *******************************/ + /***** Sequencer::Sequence::Ops::flexure_setup *******************************/ /** - * @brief defines the flexure_set operation + * @brief defines the flexure_setup operation * @return Operation * */ - Sequence::Operation Sequence::Ops::flexure_set() { + Sequence::Operation Sequence::Ops::flexure_setup() { Sequence::Operation op; - op.thr = THR_FLEXURE_SET; - op.func = [this]() { return seq->flexure_set(); }; + op.id = OP_FLEXURE_SETUP; + op.func = [this]() { return seq->flexure_setup(); }; return op; } - /***** Sequencer::Sequence::Ops::flexure_set *******************************/ + /***** Sequencer::Sequence::Ops::flexure_setup *******************************/ /***** Sequencer::Sequence::Ops::focus_init ********************************/ @@ -188,26 +188,26 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::focus_init() { Sequence::Operation op; - op.thr = THR_FOCUS_INIT; + op.id = OP_FOCUS_INIT; op.func = [this]() { return seq->focus_init(); }; return op; } /***** Sequencer::Sequence::Ops::focus_init ********************************/ - /***** Sequencer::Sequence::Ops::focus_set *********************************/ + /***** Sequencer::Sequence::Ops::focus_setup *********************************/ /** - * @brief defines the focus_set operation + * @brief defines the focus_setup operation * @return Operation * */ - Sequence::Operation Sequence::Ops::focus_set() { + Sequence::Operation Sequence::Ops::focus_setup() { Sequence::Operation op; - op.thr = THR_FOCUS_SET; - op.func = [this]() { return seq->focus_set(); }; + op.id = OP_FOCUS_SETUP; + op.func = [this]() { return seq->focus_setup(); }; return op; } - /***** Sequencer::Sequence::Ops::focus_set *********************************/ + /***** Sequencer::Sequence::Ops::focus_setup *********************************/ /***** Sequencer::Sequence::Ops::move_to_target ****************************/ @@ -224,7 +224,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::move_to_target() { Sequence::Operation op; - op.thr = THR_MOVE_TO_TARGET; + op.id = OP_MOVE_TO_TARGET; op.func = [this]() { return seq->move_to_target(); }; return op; } @@ -239,7 +239,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::power_init() { Sequence::Operation op; - op.thr = THR_POWER_INIT; + op.id = OP_POWER_INIT; op.func = [this]() { return seq->power_init(); }; return op; } @@ -254,7 +254,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::repeat_exposure() { Sequence::Operation op; - op.thr = THR_REPEAT_EXPOSURE; + op.id = OP_REPEAT_EXPOSURE; op.func = [this]() { return seq->repeat_exposure(); }; return op; } @@ -269,7 +269,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::slicecam_init() { Sequence::Operation op; - op.thr = THR_SLICECAM_INIT; + op.id = OP_SLICECAM_INIT; op.func = [this]() { return seq->slicecam_init(); }; op.params = { }; op.max_attempts = 3; @@ -293,33 +293,33 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::slit_init() { Sequence::Operation op; - op.thr = THR_SLIT_INIT; + op.id = OP_SLIT_INIT; op.func = [this]() { return seq->slit_init(); }; return op; } /***** Sequencer::Sequence::Ops::slit_init *********************************/ - /***** Sequencer::Sequence::Ops::slit_set **********************************/ + /***** Sequencer::Sequence::Ops::slit_setup **********************************/ /** - * @brief defines the slit_set operation + * @brief defines the slit_setup operation * @details Reads optional mode= param from seq->current_op_params. When * absent the default VSM_DATABASE mode is used (drives slit from * database target entry). * @return Operation * */ - Sequence::Operation Sequence::Ops::slit_set() { + Sequence::Operation Sequence::Ops::slit_setup() { Sequence::Operation op; - op.thr = THR_SLIT_SET; + op.id = OP_SLIT_SETUP; op.func = [this]() { const size_t mode = seq->current_op_params.get("mode", static_cast(VSM_DATABASE)); - return seq->slit_set(static_cast(mode)); + return seq->slit_setup(static_cast(mode)); }; return op; } - /***** Sequencer::Sequence::Ops::slit_set **********************************/ + /***** Sequencer::Sequence::Ops::slit_setup **********************************/ /***** Sequencer::Sequence::Ops::target_offset *****************************/ @@ -330,7 +330,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::target_offset() { Sequence::Operation op; - op.thr = THR_MOVE_TO_TARGET; // no dedicated THR_ for target_offset; reuse move_to_target identity + op.id = OP_MOVE_TO_TARGET; // no dedicated OP_ for target_offset; reuse move_to_target identity op.func = [this]() { return seq->target_offset(); }; return op; } @@ -345,7 +345,7 @@ namespace Sequencer { */ Sequence::Operation Sequence::Ops::tcs_init() { Sequence::Operation op; - op.thr = THR_TCS_INIT; + op.id = OP_TCS_INIT; op.func = [this]() { return seq->tcs_init(); }; return op; } From 8f0df38390b6a4d68f0b8d509b54ebcbe6aeeb72 Mon Sep 17 00:00:00 2001 From: David Hale Date: Fri, 15 May 2026 09:53:56 -0700 Subject: [PATCH 37/37] fix: correct seq state on subsystem error fix: was missing an error broadcast --- sequencerd/sequence.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sequencerd/sequence.cpp b/sequencerd/sequence.cpp index 873165b5..1fc11393 100644 --- a/sequencerd/sequence.cpp +++ b/sequencerd/sequence.cpp @@ -1441,8 +1441,8 @@ namespace Sequencer { } // end while true if ( this->op_error_manager.are_any_set() ) { - logwrite(function, "ERROR stopping sequencer due to error in: "+ - this->op_error_manager.get_set_states()); + this->broadcast.error( function, "sequence completed with errors in: "+ + this->op_error_manager.get_set_states() ); // If this target was flagged as active, then change it to unassigned on error. if ( this->target.get_next( Sequencer::TARGET_ACTIVE, targetstatus ) == TargetInfo::TARGET_FOUND ) { this->target.update_state( Sequencer::TARGET_UNASSIGNED ); @@ -1451,6 +1451,14 @@ namespace Sequencer { this->do_once.store(true); } + // Override default exit state (SEQ_READY) if any subsystem daemon is down. + // broadcast_daemonstate() suppresses NOTREADY transitions during SEQ_RUNNING, + // so we must check daemon_manager here on the way out. + // + if ( !this->daemon_manager.are_all_set() ) { + seq_state.destruct_set( Sequencer::SEQ_NOTREADY ); + } + logwrite( function, "target list processing has stopped" ); return; }