-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathearthsat.cpp
More file actions
2384 lines (2010 loc) · 77.3 KB
/
earthsat.cpp
File metadata and controls
2384 lines (2010 loc) · 77.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
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
/* manage selection and display of one earth sat.
*
* we call "pass" the overhead view shown in dx_info_b, "path" the orbit shown on the map.
*
* N.B. our satellite info server changes blanks to underscores in sat names.
* N.B. we always assign sat_state[0] first then sat_state[1] only if want a second sat
*/
#include "HamClock.h"
#define MAX_ACTIVE_SATS 2 // increasing this works here but requires more colors
bool dx_info_for_sat; // global to indicate whether dx_info_b is for DX info or sat info
// path drawing
#define MAX_PATHPTS 512 // N.B. MAX_PATHPTS must be power of 2 for dashed lines to work right
#define FOOT_ALT0 250 // n points in altitude 0 foot print
#define FOOT_ALT30 100 // n points in altitude 30 foot print
#define FOOT_ALT60 75 // n points in altitude 60 foot print
#define N_FOOT 3 // number of footprint altitude loci
#define ARROW_N 25 // n arrows
#define ARROW_EVERY (MAX_PATHPTS/ARROW_N) // one arrow every these many steps
#define ARROW_L 15 // arrow length, canonical pixels
// layout
#define ALARM_DT (1.0F/1440.0F) // flash this many days before an event
#define SATLED_RISING_HZ 1 // flash at this rate when sat about to rise
#define SATLED_SETTING_HZ 2 // flash at this rate when sat about to set
#define SAT_TOUCH_R 20U // touch radius, pixels
#define SAT_UP_R 2 // dot radius when up
#define PASS_STEP 10.0F // pass step size, seconds
#define TBORDER 50 // top border
#define FONT_H (dx_info_b.h/6) // height for SMALL_FONT
#define FONT_D 5 // font descent
#define SAT_COLOR RA8875_WHITE // overall annotation color
#define BTN_COLOR RA8875_GREEN // button fill color
#define SATUP_COLOR RGB565(0,200,0) // time color when sat is up
#define SOON_COLOR RGB565(200,0,0) // table text color for pass soon
#define SOON_MINS 10 // "soon", minutes
#define CB_SIZE 20 // size of selection check box
#define CELL_H 32 // display cell height
#define N_COLS 4 // n cols in name table
#define CELL_W (800/N_COLS) // display cell width
#define N_ROWS ((480-TBORDER)/CELL_H) // n rows in name table
#define MAX_NSAT (N_ROWS*N_COLS) // max names we can display
#define MAX_PASS_STEPS 30 // max lines to draw for pass map
#define OFFSCRN 20000 // x or y coord that is definitely off screen
static SBox ok_b = {730,10,55,35}; // Ok button
// NV_SATnFLAGS bit masks
typedef enum {
SF_PATH_MASK = 1,
} SatFlags;
// used so findNextPass() can be used for contexts other than the current sat now
typedef struct {
DateTime rise_time, set_time; // next pass times
bool rise_ok, set_ok; // whether rise_time and set_time are valid
float rise_az, set_az; // rise and set az, degrees, if valid
bool ever_up, ever_down; // whether sat is ever above or below SAT_MIN_EL in next day
} SatRiseSet;
// handy pass states from findPassState()
typedef enum {
PS_NONE, // no sat rise/set in play or unknown
PS_UPSOON, // pass lies ahead
PS_UPNOW, // pass in progress
PS_HASSET, // down after being up
} PassState;
// files
static const char esat_ufn[] = "user-esats.txt"; // name of user's tle file
static const char esat_sfn[] = "esats.txt"; // local cached file from server
static const char esat_url[] = "/esats/esats.txt"; // server file URL
#define MAX_CACHE_AGE 10000 // max cache age, seconds
// used by readNextSat()
typedef enum {
RNS_INIT = 0, // check user's list
RNS_SERVER, // check backend list
RNS_DONE // checked both
} RNS_t;
// foot configuration
static const uint16_t max_foot[N_FOOT] = {FOOT_ALT0, FOOT_ALT30, FOOT_ALT60}; // max dots on each altitude
static const float foot_alts[N_FOOT] = {0.0F, 30.0F, 60.0F}; // alt of each segment, degs
// state
typedef struct {
Satellite *sat; // satellite definition, NULL if inactive
SatRiseSet rs; // event info
SCoord *path; // full res coords for orbit, [0] always now
int n_path; // n in path[]
SCoord *foot[N_FOOT]; // full res coords for each footprint altitude
int n_foot[N_FOOT]; // n in each foot[]
bool show_path; // whether to pass as well as foot
SBox name_b; // canonical coords of name on map
char name[NV_SATNAME_LEN]; // name, spaces are underscores
NV_Name nv_name; // NV property for persistent name
NV_Name nv_flags; // NV property for persistent option flags
ColorSelection cs; // path control
} SatState;
static SatState sat_state[MAX_ACTIVE_SATS]; // [1].sat is set only if [0].sat is also set
static bool new_pass; // set when new pass is ready
#define NO_CUR_SAT (-1) // flag for currentSat and dxpaneSat
// index of sat_state to be shown in the DX pane, else NO_CUR_SAT
static int dxpaneSat = NO_CUR_SAT;
// return whether the given sat_state has a defined name
static inline bool SAT_NAME_IS_SET (SatState &s) { return s.name[0] != '\0'; }
// number of sat_states in use.
// N.B we always assign [0] first then [1] only if want a second sat
static inline int nActiveSats(void) { return sat_state[1].sat ? 2 : (sat_state[0].sat ? 1 : 0); }
// current observer (same for all sats)
static Observer *obs; // DE
#if defined(__GNUC__)
static void fatalSatError (const char *fmt, ...) __attribute__ ((format (__printf__, 1, 2)));
#else
static void fatalSatError (const char *fmt, ...);
#endif
/* return all IO pins to quiescent state
*/
void satResetIO()
{
disableBlinker (SATALARM_PIN);
}
/* set alarm SATALARM_PIN flashing with the given frequency or one of BLINKER_*.
*/
static void risetAlarm (int hz)
{
// insure helper thread is running
startBinkerThread (SATALARM_PIN, false); // on is hi
// tell helper thread what we want done
setBlinkerRate (SATALARM_PIN, hz);
}
/* return index of sat_state considered "current" by outside systems such as gimbal, or NO_CUR_SAT if none.
* N.B. [0] can still be considered current even if none are shown in the DX pane.
*/
static int currentSat(void)
{
if (sat_state[0].sat && dxpaneSat == 0)
return 0;
if (sat_state[1].sat && dxpaneSat == 1)
return 1;
// assign one?
if (dxpaneSat == NO_CUR_SAT) {
if (sat_state[0].sat) {
dxpaneSat = 0;
return 0;
}
if (sat_state[1].sat) {
dxpaneSat = 1;
return 1;
}
}
return (NO_CUR_SAT);
}
/* completely undefine and reclaim memory for the given sat
*/
static void unsetSat (SatState &s)
{
// reset sat and its path
if (s.sat) {
delete s.sat;
s.sat = NULL;
}
if (s.path) {
free (s.path);
s.path = NULL;
}
for (int i = 0; i < N_FOOT; i++) {
if (s.foot[i]) {
free (s.foot[i]);
s.foot[i] = NULL;
}
}
// reset name and flags here and in NV
s.name[0] = '\0';
NVWriteString (s.nv_name, s.name);
// no more sat if last one
if (nActiveSats() == 0) {
dx_info_for_sat = false;
risetAlarm (BLINKER_OFF);
}
}
/* fill s.foot with loci of points that see the sat at various viewing altitudes.
* N.B. call this before updateSatPath malloc's its memory
*/
static void updateFootPrint (SatState &s, float satlat, float satlng)
{
// complement of satlat
float cosc = sinf(satlat);
float sinc = cosf(satlat);
// fill each segment along each altitude
for (uint8_t alt_i = 0; alt_i < N_FOOT; alt_i++) {
// start with max n points
int n_malloc = max_foot[alt_i]*sizeof(SCoord);
s.foot[alt_i] = (SCoord *) realloc (s.foot[alt_i], n_malloc);
if (!s.foot[alt_i] && n_malloc > 0)
fatalError ("no memort for sat foot: %d", n_malloc);
// satellite viewing altitude
float valt = deg2rad(foot_alts[alt_i]);
// great-circle radius from subsat point to viewing circle at altitude valt
float vrad = s.sat->viewingRadius(valt);
// compute each unique point around viewing circle
uint16_t n_foot = 0;
uint16_t m = max_foot[alt_i];
for (uint16_t foot_i = 0; foot_i < m; foot_i++) {
// compute next point
float cosa, B;
float A = foot_i*2*M_PI/m;
solveSphere (A, vrad, cosc, sinc, &cosa, &B);
float vlat = M_PIF/2-acosf(cosa);
float vlng = fmodf(B+satlng+5*M_PIF,2*M_PIF)-M_PIF; // require [-180.180)
ll2sRaw (vlat, vlng, s.foot[alt_i][n_foot], 2);
// skip duplicate points
if (n_foot == 0 || memcmp (&s.foot[alt_i][n_foot], &s.foot[alt_i][n_foot-1], sizeof(SCoord)))
n_foot++;
}
// reduce memory to only points actually used
s.n_foot[alt_i] = n_foot;
s.foot[alt_i] = (SCoord *) realloc (s.foot[alt_i], n_foot*sizeof(SCoord));
// Serial.printf ("alt %g: n_foot %u / %u\n", foot_alts[alt_i], n, m);
}
}
/* return a DateTime for the given time
*/
static DateTime userDateTime(time_t t)
{
int yr = year(t);
int mo = month(t);
int dy = day(t);
int hr = hour(t);
int mn = minute(t);
int sc = second(t);
DateTime dt(yr, mo, dy, hr, mn, sc);
return (dt);
}
/* find next rise and set times if sat valid starting from the given time_t.
* always find rise and set in the future, so set_time will be < rise_time iff pass is in progress.
* also update flags ever_up, set_ok, ever_down and rise_ok.
* name is only used for local logging, set to NULL to avoid even this.
*/
static void findNextPass (Satellite *sat, const char *name, time_t t, SatRiseSet &rs)
{
if (!sat || !obs) {
rs.set_ok = rs.rise_ok = false;
return;
}
// measure how long this takes
uint32_t t0 = millis();
#define COARSE_DT 90L // seconds/step forward for fast search
#define FINE_DT (-2L) // seconds/step backward for refined search
float pel; // previous elevation
long dt = COARSE_DT; // search time step size, seconds
DateTime t_now = userDateTime(t); // search starting time
DateTime t_srch = t_now + -FINE_DT; // search time, start beyond any previous solution
float tel, taz, trange, trate; // target el and az, degrees
// init pel and make first step
sat->predict (t_srch);
sat->topo (obs, pel, taz, trange, trate);
t_srch += dt;
// search up to a few days ahead for next rise and set times (for example for moon)
rs.set_ok = rs.rise_ok = false;
rs.ever_up = rs.ever_down = false;
while ((!rs.set_ok || !rs.rise_ok) && t_srch < t_now + 2.0F) {
// find circumstances at time t_srch
sat->predict (t_srch);
sat->topo (obs, tel, taz, trange, trate);
// check for rising or setting events
if (tel >= SAT_MIN_EL) {
rs.ever_up = true;
if (pel < SAT_MIN_EL) {
if (dt == FINE_DT) {
// found a refined set event (recall we are going backwards),
// record and resume forward time.
rs.set_time = t_srch;
rs.set_az = taz;
rs.set_ok = true;
dt = COARSE_DT;
pel = tel;
} else if (!rs.rise_ok) {
// found a coarse rise event, go back slower looking for better set
dt = FINE_DT;
pel = tel;
}
}
} else {
rs.ever_down = true;
if (pel > SAT_MIN_EL) {
if (dt == FINE_DT) {
// found a refined rise event (recall we are going backwards).
// record and resume forward time but skip if set is within COARSE_DT because we
// would jump over it and find the NEXT set.
float check_tel, check_taz;
DateTime check_set = t_srch + COARSE_DT;
sat->predict (check_set);
sat->topo (obs, check_tel, check_taz, trange, trate);
if (check_tel >= SAT_MIN_EL) {
rs.rise_time = t_srch;
rs.rise_az = taz;
rs.rise_ok = true;
}
// regardless, resume forward search
dt = COARSE_DT;
pel = tel;
} else if (!rs.set_ok) {
// found a coarse set event, go back slower looking for better rise
dt = FINE_DT;
pel = tel;
}
}
}
// Serial.printf ("R %d S %d dt %ld from_now %8.3fs tel %g\n", rs.rise_ok, rs.set_ok, dt, SECSPERDAY*(t_srch - t_now), tel);
// advance time and save tel
t_srch += dt;
pel = tel;
}
// new pass ready
new_pass = true;
if (name) {
int yr;
uint8_t mo, dy, hr, mn, sc;
t_now.gettime(yr, mo, dy, hr, mn, sc);
Serial.printf (
"SAT: %*s @ %04d-%02d-%02d %02d:%02d:%02d next rise in %6.3f hrs, set in %6.3f (%u ms)\n",
NV_SATNAME_LEN, name, yr, mo, dy, hr, mn,sc,
rs.rise_ok ? 24*(rs.rise_time - t_now) : 0.0F, rs.set_ok ? 24*(rs.set_time - t_now) : 0.0F,
millis() - t0);
}
}
/* display next pass for sat in sky dome.
* N.B. we assume findNextPass has been called to fill sat_rs
*/
static void drawSatSkyDome (SatState &s)
{
// size and center of screen path
uint16_t r0 = satpass_c.r;
uint16_t xc = satpass_c.s.x;
uint16_t yc = satpass_c.s.y;
// erase sky dome
tft.fillRect (dx_info_b.x+1, dx_info_b.y+2*FONT_H+1, dx_info_b.w-2, dx_info_b.h-2*FONT_H-1, RA8875_BLACK);
// skip if no sat or never up
if (!s.sat || !obs || !s.rs.ever_up)
return;
// find n steps, step duration and starting time
bool full_pass = false;
int n_steps = 0;
float step_dt = 0;
DateTime t;
if (s.rs.rise_ok && s.rs.set_ok) {
// find start and pass duration in days
float pass_duration = s.rs.set_time - s.rs.rise_time;
if (pass_duration < 0) {
// rise after set means pass is underway so start now for remaining duration
DateTime t_now = userDateTime(nowWO());
pass_duration = s.rs.set_time - t_now;
t = t_now;
} else {
// full pass so start at next rise
t = s.rs.rise_time;
full_pass = true;
}
// find step size and number of steps
n_steps = pass_duration/(PASS_STEP/SECSPERDAY) + 1;
if (n_steps > MAX_PASS_STEPS)
n_steps = MAX_PASS_STEPS;
step_dt = pass_duration/n_steps;
} else {
// it doesn't actually rise or set within the next 24 hour but it's up some time
// so just show it at its current position (if it's up)
n_steps = 1;
step_dt = 0;
t = userDateTime(nowWO());
}
// draw horizon and compass points
#define HGRIDCOL RGB565(50,90,50)
tft.drawCircle (xc, yc, r0, BRGRAY);
for (float a = 0; a < 2*M_PIF; a += M_PIF/6) {
uint16_t xr = lroundf(xc + r0*cosf(a));
uint16_t yr = lroundf(yc - r0*sinf(a));
tft.drawLine (xc, yc, xr, yr, HGRIDCOL);
tft.fillCircle (xr, yr, 1, RA8875_WHITE);
}
// draw elevations
for (uint8_t el = 30; el < 90; el += 30)
tft.drawCircle (xc, yc, r0*(90-el)/90, HGRIDCOL);
// label sky directions
selectFontStyle (LIGHT_FONT, FAST_FONT);
tft.setTextColor (BRGRAY);
tft.setCursor (xc - r0, yc - r0 + 2);
tft.print ("NW");
tft.setCursor (xc + r0 - 12, yc - r0 + 2);
tft.print ("NE");
tft.setCursor (xc - r0, yc + r0 - 8);
tft.print ("SW");
tft.setCursor (xc + r0 - 12, yc + r0 - 8);
tft.print ("SE");
// connect several points from t until s.rs.set_time, find max elevation for labeling
float max_el = 0;
uint16_t max_el_x = 0, max_el_y = 0;
uint16_t prev_x = 0, prev_y = 0;
for (uint8_t i = 0; i < n_steps; i++) {
// find topocentric position @ t
float el, az, range, rate;
s.sat->predict (t);
s.sat->topo (obs, el, az, range, rate);
if (el < 0 && n_steps == 1)
break; // only showing pos now but it's down
// find screen postion
float r = r0*(90-el)/90; // screen radius, zenith at center
uint16_t x = xc + r*sinf(deg2rad(az)) + 0.5F; // want east right
uint16_t y = yc - r*cosf(deg2rad(az)) + 0.5F; // want north up
// find max el
if (el > max_el) {
max_el = el;
max_el_x = x;
max_el_y = y;
}
// connect if have prev or just dot if only one
if (i > 0 && (prev_x != x || prev_y != y)) // avoid bug with 0-length line
tft.drawLine (prev_x, prev_y, x, y, SAT_COLOR);
else if (n_steps == 1)
tft.fillCircle (x, y, SAT_UP_R, SAT_COLOR);
// label the set end if last step of several and full pass
if (full_pass && i == n_steps - 1) {
// x,y is very near horizon, try to move inside a little for clarity
x += x > xc ? -12 : 2;
y += y > yc ? -8 : 4;
tft.setCursor (x, y);
tft.print('S');
}
// save
prev_x = x;
prev_y = y;
// next t
t += step_dt;
}
// label max elevation and time up iff we have a full pass
if (max_el > 0 && full_pass) {
// max el
uint16_t x = max_el_x, y = max_el_y;
bool draw_left_of_pass = max_el_x > xc;
bool draw_below_pass = max_el_y < yc;
x += draw_left_of_pass ? -30 : 20;
y += draw_below_pass ? 5 : -18;
tft.setCursor (x, y);
tft.print(max_el, 0);
tft.drawCircle (tft.getCursorX()+2, tft.getCursorY(), 1, BRGRAY); // simple degree symbol
// pass duration
int s_up = (s.rs.set_time - s.rs.rise_time)*SECSPERDAY;
char tup_str[32];
if (s_up >= 3600) {
int h = s_up/3600;
int m = (s_up - 3600*h)/60;
snprintf (tup_str, sizeof(tup_str), "%dh%02d", h, m);
} else {
int m = s_up/60;
int s = s_up - 60*m;
snprintf (tup_str, sizeof(tup_str), "%d:%02d", m, s);
}
uint16_t bw = getTextWidth (tup_str);
if (draw_left_of_pass)
x = tft.getCursorX() - bw + 4; // account for deg
y += draw_below_pass ? 12 : -11;
tft.setCursor (x, y);
tft.print(tup_str);
}
}
/* draw name of s IFF used in dx_info box
*/
static void drawSatName (SatState &s)
{
if (!s.sat || !obs || !SAT_NAME_IS_SET(s) || !dx_info_for_sat || SHOWING_PANE_0())
return;
// retrieve saved name without '_'
char user_name[NV_SATNAME_LEN];
strncpySubChar (user_name, s.name, ' ', '_', NV_SATNAME_LEN);
// shorten until fits in satname_b
selectFontStyle (LIGHT_FONT, SMALL_FONT);
uint16_t bw = maxStringW (user_name, satname_b.w);
// draw
tft.setTextColor (SAT_COLOR);
fillSBox (satname_b, RA8875_BLACK);
tft.setCursor (satname_b.x + (satname_b.w - bw)/2, satname_b.y+FONT_H - 2);
tft.print (user_name);
}
/* set s.name_b with where sat name should go on map, else s.name_b.x = 0
*/
static void setSatMapNameLoc (SatState &s)
{
// set size
selectFontStyle (LIGHT_FONT, FAST_FONT);
s.name_b.w = getTextWidth(s.name) + 4;
s.name_b.h = 11;
// try near current location but beware edges, use canonical units
s.name_b.x = s.path[0].x/tft.SCALESZ;
s.name_b.y = s.path[0].y/tft.SCALESZ;
if (s.name_b.x) {
s.name_b.x = CLAMPF (s.name_b.x, map_b.x + 10, map_b.x + map_b.w - s.name_b.w - 10);
s.name_b.y = CLAMPF (s.name_b.y, map_b.y + 10, map_b.y + map_b.h - s.name_b.h - 10);
}
// one last check for over usable map
if (!overMap (s.name_b))
s.name_b.x = 0;
}
/* mark current sat pass location
*/
static void drawSatPassMarker()
{
SatNow satnow;
getSatNow (satnow);
// size and center of screen path
uint16_t r0 = satpass_c.r;
uint16_t xc = satpass_c.s.x;
uint16_t yc = satpass_c.s.y;
float r = r0*(90-satnow.el)/90; // screen radius, zenith at center
uint16_t x = xc + r*sinf(deg2rad(satnow.az)) + 0.5F; // want east right
uint16_t y = yc - r*cosf(deg2rad(satnow.az)) + 0.5F; // want north up
if (y + SAT_UP_R < tft.height() - 1) // beware lower edge
tft.fillCircle (x, y, SAT_UP_R, SAT_COLOR);
}
/* draw event label with time dt and current az/el in the dx_info box unless dt < 0 then just show label.
* dt is in days: if > 1 hour show HhM else M:S
*/
static void drawSatTime (SatState &s, const char *label, uint16_t color, float event_dt, float event_az)
{
if (!s.sat)
return;
// layout
const uint16_t fast_h = 10; // spacing for FAST_FONT
const uint16_t rs_y = dx_info_b.y+FONT_H + 4; // below name
const uint16_t azel_y = rs_y + fast_h;
const uint16_t age_y = azel_y + fast_h;
// erase drawing area
tft.fillRect (dx_info_b.x+1, rs_y-2, dx_info_b.w-2, 3*fast_h+2, RA8875_BLACK);
// tft.drawRect (dx_info_b.x+1, rs_y-2, dx_info_b.w-2, 3*fast_h+2, RA8875_GREEN); // RBF
tft.setTextColor (color);
// draw
if (event_dt >= 0) {
// fast font
selectFontStyle (LIGHT_FONT, FAST_FONT);
// format time as HhM else M:S
event_dt *= 24; // event_dt is now hours
int a, b;
char sep;
formatSexa (event_dt, a, sep, b);
// build label + time + az
char str[100];
snprintf (str, sizeof(str), "%s %2d%c%02d @ %.0f", label, a, sep, b, event_az);
uint16_t s_w = getTextWidth(str);
tft.setCursor (dx_info_b.x + (dx_info_b.w-s_w)/2, rs_y);
tft.print(str);
// draw az and el
DateTime t_now = userDateTime(nowWO());
float el, az, range, rate;
s.sat->predict (t_now);
s.sat->topo (obs, el, az, range, rate);
snprintf (str, sizeof(str), "Az: %.0f El: %.0f", az, el);
tft.setCursor (dx_info_b.x + (dx_info_b.w - getTextWidth(str))/2, azel_y);
tft.printf (str);
// draw TLE age
DateTime t_sat = s.sat->epoch();
snprintf (str, sizeof(str), "TLE Age %.1f days", t_now-t_sat);
uint16_t a_w = getTextWidth(str);
tft.setCursor (dx_info_b.x + (dx_info_b.w-a_w)/2, age_y);
tft.print(str);
} else {
// just draw label centered across entire box
selectFontStyle (LIGHT_FONT, SMALL_FONT); // larger font
uint16_t s_w = getTextWidth(label);
tft.setCursor (dx_info_b.x + (dx_info_b.w-s_w)/2, rs_y + FONT_H - FONT_D);
tft.print(label);
}
}
/* return whether the given line appears to be a valid TLE
* only count digits and '-' counts as 1
*/
static bool tleHasValidChecksum (const char *line)
{
// sum first 68 chars
int sum = 0;
for (uint8_t i = 0; i < 68; i++) {
char c = *line++;
if (c == '-')
sum += 1;
else if (c == '\0')
return (false); // too short
else if (c >= '0' && c <= '9')
sum += c - '0';
}
// last char is sum of previous modulo 10
return ((*line - '0') == (sum%10));
}
/* clear screen, show the given message then restart operation after user ack.
*/
static void fatalSatError (const char *fmt, ...)
{
// common prefix
char buf[65] = "Sat error: "; // max on one line
va_list ap;
// format message to fit after prefix
int prefix_l = strlen (buf);
va_start (ap, fmt);
vsnprintf (buf+prefix_l, sizeof(buf)-prefix_l, fmt, ap);
va_end (ap);
// log
Serial.println (buf);
// clear screen and show message centered
eraseScreen();
selectFontStyle (BOLD_FONT, SMALL_FONT);
uint16_t mw = getTextWidth (buf);
tft.setTextColor (RA8875_WHITE);
tft.setCursor ((tft.width()-mw)/2, tft.height()/3);
tft.print (buf);
// ok button
SBox ok_b;
const char button_msg[] = "Continue";
uint16_t bw = getTextWidth(button_msg);
ok_b.x = (tft.width() - bw)/2;
ok_b.y = tft.height() - 40;
ok_b.w = bw + 30;
ok_b.h = 35;
drawStringInBox (button_msg, ok_b, false, RA8875_WHITE);
// wait forever for user to do anything
UserInput ui = {
ok_b,
UI_UFuncNone,
UF_UNUSED,
UI_NOTIMEOUT,
UF_NOCLOCKS,
{0, 0}, TT_NONE, '\0', false, false
};
(void) waitForUser (ui);
// restart without sats
for (int i = 0; i < MAX_ACTIVE_SATS; i++)
unsetSat (sat_state[i]);
initScreen();
}
/* return whether sat epoch is known to be good at the given time.
*/
static bool satEpochOk (Satellite *sat, const char *name, time_t t)
{
if (!sat)
return (false);
DateTime t_now = userDateTime(t);
DateTime t_sat = sat->epoch();
// N.B. can not use isSatMoon because sat_name is not set
float max_age = strcasecmp(name,"Moon") == 0 ? 1.5F : maxTLEAgeDays();
bool ok = t_sat + max_age > t_now && t_now + max_age > t_sat;
if (!ok) {
int year;
uint8_t mon, day, h, m, s;
Serial.printf ("SAT: %s age %g > %g days:\n", name, t_now - t_sat, max_age);
t_now.gettime (year, mon, day, h, m, s);
Serial.printf ("SAT: Ep: now = %d-%02d-%02d %02d:%02d:%02d\n", year, mon, day, h, m, s);
t_sat.gettime (year, mon, day, h, m, s);
Serial.printf ("SAT: sat = %d-%02d-%02d %02d:%02d:%02d\n", year, mon, day, h, m, s);
}
return (ok);
}
/* each call returns the next TLE from user's file then seamlessly from the backend server.
* first time: call with fp = NULL and state = RNS_INIT then leave them alone for us to manage.
* we return true if another TLE was found from either source, else false with fp closed.
* N.B. if caller wants to stop calling us before we return false, they must fclose(fp) if it's != NULL.
* N.B. name[] will be in the internal '_' format
*/
static bool readNextSat (FILE *&fp, RNS_t &state,
char name[NV_SATNAME_LEN], char t1[TLE_LINEL], char t2[TLE_LINEL])
{
// prep for user, then server, then done.
next:
if (state == RNS_INIT) {
if (!fp) {
fp = fopenOurs (esat_ufn, "r");
if (!fp) {
if (debugLevel (DEBUG_ESATS, 1))
Serial.printf ("SAT: %s: %s\n", esat_ufn, strerror (errno));
state = RNS_SERVER;
}
}
}
if (state == RNS_SERVER) {
if (!fp) {
fp = openCachedFile (esat_sfn, esat_url, MAX_CACHE_AGE, 0); // ok if empty
if (!fp) {
Serial.printf ("SAT: no server sats file\n");
state = RNS_DONE;
}
}
}
if (state == RNS_DONE)
return (false);
// find next 3 lines other than comments or blank
int n_found;
for (n_found = 0; n_found < 3; ) {
// read next useful line
char line[TLE_LINEL+10];
if (fgets (line, sizeof(line), fp) == NULL)
break;
chompString(line);
if (line[0] == '#' || line[0] == '\0')
continue;
// assign
switch (n_found) {
case 0:
line[NV_SATNAME_LEN-1] = '\0';
strTrimAll(line);
strncpySubChar (name, line, '_', ' ', NV_SATNAME_LEN); // internal name form
n_found++;
break;
case 1:
strTrimEnds(line);
quietStrncpy (t1, line, TLE_LINEL);
n_found++;
break;
case 2:
strTrimEnds(line);
quietStrncpy (t2, line, TLE_LINEL);
n_found++;
break;
}
}
if (n_found == 3) {
if (debugLevel (DEBUG_ESATS, 1)) {
Serial.printf ("SAT: found TLE from %s:\n", state == RNS_INIT ? "user" : "server");
Serial.printf (" '%s'\n", name);
Serial.printf (" '%s'\n", t1);
Serial.printf (" '%s'\n", t2);
}
} else {
// no more from this file
if (debugLevel (DEBUG_ESATS, 1))
Serial.printf ("SAT: no more TLE from %s\n", state == RNS_INIT ? "user" : "server");
// close fp
fclose (fp);
fp = NULL;
// advance to next state
switch (state) {
case RNS_INIT: state = RNS_SERVER; break;
case RNS_SERVER: state = RNS_DONE; break;
case RNS_DONE: break;
}
// resume
goto next;
}
// if get here one or the other was a success
return (true);
}
/* look up name. if found set up sat, else inform user and remove sat altogether.
* return whether found it.
*/
static bool satLookup (SatState &s)
{
if (!SAT_NAME_IS_SET(s))
return (false);
if (debugLevel (DEBUG_ESATS, 1))
Serial.printf ("SAT: Looking up '%s'\n", s.name);
// delete then restore if found
if (s.sat) {
delete s.sat;
s.sat = NULL;
}
// prepare for readNextSat()
FILE *rns_fp = NULL;
RNS_t rns_state = RNS_INIT;
// read and check each name
char name[NV_SATNAME_LEN];
char t1[TLE_LINEL];
char t2[TLE_LINEL];
bool ok = false;
char err_msg[100] = ""; // user default err msg if this stays ""
while (!ok && readNextSat (rns_fp, rns_state, name, t1, t2)) {
if (strcasecmp (name, s.name) == 0) {
if (!tleHasValidChecksum (t1))
snprintf (err_msg, sizeof(err_msg), "Bad checksum for %s TLE line 1", name);
else if (!tleHasValidChecksum (t2))
snprintf (err_msg, sizeof(err_msg), "Bad checksum for %s TLE line 2", name);
else
ok = true;
}
}
// finished with fp regardless
if (rns_fp)
fclose(rns_fp);
// final check
if (ok) {
// TLE looks good: define new sat
s.sat = new Satellite (t1, t2);
} else {
if (err_msg[0])
fatalSatError ("%s", err_msg);
else
fatalSatError ("%s disappeared", s.name);
}
return (ok);
}
/* show table selection box marked or not
*/
static void showSelectionBox (int r, int c, bool on)
{
const uint16_t x = c*CELL_W;
const uint16_t y = TBORDER + r*CELL_H;
uint16_t fill_color = on ? BTN_COLOR : RA8875_BLACK;
tft.fillRect (x, y+(CELL_H-CB_SIZE)/2+3, CB_SIZE, CB_SIZE, fill_color);
tft.drawRect (x, y+(CELL_H-CB_SIZE)/2+3, CB_SIZE, CB_SIZE, RA8875_WHITE);
}
/* return whether one of sat_state[] is the given name
*/
static bool satNameIsActive (const char *name)
{
for (int i = 0; i < MAX_ACTIVE_SATS; i++) {
SatState &s = sat_state[i];
if (s.sat && SAT_NAME_IS_SET(s) && strcasecmp (s.name, name) == 0)
return (true);
}
return (false);
}
/* show all names and allow op to choose up to two.
* save selections and return count therein.
*/
static int askSat (char selections[MAX_ACTIVE_SATS][NV_SATNAME_LEN])
{
// init count
int n_selections = 0;
// entire display is one big menu box
SBox screen_b;
screen_b.x = 0;
screen_b.y = 0;
screen_b.w = tft.width();
screen_b.h = tft.height();
// handy
time_t now = nowWO();
// prep for user input (way up here to avoid goto warnings)
UserInput ui = {
screen_b,
UI_UFuncNone,
UF_UNUSED,
MENU_TO,
UF_NOCLOCKS,
{0, 0}, TT_NONE, '\0', false, false
};
// don't inherit anything lingering after the tap that got us here
drainTouch();
// erase screen and set font
eraseScreen();
tft.setTextColor (RA8875_WHITE);
// show title and prompt
uint16_t title_y = 3*TBORDER/4;
selectFontStyle (BOLD_FONT, SMALL_FONT);
tft.setCursor (5, title_y);
tft.print ("Select up to two satellites");
// show rise units
selectFontStyle (LIGHT_FONT, SMALL_FONT);