-
Notifications
You must be signed in to change notification settings - Fork 2
/
earthsat.cpp
1798 lines (1510 loc) · 55.9 KB
/
earthsat.cpp
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.
*/
#include "HamClock.h"
bool dx_info_for_sat; // global to indicate whether dx_info_b is for DX info or sat info
#if defined(_IS_ESP8266)
// this requires dense collection of individual dots that are plotted as the map is drawn
#define MAX_PATH 2000 // max number of points in orbit path
#define FOOT_ALT0 1000 // n dots for 0 deg altitude locus
#define FOOT_ALT30 300 // n dots for 30 deg altitude locus
#define FOOT_ALT60 100 // n dots for 60 deg altitude locus
#define N_FOOT 3 // number of footprint segments
#else
// connect the dots with lines drawn all at once after the map
#define MAX_PATH 250 // max number of points in orbit path
#define FOOT_ALT0 125 // n dots for 0 deg altitude locus
#define FOOT_ALT30 40 // n dots for 30 deg altitude locus
#define FOOT_ALT60 15 // n dots for 60 deg altitude locus
#define N_FOOT 3 // number of footprint segments
#endif
#define RISE_ALARM_DT (1.0F/1440.0F) // flash this many days before rise event
#define MAX_TLE_AGE 7.0F // max age to use a TLE, days (except moon)
#define TLE_REFRESH (3600*6) // freshen TLEs this often, seconds
#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) // font height
#define FONT_D 5 // font descent
#define SAT_COLOR RA8875_RED // annotation color
#define GIMBALWRAP_COL RA8875_CYAN // gimbal wrap az marker color
#define SOON_COLOR RA8875_GREEN // 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 Q_TO 5 // question timeout
#define CELL_W (tft.width()/N_COLS) // display cell width
#define N_ROWS ((tft.height()-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
// used so findNextPass() can be used for contexts other than the current sat now
// TODO: make another for az/el/range/rate and use them with getSatAzElNow()
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;
static const char sat_get_all[] = "/ham/HamClock/esats.pl?getall="; // command to get all TLE
static const char sat_one_page[] = "/ham/HamClock/esats.pl?tlename=%s"; // command to get one TLE
static Satellite *sat; // satellite definition, if any
static Observer *obs; // DE
static SatRiseSet sat_rs; // event info for current sat
static SCoord *sat_path; // mallocd screen coords for orbit, first always now, Moon only 1
static uint16_t n_path; // actual number in use
static SCoord *sat_foot[3]; // mallocd screen coords for each footprint altitude
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
static uint16_t n_foot[N_FOOT]; // actual dots along each altitude
static SBox map_name_b; // location of sat name on map
static SBox ok_b = {730,10,55,35}; // Ok button
static char sat_name[NV_SATNAME_LEN]; // NV_SATNAME cache (spaces are underscores)
#define SAT_NAME_IS_SET() (sat_name[0]) // whether there is a sat name defined
static time_t tle_refresh; // last TLE update
static bool new_pass; // set when new pass is ready
/* completely undefine the current sat
*/
static void unsetSat()
{
if (sat) {
delete sat;
sat = NULL;
}
if (sat_path) {
free (sat_path);
sat_path = NULL;
}
for (int i = 0; i < N_FOOT; i++) {
if (sat_foot[i]) {
free (sat_foot[i]);
sat_foot[i] = NULL;
}
}
sat_name[0] = '\0';
NVWriteString (NV_SATNAME, sat_name);
dx_info_for_sat = false;
}
/* copy from_str to to_str up to maxlen, changing all from_char to to_char
*/
void strncpySubChar (char to_str[], const char from_str[], char to_char, char from_char, int maxlen)
{
int l = 0;
char c;
while (l++ < maxlen && (c = *from_str++) != '\0')
*to_str++ = c == from_char ? to_char : c;
*to_str = '\0';
}
/* set alarm buzzer SATALARM_GPIO high if on -- RPi only
*/
static void risetAlarm (bool on)
{
#if defined(_SUPPORT_GPIO) and defined(_IS_UNIX)
// ignore if not supposed to use GPIO
if (!GPIOOk())
return;
// init if first time
static bool inited, last_on;
GPIO& gpio = GPIO::getGPIO();
if (!inited) {
gpio.setAsOutput (SATALARM_GPIO);
gpio.setLo (SATALARM_GPIO);
last_on = false;
inited = true;
}
// command if changed
if (on && !last_on) {
gpio.setAsOutput (SATALARM_GPIO);
gpio.setHi (SATALARM_GPIO);
last_on = true;
} else if (!on && last_on) {
gpio.setAsOutput (SATALARM_GPIO);
gpio.setLo (SATALARM_GPIO);
last_on = false;
}
#else
// not used
(void) on;
#endif // _SUPPORT_GPIO
}
/* fill sat_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 (float satlat, float satlng)
{
resetWatchdog();
// 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
sat_foot[alt_i] = (SCoord *) realloc (sat_foot[alt_i], max_foot[alt_i]*sizeof(SCoord));
SCoord *foot_path = sat_foot[alt_i];
if (!foot_path) {
Serial.println (F("Failed to malloc sat_foot"));
while (1); // timeout
}
// satellite viewing altitude
float valt = deg2rad(foot_alts[alt_i]);
// great-circle radius from subsat point to viewing circle at altitude valt
float vrad = sat->viewingRadius(valt);
// compute each unique point around viewing circle
uint16_t n = 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)
ll2s (vlat, vlng, foot_path[n], 2);
// skip duplicate points
if (n == 0 || memcmp (&foot_path[n], &foot_path[n-1], sizeof(SCoord)))
n++;
}
// reduce memory to only points actually used
n_foot[alt_i] = n;
sat_foot[alt_i] = (SCoord *) realloc (sat_foot[alt_i], n*sizeof(SCoord));
// Serial.printf (_FX("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(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) {
resetWatchdog();
// 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 (_FX("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) {
Serial.printf (_FX("%s: next rise in %g hrs, set in %g (%ld ms)\n"), name,
rs.rise_ok ? 24*(rs.rise_time - t_now) : 0.0F, rs.set_ok ? 24*(rs.set_time - t_now) : 0.0F,
millis() - t0);
printFreeHeap (F("findNextPass"));
}
}
/* display next pass on sky dome.
* N.B. we assume findNextPass has been called to fill sat_rs
*/
static void drawNextPass()
{
resetWatchdog();
// 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
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 (!sat || !obs || !sat_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 (sat_rs.rise_ok && sat_rs.set_ok) {
// find start and pass duration in days
float pass_duration = sat_rs.set_time - sat_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 = sat_rs.set_time - t_now;
t = t_now;
} else {
// full pass so start at next rise
t = sat_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.fillCircle (xr, yr, 1, RA8875_WHITE);
tft.drawLine (xc, yc, xr, yr, HGRIDCOL);
}
float gwaz;
if (getGimbalWrapAz (&gwaz)) {
uint16_t xr = lroundf(xc + r0*sinf(deg2rad(gwaz)));
uint16_t yr = lroundf(yc - r0*cosf(deg2rad(gwaz)));
tft.fillCircle (xr, yr, 2, GIMBALWRAP_COL);
Serial.printf (_FX("az_mnt0 %g\n"), gwaz);
}
// 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 (F("NW"));
tft.setCursor (xc + r0 - 12, yc - r0 + 2);
tft.print (F("NE"));
tft.setCursor (xc - r0, yc + r0 - 8);
tft.print (F("SW"));
tft.setCursor (xc + r0 - 12, yc + r0 - 8);
tft.print (F("SE"));
// connect several points from t until sat_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++) {
resetWatchdog();
// find topocentric position @ t
float el, az, range, rate;
sat->predict (t);
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 = (sat_rs.set_time - sat_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), _FX("%dh%02d"), h, m);
} else {
int m = s_up/60;
int s = s_up - 60*m;
snprintf (tup_str, sizeof(tup_str), _FX("%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);
}
printFreeHeap (F("drawNextPass"));
}
/* draw name of current satellite if used in dx_info box
*/
static void drawSatName()
{
if (!sat || !obs || !SAT_NAME_IS_SET() || !dx_info_for_sat)
return;
resetWatchdog();
// retrieve saved name without '_'
char user_name[NV_SATNAME_LEN];
strncpySubChar (user_name, sat_name, ' ', '_', NV_SATNAME_LEN);
// erase
tft.fillRect (dx_info_b.x, dx_info_b.y+1, dx_info_b.w, dx_info_b.h-1, RA8875_BLACK);
// 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);
tft.fillRect (satname_b.x, satname_b.y, satname_b.w, satname_b.h, RA8875_BLACK);
tft.setCursor (satname_b.x + (satname_b.w - bw)/2, satname_b.y+FONT_H - 2);
tft.print (user_name);
}
/* fill map_name_b with where sat name should go on map
*/
static void setSatMapNameLoc()
{
// retrieve saved name without '_'
char user_name[NV_SATNAME_LEN];
strncpySubChar (user_name, sat_name, ' ', '_', NV_SATNAME_LEN);
// get size
selectFontStyle (LIGHT_FONT, SMALL_FONT);
uint16_t bw, bh;
getTextBounds (user_name, &bw, &bh);
map_name_b.w = bw;
map_name_b.h = bh;
if (azm_on) {
// easy: just print on top between hemispheres
map_name_b.x = map_b.x + (map_b.w - map_name_b.w)/2 ;
map_name_b.y = map_b.y + 10;
} else {
// locate name away from current sat location and misc symbols
// start in south pacific
#define _SP_LNG (-160) // South Pacific longitude
#define _SP_LAT (-30) // " latitude above RSS and DRAP
SCoord name_l_s, name_r_s; // left and right box candidate location
ll2s (deg2rad(_SP_LAT), deg2rad(_SP_LNG), name_l_s, 0);
name_r_s.x = name_l_s.x + map_name_b.w;
name_r_s.y = name_l_s.y;
// avoid any symbols
#define _EDGE_GUARD 20
while (overAnySymbol (name_l_s) || overAnySymbol(name_r_s)) {
name_l_s.x += _EDGE_GUARD;
name_r_s.x = name_l_s.x + map_name_b.w;
}
// avoid current sat footprint
#define _SAT_FOOT_R 75 // typical footprint??
SCoord &sat_s = sat_path[0];
uint16_t dy = sat_s.y > name_l_s.y ? sat_s.y - name_l_s.y : name_l_s.y - sat_s.y;
if (dy < _SAT_FOOT_R && name_r_s.x >= sat_s.x - _SAT_FOOT_R && name_l_s.x < sat_s.x + _SAT_FOOT_R) {
name_l_s.x = sat_s.x + _SAT_FOOT_R + _EDGE_GUARD;
name_r_s.x = name_l_s.x + map_name_b.w;
}
// check for going off the right edge
if (name_r_s.x > map_b.x + map_b.w - _EDGE_GUARD) {
name_l_s.x = map_b.x + _EDGE_GUARD;
name_r_s.x = name_l_s.x + map_name_b.w;
}
// ok
map_name_b.x = name_l_s.x;
map_name_b.y = name_l_s.y;
}
}
/* mark current sat pass location
*/
static void drawSatNow()
{
resetWatchdog();
float az, el, range, rate, raz, saz;
getSatAzElNow (NULL, &az, &el, &raz, &range, &rate, &saz, NULL, NULL);
// size and center of screen path
uint16_t r0 = (dx_info_b.h-2*FONT_H)/2;
uint16_t x0 = dx_info_b.x + dx_info_b.w/2;
uint16_t y0 = dx_info_b.y + dx_info_b.h - r0;
float r = r0*(90-el)/90; // screen radius, zenith at center
uint16_t x = x0 + r*sinf(deg2rad(az)) + 0.5F; // want east right
uint16_t y = y0 - r*cosf(deg2rad(az)) + 0.5F; // want north up
tft.fillCircle (x, y, SAT_UP_R, SAT_COLOR);
}
/* draw event title and time t in the dx_info box unless t < 0 then just show title.
* t is in days: if > 1 hour show HhM else M:S
*/
static void drawSatTime (const char *title, float t)
{
if (!sat)
return;
resetWatchdog();
static char prev_title[30];
static uint8_t prev_a, prev_b;
selectFontStyle (LIGHT_FONT, SMALL_FONT);
tft.setTextColor (SAT_COLOR);
// erase if different title
if (strcmp (title, prev_title)) {
tft.fillRect (dx_info_b.x, dx_info_b.y+FONT_H, dx_info_b.w, FONT_H, RA8875_BLACK);
strncpy (prev_title, title, sizeof(prev_title)-1);
}
// draw title (even if the same, in case it was erased for some other reason)
tft.setCursor (dx_info_b.x+1, dx_info_b.y+2*FONT_H-5);
tft.print (title);
uint16_t timex = tft.getCursorX();
// draw time unless coded to be ignored
if (t >= 0) {
// assume H:M
char sep = 'h';
t *= 24;
uint8_t a = t, b;
if (a == 0) {
// change to M:S
sep = ':';
t *= 60;
a = t;
}
b = (t - a)*60;
// erase if different time
if (a != prev_a || b != prev_b) {
tft.fillRect (timex, dx_info_b.y+FONT_H, dx_info_b.w-(timex-dx_info_b.x), FONT_H, RA8875_BLACK);
prev_a = a;
prev_b = b;
}
// draw time (even if the same, like title, in case it was erased for some other reason)
tft.print(a);
tft.print(sep);
if (b < 10)
tft.print('0');
tft.print(b);
// tft.printf (_FX("%d:%02d"), a, b); often causes panic
} else {
// erase time
tft.fillRect (timex, dx_info_b.y+FONT_H, dx_info_b.w-(timex-dx_info_b.x), FONT_H, RA8875_BLACK);
}
}
/* 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 without a sat
*/
static void fatalSatError (const char *fmt, ...)
{
char buf[65] = "Sat error: "; // max on one line
va_list ap;
int l = strlen (buf);
va_start (ap, fmt);
vsnprintf (buf+l, sizeof(buf)-l, fmt, ap);
va_end (ap);
Serial.println (buf);
selectFontStyle (BOLD_FONT, SMALL_FONT);
uint16_t bw = getTextWidth (buf);
eraseScreen();
tft.setTextColor (RA8875_WHITE);
tft.setCursor ((tft.width()-bw)/2, tft.height()/2);
tft.print (buf);
wdDelay (5000);
resetWatchdog();
unsetSat();
initScreen();
}
static void showSelectionBox (uint16_t x, uint16_t y, bool on)
{
uint16_t fill_color = on ? SAT_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);
}
/* look up sat_name. if found set up sat, else inform user and remove sat altogether.
* return whether found it.
*/
static bool satLookup ()
{
Serial.printf (_FX("Looking up %s\n"), sat_name);
if (!SAT_NAME_IS_SET())
return (false);
// delete then restore if found
if (sat) {
delete sat;
sat = NULL;
}
WiFiClient tle_client;
bool ok = false;
resetWatchdog();
if (wifiOk() && tle_client.connect (svr_host, HTTPPORT)) {
resetWatchdog();
// memory
StackMalloc t1(TLE_LINEL);
StackMalloc t2(TLE_LINEL);
StackMalloc name_mem(100);
char *name = name_mem.getMem();
// query
snprintf (name, name_mem.getSize(), sat_one_page, sat_name);
httpGET (tle_client, svr_host, name);
if (!httpSkipHeader (tle_client)) {
fatalSatError (_FX("Bad http header"));
goto out;
}
// first response line is sat name, should match query
if (!getTCPLine (tle_client, name, name_mem.getSize(), NULL)) {
fatalSatError (_FX("Satellite %s not found"), sat_name);
goto out;
}
if (strcasecmp (name, sat_name)) {
fatalSatError (_FX("No match: '%s' '%s'"), sat_name, name);
goto out;
}
// next two lines are TLE
if (!getTCPLine (tle_client, t1.getMem(), TLE_LINEL, NULL)) {
fatalSatError (_FX("Error reading line 1"));
goto out;
}
if (!tleHasValidChecksum (t1.getMem())) {
fatalSatError (_FX("Bad checksum for %s in line 1"), sat_name);
goto out;
}
if (!getTCPLine (tle_client, t2.getMem(), TLE_LINEL, NULL)) {
fatalSatError (_FX("Error reading line 2"));
goto out;
}
if (!tleHasValidChecksum (t2.getMem())) {
fatalSatError (_FX("Bad checksum for %s in line 2"), sat_name);
goto out;
}
// TLE looks good, update name so cases match, define new sat
memcpy (sat_name, name, sizeof(sat_name)-1); // retain EOS
sat = new Satellite (t1.getMem(), t2.getMem());
tle_refresh = nowWO();
ok = true;
} else {
fatalSatError (_FX("network error"));
}
out:
tle_client.stop();
printFreeHeap (F("satLookup"));
return (ok);
}
/* show all names and allow op to choose one or none.
* save selection in sat_name, even if empty for no selection.
* return whether sat was selected.
*/
static bool askSat()
{
#define NO_SAT (-1) // cookie when op has chosen not to display a sat
resetWatchdog();
// don't inherit anything lingering after the tap that got us here
drainTouch();
// if stop while listing record as if it was a tap on that item
SCoord s_stop;
bool stop_tap = false;
// 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 (F("Select satellite, or none"));
// show rise units
selectFontStyle (LIGHT_FONT, SMALL_FONT);
tft.setTextColor (RA8875_WHITE);
tft.setCursor (tft.width()-450, title_y);
tft.print (F("Rise in HH:MM"));
// show what SOON_COLOR means
tft.setTextColor (SOON_COLOR);
tft.setCursor (tft.width()-280, title_y);
tft.printf (_FX("<%d Mins"), SOON_MINS);
// show what SAT_COLOR means
tft.setTextColor (SAT_COLOR);
tft.setCursor (tft.width()-170, title_y);
tft.print (F("Up Now"));
// show Ok button
drawStringInBox ("Ok", ok_b, false, RA8875_WHITE);
/// setup
StackMalloc t1(TLE_LINEL);
StackMalloc t2(TLE_LINEL);
typedef char SatNames[MAX_NSAT][NV_SATNAME_LEN];
StackMalloc name_mem(sizeof(SatNames));
SatNames *sat_names = (SatNames *) name_mem.getMem();
uint16_t prev_sel_x = 0, prev_sel_y = 0;
int8_t sel_idx = NO_SAT;
uint8_t n_sat = 0;
// open connection
WiFiClient sat_client;
resetWatchdog();
if (!wifiOk() || !sat_client.connect (svr_host, HTTPPORT))
goto out;
// query page and skip header
resetWatchdog();
httpGET (sat_client, svr_host, sat_get_all);
if (!httpSkipHeader (sat_client))
goto out;
// read and display each sat, allow tapping part way through to stop
selectFontStyle (LIGHT_FONT, SMALL_FONT);
for (n_sat = 0; n_sat < MAX_NSAT; n_sat++) {
// read name and 2 lines, done when eof or tap
if (!getTCPLine (sat_client, &(*sat_names)[n_sat][0], NV_SATNAME_LEN, NULL)
|| !getTCPLine (sat_client, t1.getMem(), TLE_LINEL, NULL)
|| !getTCPLine (sat_client, t2.getMem(), TLE_LINEL, NULL)) {
break;
}
// find row and column, col-major order
uint8_t r = n_sat % N_ROWS;
uint8_t c = n_sat / N_ROWS;
// ul corner
uint16_t x = c*CELL_W;
uint16_t y = TBORDER + r*CELL_H;
// allow early stop if tap
if (readCalTouchWS(s_stop) != TT_NONE) {
stop_tap = true;
tft.setTextColor (RA8875_WHITE);
tft.setCursor (x, y + FONT_H); // match below
tft.print (F("Listing stopped"));
break;
}
// show tick box, pre-select if saved before
if (strcmp (sat_name, (*sat_names)[n_sat]) == 0) {
sel_idx = n_sat;
showSelectionBox (x, y, true);
prev_sel_x = x;
prev_sel_y = y;
} else {
showSelectionBox (x, y, false);
}
// display next rise time of this sat
if (sat)
delete sat;
sat = new Satellite (t1.getMem(), t2.getMem());
SatRiseSet rs;
findNextPass((*sat_names)[n_sat], nowWO(), rs);
tft.setTextColor (RA8875_WHITE);
tft.setCursor (x + CB_SIZE + 8, y + FONT_H);
if (rs.rise_ok) {
DateTime t_now = userDateTime(nowWO());
if (rs.rise_time < rs.set_time) {
// pass lies ahead
float hrs_to_rise = (rs.rise_time - t_now)*24.0;
if (hrs_to_rise*60 < SOON_MINS)
tft.setTextColor (SOON_COLOR);
uint8_t mins_to_rise = (hrs_to_rise - (uint16_t)hrs_to_rise)*60;
if (hrs_to_rise < 1 && mins_to_rise < 1)
mins_to_rise = 1; // 00:00 looks wrong
if (hrs_to_rise < 10)
tft.print ('0');
tft.print ((uint16_t)hrs_to_rise);
tft.print (':');
if (mins_to_rise < 10)
tft.print ('0');
tft.print (mins_to_rise);
tft.print (' ');
} else {
// pass in progress
tft.setTextColor (SAT_COLOR);
tft.print (F("Up "));
}
} else if (!rs.ever_up) {
tft.setTextColor (GRAY);
tft.print (F("NoR "));
} else if (!rs.ever_down) {
tft.setTextColor (SAT_COLOR);
tft.print (F("NoS "));
}
// followed by scrubbed name
char user_name[NV_SATNAME_LEN];
strncpySubChar (user_name, (*sat_names)[n_sat], ' ', '_', NV_SATNAME_LEN);
tft.print (user_name);
}
// close connection
sat_client.stop();
// bale if no satellites displayed
if (n_sat == 0)
goto out;
// make box for whole screen so we can use waitForTap()
SBox screen_b;
screen_b.x = 0;
screen_b.y = 0;
screen_b.w = tft.width();
screen_b.h = tft.height();
// follow touches to make selection, done when tap Ok
selectFontStyle (BOLD_FONT, SMALL_FONT);
SCoord s_tap;
while (stop_tap || waitForTap (screen_b, screen_b, NULL, MENU_TO, s_tap)) {
// use stop tap first time if set
if (stop_tap) {
s_tap = s_stop;
stop_tap = false;
}
// tap Ok button?
if (inBox (s_tap, ok_b)) {
// show Ok button toggle
drawStringInBox ("Ok", ok_b, true, RA8875_WHITE);
goto out;
}
// else toggle tapped sat, if any
resetWatchdog();
uint8_t r = (s_tap.y - TBORDER)/CELL_H;