Skip to content

Instantly share code, notes, and snippets.

@parkerlreed
Last active December 31, 2025 19:39
Show Gist options
  • Select an option

  • Save parkerlreed/f2dc7ef356300533fe1df578efbce189 to your computer and use it in GitHub Desktop.

Select an option

Save parkerlreed/f2dc7ef356300533fe1df578efbce189 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import struct
import sys
import serial
PORT_DEFAULT = "/dev/ttyUSB0"
BAUD = 115200
ROTATE_ID = 0x28 # fixed/reused
ROTATE_OPCODE = 0x08
# Known-good captured packet
LIGHT_ON = bytes.fromhex("1E 11 00 00 01 02 01 00 00 00 00 00 1F 13 01 00")
COMMON_ANGLES = [45, 90, 180, 360]
def hexdump(b: bytes) -> str:
return " ".join(f"{x:02X}" for x in b)
def float32_word_swapped(angle_deg: float) -> bytes:
"""
Device expects float32 with 16-bit word swap:
struct.pack('<f') gives b0 b1 b2 b3 (LE)
on-wire wants b2 b3 b0 b1
"""
b0, b1, b2, b3 = struct.pack("<f", float(angle_deg))
return bytes([b2, b3, b0, b1])
def build_rotate(angle_deg: float, msg_id: int = ROTATE_ID) -> bytes:
f = float32_word_swapped(angle_deg) # 4 bytes
w0, w1, w2, w3 = f
return bytes([
msg_id, ROTATE_OPCODE,
0x00, 0x00, 0x00, 0x00,
w0, w1, w2, w3,
0x00, 0x00,
msg_id, ROTATE_OPCODE,
w0, w1
])
def send(ser: serial.Serial, payload: bytes, label: str = "") -> None:
ser.write(payload)
if label:
print(f"{label}")
print(f"TX ({len(payload)}): {hexdump(payload)}")
def ask(prompt: str) -> str:
return input(prompt).strip()
def pick_mode() -> str:
while True:
print("\nMode: (R)otate, (L)ight on, (Q)uit")
c = ask("> ").lower()
if c in ("r", "l", "q"):
return c
def pick_direction() -> str:
while True:
print("\nDirection: (1) CW, (2) CCW, (B)ack")
c = ask("> ").lower()
if c == "1":
return "CW"
if c == "2":
return "CCW"
if c == "b":
return "BACK"
def pick_angle() -> float | None:
while True:
opts = " ".join([f"({i+1}) {a}°" for i, a in enumerate(COMMON_ANGLES)])
print(f"\nAngle: {opts} (A)rbitrary (B)ack")
c = ask("> ").lower()
if c == "b":
return None
if c == "a":
v = ask("Enter degrees (e.g. 22.5) > ").strip()
try:
return float(v)
except ValueError:
print("Not a number.")
continue
# preset by number
if c.isdigit():
idx = int(c) - 1
if 0 <= idx < len(COMMON_ANGLES):
return float(COMMON_ANGLES[idx])
print("Invalid choice.")
def main():
port = sys.argv[1] if len(sys.argv) > 1 else PORT_DEFAULT
print(f"Opening {port} @ {BAUD}...")
with serial.Serial(
port=port,
baudrate=BAUD,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0,
) as ser:
while True:
mode = pick_mode()
if mode == "q":
print("Bye.")
return
if mode == "l":
send(ser, LIGHT_ON, "Light On")
continue
# mode == "r"
direction = pick_direction()
if direction == "BACK":
continue
angle = pick_angle()
if angle is None:
continue
if direction == "CCW" and angle > 0:
angle = -angle
pkt = build_rotate(angle)
send(ser, pkt, f"Rotate {direction} {abs(angle)}°")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment