-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdxcluster.cpp
More file actions
1373 lines (1148 loc) · 42.1 KB
/
dxcluster.cpp
File metadata and controls
1373 lines (1148 loc) · 42.1 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
/* handle the DX Cluster display.
*
* support Spider, AR and CC clusters, plus several UDP packet formats.
*
* We actually keep two lists:
* dxc_spots: the complete raw list, not filtered nor sorted; length in n_dxspots.
* dxwl_spots: watchlist-filtered and time-sorted for display; length in dxc_ss.n_data.
*
*/
#include "HamClock.h"
// layout
#define DXC_COLOR RA8875_GREEN
#define CLRBOX_DX 6 // clear control box left offset
#define CLRBOX_DY 6 // " top offset
#define CLRBOX_W 20 // " width
#define CLRBOX_H 11 // " height
// connection info
static WiFiClient dxc_client; // persistent TCP connection while displayed ...
static WiFiUDP udp_server; // or persistent UDP "connection" to WSJT-X client program
static bool multi_cntn; // set when cluster has noticed multiple connections
#define MAX_LCN 10 // max lost connections per MAX_LCDT
#define MAX_LCDT 3600 // max lost connections period, seconds
// ages
static const uint8_t dxc_ages[] = {10, 20, 40, 60}; // menu selections in ascending order, minutes
static uint8_t dxc_age; // one of above, once set
#define N_DXCAGES NARRAY(dxc_ages) // handy count
#define MAXKEEP_DT (60*dxc_ages[N_DXCAGES-1]) // max age to stay on dxc_spots list, secs
// timing
#define BGCHECK_DT 1000 // background checkDXCluster period, millis
#define DXCMSG_DT 500 // delay before sending each cluster message, millis
#define HBEAT_MS 60000 // heatbeat interval, millis
// state
static DXSpot *dxc_spots; // malloced list of all spots
static int n_dxspots; // n spots in dxc_spots
static DXSpot *dxwl_spots; // malloced list, filtered for display, count in dxc_ss.n_data
static ScrollState dxc_ss; // scrolling info, and count of dxwl_spots
static bool dxc_showbio; // whether click shows bio
static bool dxc_spots_changed; // set to rebuild display because dxc_spots changed
static bool dxc_updateDE; // request to send DE location when possible
static bool new_dxc_cntn; // set to commence initial server handshake
static time_t scrolledaway_tm; // time() when user scrolled away from top of list
static uint32_t dxc_activity_ms; // millis() of last socket activity
static SBox dxcclr_b; // Clear spots control box
// type
typedef enum {
CT_UNKNOWN,
CT_READONLY, // read spider spots but never send anything
CT_ARCLUSTER,
CT_DXSPIDER,
CT_VE7CC,
CT_UDP,
} DXClusterType;
static DXClusterType cl_type;
#if defined(__GNUC__)
static void showDXClusterErr (const char *fmt, ...) __attribute__ ((format (__printf__, 1, 2)));
static void dxcSendMsg (const char *fmt, ...) __attribute__ ((format (__printf__, 1, 2)));
#else
static void showDXClusterErr (const char *fmt, ...);
static void dxcSendMsg (const char *fmt, ...);
#endif
/* draw, else erase, the clear spots control
*/
static void drawClearListBtn (bool draw)
{
uint16_t color = draw ? DXC_COLOR : RA8875_BLACK;
drawSBox (dxcclr_b, color);
selectFontStyle (LIGHT_FONT, FAST_FONT);
tft.setCursor (dxcclr_b.x+1, dxcclr_b.y+2);
tft.setTextColor (color);
tft.print ("CLR");
}
/* handy check whether we are, or should, show the New spots symbol
*/
static bool showingNewSpot(void)
{
return (scrolledaway_tm > 0 && n_dxspots > 0 && dxc_spots[n_dxspots-1].spotted > scrolledaway_tm);
}
/* rebuild dxwl_spots from dxc_spots
*/
static void rebuildDXWatchList(void)
{
// update ADIF if in use in case our WL uses it
freshenADIFFile();
// extract qualifying spots
time_t oldest = myNow() - 60*dxc_age; // oldest time to display, seconds
dxc_ss.n_data = 0; // reset count, don't bother to resize dxwl_spots
for (int i = 0; i < n_dxspots; i++) {
DXSpot &spot = dxc_spots[i];
if (spot.spotted >= oldest && checkWatchListSpot (WLID_DX, spot) != WLS_NO) {
dxwl_spots = (DXSpot *) realloc (dxwl_spots, (dxc_ss.n_data+1) * sizeof(DXSpot));
if (!dxwl_spots)
fatalError ("No mem for %d watch list spots", dxc_ss.n_data+1);
dxwl_spots[dxc_ss.n_data++] = spot;
}
}
// resort and scroll to newest
qsort (dxwl_spots, dxc_ss.n_data, sizeof(DXSpot), qsDXCSpotted);
dxc_ss.scrollToNewest();
}
/* draw all currently visible spots in the pane then update scroll markers if more
*/
static void drawAllVisDXCSpots (const SBox &box)
{
drawVisibleSpots (WLID_DX, dxwl_spots, dxc_ss, box, DXC_COLOR);
drawClearListBtn (dxc_ss.n_data > 0);
}
/* handy check whether New Spot symbol needs changing on/off
*/
static void checkNewSpotSymbol (bool was_at_newest)
{
if (was_at_newest && !dxc_ss.atNewest()) {
scrolledaway_tm = myNow(); // record when moved off top
ROTHOLD_SET(PLOT_CH_DXCLUSTER); // disable rotation
} else if (!was_at_newest && dxc_ss.atNewest()) {
dxc_ss.drawNewSpotsSymbol (false, false); // turn off entirely
rebuildDXWatchList ();
scrolledaway_tm = 0;
ROTHOLD_CLR(PLOT_CH_DXCLUSTER); // resume rotation
}
}
/* shift the visible list up, if possible.
* if reach the end with the newest entry, turn off New spots and update dxwl_spots
*/
static void scrollDXCUp (const SBox &box)
{
bool was_at_newest = dxc_ss.atNewest();
if (dxc_ss.okToScrollUp()) {
dxc_ss.scrollUp();
drawAllVisDXCSpots(box);
}
checkNewSpotSymbol (was_at_newest);
}
/* shift the visible list down, if possible.
* set scrolledaway_tm if scrolling away from newest entry
*/
static void scrollDXCDown (const SBox &box)
{
bool was_at_newest = dxc_ss.atNewest();
if (dxc_ss.okToScrollDown()) {
dxc_ss.scrollDown ();
drawAllVisDXCSpots (box);
}
checkNewSpotSymbol (was_at_newest);
}
/* set bio, radio and DX from given row, known to be defined
*/
static void engageDXCRow (DXSpot &s)
{
newDX (s.tx_ll, NULL, s.tx_call);
setRadioSpot(s.kHz);
if (dxc_showbio)
openQRZBio (s);
}
/* log the given spot roughly similar to how spider spots look.
* this is intended for logging spots from UDP.
* DX de KD0AA: 18100.0 JR1FYS FT8 LOUD in FL! 2156Z
*/
static void logDXSpot (const char *label, const DXSpot &spot)
{
const struct tm *spot_time = gmtime (&spot.spotted);
dxcLog ("%s: DX de %-10s %8.1f %-44s%02d%02dZ\n", label, spot.rx_call, spot.kHz, spot.tx_call,
spot_time->tm_hour, spot_time->tm_min);
}
/* add a potentially new spot to dxc_spots[].
* set dxc_spots_changed if dxc_spots changed in either content or count.
* set DX too if asked and desired.
*/
static void addDXClusterSpot (DXSpot &new_spot, bool set_dx)
{
// just discard if already too old
time_t now = myNow();
time_t ancient = now - MAXKEEP_DT;
if (new_spot.spotted < ancient) {
dxcLog ("new %s %g dropped: age %ld already > %d mins\n", new_spot.tx_call, new_spot.kHz,
(long)((now - new_spot.spotted)/60), MAXKEEP_DT/60);
return;
}
// nice to insure calls are upper case
strtoupper (new_spot.rx_call);
strtoupper (new_spot.tx_call);
// handy
HamBandSetting new_band = findHamBand(new_spot.kHz);
// check for dup, and remove any ancient spots along the way
bool same_spot = false;
for (int i = 0; i < n_dxspots; i++) {
DXSpot &spot = dxc_spots[i];
if (spot.spotted < ancient) {
dxcLog ("%s %g: aged out\n", spot.tx_call, spot.kHz);
memmove (&dxc_spots[i], &dxc_spots[i+1], (--n_dxspots - i) * sizeof(DXSpot));
i -= 1; // examine new [i] again next loop
dxc_spots_changed = true; // update GUI with updated list
} else if (!strcmp (spot.tx_call, new_spot.tx_call) && findHamBand (spot.kHz) == new_band) {
// consider dupe if same tx call and band
int spt_hr = hour(spot.spotted);
int spt_mn = minute(spot.spotted);
int new_hr = hour(new_spot.spotted);
int new_mn = minute(new_spot.spotted);
same_spot = true;
if (new_spot.spotted > spot.spotted) {
dxcLog ("%s %g: updated %02d%02dZ > %02d%02dZ\n", new_spot.tx_call, new_spot.kHz,
new_hr, new_mn, spt_hr, spt_mn);
spot = new_spot; // update info
dxc_spots_changed = true; // update GUI with new age
} else if (new_spot.spotted == spot.spotted) {
dxcLog ("%s %g: dup time %02d%02dZ\n", spot.tx_call, spot.kHz, spt_hr, spt_mn);
} else {
dxcLog ("%s %g: superseded %02d%02dZ < %02d%02dZ\n", spot.tx_call, spot.kHz,
new_hr, new_mn, spt_hr, spt_mn);
}
}
}
// that's it if already in dxc_spots
if (same_spot)
return;
// tweak map location for unique picking
ditherLL (new_spot.tx_ll);
ditherLL (new_spot.rx_ll);
// append to dxc_spots
dxc_spots = (DXSpot *) realloc (dxc_spots, (n_dxspots+1) * sizeof(DXSpot));
if (!dxc_spots)
fatalError ("No memory for %d DX spots", n_dxspots+1);
dxc_spots[n_dxspots++] = new_spot;
// set new DX if desired
if (set_dx) {
newDX (new_spot.tx_ll, new_spot.tx_grid, new_spot.tx_call);
// move mouse too to show in info box
SCoord s;
ll2s (new_spot.tx_ll, s, 5);
tft.setMouse (s.x, s.y);
}
// update GUI with new spot
dxc_spots_changed = true;
// inform others who might care about a new spot
tellDXPedsSpotChanged();
}
/* display the given error message and shut down the connection.
*/
static void showDXClusterErr (const char *fmt, ...)
{
char buf[500];
va_list ap;
va_start (ap, fmt);
size_t ml = snprintf (buf, sizeof(buf), "DX Cluster error: ");
vsnprintf (buf+ml, sizeof(buf)-ml, fmt, ap);
va_end (ap);
mapMsg (3000, "%s", buf);
// log
dxcLog ("%s\n", buf);
// shut down connection
closeDXCluster();
}
/* increment NV_DXMAX_N
*/
static void incLostConn(void)
{
uint8_t n_lostconn;
if (!NVReadUInt8 (NV_DXMAX_N, &n_lostconn))
n_lostconn = 0;
n_lostconn += 1;
NVWriteUInt8 (NV_DXMAX_N, n_lostconn);
dxcLog ("lost connection: now %u\n", n_lostconn);
}
/* return whether max lost connection rate has been reached
*/
static bool checkLostConnRate()
{
uint32_t t0 = (uint32_t)myNow(); // time now in same units as those saved
uint32_t t_maxconn; // time when the limit was last reached
uint8_t n_lostconn; // n connections lost so far since t_maxconn
// get current state
if (!NVReadUInt32 (NV_DXMAX_T, &t_maxconn)) {
t_maxconn = t0;
NVWriteUInt32 (NV_DXMAX_T, t_maxconn);
}
if (!NVReadUInt8 (NV_DXMAX_N, &n_lostconn)) {
n_lostconn = 0;
NVWriteUInt8 (NV_DXMAX_N, n_lostconn);
}
dxcLog ("%u lost connections since %u\n", n_lostconn, t_maxconn);
// check if max lost connections have been hit
bool hit_max = false;
if (n_lostconn > MAX_LCN) {
if (t0 < t_maxconn + MAX_LCDT) {
// hit the max during the last MAX_LCDT
hit_max = true;
} else {
// record the time and start a new count
NVWriteUInt32 (NV_DXMAX_T, t0);
n_lostconn = 0;
NVWriteUInt8 (NV_DXMAX_N, n_lostconn);
}
}
return (hit_max);
}
/* given a cluster line, set multi_cntn if it seems to be telling us it has detected multiple connections
* from the same call-ssid.
* not at all sure this works everywhere.
* Only Spiders seem to care enough to dicsonnect, AR and CC clusters report but otherwise don't care.
*/
static void detectMultiConnection (const char *line)
{
// first seems typical for spiders, second for AR
if (strstr (line, "econnected") != NULL || strstr (line, "Dupe call") != NULL)
multi_cntn = true;
}
/* send a message to dxc_client.
* here for convenience of stdarg, logging and delay.
* N.B. we assume fmt will include NL
*/
static void dxcSendMsg (const char *fmt, ...)
{
// format
char msg[400];
va_list ap;
va_start (ap, fmt);
(void) vsnprintf (msg, sizeof(msg), fmt, ap);
va_end (ap);
// friendly delay
wdDelay (DXCMSG_DT);
// send and log
dxc_client.print (msg);
dxcLog ("> %s", msg);
// update activity timer
dxc_activity_ms = millis();
}
/* send a request for recent spots such that they will arrive the same as normal
*/
static void requestRecentSpots (void)
{
// silently ignored if RO
if (cl_type == CT_READONLY)
return;
const char *msg = NULL;
if (cl_type == CT_DXSPIDER)
msg = "sh/dx filter real 30";
else if (cl_type == CT_ARCLUSTER)
msg = "show/dx/30 @";
else if (cl_type == CT_VE7CC)
msg = "show/myfdx"; // always 30, does not accept a count
if (msg)
dxcSendMsg ("%s\n", msg);
}
/* free both spots lists memory
*/
static void resetDXMem()
{
if (dxc_spots) {
free (dxc_spots);
dxc_spots = NULL;
n_dxspots = 0;
}
if (dxwl_spots) {
free (dxwl_spots);
dxwl_spots = NULL;
dxc_ss.n_data = 0;
}
}
/* return whether the given host appears to be a multicast address
*/
static bool isHostMulticast (const char *host)
{
int first_octet = atoi (host);
return (first_octet >= 224 && first_octet <= 239); // will always be false if URL
}
/* send the next getDXClCommands() if more to send or restart.
* return whether all have been sent.
* N.B. although not implemented herein, it is expected we are not called in a tight loop in order
* that commands are spaced out.
*/
static bool sendNextUserCommand (bool restart)
{
static int next_cmd; // next getDXClCommands() index to send
// skip if RO
if (cl_type == CT_READONLY)
return (false);
// collect
const char *dx_cmds[N_DXCLCMDS];
bool dx_on[N_DXCLCMDS];
getDXClCommands (dx_cmds, dx_on);
// restart if desired
if (restart)
next_cmd = 0;
// send next until last
while (next_cmd < N_DXCLCMDS && (!dx_on[next_cmd] || strlen(dx_cmds[next_cmd]) == 0))
next_cmd++;
if (next_cmd < N_DXCLCMDS)
dxcSendMsg("%s\n", dx_cmds[next_cmd++]);
// all?
return (next_cmd == N_DXCLCMDS);
}
/* return whether the given line is the Spider query for setting location.
* we get this prompt from a spider only when it does not already know our location.
*/
static bool queryForQRA (const char line[])
{
return (strcistr (line, "Please enter your location with set/location or set/qra") != NULL);
}
/* display the current cluster host in the given color
*/
static void showDXCHost (const SBox &box, uint16_t c)
{
const char *dxhost = getDXClusterHost();
int dxport = getDXClusterPort();
selectFontStyle (LIGHT_FONT, FAST_FONT);
char host[50];
if (cl_type == CT_UDP && !isHostMulticast (dxhost)) {
snprintf (host, sizeof(host), "UDP port %d", dxport);
} else {
snprintf (host, sizeof(host), "%s:%d", dxhost, dxport);
uint16_t hw = getTextWidth (host);
if (hw > box.w - 5)
snprintf (host, sizeof(host), "%s", dxhost);
}
tft.setTextColor(c);
uint16_t hw = getTextWidth (host);
tft.setCursor (box.x + (box.w-hw)/2, box.y + SUBTITLE_Y0);
tft.print (host);
}
/* send our lat/long and grid to dxc_client, depending on cluster type.
*/
static void sendDELLGrid()
{
// silently succeeds if RO
if (cl_type == CT_READONLY)
return;
// DE grid as string
char maid[MAID_CHARLEN];
getNVMaidenhead (NV_DE_GRID, maid);
// command syntax depends on type
switch (cl_type) {
case CT_DXSPIDER: // fallthru
case CT_VE7CC:
// set grid
dxcSendMsg ("set/qra %s\n", maid);
break;
case CT_ARCLUSTER:
// friendly turn off skimmer just avoid getting swamped
dxcSendMsg ("set dx filter not skimmer\n");
// set grid
dxcSendMsg ("set station grid %s\n", maid);
break;
default:
// other have no such command
break;
}
}
/* prepare a fresh box but preserve any existing spots
*/
static void initDXGUI (const SBox &box)
{
// prep box
prepPlotBox (box);
// locate the Clr box
dxcclr_b = {(uint16_t)(box.x+CLRBOX_DX), (uint16_t)(box.y+CLRBOX_DY), CLRBOX_W, CLRBOX_H};
// title
const char *title = BOX_IS_PANE_0(box) ? "Cluster" : "DX Cluster";
selectFontStyle (LIGHT_FONT, SMALL_FONT);
tft.setTextColor(DXC_COLOR);
uint16_t tw = getTextWidth(title);
tft.setCursor (box.x + (box.w-tw)/2, box.y + PANETITLE_H);
tft.print (title);
// init scroller for this box size but leave n_data
dxc_ss.max_vis = (box.h - LISTING_Y0)/LISTING_DY;
dxc_ss.initNewSpotsSymbol (box, DXC_COLOR);
dxc_ss.dir = dxc_ss.DIR_FROMSETUP;
dxc_ss.scrollToNewest();
}
/* run menu to allow editing watch list
*/
static void runDXClusterMenu (const SBox &box)
{
// set up the MENU_TEXT field
MenuText mtext; // menu text prompt context
char wl_state[WLA_MAXLEN]; // wl state, menu may change
setupWLMenuText (WLID_DX, mtext, wl_state);
// build the possible age labels
char dxages_str[N_DXCAGES][10];
for (int i = 0; i < N_DXCAGES; i++)
snprintf (dxages_str[i], sizeof(dxages_str[i]), "%d m", dxc_ages[i]);
// whether to show bio on click, only show in menu at all if bio source has been set in Setup
bool show_bio_enabled = getQRZId() != QRZ_NONE;
MenuFieldType bio_lbl_mft = show_bio_enabled ? MENU_LABEL : MENU_IGNORE;
MenuFieldType bio_yes_mft = show_bio_enabled ? MENU_1OFN : MENU_IGNORE;
MenuFieldType bio_no_mft = show_bio_enabled ? MENU_1OFN : MENU_IGNORE;
// optional bio and watch list
#define MI_AGE_GRP 3 // MenuItem.group for the age items
MenuItem mitems[10] = {
// column 1
{bio_lbl_mft, false, 0, 2, "Bio:", NULL}, // 0
{MENU_LABEL, false, 1, 2, "Age:", NULL}, // 1
{MENU_BLANK, false, 2, 0, NULL, NULL}, // 2
// column 2
{bio_yes_mft, dxc_showbio, 5, 2, "Yes", NULL}, // 3
{MENU_1OFN, dxc_age == dxc_ages[0], MI_AGE_GRP, 2, dxages_str[0], NULL}, // 4
{MENU_1OFN, dxc_age == dxc_ages[2], MI_AGE_GRP, 2, dxages_str[2], NULL}, // 5
// column 3
{bio_no_mft, !dxc_showbio, 5, 2, "No", NULL}, // 6
{MENU_1OFN, dxc_age == dxc_ages[1], MI_AGE_GRP, 2, dxages_str[1], NULL}, // 7
{MENU_1OFN, dxc_age == dxc_ages[3], MI_AGE_GRP, 2, dxages_str[3], NULL}, // 8
// watch list
{MENU_TEXT, false, 4, 2, wl_state, &mtext}, // 9
};
SBox menu_b = box; // copy, not ref!
menu_b.x = box.x + 5;
menu_b.y = box.y + SUBTITLE_Y0;
menu_b.w = box.w-10;
SBox ok_b;
MenuInfo menu = {menu_b, ok_b, UF_CLOCKSOK, M_CANCELOK, 3, NARRAY(mitems), mitems};
if (runMenu (menu)) {
// check bio
if (show_bio_enabled) {
dxc_showbio = mitems[3].set;
NVWriteUInt8 (NV_DXCBIO, dxc_showbio);
}
// set desired age
for (int i = 0; i < NARRAY(mitems); i++) {
MenuItem &mi = mitems[i];
if (mi.group == MI_AGE_GRP && mi.set) {
dxc_age = atoi (mi.label);
NVWriteUInt8 (NV_DXCAGE, dxc_age);
break;
}
}
// must recompile to update wl but runMenu already insured wl compiles ok
Message ynot;
if (lookupWatchListState (mtext.label) != WLA_OFF && !compileWatchList (WLID_DX, mtext.text, ynot))
fatalError ("dxc failed recompling wl %s: %s", mtext.text, ynot.get());
setWatchList (WLID_DX, mtext.label, mtext.text);
dxcLog ("set WL to %s %s\n", mtext.label, mtext.text);
// rebuild with new options
rebuildDXWatchList();
// full update to capture any/all changes
dxc_spots_changed = true;
scheduleNewPlot (PLOT_CH_DXCLUSTER);
}
// always free the working watch list text
free (mtext.text);
}
/* send something end-to-end just to grease the connection for all intermediate routers
*/
static void sendHeartbeat(void)
{
// can not find anything published for AR Cluster
dxcSendMsg ("\r\n");
}
/* get next line from dxc_client.
* return whether line is ready.
*/
static bool getNextDXCLine (char line[], size_t ll)
{
bool ok = dxc_client.available() && getTCPLine (dxc_client, line, ll, NULL);
if (ok) {
if (queryForQRA (line))
dxc_updateDE = true;
dxc_activity_ms = millis();
}
return (ok);
}
/* get next UDP packet up to packet_size from udp_server.
* return length or 0 if nothing or trouble..
* N.B. we do not block, we return whether new packet is immediately ready.
*/
static int getUDPPacket (uint8_t packet[], int packet_size)
{
// get expected size
int psize = udp_server.parsePacket();
// always read, even if have to ditch it
if (psize > packet_size) {
uint8_t big_buf[10000];
dxcLog ("UDP size %d > available %d\n", udp_server.read (big_buf, sizeof(big_buf)), packet_size);
return (0);
}
// ok, should fit in packet
if (psize > 0) {
int n_read = udp_server.read (packet, psize);
if (n_read == psize)
return (psize);
fatalError ("bogus UDP packet size %d != %d", n_read, psize);
}
// nothing heard
return (0);
}
/* handle all incoming UDP -- called by checkDXCluster()
*/
static void incomingUDP()
{
// drain ALL pending UDP packets
uint8_t packet[2000];
int p_len;
while ((p_len = getUDPPacket (packet, sizeof(packet)-1)) > 0) { // allow for adding EOS
// add EOS
if (debugLevel (DEBUG_DXC, 1))
dxcLog ("UDP: read packet containing %d bytes\n", p_len);
packet[p_len] = '\0';
// auto-check several popular formats
DXSpot spot;
uint8_t *bp = packet;
if (wsjtxIsStatusMsg (&bp)) {
if (wsjtxParseStatusMsg (bp, spot)) {
if (useUDPSpot (spot)) {
logDXSpot ("WSJT-X", spot);
addDXClusterSpot (spot, UDPSetsDX());
}
}
} else if (crackXMLSpot ((char *)packet, spot)) {
if (useUDPSpot (spot)) {
logDXSpot ("XML", spot);
addDXClusterSpot (spot, UDPSetsDX());
}
} else if (crackADIFSpot ((char *)packet, spot)) {
if (useUDPSpot (spot)) {
logDXSpot ("ADIF", spot);
addDXClusterSpot (spot, UDPSetsDX());
}
} else {
dxcLog ("received unrecognized UDP packet\n");
}
}
}
/* handle all incoming DX Cluster -- called by checkDXCluster()
*/
static void incomingDXC()
{
// roll all pending new spots into list as fast as possible but don't block if nothing waiting
char line[120];
while (getNextDXCLine (line, sizeof(line))) {
// note incoming message and time
dxcLog ("< %s\n", line);
detectMultiConnection (line);
// look alive
updateClocks(false);
// crack and add
DXSpot new_spot;
if (crackClusterSpot (line, new_spot))
addDXClusterSpot (new_spot, false); // already logged above
}
// send fresh location whenever requested
if (dxc_updateDE) {
sendDELLGrid();
dxc_updateDE = false;
}
// new connections first send all user commands then request recent spots subject to those cmds.
if (new_dxc_cntn) {
static bool first_user_sent;
if (sendNextUserCommand (new_dxc_cntn && !first_user_sent)) {
requestRecentSpots();
first_user_sent = false;
new_dxc_cntn = false;
} else
first_user_sent = true;
}
// send heartbeat if idle too long
if (cl_type != CT_READONLY && timesUp (&dxc_activity_ms, HBEAT_MS))
sendHeartbeat();
// check connection still ok
if (!dxc_client) {
dxcLog ("bg lost connection\n");
incLostConn();
closeDXCluster();
}
}
/* insure cluster connection is closed
*/
void closeDXCluster()
{
// make sure either/both connection is/are closed
if (dxc_client) {
dxc_client.stop();
dxcLog ("disconnect %s\n", dxc_client ? "failed" : "ok");
}
if (udp_server) {
udp_server.stop();
dxcLog ("WSTJ-X disconnect %s\n", udp_server ?"failed":"ok");
}
// reset mem and multi flag
resetDXMem();
multi_cntn = false;
}
/* try to connect to the cluster.
* if success: dxc_client or udp_server is live, perform other prep and return true,
* else: both are closed, display mapMsg, return false.
* use mapMsg to display progress or errors.
* N.B. inforce MAX_LCN
*/
bool connectDXCluster (void)
{
// check max lost connection rate
if (checkLostConnRate()) {
showDXClusterErr ("Hit max %d lost connections/hr limit", MAX_LCN);
return (false);
}
// reset list and view
resetDXMem();
// get cluster connection info
const char *dxhost = getDXClusterHost();
int dxport = getDXClusterPort();
if (useWSJTX()) {
// create fresh UDP for WSJT-X
udp_server.stop();
// open normal or multicast
if (isHostMulticast (dxhost)) {
mapMsg (0, "Connecting to multicat UDP %s:%d", dxhost, dxport);
// reformat as IPAddress
unsigned o1, o2, o3, o4;
if (sscanf (dxhost, "%u.%u.%u.%u", &o1, &o2, &o3, &o4) != 4) {
showDXClusterErr ("Bad Multicast format: %s", dxhost);
return (false);
}
IPAddress ifIP(0,0,0,0); // ignored
IPAddress mcIP(o1,o2,o3,o4);
if (udp_server.beginMulticast (ifIP, mcIP, dxport)) {
dxcLog ("multicast %s:%d ok\n", dxhost, dxport);
cl_type = CT_UDP;
} else {
showDXClusterErr ("multicast %s:%d failed", dxhost, dxport);
return (false);
}
mapMsg (0, "Listening to UDP %s:%d", dxhost, dxport);
} else {
mapMsg (0, "Connecting to UDP port %d", dxport);
if (udp_server.begin(dxport)) {
dxcLog ("Listening to UDP port %d\n", dxport);
cl_type = CT_UDP;
} else {
showDXClusterErr ("Failed UDP port %d connection", dxport);
return (false);
}
mapMsg (0, "Listening to UDP port %d", dxport);
}
} else {
mapMsg (0, "Connecting to %s:%d", dxhost, dxport);
// open fresh socket
dxc_client.stop();
if (dxc_client.connect(dxhost, dxport)) {
// valid connection -- keep an eye out for lost connection
// look alive
updateClocks(false);
dxcLog ("connect %s:%d ok\n", dxhost, dxport);
// assume first question is asking for call.
// don't try to read with getTCPLine because first line is "login: " without trailing nl
const char *login = getDXClusterLogin();
dxcLog ("logging in as %s\n", login);
dxcSendMsg ("%s\n", login);
// look for first prompt and watch for clue about type of cluster along the way
uint16_t bl;
char buf[200];
const size_t bufl = sizeof(buf);
cl_type = CT_UNKNOWN;
bool rx_gt = false;
while (cl_type == CT_UNKNOWN && rx_gt == false && getTCPLine (dxc_client, buf, bufl, &bl)) {
dxcLog ("< %s\n", buf);
strtolower(buf);
detectMultiConnection (buf);
if (queryForQRA (buf)) {
dxc_updateDE = true;
cl_type = CT_DXSPIDER;
} else if (strstr (buf, "dx") && strstr (buf, "spider"))
cl_type = CT_DXSPIDER;
else if (strstr (buf, " cc "))
cl_type = CT_VE7CC;
else if (strstr (buf, "ar-cluster"))
cl_type = CT_ARCLUSTER;
// could just wait for timeout but usually ok to stop if find what looks like a prompt
if (buf[bl-1] == '>')
rx_gt = true;
}
// if no id string but do see > assume it's M0CKE's fire hose
if (cl_type == CT_UNKNOWN && rx_gt)
cl_type = CT_READONLY;
// what is it?
if (cl_type == CT_READONLY)
dxcLog ("Cluster type is unknown but did see \">\" so assuming read-only\n");
else if (cl_type == CT_DXSPIDER)
dxcLog ("Cluster is Spider\n");
else if (cl_type == CT_ARCLUSTER)
dxcLog ("Cluster is AR\n");
else if (cl_type == CT_VE7CC)
dxcLog ("Cluster is CC\n");
else {
incLostConn();
showDXClusterErr ("Unknown cluster type");
return (false);
}
// confirm still ok
if (!dxc_client) {
incLostConn();
if (multi_cntn)
showDXClusterErr ("Multiple logins");
else
showDXClusterErr ("Login failed");
return (false);
}
// all ok
dxcLog ("Cluster connection now operational\n");
mapMsg (1000, "Connected to %s:%d", dxhost, dxport);
} else {
showDXClusterErr ("%s:%d Connection failed", dxhost, dxport);
return (false);
}
}
// if get here the connection is ready, finish remaining prep
// get max age
if (!NVReadUInt8 (NV_DXCAGE, &dxc_age)) {
dxc_age = dxc_ages[1];
NVWriteUInt8 (NV_DXCAGE, dxc_age);
}
// determine dxc_showbio
uint8_t bio = 0;
if (getQRZId() != QRZ_NONE) {
if (!NVReadUInt8 (NV_DXCBIO, &bio)) {
bio = 0;
NVWriteUInt8 (NV_DXCBIO, bio);
}
}
dxc_showbio = (bio != 0);
// note for background
new_dxc_cntn = true;
// fresh heartbeat
dxc_activity_ms = myNow();
// ok
return (true);
}
/* called often while pane is visible, fresh is set when newly so.
* connect if not already then show list.
* return whether connection is open.
* N.B. we never read new spots here, that is done by checkDXCluster()
*/
bool updateDXCluster (const SBox &box, bool fresh)
{
// insure connected
if (!isDXClusterConnected()) {
if (!connectDXCluster()) { // shows mapMsg if trouble
initDXGUI (box);
showDXCHost (box, RA8875_RED);
return (false);
}