# USB-to-RS-485 adapter compatible with EVBox ChargeStation Tool
# Written 2026 by Maarten Tromp <maarten@geekabit.nl>
# Website: https://www.geekabit.nl/projects/reversing-the-evbox-chargestation-tool/
# License: CC0 1.0 Universal (Public Domain Dedication)
# For more info see README


import time
import usb_hid
import busio
import board
import digitalio


now = time.monotonic_ns()
frame_buf = None
REPORT_LEN = 64


# LED
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led_last_act = now


# RS-485 UART
uart = busio.UART(
	tx = board.GP0,
	rx = board.GP1,
	baudrate = 38400,
	bits = 8,
	parity = None,
	stop = 1,
	timeout = 0.01
)
uart_data = bytearray(REPORT_LEN)
uart_buf = bytearray()
uart_last_act = now


# USB HID
hid = usb_hid.devices[0]
hid_data = bytearray(REPORT_LEN)
hid_buf = bytearray()


def valid_frame(frame):
	for b in frame[1:-2]:
		if not (
			b in (0x00, 0x0A, 0x0D, 0x3A) or
			0x30 <= b <= 0x39 or
			0x41 <= b <= 0x5A
		):
			return False

	payload = frame[1:-6]
	checksum = sum(payload) & 0xFF
	if frame[-6:-4] != f"{checksum:02X}".encode():
		return False

	parity = 0
	for b in payload:
		parity ^= b
	if frame[-4:-2] != f"{parity:02X}".encode():
		return False

	return True


while True:
	now = time.monotonic_ns()

	# read USB
	hid_data = hid.get_last_received_report()
	if hid_data:
		hid_buf.extend(bytearray(hid_data).rstrip(b"\x00"))

	# read serial
	uart_data = uart.read(REPORT_LEN)
	if uart_data:
		uart_buf.extend(uart_data)
		uart_last_act = now

		sof = uart_buf.rfind(b"\x02")
		if sof >= 0:
			if sof > 0:
				uart_buf = uart_buf[sof:]
			eof = uart_buf.find(b"\x03\xff")
			if eof > 0:
				frame_buf = uart_buf[:eof + 2]
				uart_buf = uart_buf[eof + 2:]
				if not valid_frame(frame_buf):
					frame_buf = None

	# write serial
	if hid_buf:
		if hid_buf[0] == 0x02:
			if now > uart_last_act + 10000000: # 10 ms
				uart.write(hid_buf)
				hid_buf = bytearray()
		else:
			uart.write(hid_buf)
			hid_buf = bytearray()

	# write USB
	while frame_buf:
		chunk = frame_buf[:REPORT_LEN]
		frame_buf = frame_buf[REPORT_LEN:]
		chunk.extend(b"\x00" * (REPORT_LEN - len(chunk)))
		hid.send_report(chunk)

	# Heartbeat LED
	if now > led_last_act + 1000000000: # 1 s
		led_last_act = now
		led.value = not led.value
