Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions utils/xtm330-paneld/Makefile
Original file line number Diff line number Diff line change
@@ -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))
79 changes: 79 additions & 0 deletions utils/xtm330-paneld/README.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions utils/xtm330-paneld/files/xtm330-paneld.init
Original file line number Diff line number Diff line change
@@ -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
}
188 changes: 188 additions & 0 deletions utils/xtm330-paneld/files/xtm330-paneld.py
Original file line number Diff line number Diff line change
@@ -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())
Loading
Loading