From 27c80d8ae2d3c46294cff65eb78d64cc56c6bc4c Mon Sep 17 00:00:00 2001 From: prkrtg Date: Tue, 7 Apr 2026 11:24:29 -1000 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] * 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/18] 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/18] 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/18] 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/18] 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/18] * 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/18] 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/18] 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/18] 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/18] 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. //