diff --git a/utils/gl-puli-mcu/Makefile b/utils/gl-puli-mcu/Makefile new file mode 100644 index 0000000000..a0be04b82d --- /dev/null +++ b/utils/gl-puli-mcu/Makefile @@ -0,0 +1,33 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=gl-puli-mcu +PKG_VERSION:=1 +PKG_RELEASE:=$(AUTORELEASE) + +PKG_MAINTAINER:=Nuno Goncalves +PKG_LICENSE:=GPL-3.0-or-later + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/gl-puli-mcu + SECTION:=utils + CATEGORY:=Utilities + TITLE:=GL.iNet GL-XE300 (Puli) power monitoring support + DEPENDS:=+kmod-usb-serial-ch341 +libubus +libubox +endef + +define Package/gl-puli-mcu/description + Interfaces with GL-XE300 (Puli) power monitoring MCU over + a USB to UART adapter present on the device and provides + battery SOC, temperature, charging state and cycle count at + ubus battery/info. +endef + +define Package/gl-puli-mcu/install + $(CP) ./files/* $(1)/ + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/gl-puli-mcu $(1)/usr/sbin/ +endef + +$(eval $(call BuildPackage,gl-puli-mcu)) diff --git a/utils/gl-puli-mcu/files/etc/init.d/gl-puli-mcu b/utils/gl-puli-mcu/files/etc/init.d/gl-puli-mcu new file mode 100755 index 0000000000..d8252973a3 --- /dev/null +++ b/utils/gl-puli-mcu/files/etc/init.d/gl-puli-mcu @@ -0,0 +1,13 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command /usr/sbin/gl-puli-mcu + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} diff --git a/utils/gl-puli-mcu/src/CMakeLists.txt b/utils/gl-puli-mcu/src/CMakeLists.txt new file mode 100644 index 0000000000..de0d2c1a9c --- /dev/null +++ b/utils/gl-puli-mcu/src/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.12) +PROJECT(gl-puli-mcu C) +ADD_DEFINITIONS(-Os -ggdb -Wall --std=gnu17 -Wmissing-declarations) +SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") +ADD_EXECUTABLE(gl-puli-mcu gl-puli-mcu.c) +TARGET_LINK_LIBRARIES(gl-puli-mcu ubox ubus ${CMAKE_DL_LIBS}) +INSTALL(TARGETS gl-puli-mcu RUNTIME DESTINATION sbin) diff --git a/utils/gl-puli-mcu/src/gl-puli-mcu.c b/utils/gl-puli-mcu/src/gl-puli-mcu.c new file mode 100644 index 0000000000..fe55cc42d3 --- /dev/null +++ b/utils/gl-puli-mcu/src/gl-puli-mcu.c @@ -0,0 +1,214 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + * + * Copyright (C) 2014 John Crispin + * Copyright (C) 2021 Nuno Goncalves + */ + +#include +#include + +#include +#include +#include +#include +#include + +static struct ustream_fd stream; +static struct ubus_auto_conn conn; +static struct blob_buf b; + +struct Battery +{ + float temperature; + uint16_t cycles; + uint8_t soc; + bool charging; + bool set; +} battery; + +static bool +process(char *read) +{ + if (read[0] != '{' || + read[1] != 'O' || + read[2] != 'K' || + read[3] != '}' || + read[4] != ',') + return false; + const char *from = read + 5; + char *to; + battery.soc = strtoul(from, &to, 10); + if (from == to) + return false; + from = to + 1; + battery.temperature = strtoul(from, &to, 10) / 10.0f; + if (from == to) + return false; + if (to[0] != ',' || (to[1] != '0' && to[1] != '1') || to[2] != ',') + return false; + battery.charging = to[1] == '1'; + from = to + 3; + battery.cycles = strtoul(from, &to, 10); + if (from == to) + return false; + return true; +} + +static int +consume(struct ustream *s, char **a) +{ + char *eol = strstr(*a, "\n"); + + if (!eol) + return -1; + + *eol++ = '\0'; + + battery.set = process(*a); + if (!battery.set) + ULOG_ERR("failed to parse message from serial: %s", a); + + ustream_consume(s, eol - *a); + *a = eol; + + return 0; +} + +static void +msg_cb(struct ustream *s, int bytes) +{ + int len; + char *a = ustream_get_read_buf(s, &len); + + while (!consume(s, &a)) + ; +} + +static void +notify_cb(struct ustream *s) +{ + if (!s->eof) + return; + + ULOG_ERR("tty error, shutting down\n"); + exit(-1); +} + +static int +serial_open(char *dev) +{ + const int tty = open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (tty < 0) + { + ULOG_ERR("%s: device open failed: %s\n", dev, strerror(errno)); + return -1; + } + + struct termios config; + tcgetattr(tty, &config); + cfmakeraw(&config); + cfsetispeed(&config, B9600); + cfsetospeed(&config, B9600); + tcsetattr(tty, TCSANOW, &config); + + stream.stream.string_data = true; + stream.stream.notify_read = msg_cb; + stream.stream.notify_state = notify_cb; + + ustream_fd_init(&stream, tty); + + tcflush(tty, TCIFLUSH); + + return 0; +} + +static struct uloop_timeout serial_query_timer; +static void +serial_query_handler(struct uloop_timeout *timeout) +{ + const char cmd[] = "{ \"mcu_status\": \"1\" }\n"; + const unsigned cmd_len = sizeof(cmd) - 1; + ustream_write(&stream.stream, cmd, cmd_len, false); + uloop_timeout_set(&serial_query_timer, 3000); // timeout in 3 sec + uloop_timeout_add(timeout); +} + +static int +battery_info(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + blob_buf_init(&b, 0); + + if (!battery.set) + { + blobmsg_add_u8(&b, "error", 1); + } + else + { + blobmsg_add_u16(&b, "soc", battery.soc); + blobmsg_add_u8(&b, "charging", battery.charging); + blobmsg_add_double(&b, "temperature", battery.temperature); + blobmsg_add_u16(&b, "cycles", battery.cycles); + } + ubus_send_reply(ctx, req, b.head); + + return UBUS_STATUS_OK; +} + +static const struct ubus_method battery_methods[] = { + UBUS_METHOD_NOARG("info", battery_info), +}; + +static struct ubus_object_type battery_object_type = + UBUS_OBJECT_TYPE("battery", battery_methods); + +static struct ubus_object battery_object = { + .name = "battery", + .type = &battery_object_type, + .methods = battery_methods, + .n_methods = ARRAY_SIZE(battery_methods), +}; + +static void +ubus_connect_handler(struct ubus_context *ctx) +{ + int ret; + + ret = ubus_add_object(ctx, &battery_object); + if (ret) + fprintf(stderr, "Failed to add object: %s\n", ubus_strerror(ret)); +} + +int +main(int argc, char **argv) +{ + + uloop_init(); + conn.path = NULL; + conn.cb = ubus_connect_handler; + ubus_auto_connect(&conn); + + if (serial_open("/dev/ttyUSB0") < 0) + return -1; + + serial_query_timer.cb = serial_query_handler; + serial_query_handler(&serial_query_timer); + uloop_run(); + uloop_done(); + + return 0; +}