console-server: Add UART Mux Support

This commit adds support for uart-muxes which can be controlled via
gpios.

Change-Id: I91a4de1962554adf4302a2a59d2b371f492dc21d
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
Signed-off-by: Andrew Jeffery <andrew@codeconstruct.com.au>
diff --git a/console-dbus.c b/console-dbus.c
index f6749e1..778f1cd 100644
--- a/console-dbus.c
+++ b/console-dbus.c
@@ -20,8 +20,9 @@
 #include <string.h>
 #include <sys/socket.h>
 
-#include "console-server.h"
 #include "config.h"
+#include "console-mux.h"
+#include "console-server.h"
 
 /* size of the dbus object path length */
 const size_t dbus_obj_path_len = 1024;
@@ -127,6 +128,8 @@
 		return sd_bus_reply_method_error(msg, err);
 	}
 
+	console_mux_activate(console);
+
 	/* Register the consumer. */
 	socket_fd = dbus_create_socket_consumer(console);
 	if (socket_fd < 0) {
diff --git a/console-mux.c b/console-mux.c
new file mode 100644
index 0000000..105d968
--- /dev/null
+++ b/console-mux.c
@@ -0,0 +1,320 @@
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "console-server.h"
+#include "console-mux.h"
+#include "config.h"
+
+struct console_gpio {
+	char *name;
+	struct gpiod_line *line;
+};
+
+struct console_mux {
+	struct console_gpio *mux_gpios;
+	size_t n_mux_gpios;
+};
+
+static const char *key_mux_gpios = "mux-gpios";
+static const char *key_mux_index = "mux-index";
+
+__attribute__((nonnull)) static size_t strtokcnt(const char *str,
+						 const char sep)
+{
+	ssize_t n = 1;
+
+	while (*str) {
+		if (*str == sep) {
+			n++;
+		}
+		str++;
+	}
+
+	return n;
+}
+
+__attribute__((nonnull)) static size_t
+count_mux_gpios(const char *config_mux_gpios)
+{
+	return strtokcnt(config_mux_gpios, ',');
+}
+
+__attribute__((nonnull)) static char *
+extract_mux_gpio_name(const char **config_gpio_names)
+{
+	const char *current;
+	const char *comma;
+	ptrdiff_t length;
+
+	assert(*config_gpio_names);
+	current = *config_gpio_names;
+	comma = strchrnul(current, ',');
+	length = comma - current;
+
+	if (length == 0) {
+		return NULL;
+	}
+
+	char *word = calloc(length + 1, 1);
+	if (!word) {
+		return NULL;
+	}
+
+	strncpy(word, current, length);
+
+	*config_gpio_names = comma + !!(*comma);
+
+	return word;
+}
+
+__attribute__((nonnull)) static struct console_gpio *
+console_mux_find_gpio_by_index(struct console_gpio *gpio,
+			       const char **config_gpio_names)
+{
+	assert(*config_gpio_names);
+
+	gpio->name = extract_mux_gpio_name(config_gpio_names);
+	if (gpio->name == NULL) {
+		warnx("could not extract mux gpio name from config '%s'",
+		      *config_gpio_names);
+		return NULL;
+	}
+
+	gpio->line = gpiod_line_find(gpio->name);
+	if (gpio->line == NULL) {
+		warnx("libgpiod: could not find line %s", gpio->name);
+		free(gpio->name);
+		return NULL;
+	}
+
+	return gpio;
+}
+
+__attribute__((nonnull)) static void
+console_mux_release_gpio_lines(struct console_server *server)
+{
+	for (unsigned long i = 0; i < server->mux->n_mux_gpios; i++) {
+		struct console_gpio *gpio = &server->mux->mux_gpios[i];
+		gpiod_line_release(gpio->line);
+		gpiod_line_close_chip(gpio->line);
+
+		free(gpio->name);
+		gpio->name = NULL;
+	}
+}
+
+__attribute__((nonnull)) static int
+console_mux_request_gpio_lines(struct console_server *server,
+			       const char *config_gpio_names)
+{
+	const char *current = config_gpio_names;
+	struct console_gpio *gpio;
+	int status = 0;
+
+	for (server->mux->n_mux_gpios = 0; *current;
+	     server->mux->n_mux_gpios++) {
+		size_t i = server->mux->n_mux_gpios;
+		gpio = console_mux_find_gpio_by_index(
+			&server->mux->mux_gpios[i], &current);
+		if (gpio == NULL) {
+			console_mux_release_gpio_lines(server);
+			return -1;
+		}
+
+		status = gpiod_line_request_output(
+			gpio->line, program_invocation_short_name, 0);
+		if (status != 0) {
+			warnx("could not set line %s as output", gpio->name);
+			warnx("releasing all lines already requested");
+			console_mux_release_gpio_lines(server);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+int console_server_mux_init(struct console_server *server)
+{
+	const char *config_gpio_names;
+	size_t max_ngpios;
+	size_t ngpios;
+
+	config_gpio_names = config_get_value(server->config, key_mux_gpios);
+	if (!config_gpio_names) {
+		return 0;
+	}
+
+	ngpios = count_mux_gpios(config_gpio_names);
+	max_ngpios = sizeof(((struct console *)0)->mux_index) * CHAR_BIT;
+	if (ngpios > max_ngpios) {
+		return -1;
+	}
+
+	server->mux = calloc(1, sizeof(struct console_mux));
+	if (!server->mux) {
+		return -1;
+	}
+
+	server->mux->n_mux_gpios = 0;
+	server->mux->mux_gpios = calloc(ngpios, sizeof(struct console_gpio));
+	if (!server->mux->mux_gpios) {
+		return -1;
+	}
+
+	return console_mux_request_gpio_lines(server, config_gpio_names);
+}
+
+void console_server_mux_fini(struct console_server *server)
+{
+	if (!server->mux) {
+		return;
+	}
+
+	console_mux_release_gpio_lines(server);
+
+	free(server->mux->mux_gpios);
+	server->mux->mux_gpios = NULL;
+
+	free(server->mux);
+	server->mux = NULL;
+}
+
+int console_mux_init(struct console *console, struct config *config)
+{
+	if (!console->server->mux) {
+		return 0;
+	}
+
+	if (console->server->mux->n_mux_gpios == 0) {
+		return 0;
+	}
+
+	const char *gpio_value = config_get_section_value(
+		config, console->console_id, key_mux_index);
+
+	if (gpio_value == NULL) {
+		warnx("console %s does not have property %s in config",
+		      console->console_id, key_mux_index);
+		return -1;
+	}
+
+	errno = 0;
+	console->mux_index = strtoul(gpio_value, NULL, 0);
+	if (errno == ERANGE) {
+		return -1;
+	}
+
+	return 0;
+}
+
+static int console_timestamp(char *buffer, size_t size)
+{
+	size_t status;
+	time_t rawtime;
+	struct tm *timeinfo;
+
+	time(&rawtime);
+	timeinfo = gmtime(&rawtime);
+
+	status = strftime(buffer, size, "%Y-%m-%d %H:%M:%S UTC", timeinfo);
+	return !status;
+}
+
+static int console_print_timestamped(struct console *console,
+				     const char *message)
+{
+#define TIMESTAMP_MAX_SIZE 32
+	char buf_timestamp[TIMESTAMP_MAX_SIZE];
+	int status;
+	char *buf;
+
+	status = console_timestamp(buf_timestamp, sizeof(buf_timestamp));
+	if (status != 0) {
+		warnx("Error: unable to print timestamp");
+		return status;
+	}
+
+	status = asprintf(&buf, "[obmc-console] %s %s\n", buf_timestamp,
+			  message);
+	if (status == -1) {
+		return -1;
+	}
+
+	ringbuffer_queue(console->rb, (uint8_t *)buf, strlen(buf));
+
+	free(buf);
+
+	return 0;
+}
+
+static int console_mux_set_lines(struct console *console)
+{
+	int status = 0;
+
+	for (size_t i = 0; i < console->server->mux->n_mux_gpios; i++) {
+		struct console_gpio *gpio = &console->server->mux->mux_gpios[i];
+		const uint8_t value = (console->mux_index >> i) & 0x1;
+
+		status = gpiod_line_set_value(gpio->line, value);
+		if (status != 0) {
+			warnx("could not set line %s", gpio->name);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+int console_mux_activate(struct console *console)
+{
+	struct console_server *server = console->server;
+	const bool first_activation = server->active == NULL;
+	const bool is_active = server->active == console;
+	int status = 0;
+
+	if (is_active) {
+		return 0;
+	}
+
+	if (server->mux) {
+		status = console_mux_set_lines(console);
+	}
+
+	if (status != 0) {
+		warnx("Error: unable to set mux gpios");
+		return status;
+	}
+
+	server->active = console;
+
+	/* Don't print disconnect/connect events on startup */
+	if (first_activation) {
+		return 0;
+	}
+
+	for (size_t i = 0; i < server->n_consoles; i++) {
+		struct console *other = server->consoles[i];
+		if (other == console) {
+			continue;
+		}
+		console_print_timestamped(other, "DISCONNECTED");
+
+		for (long j = 0; j < other->n_handlers; j++) {
+			struct handler *h = other->handlers[j];
+
+			if (h->type->deselect) {
+				h->type->deselect(h);
+			}
+		}
+	}
+
+	console_print_timestamped(console, "CONNECTED");
+
+	return 0;
+}
diff --git a/console-mux.h b/console-mux.h
new file mode 100644
index 0000000..7b2942a
--- /dev/null
+++ b/console-mux.h
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2024 9elements
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+struct config;
+struct console;
+struct console_server;
+
+int console_server_mux_init(struct console_server *server);
+void console_server_mux_fini(struct console_server *server);
+int console_mux_init(struct console *console, struct config *config);
+int console_mux_activate(struct console *console);
diff --git a/console-server.c b/console-server.c
index 870e5a7..60d2d2e 100644
--- a/console-server.c
+++ b/console-server.c
@@ -38,6 +38,8 @@
 #include <sys/socket.h>
 #include <poll.h>
 
+#include "console-mux.h"
+
 #include "console-server.h"
 #include "config.h"
 
@@ -1075,6 +1077,12 @@
 		goto cleanup_console;
 	}
 
+	rc = console_mux_init(console, config);
+	if (rc) {
+		warnx("could not set mux gpios from config, exiting");
+		goto cleanup_rb;
+	}
+
 	if (set_socket_info(console, config, console_id)) {
 		warnx("set_socket_info failed");
 		goto cleanup_rb;
@@ -1210,6 +1218,11 @@
 		return -1;
 	}
 
+	rc = console_server_mux_init(server);
+	if (rc != 0) {
+		return -1;
+	}
+
 	uart_routing_init(server->config);
 
 	rc = tty_init(server, server->config, config_tty_kname);
@@ -1224,8 +1237,14 @@
 		return -1;
 	}
 
-	server->active = console_server_add_consoles(server, console_id);
-	if (server->active == NULL) {
+	struct console *initial_active =
+		console_server_add_consoles(server, console_id);
+	if (initial_active == NULL) {
+		return -1;
+	}
+
+	rc = console_mux_activate(initial_active);
+	if (rc != 0) {
 		return -1;
 	}
 
@@ -1242,6 +1261,7 @@
 	dbus_server_fini(server);
 	tty_fini(server);
 	free(server->pollfds);
+	console_server_mux_fini(server);
 	config_fini(server->config);
 }
 
diff --git a/console-server.h b/console-server.h
index 7c2e711..0636dc7 100644
--- a/console-server.h
+++ b/console-server.h
@@ -52,6 +52,7 @@
 				struct console *console, struct config *config);
 	void (*fini)(struct handler *handler);
 	int (*baudrate)(struct handler *handler, speed_t baudrate);
+	void (*deselect)(struct handler *handler);
 };
 
 struct handler {
@@ -136,6 +137,9 @@
 	size_t dbus_pollfd_index;
 
 	struct sd_bus *bus;
+
+	// may be NULL in case there is no mux
+	struct console_mux *mux;
 };
 
 struct console {
@@ -156,6 +160,9 @@
 
 	struct poller **pollers;
 	long n_pollers;
+
+	// values to configure the mux
+	unsigned long mux_index;
 };
 
 /* poller API */
diff --git a/log-handler.c b/log-handler.c
index 732fe2e..79b0f99 100644
--- a/log-handler.c
+++ b/log-handler.c
@@ -180,7 +180,13 @@
 	}
 	lh->maxsize = logsize <= lh->pagesize ? lh->pagesize + 1 : logsize;
 
-	filename = config_get_value(config, "logfile");
+	filename = config_get_section_value(config, console->console_id,
+					    "logfile");
+
+	if (!filename && console->server->n_consoles == 1) {
+		filename = config_get_value(config, "logfile");
+	}
+
 	if (!filename) {
 		filename = default_filename;
 	}
diff --git a/meson.build b/meson.build
index c0a600d..27ac968 100644
--- a/meson.build
+++ b/meson.build
@@ -59,6 +59,7 @@
            'console-dbus.c',
            'console-server.c',
            'console-socket.c',
+           'console-mux.c',
            'log-handler.c',
            'ringbuffer.c',
            'socket-handler.c',
@@ -71,6 +72,7 @@
            dependencies: [
              dependency('libsystemd'),
              iniparser_dep,
+             dependency('libgpiod'),
              meson.get_compiler('c').find_library('rt')
            ],
            install_dir: get_option('sbindir'),
diff --git a/socket-handler.c b/socket-handler.c
index 6268fca..efcd5b8 100644
--- a/socket-handler.c
+++ b/socket-handler.c
@@ -30,6 +30,7 @@
 #include <sys/un.h>
 #include <systemd/sd-daemon.h>
 
+#include "console-mux.h"
 #include "console-server.h"
 
 #define SOCKET_HANDLER_PKT_SIZE 512
@@ -321,6 +322,8 @@
 		return POLLER_OK;
 	}
 
+	console_mux_activate(sh->console);
+
 	client = malloc(sizeof(*client));
 	memset(client, 0, sizeof(*client));
 
@@ -496,6 +499,17 @@
 	return NULL;
 }
 
