--- /dev/null
+#
+# Copyright (C) 2021 Felix Fietkau <nbd@nbd.name>
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=minstrel-rcd
+PKG_RELEASE:=1
+
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)-$(PKG_RELEASE)
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+define Package/minstrel-rcd
+ SECTION:=utils
+ CATEGORY:=Utilities
+ TITLE:=Daemon for remote controlling mac80211 minstrel rate control
+ DEPENDS:=+libubox
+ MAINTAINER:=Felix Fietkau <nbd@nbd.name>
+endef
+
+define Package/minstrel-rcd/conffiles
+/etc/config/minstrel-rcd
+endef
+
+define Package/minstrel-rcd/install
+ $(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config
+ $(INSTALL_DATA) ./files/minstrel-rcd.config $(1)/etc/config/minstrel-rcd
+ $(INSTALL_BIN) ./files/minstrel-rcd.init $(1)/etc/init.d/minstrel-rcd
+ $(INSTALL_BIN) $(PKG_BUILD_DIR)/minstrel-rcd $(1)/usr/sbin/
+endef
+
+$(eval $(call BuildPackage,minstrel-rcd))
--- /dev/null
+config rcd rcd
+ option enabled '0'
+ option listen '0.0.0.0'
--- /dev/null
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2020 OpenWrt.org
+
+START=99
+USE_PROCD=1
+PROG=/usr/sbin/minstrel-rcd
+
+service_triggers() {
+ procd_add_reload_trigger "minstrel-rcd"
+}
+
+validate_rcd_section() {
+ uci_load_validate minstrel-rcd rcd "$1" "$2" \
+ 'listen:list(host)' 'enabled:bool:1'
+}
+
+start_rcd_instance() {
+ [ "$enabled" -eq 0 ] && return
+
+ procd_open_instance
+ procd_set_param command "$PROG"
+ for addr in $listen; do
+ procd_append_param command -h $addr
+ done
+ procd_set_param respawn
+ procd_close_instance
+}
+
+start_service() {
+ validate_rcd_section rcd start_rcd_instance
+}
--- /dev/null
+cmake_minimum_required(VERSION 2.6)
+
+PROJECT(minstrel-rcd C)
+
+ADD_DEFINITIONS(-Wall -Werror)
+IF(CMAKE_C_COMPILER_VERSION VERSION_GREATER 6)
+ ADD_DEFINITIONS(-Wextra -Werror=implicit-function-declaration)
+ ADD_DEFINITIONS(-Wformat -Werror=format-security -Werror=format-nonliteral)
+ENDIF()
+ADD_DEFINITIONS(-Os -std=gnu99 -g3 -Wmissing-declarations -Wno-unused-parameter)
+
+FIND_LIBRARY(ubox_library NAMES ubox)
+FIND_PATH(ubox_include_dir libubox/usock.h)
+INCLUDE_DIRECTORIES(${ubox_include_dir})
+
+ADD_EXECUTABLE(minstrel-rcd main.c phy.c server.c client.c)
+TARGET_LINK_LIBRARIES(minstrel-rcd ${ubox_library})
+
+INSTALL(TARGETS minstrel-rcd
+ RUNTIME DESTINATION sbin
+)
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 Felix Fietkau <nbd@nbd.name> */
+#include "rcd.h"
+
+static LIST_HEAD(clients);
+
+void rcd_client_set_phy_state(struct client *cl, struct phy *phy, bool add)
+{
+ if (!cl) {
+ list_for_each_entry(cl, &clients, list)
+ rcd_client_set_phy_state(cl, phy, add);
+ return;
+ }
+
+ if (add && !cl->init_done) {
+ rcd_phy_dump(cl, phy);
+ cl->init_done = true;
+ }
+
+ client_phy_printf(cl, phy, "0;%s\n", add ? "add" : "remove");
+}
+
+void rcd_client_phy_event(struct phy *phy, const char *str)
+{
+ struct client *cl;
+
+ list_for_each_entry(cl, &clients, list)
+ client_phy_printf(cl, phy, "%s\n", str);
+}
+
+static void
+client_start(struct client *cl)
+{
+ struct phy *phy;
+
+ vlist_for_each_element(&phy_list, phy, node)
+ rcd_client_set_phy_state(cl, phy, true);
+}
+
+static int
+client_handle_data(struct client *cl, char *data)
+{
+ char *sep;
+ int len = 0;
+
+ while ((sep = strchr(data, '\n')) != NULL) {
+ len += sep - data + 1;
+ if (sep[-1] == '\r')
+ sep[-1] = 0;
+ *sep = 0;
+ rcd_phy_control(cl, data);
+ data = sep + 1;
+ }
+
+ return len;
+}
+
+static void
+client_notify_read(struct ustream *s, int bytes)
+{
+ struct client *cl = container_of(s, struct client, sfd.stream);
+ char *data;
+ int len;
+
+ while (1) {
+ data = ustream_get_read_buf(s, &len);
+ if (!data)
+ return;
+
+ len = client_handle_data(cl, data);
+ if (!len)
+ return;
+
+ ustream_consume(s, len);
+ }
+}
+
+static void
+client_notify_state(struct ustream *s)
+{
+ struct client *cl = container_of(s, struct client, sfd.stream);
+
+ if (!s->write_error && !s->eof)
+ return;
+
+ ustream_free(s);
+ close(cl->sfd.fd.fd);
+ list_del(&cl->list);
+ free(cl);
+}
+
+void rcd_client_accept(int fd)
+{
+ struct ustream *us;
+ struct client *cl;
+
+ cl = calloc(1, sizeof(*cl));
+ us = &cl->sfd.stream;
+ us->notify_read = client_notify_read;
+ us->notify_state = client_notify_state;
+ us->string_data = true;
+ ustream_fd_init(&cl->sfd, fd);
+ list_add_tail(&cl->list, &clients);
+ client_start(cl);
+}
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 Felix Fietkau <nbd@nbd.name> */
+#include <libubox/usock.h>
+#include <libubox/ustream.h>
+#include "rcd.h"
+
+int main(int argc, char **argv)
+{
+ int ch;
+
+ uloop_init();
+
+ while ((ch = getopt(argc, argv, "h:")) != -1) {
+ switch (ch) {
+ case 'h':
+ rcd_server_add(optarg);
+ break;
+ }
+ }
+
+ rcd_phy_init();
+ rcd_server_init();
+ uloop_run();
+
+ uloop_end();
+
+ return 0;
+}
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 Felix Fietkau <nbd@nbd.name> */
+#include <libubox/avl-cmp.h>
+#include <glob.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <libgen.h>
+#include "rcd.h"
+
+static void phy_update(struct vlist_tree *tree, struct vlist_node *node_new,
+ struct vlist_node *node_old);
+
+VLIST_TREE(phy_list, avl_strcmp, phy_update, true, false);
+
+static const char *
+phy_file_path(struct phy *phy, const char *file)
+{
+ static char path[64];
+
+ snprintf(path, sizeof(path), "/sys/kernel/debug/ieee80211/%s/rc/%s", phy_name(phy), file);
+
+ return path;
+}
+
+static int
+phy_event_read_buf(struct phy *phy, char *buf)
+{
+ char *cur, *next;
+ int len;
+
+ for (cur = buf; (next = strchr(cur, '\n')); cur = next + 1) {
+ *next = 0;
+
+ rcd_client_phy_event(phy, cur);
+ }
+
+ len = strlen(cur);
+ if (cur > buf)
+ memmove(buf, cur, len + 1);
+
+ return len;
+}
+
+static void
+phy_event_cb(struct uloop_fd *fd, unsigned int events)
+{
+ struct phy *phy = container_of(fd, struct phy, event_fd);
+ char buf[512];
+ int len, offset = 0;
+
+ while (1) {
+ len = read(fd->fd, buf + offset, sizeof(buf) - 1 - offset);
+ if (len < 0) {
+ if (errno == EAGAIN)
+ return;
+
+ if (errno == EINTR)
+ continue;
+
+ vlist_delete(&phy_list, &phy->node);
+ return;
+ }
+
+ if (!len)
+ return;
+
+ buf[offset + len] = 0;
+ offset = phy_event_read_buf(phy, buf);
+ }
+}
+
+static void
+phy_init(struct phy *phy)
+{
+ phy->control_fd = -1;
+}
+
+static void
+phy_add(struct phy *phy)
+{
+ int cfd, efd;
+
+ cfd = open(phy_file_path(phy, "api_control"), O_WRONLY);
+ if (cfd < 0)
+ goto remove;
+
+ efd = open(phy_file_path(phy, "api_event"), O_RDONLY);
+ if (efd < 0)
+ goto close_cfd;
+
+ phy->control_fd = cfd;
+ phy->event_fd.fd = efd;
+ phy->event_fd.cb = phy_event_cb;
+ uloop_fd_add(&phy->event_fd, ULOOP_READ);
+
+ rcd_client_set_phy_state(NULL, phy, true);
+ return;
+
+close_cfd:
+ close(cfd);
+remove:
+ vlist_delete(&phy_list, &phy->node);
+}
+
+static void
+phy_remove(struct phy *phy)
+{
+ if (phy->control_fd < 0)
+ goto out;
+
+ rcd_client_set_phy_state(NULL, phy, false);
+ uloop_fd_delete(&phy->event_fd);
+ close(phy->control_fd);
+ close(phy->event_fd.fd);
+
+out:
+ free(phy);
+}
+
+static void
+phy_update(struct vlist_tree *tree, struct vlist_node *node_new,
+ struct vlist_node *node_old)
+{
+ struct phy *phy_new = node_new ? container_of(node_new, struct phy, node) : NULL;
+ struct phy *phy_old = node_old ? container_of(node_old, struct phy, node) : NULL;
+
+ if (phy_new && phy_old)
+ phy_remove(phy_new);
+ else if (phy_new)
+ phy_add(phy_new);
+ else
+ phy_remove(phy_old);
+}
+
+static void phy_refresh_timer(struct uloop_timeout *t)
+{
+ unsigned int i;
+ glob_t gl;
+
+ glob("/sys/kernel/debug/ieee80211/phy*", 0, NULL, &gl);
+ for (i = 0; i < gl.gl_pathc; i++) {
+ struct phy *phy;
+ char *name, *name_buf;
+
+ name = basename(gl.gl_pathv[i]);
+ phy = calloc_a(sizeof(*phy), &name_buf, strlen(name) + 1);
+ phy_init(phy);
+ vlist_add(&phy_list, &phy->node, strcpy(name_buf, name));
+ }
+ globfree(&gl);
+
+ uloop_timeout_set(t, 1000);
+}
+
+void rcd_phy_init_client(struct client *cl)
+{
+ struct phy *phy;
+
+ vlist_for_each_element(&phy_list, phy, node)
+ rcd_client_set_phy_state(cl, phy, true);
+}
+
+void rcd_phy_dump(struct client *cl, struct phy *phy)
+{
+ char buf[128];
+ FILE *f;
+
+ f = fopen(phy_file_path(phy, "api_info"), "r");
+ if (!f)
+ return;
+
+ while (fgets(buf, sizeof(buf), f) != NULL)
+ client_printf(cl, "*;0;%s", buf);
+
+ fclose(f);
+}
+
+void rcd_phy_control(struct client *cl, char *data)
+{
+ struct phy *phy;
+ const char *err;
+ char *sep;
+
+ sep = strchr(data, ';');
+ if (!sep) {
+ err = "Syntax error";
+ goto error;
+ }
+
+ *sep = 0;
+ phy = vlist_find(&phy_list, data, phy, node);
+ if (!phy) {
+ err = "PHY not found";
+ goto error;
+ }
+
+ data = sep + 1;
+retry:
+ if (write(phy->control_fd, data, strlen(data)) < 0) {
+ if (errno == EINTR || errno == EAGAIN)
+ goto retry;
+
+ err = strerror(errno);
+ goto error;
+ }
+
+ return;
+
+error:
+ client_printf(cl, "*;0;#error;%s\n", err);
+}
+
+void rcd_phy_init(void)
+{
+ static struct uloop_timeout t = {
+ .cb = phy_refresh_timer
+ };
+
+ uloop_timeout_set(&t, 1);
+}
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 Felix Fietkau <nbd@nbd.name> */
+#ifndef __MINSTREL_RCD_H
+#define __MINSTREL_RCD_H
+
+#include <libubox/list.h>
+#include <libubox/vlist.h>
+#include <libubox/uloop.h>
+#include <libubox/ustream.h>
+#include <libubox/utils.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define RCD_PORT 0x5243
+
+struct phy {
+ struct vlist_node node;
+
+ struct uloop_fd event_fd;
+ int control_fd;
+};
+
+struct client {
+ struct list_head list;
+ struct ustream_fd sfd;
+ bool init_done;
+};
+
+struct server {
+ struct list_head list;
+ struct uloop_fd fd;
+ const char *addr;
+};
+
+static inline const char *phy_name(struct phy *phy)
+{
+ return phy->node.avl.key;
+}
+
+extern struct vlist_tree phy_list;
+
+void rcd_server_add(const char *addr);
+void rcd_server_init(void);
+
+void rcd_client_accept(int fd);
+void rcd_client_phy_event(struct phy *phy, const char *str);
+void rcd_client_set_phy_state(struct client *cl, struct phy *phy, bool add);
+
+void rcd_phy_init(void);
+void rcd_phy_init_client(struct client *cl);
+void rcd_phy_dump(struct client *cl, struct phy *phy);
+void rcd_phy_control(struct client *cl, char *data);
+
+#define client_printf(cl, ...) ustream_printf(&(cl)->sfd.stream, __VA_ARGS__)
+#define client_phy_printf(cl, phy, fmt, ...) client_printf(cl, "%s;" fmt, phy_name(phy), ## __VA_ARGS__)
+
+#endif
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 Felix Fietkau <nbd@nbd.name> */
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <libubox/usock.h>
+
+#include "rcd.h"
+
+static LIST_HEAD(servers);
+static LIST_HEAD(pending);
+static bool in_init;
+static struct uloop_timeout restart_timer;
+
+static void
+server_cb(struct uloop_fd *fd, unsigned int events)
+{
+ struct server *s = container_of(fd, struct server, fd);
+ struct sockaddr_in6 addr;
+ unsigned int sl;
+ int cfd;
+
+ while (1) {
+ sl = sizeof(addr);
+ cfd = accept(fd->fd, (struct sockaddr *)&addr, &sl);
+
+ if (cfd < 0) {
+ if (errno == EAGAIN)
+ return;
+
+ if (errno == EINTR)
+ continue;
+
+ /* other error, restart */
+ uloop_fd_delete(fd);
+ close(fd->fd);
+ list_move_tail(&s->list, &pending);
+ uloop_timeout_set(&restart_timer, 1000);
+ }
+
+ rcd_client_accept(cfd);
+ }
+}
+
+static void server_start(struct server *s)
+{
+ s->fd.fd = usock(USOCK_SERVER | USOCK_NONBLOCK | USOCK_TCP, s->addr, usock_port(RCD_PORT));
+ if (s->fd.fd < 0) {
+ if (in_init)
+ fprintf(stderr, "WARNING: Failed to open server port on %s\n", s->addr);
+ return;
+ }
+
+ s->fd.cb = server_cb;
+ uloop_fd_add(&s->fd, ULOOP_READ);
+ list_move_tail(&s->list, &servers);
+}
+
+static void
+server_start_pending(struct uloop_timeout *timeout)
+{
+ struct server *s, *tmp;
+
+ list_for_each_entry_safe(s, tmp, &pending, list)
+ server_start(s);
+
+ if (!list_empty(&pending))
+ uloop_timeout_set(timeout, 1000);
+}
+
+void rcd_server_add(const char *addr)
+{
+ struct server *s;
+
+ s = calloc(1, sizeof(*s));
+ s->addr = addr;
+ list_add_tail(&s->list, &pending);
+
+}
+
+void rcd_server_init(void)
+{
+ if (list_empty(&pending))
+ rcd_server_add("127.0.0.1");
+
+ restart_timer.cb = server_start_pending;
+
+ in_init = true;
+ server_start_pending(&restart_timer);
+ in_init = false;
+}