contest: add a window with summary of other instances on the network

This window has some limitations.  For example, it is fixed size so only the
first 5 instances will ever be displayed, instances that disappear forever
will take up one of the 5 lines, etc.

Signed-off-by: Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
4 files changed, 210 insertions(+), 1 deletions(-)

M hlog/CMakeLists.txt
M hlog/contest/contest.h
A => hlog/contest/ui-net.c
M hlog/contest/ui.c
M hlog/CMakeLists.txt +1 -0
@@ 61,6 61,7 @@ add_executable(hlog-contest
 	contest/save.c
 	contest/script.c
 	contest/ui-current.c
+	contest/ui-net.c
 	contest/ui-work.c
 	contest/ui.c
 	contest/wsjtx.c

          
M hlog/contest/contest.h +11 -0
@@ 24,6 24,7 @@ 
 #define __CONTEST_H
 
 #include <hlog/qso.h>
+#include <hlog-rpc/announce-sub.h>
 #include <hlog-lua/xlua.h>
 
 #include "../common/recent.h"

          
@@ 32,6 33,8 @@ 
 
 #define ANNOUNCE_INFO_PERIOD	10 /* s */
 
+#define NET_LIST_LINES		5 /* number of network window lines */
+
 struct contest_params {
 	struct str *name;
 	uint32_t year;

          
@@ 71,6 74,14 @@ extern void callout_destroy(void);
 extern void callout_set(const char *str);
 
 /*
+ * network window
+ */
+extern int net_init(int height, int width, int row, int col);
+extern void net_destroy(void);
+extern void net_refresh(void);
+extern void net_update_row(const struct instance_info *inst);
+
+/*
  * message window
  */
 enum msg_src {

          
A => hlog/contest/ui-net.c +181 -0
@@ 0,0 1,181 @@ 
+/*
+ * Copyright (c) 2024 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <jeffpc/mem.h>
+
+#include <hlog/util.h>
+
+#include "contest.h"
+
+static LOCK_CLASS(lines_lc);
+static struct lock lines_lock;
+static WINDOW *lines_win;
+static size_t nrows;
+static size_t lwidth;
+
+struct line {
+	uint64_t ts;
+	size_t row;
+	uint32_t id;
+	char buf[];
+};
+static struct line **lines;
+
+static void __attribute__((constructor)) net_init_mutex(void)
+{
+	MXINIT(&lines_lock, &lines_lc);
+}
+
+int net_init(int height, int width, int row, int col)
+{
+	size_t i;
+
+	lines = mem_recallocarray(NULL, 0, height, sizeof(struct line *));
+	if (!lines)
+		return -ENOMEM;
+
+	for (i = 0; i < height; i++) {
+		lines[i] = zalloc(sizeof(struct line) + width + 1);
+		ASSERT3P(lines[i], !=, NULL);
+
+		lines[i]->row = i;
+	}
+
+	lines_win = newwin(height, width, row, col);
+	nrows = height;
+	lwidth = width;
+
+	return 0;
+}
+
+void net_destroy(void)
+{
+	size_t i;
+
+	/*
+	 * destroying even though it was initialized by the constructor to
+	 * catch use-after-destory of the net window & data
+	 */
+	MXDESTROY(&lines_lock);
+
+	delwin(lines_win);
+
+	for (i = 0; i < nrows; i++)
+		free(lines[i]);
+	free(lines);
+}
+
+void net_refresh(void)
+{
+	const uint64_t now = gettime();
+	size_t i;
+
+	MXLOCK(&lines_lock);
+	for (i = 0; i < nrows; i++) {
+		struct line *line = lines[i];
+		const char *age;
+		char age_buf[7];
+
+		/* calculate the age */
+		if (!line->ts || (line->ts > now)) {
+			age = "-";
+		} else {
+			const uint64_t delta = now - line->ts;
+			unsigned secs;
+
+			/* whole seconds */
+			secs = delta / 1000000000ull;
+
+			/* round up to the nearest second */
+			secs += ((delta % 1000000000ull) >= 500000000ull);
+
+			if (secs >= (100 * 60)) {
+				age = ">=100m";
+			} else if (secs > 59) {
+				snprintf(age_buf, sizeof(age_buf), "%um%us ",
+					 secs / 60, secs % 60);
+				age = age_buf;
+			} else {
+				snprintf(age_buf, sizeof(age_buf), "%us   ",
+					 secs);
+				age = age_buf;
+			}
+		}
+
+		mvwprintw(lines_win, i, 0, "%s", lines[i]->buf);
+		mvwprintw(lines_win, i, lwidth - 5 - 1, "%s", age);
+	}
+	MXUNLOCK(&lines_lock);
+
+	wrefresh(lines_win);
+}
+
+void net_update_row(const struct instance_info *inst)
+{
+	const char *tx_mode;
+	const char *rx_mode;
+	char tx_freq[11];
+	char rx_freq[11];
+	char power[6];
+	struct line *line;
+	size_t i;
+
+	MXLOCK(&lines_lock);
+
+	for (i = 0, line = NULL; i < nrows; i++) {
+		if (lines[i]->id == inst->id) {
+			line = lines[i];
+			goto found;
+		}
+
+		if (!lines[i]->id && !line)
+			line = lines[i];
+	}
+
+	/* no instance id match found */
+
+	if (!line)
+		goto out; /* no free entries; just ignore this announcement */
+
+	line->id = inst->id;
+
+found:
+	pretty_print_power(power, sizeof(power), VAL_ALLOC_INT(inst->rig.power));
+	pretty_print_freq(tx_freq, sizeof(tx_freq), VAL_ALLOC_INT(inst->rig.tx_freq));
+	pretty_print_freq(rx_freq, sizeof(rx_freq), VAL_ALLOC_INT(inst->rig.rx_freq));
+
+	tx_mode = (inst->rig.tx_mode == HRIG_MODE_UNKNOWN) ? "?" :
+		hamlib_mode_name_raw(inst->rig.tx_mode);
+	rx_mode = (inst->rig.rx_mode == HRIG_MODE_UNKNOWN) ? "?" :
+		hamlib_mode_name_raw(inst->rig.rx_mode);
+
+	/* generate line */
+	snprintf(line->buf, lwidth, "%-11s %-11s %10s %-6s %-5s %10s %-6s",
+		 inst->hostname, inst->op,
+		 tx_freq, tx_mode, power,
+		 rx_freq, rx_mode);
+
+	line->ts = gettime();
+
+out:
+	MXUNLOCK(&lines_lock);
+}

          
M hlog/contest/ui.c +17 -1
@@ 78,6 78,10 @@ 
 #define RECENT_WIN_ROW		(RECENT_WORK_WIN_ROW + RECENT_WORK_WIN_HEIGHT + 1)
 #define RECENT_WIN_COL		0
 #define RECENT_WIN_HEIGHT	(CURRENT_WIN_ROW - RECENT_WIN_ROW)
+#define NET_WIN_HEIGHT		NET_LIST_LINES
+#define NET_WIN_WIDTH		RIGHT_PANEL_WIDTH
+#define NET_WIN_ROW		(MESSAGES_WIN_ROW - NET_WIN_HEIGHT - 1)
+#define NET_WIN_COL		(LEFT_PANEL_WIDTH + 1)
 #define MESSAGES_WIN_HEIGHT	(LINES / 2)
 #define MESSAGES_WIN_WIDTH	RIGHT_PANEL_WIDTH
 #define MESSAGES_WIN_ROW	(LINES - MESSAGES_WIN_HEIGHT)

          
@@ 701,6 705,12 @@ static void setup_windows(void)
 	xwhline(stdscr, RECENT_WIN_ROW - 1, 0, RECENT_WIN_WIDTH + 1,
 		ACS_HLINE, ACS_HLINE, ACS_RTEE);
 
+	/* net separator */
+	xwhline(stdscr, NET_WIN_ROW - 1, NET_WIN_COL - 1, NET_WIN_WIDTH + 1,
+		ACS_LTEE, ACS_HLINE, ACS_HLINE);
+	mvwprintw(stdscr, NET_WIN_ROW - 1, NET_WIN_COL + 1,
+		  " Network ");
+
 	/* messages separator */
 	xwhline(stdscr, MESSAGES_WIN_ROW - 1, MESSAGES_WIN_COL - 1,
 		MESSAGES_WIN_WIDTH + 1, ACS_LTEE, ACS_HLINE, ACS_HLINE);

          
@@ 758,6 768,10 @@ static void setup_windows(void)
 			  cfields, ARRAY_LEN(cfields));
 	ASSERT0(ret);
 
+	/* net */
+	ret = net_init(NET_WIN_HEIGHT, NET_WIN_WIDTH, NET_WIN_ROW, NET_WIN_COL);
+	ASSERT0(ret);
+
 	/* messages */
 	messages_win = newwin(MESSAGES_WIN_HEIGHT, MESSAGES_WIN_WIDTH,
 			      MESSAGES_WIN_ROW, MESSAGES_WIN_COL);

          
@@ 773,6 787,7 @@ static void refresh_windows(void)
 		       qso_get_station_call(qso_get_rx_side(contact)));
 	recent_refresh(&recent_list, NULL);
 	xform_refresh(&current);
+	net_refresh();
 	wrefresh(messages_win);
 
 	/* move cursor to the active form */

          
@@ 872,7 887,7 @@ int contest_init(struct contest_params *
 		goto err_template;
 	}
 
-	ret = announce_connect(NULL,
+	ret = announce_connect(net_update_row,
 			       handle_qso_notification,
 			       handle_location_notification,
 			       handle_rig_notification);

          
@@ 1009,6 1024,7 @@ int contest_run(lua_State *L, struct xfi
 	recent_destroy(&recent_work_list);
 	recent_destroy(&recent_list);
 	status_deinit();
+	net_destroy();
 	delwin(messages_win);
 
 	endwin();