-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrov_agent.py
More file actions
294 lines (253 loc) · 9.89 KB
/
Copy pathrov_agent.py
File metadata and controls
294 lines (253 loc) · 9.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import socket
import json
import time
import threading
import math
from pymavlink import mavutil
from manual_control import axes_to_manual_control, NEUTRAL
# =========================
# Konfigurasi jaringan
# =========================
LAPTOP_IP = "192.168.2.1" # IP laptop / ground station
UDP_TELEM_PORT = 14551 # telemetry ke laptop (sesuai server.js)
UDP_CMD_PORT = 14550 # command dari laptop ke Pi
# =========================
# Konfigurasi Pixhawk
# =========================
PIXHAWK_PORT = "/dev/ttyACM0"
PIXHAWK_BAUD = 115200
# =========================
# Socket UDP
# =========================
telem_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cmd_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cmd_sock.bind(("0.0.0.0", UDP_CMD_PORT))
cmd_sock.settimeout(0.2)
# =========================
# Status telemetry lokal
# =========================
state = {
"heading": 0.0,
"depth": 0.0, # sementara 0 dulu, nanti kita isi dari sensor depth
"roll": 0.0,
"pitch": 0.0,
"temp": 0.0, # sementara 0 dulu, nanti bisa dari sensor suhu
"voltage": 0.0,
"armed": False,
"light": False,
"mode": "unknown",
}
master = None
# =========================
# Kontrol manual (joystick GUI -> MANUAL_CONTROL)
# =========================
# Nilai axis dalam persen -100..100 seperti dikirim dashboard (surge/sway/yaw/heave).
# Sebuah thread terpisah mengubahnya jadi MANUAL_CONTROL dan mengirim ~15 Hz.
manual_axes = {"surge": 0.0, "sway": 0.0, "yaw": 0.0, "heave": 0.0}
manual_lock = threading.Lock()
# last_ts: kapan terakhir menerima perintah axis manual (untuk fail-safe timeout).
# neutral_sent: sudah mengirim satu perintah netral setelah idle? (hindari spam).
manual_ctx = {"last_ts": 0.0, "neutral_sent": True}
MANUAL_SEND_HZ = 15 # laju kirim MANUAL_CONTROL ke Pixhawk
MANUAL_TIMEOUT = 0.5 # detik; kalau tak ada axis baru -> netralkan (fail-safe)
# =========================
# Utility
# ========================
def send_telemetry():
payload = json.dumps(state).encode("utf-8")
telem_sock.sendto(payload, (LAPTOP_IP, UDP_TELEM_PORT))
print(f"[SEND] -> {LAPTOP_IP}:{UDP_TELEM_PORT} | {state}")
def normalize_heading(deg):
if deg < 0:
deg += 360.0
return deg % 360.0
# =========================
# Command handler dari laptop
# =========================
def command_listener():
global master
print(f"[UDP] Listening command on 0.0.0.0:{UDP_CMD_PORT}")
while True:
try:
data, addr = cmd_sock.recvfrom(4096)
except socket.timeout:
continue
except Exception as e:
print("[UDP] command socket error:", e)
continue
try:
msg = json.loads(data.decode("utf-8"))
except Exception:
print("[UDP] invalid JSON command")
continue
name = msg.get("name")
value = msg.get("value")
print(f"[CMD] {name} = {value} from {addr}")
if master is None:
print("[CMD] Pixhawk not connected yet")
continue
try:
if name == "arm":
if value:
print("[MAV] ARM")
master.arducopter_arm()
else:
print("[MAV] DISARM")
master.arducopter_disarm()
elif name == "control_mode":
# contoh mode umum ArduSub / ArduPilot:
# MANUAL, STABILIZE, ALT_HOLD, POSHOLD, GUIDED, dsb
mode = str(value).upper()
if mode in master.mode_mapping():
mode_id = master.mode_mapping()[mode]
master.set_mode(mode_id)
print(f"[MAV] set mode {mode}")
else:
print(f"[MAV] mode '{mode}' tidak ada di mode_mapping()")
elif name in ("surge", "sway", "yaw", "heave"):
# Axis kontrol manual dari dashboard (persen -100..100).
# Server sudah clamp, tapi tetap defensif di sisi Pi.
try:
v = float(value)
except (TypeError, ValueError):
v = 0.0
v = max(-100.0, min(100.0, v))
with manual_lock:
manual_axes[name] = v
manual_ctx["last_ts"] = time.time()
manual_ctx["neutral_sent"] = False
elif name == "stop":
# Failsafe sederhana: disarm + netralkan axis manual.
print("[MAV] STOP -> DISARM")
master.arducopter_disarm()
with manual_lock:
for k in manual_axes:
manual_axes[k] = 0.0
# picu satu kali kirim netral (x=y=r=0, z=500)
manual_ctx["last_ts"] = time.time()
manual_ctx["neutral_sent"] = False
elif name == "light":
# Belum dihubungkan ke hardware lampu, simpan status saja
state["light"] = bool(value)
print(f"[LIGHT] set to {state['light']}")
else:
print(f"[CMD] unknown command: {name}")
except Exception as e:
print("[CMD] error executing command:", e)
# =========================
# Pengirim MANUAL_CONTROL periodik ke Pixhawk
# =========================
def manual_control_sender():
"""Kirim MANUAL_CONTROL ~15 Hz selama ada aktivitas axis manual.
Fail-safe: kalau tak ada axis baru selama MANUAL_TIMEOUT (mis. joystick
dicabut, E-Stop, atau mode Autonomous mematikan pengiriman di GUI), kirim
SATU perintah netral (x=y=r=0, z=500) lalu berhenti mengirim sampai ada
perintah manual lagi — sehingga command terakhir tidak "nyangkut" dan kita
tidak mengganggu mode autonomous dengan MANUAL_CONTROL terus-menerus.
"""
period = 1.0 / MANUAL_SEND_HZ
print(f"[MANUAL] sender aktif @ {MANUAL_SEND_HZ} Hz (timeout {MANUAL_TIMEOUT}s)")
while True:
time.sleep(period)
if master is None:
continue
now = time.time()
with manual_lock:
active = (now - manual_ctx["last_ts"]) <= MANUAL_TIMEOUT
axes = dict(manual_axes)
neutral_sent = manual_ctx["neutral_sent"]
try:
if active:
mc = axes_to_manual_control(
axes["surge"], axes["sway"], axes["yaw"], axes["heave"]
)
master.mav.manual_control_send(
master.target_system,
mc["x"], mc["y"], mc["z"], mc["r"], mc["buttons"],
)
elif not neutral_sent:
mc = NEUTRAL
master.mav.manual_control_send(
master.target_system,
mc["x"], mc["y"], mc["z"], mc["r"], mc["buttons"],
)
with manual_lock:
manual_ctx["neutral_sent"] = True
print("[MANUAL] idle -> kirim netral (fail-safe)")
except Exception as e:
print("[MANUAL] send error:", e)
# =========================
# Main koneksi Pixhawk
# =========================
def main():
global master
print(f"[MAV] Connecting to Pixhawk on {PIXHAWK_PORT} @ {PIXHAWK_BAUD} ...")
master = mavutil.mavlink_connection(PIXHAWK_PORT, baud=PIXHAWK_BAUD)
print("[MAV] Waiting heartbeat...")
master.wait_heartbeat(timeout=30)
print("[MAV] Heartbeat received!")
print(f"[MAV] System {master.target_system}, Component {master.target_component}")
# Minta stream data secara periodik
try:
master.mav.request_data_stream_send(
master.target_system,
master.target_component,
mavutil.mavlink.MAV_DATA_STREAM_ALL,
10, # 10 Hz
1
)
except Exception as e:
print("[MAV] request_data_stream_send warning:", e)
# Thread listener command
threading.Thread(target=command_listener, daemon=True).start()
# Thread pengirim MANUAL_CONTROL periodik (kontrol manual joystick/keyboard)
threading.Thread(target=manual_control_sender, daemon=True).start()
last_send = 0
while True:
msg = master.recv_match(blocking=True, timeout=1)
if msg is None:
continue
mtype = msg.get_type()
# --------------------------------
# ATTITUDE: roll, pitch, yaw
# --------------------------------
if mtype == "ATTITUDE":
state["roll"] = math.degrees(msg.roll)
state["pitch"] = math.degrees(msg.pitch)
yaw_deg = math.degrees(msg.yaw)
state["heading"] = normalize_heading(yaw_deg)
# --------------------------------
# VFR_HUD: heading kadang tersedia di sini juga
# --------------------------------
elif mtype == "VFR_HUD":
if hasattr(msg, "heading"):
state["heading"] = float(msg.heading)
# --------------------------------
# SYS_STATUS: tegangan baterai
# voltage_battery dalam mV
# --------------------------------
elif mtype == "SYS_STATUS":
if msg.voltage_battery != 65535:
state["voltage"] = msg.voltage_battery / 1000.0
# --------------------------------
# HEARTBEAT: mode dan armed
# --------------------------------
elif mtype == "HEARTBEAT":
try:
state["mode"] = mavutil.mode_string_v10(msg)
except Exception:
pass
base_mode = msg.base_mode
state["armed"] = bool(base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED)
# Kirim telemetry periodik ke laptop
now = time.time()
if now - last_send >= 0.1: # 10 Hz
send_telemetry()
last_send = now
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[EXIT] rov_agent stopped by user")
except Exception as e:
print("[FATAL]", e)