diff --git a/utils/xtm330-paneld/Makefile b/utils/xtm330-paneld/Makefile new file mode 100644 index 00000000000000..2a383c94f2e859 --- /dev/null +++ b/utils/xtm330-paneld/Makefile @@ -0,0 +1,31 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=xtm330-paneld +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/xtm330-paneld + SECTION:=utils + CATEGORY:=Utilities + TITLE:=WatchGuard XTM330 front panel daemon +endef + +define Package/xtm330-paneld/description + Small userspace daemon for the WatchGuard XTM330 front panel. + It initializes the LCD/keypad controller on ttyS1, writes status + text to the display, and logs keypad events. +endef + +define Build/Compile + $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) -o $(PKG_BUILD_DIR)/xtm330-paneld ./src/xtm330-paneld.c +endef + +define Package/xtm330-paneld/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/xtm330-paneld $(1)/usr/sbin/xtm330-paneld + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/xtm330-paneld.init $(1)/etc/init.d/xtm330-paneld +endef + +$(eval $(call BuildPackage,xtm330-paneld)) diff --git a/utils/xtm330-paneld/README.md b/utils/xtm330-paneld/README.md new file mode 100644 index 00000000000000..bb6940c441e799 --- /dev/null +++ b/utils/xtm330-paneld/README.md @@ -0,0 +1,79 @@ +# xtm330-paneld + +Recovered userspace front-panel support for the WatchGuard XTM330. + +## Confirmed details + +- Transport: `/dev/ttyS1` +- UART: `19200 8N1` +- Packet format: `[cmd][len][payload...][crc16_be]` +- CRC: seed `0xffff`, reflected polynomial `0x8408`, final invert, then byte swap +- Known commands: + - `0x00` panel init / probe, payload `Hello!` + - `0x07` write LCD line 1 + - `0x08` write LCD line 2 + - `0x17` enable keypad events, payload `0x0f 0x0f` + - `0x20` set LED state, payload `[index, value, 0x01]` +- Key events arrive as 5-byte frames beginning with `0x80` + - `0x01/0x11` first key press/release + - `0x02/0x12` second key press/release + - `0x04/0x14` third key press/release + - `0x08/0x18` fourth key press/release + +## Confirmed front-panel behavior + +- LCD text writes are working on OpenWrt. +- Button handling is working again in the C daemon after switching to a response-aware parser. +- The stable daemon behavior is: + - idle LCD: `OpenWrt Panel` / `Ready` + - button press: show the correct button label on line 2 + - button release: restore `Ready` +- Button handling is intentionally decoupled from LED transitions. + +## Confirmed LED mapping on the recovered UART path + +- LED `0`: visible power LED + - best-effort solid command works, but tested values did not visibly change behavior +- LED `1`: arm/disarm green channel +- LED `2`: arm/disarm red channel + +## Confirmed LED values + +- `0x00`: off +- `0x01`: solid +- `0x09`: fast blink +- `0x28`: medium blink +- `0x96`: slow blink + +## Stock firmware findings + +- Stock `frontpaneld` uses `libs6a0069.so` on `/dev/ttyS1`. +- Stock `armled` uses `libwgpanel.so` and proves a higher-level semantic API: + - `setLed(selector, color, rate)` +- Confirmed from `armled`: + - selector `1` = arm/disarm + - color `1` = green + - color `2` = red + - rate `1` = solid +- Stock also has a separate LED control plane through: + - `/proc/hwctrl/led_power` + - `/proc/hwctrl/led_second_power` + - `/proc/hwctrl/led_wlan1` + - `/proc/hwctrl/led_wlan2` + - `/proc/hwctrl/led_lan*` + +## Current conclusion on the disk LED + +- The disk LED was not found on the recovered `ttyS1` selector path. +- Focused raw LED selector sweeps only changed the arm/disarm LED. +- The disk LED is therefore most likely controlled through the stock kernel-side + `hwctrl`/`frontpanel` interface rather than the recovered UART LED path. +- Future work for the disk LED should target the stock kernel-side interface, + not more blind UART LED selector probing. + +## Gaps + +- The disk LED is still unmapped. +- The power LED currently appears fixed in hardware or needs a different command path. +- The daemon is useful and stable for LCD + button handling, but it is not yet a full replacement for the stock WatchGuard front-panel stack. +- Stock firmware also exposes `/proc/frontpanel`, `/proc/wg/frontpanel`, and `/proc/hwctrl/led_*`; those kernel hooks are not yet reproduced. diff --git a/utils/xtm330-paneld/files/xtm330-paneld.init b/utils/xtm330-paneld/files/xtm330-paneld.init new file mode 100755 index 00000000000000..91b935fd890abc --- /dev/null +++ b/utils/xtm330-paneld/files/xtm330-paneld.init @@ -0,0 +1,14 @@ +#!/bin/sh /etc/rc.common +USE_PROCD=1 +START=95 +STOP=10 + +start_service() { + [ "$(cat /tmp/sysinfo/board_name 2>/dev/null)" = "watchguard,xtm330" ] || return 0 + procd_open_instance + procd_set_param command /usr/sbin/xtm330-paneld + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} diff --git a/utils/xtm330-paneld/files/xtm330-paneld.py b/utils/xtm330-paneld/files/xtm330-paneld.py new file mode 100755 index 00000000000000..e86ddd2ff83fee --- /dev/null +++ b/utils/xtm330-paneld/files/xtm330-paneld.py @@ -0,0 +1,188 @@ +#!/usr/bin/python3 +import os +import signal +import sys +import termios +import time + +DEV = '/dev/ttyS1' +BAUD = termios.B19200 +BUTTONS = { + 0x01: 'UP', + 0x02: 'DOWN', + 0x04: 'LEFT', + 0x08: 'RIGHT', +} +RUN = True + +LED_POWER = 0 +LED_ARM_GREEN = 1 +LED_ARM_RED = 2 + +LED_OFF = 0 +LED_SOLID = 1 +LED_FAST = 9 +LED_MEDIUM = 0x28 +LED_SLOW = 0x96 + + +def handle_signal(_signum, _frame): + global RUN + RUN = False + + +def crc(data, c=0xFFFF): + for byte in data: + c ^= byte + for _ in range(8): + c = (c >> 1) ^ 0x8408 if c & 1 else c >> 1 + c = (~c) & 0xFFFF + return ((c >> 8) | ((c & 0xFF) << 8)) & 0xFFFF + + +def pkt(cmd, payload=b''): + body = bytes([cmd, len(payload)]) + payload + value = crc(body) + return body + bytes([value >> 8, value & 0xFF]) + + +def read_for(fd, secs): + out = b'' + end = time.time() + secs + while time.time() < end and RUN: + try: + chunk = os.read(fd, 256) + if chunk: + out += chunk + except BlockingIOError: + pass + time.sleep(0.05) + return out + + +def xfer(fd, packet, wait=0.4): + os.write(fd, packet) + return read_for(fd, wait) + + +def set_line(fd, row, text): + cmd = 0x07 if row == 0 else 0x08 + payload = text.encode('ascii', 'replace')[:16].ljust(16, b' ') + xfer(fd, pkt(cmd, payload)) + + +def show_default(fd): + set_line(fd, 0, 'OpenWrt Panel') + set_line(fd, 1, 'Ready') + + +def set_led(fd, led, value): + xfer(fd, pkt(0x20, bytes([led, value, 1]))) + + +def apply_known_leds(fd): + set_led(fd, LED_POWER, LED_SOLID) + set_led(fd, LED_ARM_GREEN, LED_OFF) + set_led(fd, LED_ARM_RED, LED_OFF) + + +def show_key_state(fd, key): + if key == 'UP': + set_led(fd, LED_ARM_RED, LED_OFF) + set_led(fd, LED_ARM_GREEN, LED_SOLID) + elif key == 'DOWN': + set_led(fd, LED_ARM_GREEN, LED_OFF) + set_led(fd, LED_ARM_RED, LED_SOLID) + elif key == 'LEFT': + set_led(fd, LED_ARM_RED, LED_OFF) + set_led(fd, LED_ARM_GREEN, LED_SLOW) + elif key == 'RIGHT': + set_led(fd, LED_ARM_GREEN, LED_OFF) + set_led(fd, LED_ARM_RED, LED_SLOW) + + +def clear_key_state(fd): + set_led(fd, LED_ARM_GREEN, LED_OFF) + set_led(fd, LED_ARM_RED, LED_OFF) + apply_known_leds(fd) + + +def open_panel(): + fd = os.open(DEV, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + attrs = termios.tcgetattr(fd) + attrs[0] = 0 + attrs[1] = 0 + attrs[2] = termios.CS8 | termios.CREAD | termios.CLOCAL + attrs[3] = 0 + attrs[4] = BAUD + attrs[5] = BAUD + attrs[6][termios.VMIN] = 0 + attrs[6][termios.VTIME] = 1 + termios.tcsetattr(fd, termios.TCSANOW, attrs) + termios.tcflush(fd, termios.TCIOFLUSH) + return fd + + +def init_panel(fd): + xfer(fd, pkt(0x00, b'Hello!'), 0.8) + xfer(fd, pkt(0x17, b'\x0f\x0f'), 0.8) + apply_known_leds(fd) + show_default(fd) + + +def parse_event(frame): + if len(frame) != 5 or frame[0] != 0x80: + return None + code = frame[2] + key = BUTTONS.get(code & 0x0F, '0x%02x' % (code & 0x0F)) + state = 'release' if code & 0x10 else 'press' + return key, state + + +def board_name(): + try: + with open('/tmp/sysinfo/board_name', 'r', encoding='ascii') as fh: + return fh.read().strip() + except OSError: + return '' + + +def main(): + if board_name() != 'watchguard,xtm330': + return 0 + + signal.signal(signal.SIGTERM, handle_signal) + signal.signal(signal.SIGINT, handle_signal) + + fd = open_panel() + try: + init_panel(fd) + while RUN: + data = read_for(fd, 0.2) + if not data: + continue + for i in range(0, len(data) - (len(data) % 5), 5): + parsed = parse_event(data[i:i + 5]) + if not parsed: + continue + key, state = parsed + print('panel key %s %s' % (key, state), flush=True) + if state == 'press': + set_line(fd, 0, 'OpenWrt Panel') + set_line(fd, 1, key) + show_key_state(fd, key) + elif state == 'release': + show_default(fd) + clear_key_state(fd) + finally: + try: + clear_key_state(fd) + show_default(fd) + except OSError: + pass + os.close(fd) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/utils/xtm330-paneld/src/xtm330-paneld.c b/utils/xtm330-paneld/src/xtm330-paneld.c new file mode 100644 index 00000000000000..0c259ecca824c4 --- /dev/null +++ b/utils/xtm330-paneld/src/xtm330-paneld.c @@ -0,0 +1,313 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEV_PATH "/dev/ttyS1" +#define LINE_LEN 16 +#define RX_BUF_LEN 512 +#define EVENT_Q_LEN 16 + +#define CMD_INIT 0x00 +#define CMD_LINE0 0x07 +#define CMD_LINE1 0x08 +#define CMD_ENABLE_KEYS 0x17 +#define CMD_SET_LED 0x20 + +#define LED_POWER 0 +#define LED_ARM_GREEN 1 +#define LED_ARM_RED 2 + +#define LED_OFF 0 +#define LED_SOLID 1 + +struct panel_state { + int fd; + uint8_t rxbuf[RX_BUF_LEN]; + size_t rxlen; + uint8_t event_q[EVENT_Q_LEN][5]; + size_t event_head; + size_t event_tail; +}; + +static volatile sig_atomic_t running = 1; + +static void handle_signal(int sig) { (void)sig; running = 0; } + +static uint16_t crc16(const uint8_t *data, size_t len) +{ + uint16_t c = 0xffff; size_t i; int bit; + for (i = 0; i < len; i++) { + c ^= data[i]; + for (bit = 0; bit < 8; bit++) + c = (c & 1) ? (c >> 1) ^ 0x8408 : (c >> 1); + } + c = ~c; + return (uint16_t)((c >> 8) | ((c & 0xff) << 8)); +} + +static int queue_event(struct panel_state *st, const uint8_t *frame) +{ + size_t next = (st->event_tail + 1) % EVENT_Q_LEN; + if (next == st->event_head) + return -1; + memcpy(st->event_q[st->event_tail], frame, 5); + st->event_tail = next; + return 0; +} + +static int pop_event(struct panel_state *st, uint8_t *frame) +{ + if (st->event_head == st->event_tail) + return 0; + memcpy(frame, st->event_q[st->event_head], 5); + st->event_head = (st->event_head + 1) % EVENT_Q_LEN; + return 1; +} + +static int plausible_cmd(uint8_t cmd) +{ + switch (cmd) { + case 0x20: case 0x40: case 0x41: case 0x43: case 0x44: case 0x45: + case 0x57: case 0x80: case 0xc7: case 0xc8: case 0xcb: case 0xcc: + return 1; + default: + return 0; + } +} + +static int read_into_rx(struct panel_state *st, unsigned int timeout_ms) +{ + struct pollfd pfd = { .fd = st->fd, .events = POLLIN }; + int rv = poll(&pfd, 1, (int)timeout_ms); + if (rv > 0 && (pfd.revents & POLLIN)) { + ssize_t n; + if (st->rxlen >= sizeof(st->rxbuf)) + st->rxlen = 0; + n = read(st->fd, st->rxbuf + st->rxlen, sizeof(st->rxbuf) - st->rxlen); + if (n > 0) { + st->rxlen += (size_t)n; + return 1; + } + } + return 0; +} + +static int extract_frame(struct panel_state *st, uint8_t *out, size_t *out_len) +{ + size_t pos = 0; + while (st->rxlen - pos >= 4) { + uint8_t cmd = st->rxbuf[pos]; + uint8_t len = st->rxbuf[pos + 1]; + size_t frame_len; + uint16_t got_crc, want_crc; + + if (!plausible_cmd(cmd)) { pos++; continue; } + frame_len = (size_t)len + 4; + if (frame_len > RX_BUF_LEN || len > 0x40) { pos++; continue; } + if (st->rxlen - pos < frame_len) + break; + got_crc = ((uint16_t)st->rxbuf[pos + frame_len - 2] << 8) | st->rxbuf[pos + frame_len - 1]; + want_crc = crc16(st->rxbuf + pos, frame_len - 2); + if (got_crc != want_crc) { pos++; continue; } + memcpy(out, st->rxbuf + pos, frame_len); + *out_len = frame_len; + pos += frame_len; + if (pos < st->rxlen) + memmove(st->rxbuf, st->rxbuf + pos, st->rxlen - pos); + st->rxlen -= pos; + return 1; + } + if (pos && pos < st->rxlen) + memmove(st->rxbuf, st->rxbuf + pos, st->rxlen - pos); + st->rxlen -= pos; + return 0; +} + +static int next_frame(struct panel_state *st, uint8_t *frame, size_t *frame_len, unsigned int timeout_ms) +{ + unsigned int elapsed = 0; + while (running && elapsed <= timeout_ms) { + if (extract_frame(st, frame, frame_len)) + return 1; + read_into_rx(st, 50); + elapsed += 50; + } + return 0; +} + +static int expected_reply(uint8_t cmd, uint8_t *reply_cmd, uint8_t *reply_len) +{ + switch (cmd) { + case CMD_INIT: *reply_cmd = 0x40; *reply_len = 6; return 1; + case CMD_ENABLE_KEYS: *reply_cmd = 0x57; *reply_len = 0; return 1; + case CMD_SET_LED: *reply_cmd = 0x20; *reply_len = 0; return 1; + case CMD_LINE0: *reply_cmd = 0xc7; *reply_len = 16; return 1; + case CMD_LINE1: *reply_cmd = 0xc8; *reply_len = 16; return 1; + default: return 0; + } +} + +static size_t make_packet(uint8_t cmd, const uint8_t *payload, uint8_t payload_len, uint8_t *out, size_t out_len) +{ + uint16_t crc; + if (out_len < (size_t)payload_len + 4) + return 0; + out[0] = cmd; + out[1] = payload_len; + if (payload_len && payload) + memcpy(out + 2, payload, payload_len); + crc = crc16(out, (size_t)payload_len + 2); + out[2 + payload_len] = (uint8_t)(crc >> 8); + out[3 + payload_len] = (uint8_t)(crc & 0xff); + return (size_t)payload_len + 4; +} + +static int panel_command(struct panel_state *st, uint8_t cmd, const uint8_t *payload, uint8_t payload_len, unsigned int timeout_ms) +{ + uint8_t packet[32], frame[64], want_cmd, want_len; + size_t len, frame_len; + ssize_t wr; + + len = make_packet(cmd, payload, payload_len, packet, sizeof(packet)); + if (!len) + return -1; + wr = write(st->fd, packet, len); + if (wr < 0 || (size_t)wr != len) + return -1; + if (!expected_reply(cmd, &want_cmd, &want_len)) + return 0; + + while (running && next_frame(st, frame, &frame_len, timeout_ms)) { + if (frame[0] == 0x80 && frame[1] == 0x01) { + queue_event(st, frame); + continue; + } + if (frame[0] == want_cmd && frame[1] == want_len) + return 0; + } + return -1; +} + +static int set_led(struct panel_state *st, uint8_t led, uint8_t value) +{ + uint8_t payload[3] = { led, value, 1 }; + return panel_command(st, CMD_SET_LED, payload, sizeof(payload), 1000); +} + +static int set_line(struct panel_state *st, uint8_t row, const char *text) +{ + uint8_t payload[LINE_LEN]; + size_t len = strlen(text); + memset(payload, ' ', sizeof(payload)); + if (len > sizeof(payload)) + len = sizeof(payload); + memcpy(payload, text, len); + return panel_command(st, row == 0 ? CMD_LINE0 : CMD_LINE1, payload, sizeof(payload), 1500); +} + +static void show_default(struct panel_state *st) +{ + set_line(st, 0, "OpenWrt Panel"); + set_line(st, 1, "Ready"); +} + +static void apply_known_leds(struct panel_state *st) +{ + set_led(st, LED_POWER, LED_SOLID); + set_led(st, LED_ARM_GREEN, LED_OFF); + set_led(st, LED_ARM_RED, LED_SOLID); +} + +static const char *parse_key(uint8_t code, int *is_release) +{ + *is_release = !!(code & 0x10); + switch (code & 0x0f) { + case 0x01: return "UP"; + case 0x02: return "DOWN"; + case 0x04: return "LEFT"; + case 0x08: return "RIGHT"; + default: return NULL; + } +} + +static int open_panel(void) +{ + struct termios tio; + int fd = open(DEV_PATH, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) return -1; + if (tcgetattr(fd, &tio) < 0) { close(fd); return -1; } + cfmakeraw(&tio); + tio.c_cflag |= CREAD | CLOCAL; + cfsetispeed(&tio, B19200); + cfsetospeed(&tio, B19200); + if (tcsetattr(fd, TCSANOW, &tio) < 0) { close(fd); return -1; } + tcflush(fd, TCIOFLUSH); + return fd; +} + +static int init_panel(struct panel_state *st) +{ + static const uint8_t hello[] = { 'H','e','l','l','o','!' }; + static const uint8_t keys[] = { 0x0f, 0x0f }; + if (panel_command(st, CMD_INIT, hello, sizeof(hello), 1200) < 0) return -1; + if (panel_command(st, CMD_ENABLE_KEYS, keys, sizeof(keys), 1200) < 0) return -1; + apply_known_leds(st); + show_default(st); + return 0; +} + +static int board_supported(void) +{ + FILE *fp = fopen("/tmp/sysinfo/board_name", "r"); char buf[64]; + if (!fp) return 0; + if (!fgets(buf, sizeof(buf), fp)) { fclose(fp); return 0; } + fclose(fp); buf[strcspn(buf, "\r\n")] = '\0'; + return !strcmp(buf, "watchguard,xtm330"); +} + +int main(void) +{ + struct panel_state st; + uint8_t frame[64]; + size_t frame_len; + + if (!board_supported()) return 0; + memset(&st, 0, sizeof(st)); + signal(SIGTERM, handle_signal); + signal(SIGINT, handle_signal); + st.fd = open_panel(); + if (st.fd < 0) return 1; + if (init_panel(&st) < 0) return 1; + + while (running) { + if (!pop_event(&st, frame)) { + if (!next_frame(&st, frame, &frame_len, 200)) + continue; + } else { + frame_len = 5; + } + if (frame[0] == 0x80 && frame[1] == 0x01) { + const char *key; + int release; + key = parse_key(frame[2], &release); + if (!key) continue; + if (release) + show_default(&st); + else { + set_line(&st, 0, "OpenWrt Panel"); + set_line(&st, 1, key); + } + } + } + + show_default(&st); + close(st.fd); + return 0; +}