Aplikacja.pyw 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. from concurrent.futures import thread
  2. import gi
  3. gi.require_version('Gtk', '4.0')
  4. from gi.repository import Gtk, GLib, Gdk
  5. import threading
  6. from inputs import get_gamepad
  7. import math
  8. # Importowanie stylów CSS
  9. css_provider = Gtk.CssProvider()
  10. css_provider.load_from_path('style.css')
  11. Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
  12. # Funkcja pomocnicza
  13. def setMarginAll(widget, margin):
  14. widget.set_margin_top(margin)
  15. widget.set_margin_bottom(margin)
  16. widget.set_margin_start(margin)
  17. widget.set_margin_end(margin)
  18. class DroneApplication(Gtk.ApplicationWindow):
  19. def __init__(self, application):
  20. super().__init__(application=application, title="cx.copter")
  21. self.set_default_size(1280, 960)
  22. settings = Gtk.Settings.get_default()
  23. settings.set_property("gtk-application-prefer-dark-theme", True)
  24. # Domyślne wartości dla zmiennych przechowujących stan gamepada
  25. self.gamepad_connected = False
  26. self.right_x = 0
  27. self.right_y = 0
  28. self.left_y = 0
  29. self.flaps_zl = 0
  30. self.flaps_zr = 0
  31. self.flaps_z = 0
  32. self.toggle = False
  33. self.prev = 1
  34. # Inne zmienne
  35. self.ZAKRES = 10 # Jaki zakres [-n; n] mają mieć sygnały wejścia gamepada.
  36. self.PITCH_STEPS = 8 # Ile ma być łącznie szczebli głównych na połowie sztucznego horyzontu.
  37. self.PITCH_DEG_STEP = 15 # Co ile stopni rysować szczebel główny w sztucznym horyzoncie.
  38. # Uruchomienie wątku do monitorowania gamepada
  39. thread = threading.Thread(target=self.monitor_controller, daemon=True)
  40. thread.start()
  41. # INTERFEJS UŻYTKOWNIKA
  42. main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  43. # Nagłówek
  44. logo = Gtk.Image.new_from_file("assets/logo.png")
  45. logo.props.pixel_size = 64
  46. title = Gtk.Label(label = "Aplikacja do sterowania dronem")
  47. title.add_css_class("title-1")
  48. batteryBar = Gtk.ProgressBar()
  49. batteryBar.set_fraction(0.5)
  50. batteryBar.set_text("Bateria drona: NULL%")
  51. batteryBar.set_show_text(True)
  52. batteryBar.set_valign(Gtk.Align.CENTER)
  53. batteryBar.set_css_classes(['battery-bar'])
  54. header = Gtk.CenterBox(
  55. start_widget=logo,
  56. center_widget=title,
  57. end_widget=batteryBar
  58. )
  59. setMarginAll(logo, 10)
  60. setMarginAll(batteryBar, 10)
  61. main.append(header)
  62. main.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
  63. # Ciało
  64. body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=15)
  65. self.sliderAltitudeBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # sterowanie wysokością (lewy joystick)
  66. self.sliderAltitudeLabel = Gtk.Label(label=f"ALT: {self.left_y}")
  67. self.sliderAltitude = Gtk.Scale(orientation=Gtk.Orientation.VERTICAL)
  68. self.sliderAltitude.set_range(-self.ZAKRES, self.ZAKRES)
  69. self.sliderAltitude.set_sensitive(False)
  70. self.sliderAltitude.set_inverted(True)
  71. self.sliderAltitude.set_size_request(54, 250)
  72. self.sliderAltitude.set_vexpand(True)
  73. self.sliderAltitudeBox.append(self.sliderAltitudeLabel)
  74. self.sliderAltitudeBox.append(self.sliderAltitude)
  75. self.movementAreaBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # sterowanie poruszaniem się (prawy joystick)
  76. self.movementAreaLabel = Gtk.Label(label=f"PORUSZANIE")
  77. self.movementArea = Gtk.DrawingArea()
  78. self.movementArea.set_hexpand(True)
  79. self.movementArea.set_vexpand(True)
  80. self.movementArea.set_draw_func(self.draw_movement)
  81. self.movementAreaBox.append(self.movementAreaLabel)
  82. self.movementAreaBox.append(self.movementArea)
  83. self.sliderZAxisBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # sterowanie poruszaniem po osi Z (klapy)
  84. self.sliderZAxisLabel = Gtk.Label(label=f"OBRACANIE: {self.flaps_z}")
  85. self.sliderZAxis = Gtk.Scale()
  86. self.sliderZAxis.set_range(-self.ZAKRES, self.ZAKRES)
  87. self.sliderZAxis.set_sensitive(False)
  88. self.sliderZAxisBox.append(self.sliderZAxisLabel)
  89. self.sliderZAxisBox.append(self.sliderZAxis)
  90. self.horizon = Gtk.DrawingArea() # odczyt z żyroskopu - sztuczny horyzont
  91. self.horizon.set_hexpand(True)
  92. self.horizon.set_vexpand(True)
  93. self.horizon.set_draw_func(self.draw_horizon)
  94. horizonFrame = Gtk.AspectFrame( xalign=0.5, yalign=0.5, ratio=1.0, obey_child=False )
  95. horizonFrame.set_child(self.horizon)
  96. self.sliderAltitude.set_css_classes(['grid-elements'])
  97. self.movementArea.set_css_classes(['grid-elements'])
  98. self.sliderZAxis.set_css_classes(['grid-elements'])
  99. self.horizon.set_css_classes(['grid-elements'])
  100. leftPanelGrid = Gtk.Grid(hexpand=True)
  101. leftPanelGrid.attach(self.sliderAltitudeBox, 0, 0, 1, 1)
  102. leftPanelGrid.attach(self.movementAreaBox, 1, 0, 1, 1)
  103. leftPanelGrid.attach(self.sliderZAxisBox, 0, 1, 2, 1)
  104. body.append(leftPanelGrid)
  105. body.append(horizonFrame)
  106. setMarginAll(body, 20)
  107. main.append(body)
  108. main.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
  109. # Stopka
  110. footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
  111. footer.set_size_request(-1, 150)
  112. setMarginAll(footer, 20)
  113. diagnosticBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  114. diagnosticLabel = Gtk.Label(label="DIAGNOSTYKA", halign=Gtk.Align.CENTER)
  115. diagnosticLabel.add_css_class("title-4")
  116. diagnosticBox.append(diagnosticLabel)
  117. self.droneStatusLabel = Gtk.Label(label="Dron: Not Found")
  118. self.droneStatusLabel.set_halign(Gtk.Align.START)
  119. self.droneStatusLabel.set_size_request(180, -1)
  120. self.droneStatusLabel.set_halign(Gtk.Align.CENTER)
  121. self.droneStatusLabel.set_css_classes(['status-not-ok'])
  122. self.gamepadStatusLabel = Gtk.Label(label="Gamepad: Not Found")
  123. self.gamepadStatusLabel.set_halign(Gtk.Align.START)
  124. self.gamepadStatusLabel.set_size_request(180, -1)
  125. self.gamepadStatusLabel.set_css_classes(['status-inaccessible'])
  126. self.motorsStatusButton = Gtk.Button(label="Silniki: Off", sensitive=False)
  127. self.motorsStatusButton.set_halign(Gtk.Align.START)
  128. self.motorsStatusButton.set_size_request(180, -1)
  129. self.motorsStatusButton.set_css_classes(['status-inaccessible'])
  130. diagnosticBox.append(self.droneStatusLabel)
  131. diagnosticBox.append(self.gamepadStatusLabel)
  132. diagnosticBox.append(self.motorsStatusButton)
  133. logsBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15, homogeneous=True) # Telemetria
  134. logsBox.set_hexpand(True)
  135. sentBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  136. sentLabel = Gtk.Label(label="WYSŁANE:")
  137. sentBox.append(sentLabel)
  138. sentScroll = Gtk.ScrolledWindow()
  139. sentScroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
  140. sentScroll.set_vexpand(True)
  141. sentScroll.set_size_request(150, 40)
  142. self.sent_logs = Gtk.TextView()
  143. self.sent_logs.set_editable(False)
  144. self.sent_logs.set_css_classes(['grid-elements'])
  145. sentScroll.set_child(self.sent_logs)
  146. sentBox.append(sentScroll)
  147. recvBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  148. recvLabel = Gtk.Label(label="ODEBRANE:")
  149. recvBox.append(recvLabel)
  150. recvScroll = Gtk.ScrolledWindow()
  151. recvScroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
  152. recvScroll.set_vexpand(True)
  153. recvScroll.set_size_request(150, 40)
  154. self.recv_logs = Gtk.TextView()
  155. self.recv_logs.set_editable(False)
  156. self.recv_logs.set_css_classes(['grid-elements'])
  157. recvScroll.set_child(self.recv_logs)
  158. recvBox.append(recvScroll)
  159. logsBox.append(sentBox)
  160. logsBox.append(recvBox)
  161. actionsBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) # Przyciski
  162. actionsBox.set_valign(Gtk.Align.CENTER)
  163. self.btn_test = Gtk.Button(label="Test LED / Silników")
  164. self.btn_test.set_size_request(150, -1)
  165. self.btn_cali = Gtk.Button(label="Kalibracja Żyroskopu")
  166. self.btn_cali.set_size_request(150, -1)
  167. actionsBox.append(self.btn_test)
  168. actionsBox.append(self.btn_cali)
  169. footer.append(diagnosticBox)
  170. footer.append(logsBox)
  171. footer.append(actionsBox)
  172. main.append(footer)
  173. self.set_child(main)
  174. # Funkcja do rysowania kierunku poruszania
  175. def draw_movement(self, area, c, width, height):
  176. cx = width / 2
  177. cy = height / 2
  178. target_x = cx + ((self.right_x / self.ZAKRES) * cx)
  179. target_y = cy - ((self.right_y / self.ZAKRES) * cy)
  180. # Linia
  181. c.set_source_rgb(1, 0, 0)
  182. c.move_to(cx, cy)
  183. c.line_to(target_x, target_y)
  184. c.stroke()
  185. # Kropka
  186. c.set_source_rgb(1, 0, 0)
  187. c.arc(target_x, target_y, 4, 0, 2 * math.pi)
  188. c.fill()
  189. # Tekst
  190. c.set_source_rgb(1, 1, 1)
  191. c.move_to(cx, cy)
  192. c.set_font_size(12)
  193. c.show_text(f" {self.right_x} | {self.right_y}")
  194. c.move_to(target_x, target_y)
  195. # Funkcja do rysowania sztucznego horyzontu
  196. def draw_horizon(self, area, c, width, height):
  197. cx = width / 2.0
  198. cy = height / 2.0
  199. # DLA CELÓW SYMULACJI
  200. max_pitch = 45
  201. max_roll = 45
  202. pixels_per_step = cy - (cy + (height / self.PITCH_STEPS))
  203. roll_angle = (self.right_x / self.ZAKRES) * max_roll
  204. pitch_offset = (self.right_y / self.ZAKRES) * max_pitch
  205. pitch_final = pixels_per_step * (pitch_offset / self.PITCH_DEG_STEP)
  206. c.save()
  207. c.translate(cx, cy)
  208. c.rotate(-math.radians(roll_angle))
  209. c.translate(0, pitch_final)
  210. c.translate(-cx, -cy)
  211. # Rysowanie tarczy
  212. c.set_source_rgb(0.14, 0.63, 1) # niebo
  213. c.rectangle(-width, -height, width*4, height*2)
  214. c.fill()
  215. c.set_source_rgb(0.55, 0.38, 0.22) # ziemia
  216. c.rectangle(-width, cy, width*4, height*2)
  217. c.fill()
  218. # Rysowanie linii
  219. c.set_source_rgb(1, 1, 1)
  220. c.set_line_width(2)
  221. c.move_to(-cx, cy)
  222. c.line_to(width*2, cy)
  223. c.stroke()
  224. for x in range(-self.PITCH_STEPS, self.PITCH_STEPS + 1):
  225. if x == 0: continue # pomijamy środkową kreskę
  226. # główna linia
  227. target_pitch_y = cy + ((height / self.PITCH_STEPS) * x)
  228. c.set_source_rgb(1, 1, 1)
  229. c.set_line_width(2)
  230. c.move_to(cx / 2, target_pitch_y)
  231. c.line_to((cx / 2) + cx, target_pitch_y)
  232. c.stroke()
  233. pitch_text = str(-x * self.PITCH_DEG_STEP)
  234. c.set_font_size(max(10, cx * 0.06))
  235. text_width = c.text_extents(pitch_text).width
  236. c.move_to(cx / 2, target_pitch_y - 5)
  237. c.show_text(pitch_text)
  238. c.move_to((cx / 2) + cx - text_width, target_pitch_y - 5)
  239. c.show_text(pitch_text)
  240. # pomocnicza linia
  241. kierunek = 1 if x > 0 else -1
  242. target_pitch_y_sub = cy + ((height / self.PITCH_STEPS) * (x - 0.5 * kierunek))
  243. c.set_source_rgb(1, 1, 1)
  244. c.set_line_width(2)
  245. c.move_to(cx - (cx / 4), target_pitch_y_sub)
  246. c.line_to(cx + (cx / 4), target_pitch_y_sub)
  247. c.stroke()
  248. c.restore()
  249. # Rysowanie dziobu
  250. c.set_source_rgb(1, 1, 0)
  251. scalar = cx * 0.08
  252. c.set_line_width(max(2.0, scalar * 0.2))
  253. c.move_to(cx - 6 * scalar, cy)
  254. c.line_to(cx - 2 * scalar, cy)
  255. c.line_to(cx - 1 * scalar, cy + 1 * scalar)
  256. c.line_to(cx, cy)
  257. c.line_to(cx + 1 * scalar, cy + 1 * scalar)
  258. c.line_to(cx + 2 * scalar, cy)
  259. c.line_to(cx + 6 * scalar, cy)
  260. c.stroke()
  261. c.arc(cx, cy, max(2.0, scalar * 0.15), 0, 2 * math.pi)
  262. c.fill()
  263. # Funkcja do aktualizacji interfejsu użytkownika z aktualnymi wartościami stanu gamepada
  264. def update_ui(self):
  265. # Input gamepada
  266. self.sliderAltitude.set_value(self.left_y)
  267. self.sliderAltitudeLabel.set_text(f"ALT: {self.left_y}")
  268. self.movementArea.queue_draw()
  269. self.movementAreaLabel.set_text(f"PORUSZANIE")
  270. self.sliderZAxis.set_value(self.flaps_z)
  271. self.sliderZAxisLabel.set_text(f"OBRACANIE: {self.flaps_z}")
  272. self.horizon.queue_draw()
  273. # Statusy
  274. if self.gamepad_connected:
  275. self.gamepadStatusLabel.set_text("Gamepad: Ok")
  276. self.gamepadStatusLabel.set_css_classes(['status-ok'])
  277. else:
  278. self.gamepadStatusLabel.set_text("Gamepad: Not Found")
  279. self.gamepadStatusLabel.set_css_classes(['status-not-ok'])
  280. return False
  281. # Słuchanie zdarzeń z kontrolera
  282. def monitor_controller(self):
  283. raw_rx = 0
  284. raw_ry = 0
  285. while True:
  286. try:
  287. events = get_gamepad()
  288. if not self.gamepad_connected:
  289. print("Pad podłączony")
  290. self.gamepad_connected = True
  291. for event in events:
  292. if event.code == 'BTN_START': # Przycisk START
  293. if self.prev == 1 and event.state == 1:
  294. self.toggle = not self.toggle
  295. self.prev = not self.prev
  296. elif event.code == 'ABS_Y': # Lewy Joystick, oś Y
  297. if abs(event.state) > 3276:
  298. self.left_y = round((event.state / 32768.0) * self.ZAKRES, 2)
  299. else:
  300. self.left_y = 0
  301. elif event.code == 'ABS_RX': # Prawy Joystick, oś X
  302. raw_rx = event.state
  303. elif event.code == 'ABS_RY': # Prawy Joystick, oś Y
  304. raw_ry = event.state
  305. elif event.code == 'ABS_Z': # Klapa tylna lewa
  306. self.flaps_zl = event.state
  307. elif event.code == 'ABS_RZ': # Klapa tylna prawa
  308. self.flaps_zr = event.state
  309. # Wyprowadzenie wejścia z prawego joysticka z wykluczeniem kołowej martwej strefy
  310. magnitude = math.sqrt(raw_rx**2 + raw_ry**2)
  311. if magnitude > 3276:
  312. self.right_x = round((raw_rx / 32768.0) * self.ZAKRES, 2)
  313. self.right_y = round((raw_ry / 32768.0) * self.ZAKRES, 2)
  314. else:
  315. self.right_x = 0
  316. self.right_y = 0
  317. # Przeliczenie różnicy klap i skalowanie
  318. self.flaps_z = round(((self.flaps_zr - self.flaps_zl) / 255) * self.ZAKRES, 2)
  319. GLib.idle_add(self.update_ui)
  320. except Exception as e:
  321. if self.gamepad_connected:
  322. print(f"Błąd: {e}")
  323. self.gamepad_connected = False
  324. self.right_x = 0
  325. self.right_y = 0
  326. self.left_y = 0
  327. self.flaps_z = 0
  328. GLib.idle_add(self.update_ui)
  329. time.sleep(1)
  330. # Inicjalizacja aplikacji
  331. def on_activate(app):
  332. window = DroneApplication(application=app)
  333. window.present()
  334. app = Gtk.Application(application_id="com.cx.copter.app")
  335. app.connect("activate", on_activate)
  336. app.run(None)