+static void socket_deselect(struct handler *handler)
+{
+	struct socket_handler *sh = to_socket_handler(handler);
+
+	while (sh->n_clients) {
+		struct client *c = sh->clients[0];
+		client_drain_queue(c, 0);
+		client_close(c);
+	}
+}
+
 static void socket_fini(struct handler *handler)
 {
 	struct socket_handler *sh = to_socket_handler(handler);
@@ -515,6 +529,7 @@
 static const struct handler_type socket_handler = {
 	.name = "socket",
 	.init = socket_init,
+	.deselect = socket_deselect,
 	.fini = socket_fini,
 };
 
diff --git a/subprojects/libgpiod.wrap b/subprojects/libgpiod.wrap
new file mode 100644
index 0000000..3a92657
--- /dev/null
+++ b/subprojects/libgpiod.wrap
@@ -0,0 +1,14 @@
+[wrap-file]
+directory = libgpiod-1.6.5
+source_url = https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/libgpiod-1.6.5.tar.gz
+source_filename = libgpiod-1.6.5.tar.gz
+source_hash = 1473d3035b506065393a4569763cf6b5c98e59c8f865326374ebadffa2578f3a
+patch_filename = libgpiod_1.6.5-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/libgpiod_1.6.5-1/get_patch
+patch_hash = a8ee005ecaeaf9d4382d196a1b82e808e72b750f6b8cf61b84f7ccee134b8d4f
+source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libgpiod_1.6.5-1/libgpiod-1.6.5.tar.gz
+wrapdb_version = 1.6.5-1
+
+[provide]
+libgpiod = gpiod_dep
+libgpiodcxx = gpiodcxx_dep