Skip to content

Commit 99c3589

Browse files
committed
feat: add MQTT keyboard control python client for DCS
1 parent 0eec9a7 commit 99c3589

3 files changed

Lines changed: 184 additions & 2 deletions

File tree

data/config/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"robotName": "",
2+
"robotName": "Zumo",
33
"wifi": {
44
"ssid": "",
55
"pswd": ""
@@ -29,4 +29,4 @@
2929
"host": "127.0.0.1",
3030
"port": "8888"
3131
}
32-
}
32+
}

scripts/keyboard_control.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Keyboard remote control for the Zumo robot via MQTT.
4+
5+
Controls:
6+
W / Up - Forward
7+
S / Down - Backward
8+
A / Left - Turn left
9+
D / Right - Turn right
10+
Space - Stop
11+
Q / Esc - Quit
12+
13+
Usage:
14+
python3 keyboard_control.py [--broker <host>] [--port <port>] [--robot <name>] [--speed <mm/s>]
15+
16+
The robot name must match the DCS config (data/config/config.json "robotName").
17+
If robotName is empty in the config, DCS derives it from its process ID — set a
18+
fixed name in the config and pass it here.
19+
"""
20+
21+
import argparse
22+
import json
23+
import sys
24+
import termios
25+
import tty
26+
27+
import paho.mqtt.client as mqtt
28+
29+
DEFAULT_BROKER = "localhost"
30+
DEFAULT_PORT = 1883
31+
DEFAULT_ROBOT = "Zumo"
32+
DEFAULT_SPEED = 100 # mm/s
33+
34+
TOPIC_MOTOR_SPEEDS = "{robot}/motorSpeeds"
35+
TOPIC_CMD = "{robot}/cmd"
36+
37+
CMD_ID_START_DRIVING = 5
38+
39+
40+
def parse_args():
41+
"""Parse command-line arguments and return the populated namespace."""
42+
p = argparse.ArgumentParser(
43+
description="Keyboard MQTT remote control for Zumo robot")
44+
p.add_argument("--broker", default=DEFAULT_BROKER, help="MQTT broker host")
45+
p.add_argument("--port", type=int, default=DEFAULT_PORT,
46+
help="MQTT broker port")
47+
p.add_argument("--robot", default=DEFAULT_ROBOT,
48+
help="Robot name (MQTT client ID prefix)")
49+
p.add_argument("--speed", type=int, default=DEFAULT_SPEED,
50+
help="Drive speed in mm/s")
51+
return p.parse_args()
52+
53+
54+
def publish_motor_speeds(client, topic, left, right):
55+
"""Publish left/right motor speeds (mm/s) as JSON to the given MQTT topic."""
56+
payload = json.dumps({"LEFT": left, "RIGHT": right})
57+
client.publish(topic, payload)
58+
59+
60+
def publish_start_driving(client, cmd_topic):
61+
"""Send CMD_ID_START_DRIVING to the robot's command topic, enabling motor control."""
62+
payload = json.dumps({"CMD_ID": CMD_ID_START_DRIVING})
63+
client.publish(cmd_topic, payload)
64+
65+
66+
def on_connect(client, userdata, flags, reason_code, properties):
67+
"""MQTT on-connect callback — sends START_DRIVING on success, exits on failure."""
68+
if reason_code == 0:
69+
print(
70+
f"Connected to MQTT broker at {userdata['broker']}:{userdata['port']}")
71+
# Tell DCS to enter driving mode
72+
publish_start_driving(client, userdata["cmd_topic"])
73+
else:
74+
print(f"Connection failed with code {reason_code}", file=sys.stderr)
75+
sys.exit(1)
76+
77+
78+
def get_char():
79+
"""Read a single character from stdin without requiring Enter."""
80+
fd = sys.stdin.fileno()
81+
old = termios.tcgetattr(fd)
82+
try:
83+
tty.setraw(fd)
84+
ch = sys.stdin.read(1)
85+
# Handle escape sequences (arrow keys)
86+
if ch == "\x1b":
87+
ch2 = sys.stdin.read(1)
88+
if ch2 == "[":
89+
ch3 = sys.stdin.read(1)
90+
return "\x1b[" + ch3
91+
return ch
92+
finally:
93+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
94+
95+
96+
def main():
97+
"""Connect to the MQTT broker and run the keyboard control loop until quit."""
98+
args = parse_args()
99+
100+
motor_topic = TOPIC_MOTOR_SPEEDS.format(robot=args.robot)
101+
cmd_topic = TOPIC_CMD.format(robot=args.robot)
102+
103+
client = mqtt.Client(
104+
mqtt.CallbackAPIVersion.VERSION2,
105+
client_id=f"keyboard_control_{args.robot}",
106+
)
107+
client.user_data_set({
108+
"broker": args.broker,
109+
"port": args.port,
110+
"cmd_topic": cmd_topic,
111+
})
112+
client.on_connect = on_connect
113+
114+
try:
115+
client.connect(args.broker, args.port, keepalive=60)
116+
except Exception as e:
117+
print(f"Cannot connect to broker: {e}", file=sys.stderr)
118+
sys.exit(1)
119+
120+
client.loop_start()
121+
122+
speed = args.speed
123+
124+
print()
125+
print("=== Zumo Keyboard Control ===")
126+
print(f" Robot : {args.robot}")
127+
print(f" Broker: {args.broker}:{args.port}")
128+
print(f" Speed : {speed} mm/s")
129+
print()
130+
print(" W/Up – Forward")
131+
print(" S/Down – Backward")
132+
print(" A/Left – Turn left (in place)")
133+
print(" D/Right – Turn right (in place)")
134+
print(" Space – Stop")
135+
print(" Q/Esc – Quit")
136+
print()
137+
138+
try:
139+
current = (0, 0)
140+
141+
while True:
142+
ch = get_char().lower()
143+
144+
if ch in ("q", "\x1b"):
145+
break
146+
147+
if ch in ("w", "\x1b[a"): # W or Up arrow
148+
left, right = speed, speed
149+
label = "FORWARD"
150+
elif ch in ("s", "\x1b[b"): # S or Down arrow
151+
left, right = -speed, -speed
152+
label = "BACKWARD"
153+
elif ch in ("a", "\x1b[d"): # A or Left arrow
154+
left, right = -speed, speed
155+
label = "LEFT"
156+
elif ch in ("d", "\x1b[c"): # D or Right arrow
157+
left, right = speed, -speed
158+
label = "RIGHT"
159+
elif ch == " ":
160+
left, right = 0, 0
161+
label = "STOP"
162+
else:
163+
continue
164+
165+
if (left, right) != current:
166+
current = (left, right)
167+
publish_motor_speeds(client, motor_topic, left, right)
168+
print(
169+
f"\r [{label}] L={left:+5d} mm/s R={right:+5d} mm/s ", end="", flush=True)
170+
171+
except KeyboardInterrupt:
172+
pass
173+
finally:
174+
publish_motor_speeds(client, motor_topic, 0, 0)
175+
client.loop_stop()
176+
client.disconnect()
177+
print("\nStopped.")
178+
179+
180+
if __name__ == "__main__":
181+
main()

scripts/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
paho-mqtt>=2.0

0 commit comments

Comments
 (0)