-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_rov_pid.py
More file actions
162 lines (133 loc) · 6.34 KB
/
Copy pathtest_rov_pid.py
File metadata and controls
162 lines (133 loc) · 6.34 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
"""Unit test pemetaan PID GUI -> param ArduSub + validasi kedalaman kolam.
python3 -m unittest test_rov_pid -v
"""
import unittest
from rov_pid import (
PARAM_TO_PID,
PID_PARAM_MAP,
PID_WRITE_ORDER,
REAL32,
pid_param_names,
resolve_pid_writes,
valid_pool_depth,
)
# Nilai gain yang benar-benar ada di Pixhawk wahana (parameters_ardusub.params).
FC_NYATA = {
"yaw": {"p": 0.18, "i": 0.018, "d": 0.0},
"roll": {"p": 0.135, "i": 0.09, "d": 0.0036, "ang_p": 6.0},
"pitch": {"p": 0.135, "i": 0.09, "d": 0.0036, "ang_p": 6.0},
"depth": {"p": 0.5, "i": 0.1, "d": 0.0},
}
# Default LAMA di public/js/config.js — bukan satuan ArduSub.
GUI_DEFAULT_LAMA = {
"yaw": {"p": 2.0, "i": 0.0, "d": 0.5},
"depth": {"p": 10.0, "i": 0.5, "d": 2.0},
}
class TestPeta(unittest.TestCase):
def test_empat_belas_gain_terpetakan(self):
self.assertEqual(len(PID_PARAM_MAP), 14)
self.assertEqual(len(PID_WRITE_ORDER), 14)
self.assertEqual(set(PID_WRITE_ORDER), set(PID_PARAM_MAP))
def test_nama_param_tanpa_duplikat(self):
# Menjaga agar penambahan gain baru tidak menimpa param lama diam-diam.
names = pid_param_names()
self.assertEqual(len(set(names)), len(names))
self.assertEqual(len(PARAM_TO_PID), 14)
def test_nama_param_sesuai_keputusan(self):
self.assertEqual(pid_param_names(), [
"ATC_RAT_YAW_P", "ATC_RAT_YAW_I", "ATC_RAT_YAW_D",
"ATC_RAT_RLL_P", "ATC_RAT_RLL_I", "ATC_RAT_RLL_D", "ATC_ANG_RLL_P",
"ATC_RAT_PIT_P", "ATC_RAT_PIT_I", "ATC_RAT_PIT_D", "ATC_ANG_PIT_P",
"PSC_ACCZ_P", "PSC_ACCZ_I", "PSC_ACCZ_D",
])
def test_semua_real32(self):
# Sudah dikonfirmasi dari dump nyata Pixhawk (kolom tipe = 9).
for _name, type_id, _lo, _hi in PID_PARAM_MAP.values():
self.assertEqual(type_id, REAL32)
def test_rentang_masuk_akal(self):
for name, (_n, _t, lo, hi) in zip(PID_PARAM_MAP, PID_PARAM_MAP.values()):
self.assertLess(lo, hi, msg=str(name))
self.assertGreaterEqual(lo, 0.0, msg=str(name))
def test_nilai_fc_nyata_ada_di_dalam_rentang(self):
# Kalau rentang yang ditulis tangan sampai menolak nilai yang SEDANG
# BERJALAN di wahana, rentangnya yang salah.
writes, rejects = resolve_pid_writes(FC_NYATA)
self.assertEqual(rejects, [])
self.assertEqual(len(writes), 14)
class TestResolvePidWrites(unittest.TestCase):
def test_payload_lengkap_valid(self):
writes, rejects = resolve_pid_writes(FC_NYATA)
self.assertEqual(rejects, [])
self.assertEqual([w[0] for w in writes], pid_param_names())
self.assertEqual(writes[0], ("ATC_RAT_YAW_P", 0.18, REAL32))
for _name, value, _t in writes:
self.assertIsInstance(value, float)
def test_default_gui_lama_DITOLAK(self):
# REGRESI INTI: inilah yang membuat menyambungkan `pid` secara lugas
# berbahaya. yaw.p 2.0 (FC 0.18), depth.p 10.0 (FC 0.5).
writes, rejects = resolve_pid_writes(GUI_DEFAULT_LAMA)
ditolak = {name for name, _alasan in rejects}
self.assertIn("ATC_RAT_YAW_P", ditolak)
self.assertIn("ATC_RAT_YAW_D", ditolak)
self.assertIn("PSC_ACCZ_P", ditolak)
self.assertIn("PSC_ACCZ_D", ditolak)
# Yang ditolak TIDAK BOLEH ikut ditulis.
ditulis = {name for name, _v, _t in writes}
self.assertTrue(ditulis.isdisjoint(ditolak))
def test_alasan_penolakan_menyebut_rentang(self):
_writes, rejects = resolve_pid_writes({"yaw": {"p": 2.0}})
self.assertEqual(len(rejects), 1)
name, alasan = rejects[0]
self.assertEqual(name, "ATC_RAT_YAW_P")
self.assertIn("2", alasan)
self.assertIn("rentang aman", alasan)
def test_batas_rentang_inklusif(self):
for axis, gain in PID_WRITE_ORDER:
_n, _t, lo, hi = PID_PARAM_MAP[(axis, gain)]
for edge in (lo, hi):
writes, rejects = resolve_pid_writes({axis: {gain: edge}})
self.assertEqual(rejects, [], msg=f"{axis}.{gain}={edge}")
self.assertEqual(len(writes), 1)
def test_payload_sebagian_boleh(self):
# Halaman Setup boleh mengirim sebagian gain saja.
writes, rejects = resolve_pid_writes({"yaw": {"p": 0.2}})
self.assertEqual(rejects, [])
self.assertEqual(writes, [("ATC_RAT_YAW_P", 0.2, REAL32)])
def test_nilai_tidak_valid_jadi_reject_bukan_dilewati(self):
# Angka yang sudah diketik operator tidak boleh hilang diam-diam.
for bad in ("abc", None, [1], {"v": 1}, True, float("nan"), float("inf")):
writes, rejects = resolve_pid_writes({"yaw": {"p": bad}})
self.assertEqual(writes, [], msg=repr(bad))
self.assertEqual(len(rejects), 1, msg=repr(bad))
def test_string_angka_diterima(self):
writes, rejects = resolve_pid_writes({"yaw": {"p": "0.2"}})
self.assertEqual(rejects, [])
self.assertEqual(writes[0][1], 0.2)
def test_payload_cacat(self):
for bad in (None, "pid", 42, []):
writes, rejects = resolve_pid_writes(bad)
self.assertEqual(writes, [], msg=repr(bad))
self.assertTrue(rejects, msg=repr(bad))
def test_bagian_bukan_objek_ditolak(self):
writes, rejects = resolve_pid_writes({"yaw": 0.2})
self.assertEqual(writes, [])
self.assertEqual(rejects, [("yaw", "bagian bukan objek")])
def test_payload_kosong_dilaporkan(self):
# Jangan diam saja: operator menekan Apply dan tidak terjadi apa-apa.
writes, rejects = resolve_pid_writes({})
self.assertEqual(writes, [])
self.assertTrue(rejects)
def test_kunci_asing_diabaikan_bukan_error(self):
writes, rejects = resolve_pid_writes({"yaw": {"p": 0.2, "ff": 9.9}, "sway": {"p": 1}})
self.assertEqual(rejects, [])
self.assertEqual(len(writes), 1)
class TestValidPoolDepth(unittest.TestCase):
def test_nilai_sah(self):
self.assertEqual(valid_pool_depth(0.9), 0.9)
self.assertEqual(valid_pool_depth("1.5"), 1.5)
self.assertEqual(valid_pool_depth(3), 3.0)
def test_nilai_ditolak(self):
for bad in (0, -1, "abc", None, True, False, [1], float("nan"), float("inf")):
self.assertIsNone(valid_pool_depth(bad), msg=repr(bad))
if __name__ == "__main__":
unittest.main()