-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.py
846 lines (701 loc) · 36.7 KB
/
main.py
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
import sys
import os
import platform
import logging
import re
import random
import time
import numpy as np
import webbrowser
import json
from modules import *
from widgets import *
from pyqtgraph import PlotDataItem
import bluetooth_numbers as ble_uuid
from bluetooth_numbers import service
from uuid import UUID
from ctypes import *
import pyqtgraph as pg
from PySide6 import QtUiTools, QtWidgets, QtGui
from PySide6.QtWidgets import QMessageBox, QTableWidget, QMenu, QApplication
from PySide6.QtGui import QCursor, QAction, QClipboard , QPen , QBrush , QColor
from PySide6.QtCore import QThread, Signal, QMutex, QMutexLocker, Qt , QTimer
from PySide6.QtWidgets import QTableWidget, QTableWidgetItem, QCheckBox, QWidget, QHBoxLayout
from PySide6.QtCore import Qt
from PySide6 import QtCharts
from PySide6.QtCharts import QLineSeries
from math import sin, pi
os.environ["QT_FONT_DPI"] = Settings.HIGH_DPI_DISPLAY_FONT_DPI # FIX Problem for High DPI and Scale above 100%
# SET AS GLOBAL WIDGETS
widgets = None
from char import Ui_char_widget
# TODO : Move a lot of these functions to their related modules
class MainWindow(QMainWindow):
add_adv_table_item = Signal(str)
toplevel = None
child = None
chars_vbox = QGridLayout()
charCount= 1
char_dict = {}
cleanUp = Signal(object)
# RSSI graph variables
axisX = QtCharts.QValueAxis()
axisY = QtCharts.QValueAxis()
axisX.setRange(0, 10)
axisY.setRange(-1, 1)
elfFilePath = None
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
global widgets
widgets = self.ui
# Install the event filter
self.installEventFilter(self)
self.vars_watched_dict={}
self.device_address = None
# Graphing variables
self.device_data_sets = {}
self.device_data_curves = {}
self.device_original_colors = {}
self.start_time = None #time.time()
self.current_time = time.time()
self.plot_colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
# self.ui.widget_rssi_graph.setBackground((40, 44, 52))
# self.ui.widget_rssi_graph.getPlotItem().getAxis('bottom').setStyle(showValues=False)
# OTA variables
self.fileName = None
self.fileLen = None
self.fileCrc32 = None
# Initialize logging
console = logging.getLogger("PDexLogger")
handler = QLogHandler(self.ui.console)
handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
handler.emitter.logMessage.connect(self.logToTextbox)
console.addHandler(handler)
console.setLevel(logging.DEBUG)
self.logger = logging.getLogger("PDexLogger")
# connected mode variables
self.connectedDevice = BLE_ConnectDevice()
self.connectedDevice.device_notification_recevied.connect(self.char_notification_handler)
self.connectedDevice.device_char_read_response.connect(self.char_read_response_handler)
self.connectedDevice.device_ota_update_reset.connect(self.ota_reset)
#OTA related
self.connectedDevice.otas_progress_value.connect(
lambda value: self.otas_progress_update(value))
self.update_rssi_thread = UpdateRSSIGraphThread(self)
self.update_rssi_thread.dataUpdated.connect(self.update_graph)
if self.ui.graph_enabled.isChecked():
self.update_rssi_thread.GraphActive = True
#self.update_rssi_thread.start()
# Global BLE objects
self.bleScanner = ble_functions.BLE_DiscoverDevices()
# USE CUSTOM TITLE BAR | USE AS "False" FOR MAC OR LINUX
Settings.ENABLE_CUSTOM_TITLE_BAR = False
# used to store the current popup window when selecting var type
self.current_popup = None
# APP NAME
title = "BLE-PyDex"
description = "Bluetooth Low Energy Scanner , Explorer, Logger and more..."
# APPLY TEXTS
self.setWindowTitle(title)
self.ui.titleRightInfo.setText(description)
self.ui.tbl_vars.setColumnWidth(3, 50)
# TOGGLE MENU
self.ui.toggleButton.clicked.connect(lambda: UIFunctions.toggleMenu(self, True))
self.ui.elfSettings.hide()
# SET UI DEFINITIONS
UIFunctions.uiDefinitions(self)
# QTableWidget PARAMETERS
#self.ui.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
header = self.ui.tableWidget_2.verticalHeader()
header.setDefaultAlignment(Qt.AlignCenter)
self.ui.tableWidget_2.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
# UI MENU BASIC BUTTONS CLICK
# LEFT MENUS
self.ui.btn_home.clicked.connect(self.buttonClick)
self.ui.btn_widgets.clicked.connect(self.buttonClick)
self.ui.btn_gatt_explorer.clicked.connect(self.buttonClick)
self.ui.btn_save.clicked.connect(self.buttonClick)
# Register signal handlers
self.add_adv_table_item.connect(lambda data :self.add_table_item(data))
self.connectedDevice.discovered_services.connect(self.discovered_services)
self.ui.gatt_treeView.itemClicked.connect(self.gatt_tree_view_clicked)
# Register none-UI button callbacks
btn_callbacks.register_button_callbacks(self)
#self.ui.tableWidget_2.reset()
# stylesheets
self.btn_stylesheet = open("button_stylesheet.txt", "r").read()
self.scroll_area_stylesheet = open("scroll_area_stylesheet.txt", "r").read()
self.comboBox_stylesheet = open("combobox_stylesheet.txt", "r").read()
self.ui.scrollArea_2.setStyleSheet(self.scroll_area_stylesheet)
# EXTRA LEFT BOX
def openCloseLeftBox():
UIFunctions.toggleLeftBox(self, True)
self.ui.toggleLeftBox.clicked.connect(openCloseLeftBox)
self.ui.extraCloseColumnBtn.clicked.connect(openCloseLeftBox)
# EXTRA RIGHT BOX
def openCloseRightBox():
UIFunctions.toggleRightBox(self, True)
self.ui.settingsTopBtn.clicked.connect(openCloseRightBox)
# Init signals and slots
slots.init_signals_and_slots(self)
# register cleanup callback and pass widgets object to it with lambda
self.cleanUp.connect(lambda: self.clean_up())
# SHOW APP
self.show()
# right box visible content initially hide all execept scanner settings
self.ui.ota_frame.hide()
self.ui.ota_frame.setMaximumHeight(0)
self.ui.elfSettings.hide()
self.ui.elfSettings.setMaximumHeight(0)
self.ui.scannerSettigns.setMaximumHeight(1000000)
self.ui.scannerSettigns.show()
#hide unfinished sections
self.ui.btn_exit.hide()
self.ui.btn_save.hide()
self.ui.btn_widgets.hide()
# Set up the axes (assuming the chart is already set up in the .ui file)
self.axisX = QtCharts.QValueAxis()
self.axisY = QtCharts.QValueAxis()
self.axisX.setRange(0, 10)
self.axisY.setRange(Settings.RSS_RANGE_BOTTOM, Settings.RSSI_RANGE_TOP)
self.ui.qtchart_widgetholder.chart().addAxis(self.axisX, Qt.AlignBottom)
self.ui.qtchart_widgetholder.chart().addAxis(self.axisY, Qt.AlignLeft)
self.ui.qtchart_widgetholder.chart().layout().setContentsMargins(0, 0, 0, 0)
self.ui.qtchart_widgetholder.chart().setMargins(QMargins(0, 0, 0, 0))
# change background color to rgb(40, 44, 52)
# Create a QColor object with the desired background color
background_color = QColor(33, 37,43)
# Create a QBrush object with the QColor object
background_brush = QBrush(background_color)
# Set the background brush of the QChart
self.ui.qtchart_widgetholder.chart().setBackgroundBrush(background_brush)
# change grideline colors to rgb(52, 59, 72)
axes = self.ui.qtchart_widgetholder.chart().axes(Qt.Horizontal)
if axes:
axes[0].setGridLineColor(QColor(52, 59, 72))
axes = self.ui.qtchart_widgetholder.chart().axes(Qt.Vertical)
if axes:
axes[0].setGridLineColor(QColor(52, 59, 72))
# change axis label colors to rgb(255, 255, 255)
axes_x = self.ui.qtchart_widgetholder.chart().axes(Qt.Horizontal)
if axes_x:
axes_x[0].setLabelsColor(QColor(52, 59, 72))
axes_y = self.ui.qtchart_widgetholder.chart().axes(Qt.Vertical)
if axes_y:
axes_y[0].setLabelsColor(QColor(52, 59, 72))
# hide legend
self.ui.qtchart_widgetholder.chart().legend().hide()
# set chart title
self.ui.qtchart_widgetholder.chart().setTitle("RSSI (dBm)")
# Create a QPen object for the main axis lines
pen = QPen(QColor(52, 59, 72)) # Change the color to whatever you want
pen.setWidth(2) # Change the width to your desired size
# Apply the pen to the axis
self.axisX.setLinePen(pen)
self.axisY.setLinePen(pen)
self.ui.list_widget_discovered.itemClicked.connect(self.highlight_selected_device)
# SET CUSTOM THEME
useCustomTheme = True
themeFile = "themes/py_dracula_dark.qss"
# SET THEME AND HACKS
if useCustomTheme:
# LOAD AND APPLY STYLE
UIFunctions.theme(self, themeFile, True)
# SET HACKS : some gui elements dont inherit parent styles, do it manually
AppFunctions.setThemeHack(self)
# SET HOME PAGE AND SELECT MENU
self.ui.stackedWidget.setCurrentWidget(self.ui.home)
self.ui.btn_home.setStyleSheet(UIFunctions.selectMenu(self.ui.btn_home.styleSheet()))
def highlight_selected_device(self, item):
selected_device = item.text()
blue_color = QColor(153, 193, 241) # Light gray color
blue_pen = QPen(blue_color)
blue_pen.setWidth(2)
# Temporarily store the series for the selected device
selected_device_series = None
# Loop through all device_data_curves to update their color
for device_name, device_series in self.device_data_curves.items():
if device_name == selected_device:
# This is the selected device, so change its color
device_series.setPen(blue_pen)
# Store the series for adding it again later to bring it to the front
selected_device_series = device_series
else:
original_color_pen = QPen(self.device_original_colors[device_name])
original_color_pen.setWidth(2)
device_series.setPen(original_color_pen)
# Remove and add the series again to bring it to the front
# This is a workarouund for the fact that QChart doesn't have a "bring to front" method
# and it will redraw other series on top of the selected one
if selected_device_series:
self.ui.qtchart_widgetholder.chart().removeSeries(selected_device_series)
self.ui.qtchart_widgetholder.chart().addSeries(selected_device_series)
selected_device_series.attachAxis(self.axisX)
selected_device_series.attachAxis(self.axisY)
# Trigger a redraw
self.ui.qtchart_widgetholder.chart().update()
def update_graph(self,device_name,rssi_value,current_time):
MAX_DEVICES = 50 # Maximum number of devices to display
# Initialize start time and max duration
if self.start_time is None:
self.start_time = current_time
max_duration = 5 # Maximum duration to display (in seconds)
time_delta = current_time - self.start_time
if time_delta > max_duration:
self.start_time = current_time - max_duration
self.axisX.setRange(self.start_time, current_time)
else:
self.axisX.setRange(self.start_time, current_time)
if device_name in self.device_data_curves:
# Retrieve the existing QLineSeries for the device
device_series = self.device_data_curves[device_name]
# Convert QLineSeries to numpy arrays for easy manipulation
old_data = [(point.x(), point.y()) for point in device_series.points()]
device_data_x, device_data_y = np.array(old_data).T
# Append new data and update QLineSeries
device_data_x = np.append(device_data_x, current_time)
device_data_y = np.append(device_data_y, rssi_value)
# Create a list of QPointF objects
points = [QPointF(x, y) for x, y in zip(device_data_x, device_data_y)]
# we have more than 50 points so we remove the first one
if len(points) > 50:
points.pop(0)
device_series.replace(points)
else:
if len(self.device_data_curves) >= MAX_DEVICES:
return # Ignore new device if max is reached
# Generate a random color for the new device
light_gray = QColor(72, 79, 92) # Light gray color
pen = QPen((light_gray))
pen.setWidth(2)
# Create a new QLineSeries for the device
new_device_series = QLineSeries()
new_device_series.setPen(pen)
new_device_series.append(current_time, rssi_value)
# Update color of QListWidgetItem to match the line color
# for i in range(self.ui.list_widget_discovered.count()):
# item = self.ui.list_widget_discovered.item(i)
# if item.text() == device_name:
# item.setForeground(QBrush(random_color))
# break
# Add to chart and attach axes
self.ui.qtchart_widgetholder.chart().addSeries(new_device_series)
new_device_series.attachAxis(self.axisX)
new_device_series.attachAxis(self.axisY)
# Store the new series in the dictionary
self.device_data_curves[device_name] = new_device_series
# Save the original color in the new dictionary
self.device_original_colors[device_name] = light_gray
def stop_rssi_thread(self):
# the RSSI thread
if self.update_rssi_thread.GraphActive == True:
self.update_rssi_thread.GraphActive = False # Request the thread to stop
self.update_rssi_thread.quit() # Request the thread to stop
self.update_rssi_thread.wait() # Wait until the thread has actually stopped
self.logger.info("RSSI thread stopped")
def add_table_item(self, data):
logger = logging.getLogger("PDexLogger")
# data looks like this:
# AdvertisementData(manufacturer_data={301: b'\x04\x00\x02\x02\xb02\x06\x02\xc2\x00\xdd\xb6\xb2\x10\x02\x003\x00\x00\x00'},
# service_data={'0000fe2c-0000-1000-8000-00805f9b34fb': b'\x000\x00\x00\x00\x11\x17402G'},
# service_uuids=['0000fd82-0000-1000-8000-00805f9b34fb'], tx_power=-21, rssi=-62)
# check if the string manufacturer_data= is in the data string
if "manufacturer_data={" in data:
# extract the numbers after the string "manufacturer_data={" and before the ":" , these are manufacturer ID
manufacturer_id = data.split("manufacturer_data={")[1].split(":")[0]
manufacturer_id = int(manufacturer_id)
# check if that manufacturer ID is in bluetooth numbers library
try:
companyID= ble_uuid.company[manufacturer_id]
#replace the manufacturer ID with the company name in data string
data = data.replace(str(manufacturer_id),str(companyID))
except Exception as e:
companyID = "Unknown"
# Add the data from device[1] into the tableWidget_2
rowPosition = self.ui.tableWidget_2.rowCount()
self.ui.tableWidget_2.insertRow(rowPosition)
self.ui.tableWidget_2.setColumnCount(1) # Set the number of columns to 1
self.ui.tableWidget_2.setHorizontalHeaderItem(0, QTableWidgetItem("Advertised Data")) # Set the column header
# align header text to left
self.ui.tableWidget_2.horizontalHeaderItem(0).setTextAlignment(Qt.AlignLeft)
#hide horizontal header
self.ui.tableWidget_2.horizontalHeader().setVisible(True)
#hide vertical header
self.ui.tableWidget_2.verticalHeader().setVisible(True)
item = QTableWidgetItem(data) # Convert data to string explicitly
item.setTextAlignment(Qt.AlignLeft) # Center the text
self.ui.tableWidget_2.setItem(rowPosition, 0, item) # Place the data in the first column
self.ui.tableWidget_2.setColumnWidth(0, 700) # Set the width to your desired value
self.ui.tableWidget_2.setRowHeight(rowPosition, 40) # Set the height to your desired value
if self.ui.check_scroll_to_bottom.isChecked():
self.ui.tableWidget_2.scrollToBottom()
def logToTextbox(self, data):
self.ui.console.append(data)
def discovered_services(self,data):
PARENT = 0
CHILD = 1
GRANDCHILD = 2
''' data[0]
comes in looking like this:
['[Service] 00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile', 0]
['\t[Characteristic] 00002a05-0000-1000-8000-00805f9b34fb (Handle: 17): Service Changed (indicate), Value: None', 1]
And leaves looking like this after the parsing below:
Service : 00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile
Characteristic : 00002a05-0000-1000-8000-00805f9b34fb (Handle: 17): Service Changed (indicate), Value: None'''
#clean up data per above commenr
item = data[0]
item = item.replace("\t", "")
item = item.replace("[", "")
item = item.replace("]", " : ")
''' data[1]
is a level indicator for the tree widget, this is emited by
modules->ble_functions->discover_device_services
0 = PARENT = service
1 = CHILD = characteristic
2 = GRANDCHILD = descriptor
'''
level = data[1]
permissions = data[2]
if level == PARENT:
# data[0] looks like this: 00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile
# extract the UUID from the string which is this: 00001801-0000-1000-8000-00805f9b34fb
# check if UUID exist in ble numbers to get a name for this UUI
char_name = self.extract_uuid_name(item)
self.logger.info("Adding service widget for UUID: " + str(char_name))
self.toplevel = QTreeWidgetItem([str(char_name)])
# Set the icon for the top-level item.
icon = QIcon()
icon.addPixmap(QPixmap("char_s.png"), QIcon.Normal, QIcon.On)
self.toplevel.setIcon(0, icon)
self.ui.gatt_treeView.addTopLevelItem(self.toplevel)
elif level == CHILD and self.toplevel != None:
# check if UUID exist in ble numbers to get a name for this UUID
char_name = self.extract_uuid_name(item)
self.child = QTreeWidgetItem([str(char_name)])
# Set the icon for the top-level item.
icon = QIcon()
icon.addPixmap(QPixmap("char_c.png"), QIcon.Normal, QIcon.On)
self.child.setIcon(0, icon)
self.toplevel.addChild(self.child)
elif level == GRANDCHILD and self.child != None:
# check if UUID exist in ble numbers to get a name for this UUI
char_name = self.extract_uuid_name(item)
self.subchild = QTreeWidgetItem([str(char_name)])
# Set the icon for the top-level item.
icon = QIcon()
icon.addPixmap(QPixmap("char_d.png"), QIcon.Normal, QIcon.On)
self.subchild.setIcon(0, icon)
self.child.addChild(self.subchild)
# adds new widget to scroll area only for characteristics
if permissions is not None:
# send full item text to add char widget because it needs the name and uuid
self.add_char_widget(item, permissions)
def mousePressEvent(self, event):
# SET DRAG POS WINDOW
pass
#print('Mouse click: RIGHT CLICK')
def mouseMoveEvent(self, event):
# Get the mouse cursor position in scene coordinates
pass
def extract_uuid_name(self, data):
char_uuid = self.extract_uuid_hex(data)
found_match = False
with open("user_UUIDs.json", "r") as f:
json_data = json.load(f)
for uuid_type in [ble_uuid.service, ble_uuid.characteristic, ble_uuid.descriptor]:
try:
temp = uuid_type[UUID(char_uuid)]
if temp != char_uuid:
data = temp
found_match = True
break # Exit the loop if a match is found
except Exception as e:
pass
#print(f"Exception occurred: {e}") # Debug print
if not found_match:
search_uuid = char_uuid
if search_uuid in json_data:
data = json_data[search_uuid]
found_match = True
if not found_match:
# 00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile
# extract the UUID from the string which is this 00001801-0000-1000-8000-00805f9b34fb
#if not match is found for UUID then make char name the UUID
data = self.extract_uuid_hex(data)
return data
def extract_uuid_hex(self, data):
# data[0] looks like this "00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile"
# extract the UUID from the string which is this "00001801-0000-1000-8000-00805f9b34fb"
raw_uuid = data.split("(")[0].strip()
return raw_uuid
def extract_handle(self, data):
# data[0] looks like this "00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile"
# extract the handle value, not handle text, from the string which is this "16"
handle_value = data.split("(")[1].split(")")[0].split(":")[1].strip()
return handle_value
def gatt_tree_view_clicked(self,tree_item, column):
# when user clicks on a tree item, check if it exists in char_dict
# if it does exist then scroll to that widget
# uuid = self.extract_uuid_hex(tree_item.text(column))
for key, value in self.char_dict.items():
# check if text exist in char_name or key itself
if tree_item.text(column) in value['char name'] or tree_item.text(column) in key:
self.ui.scrollArea_2.ensureWidgetVisible(self.char_dict[key]["widgetlocation"])
def add_char_widget(self, char_uuid, permissions):
"""
Adds a new widget representing a Bluetooth Low Energy (BLE) characteristic to the main scroll area.
This widget is loaded from a generic compiled UI file (`char.py`) and displays buttons and labels corresponding
to different characteristic properties like 'write', 'read', 'notify', etc. It also sets up necessary UI elements
and hooks for handling interactions with those properties.
Parameters:
- char_uuid (str): The UUID of the BLE characteristic to be displayed. This string must be parsed
it comes in looking like this: 00001801-0000-1000-8000-00805f9b34fb (Handle: 16): Generic Attribute Profile
- permissions (list): List of permissions associated with the characteristic.
Possible values include 'write-without-response', 'write', 'notify', 'read', and 'indicate'.
Notes:
1. The method registers callbacks for different characteristic properties like 'write', 'read', etc.
based on the permissions list.
2. Updates the main scroll area to include this newly created widget.
3. Enabled/Disables different UI elements based on the permissions list.
4. Stores a reference to this widget in 'char_dict' for future interactions.
"""
# Add widget to Main Scroll Area
scroll = QScrollArea() # Scroll Area which contains the widgets, set as the centralWidget
widget = QWidget() # Widget that contains the collection of Vertical Box
tempWidget = QtWidgets.QWidget()
uiwidget = Ui_char_widget()
tempWidget.setMinimumHeight(500)
uiwidget.setupUi(tempWidget)
# At this point we can access ui elements of the new char widget, we also store a reference to it in service_dict
char_name = self.extract_uuid_name(char_uuid)
if char_name == char_uuid:
char_name = "Unknown"
uiwidget.characteristic_name_lbl.setText(f"characteristic : {char_name}")
uiwidget.uuid_lbl.setText(f"UUID : {self.extract_uuid_hex(char_uuid)}")
uiwidget.handle_lbl.setText(f"Handle : {self.extract_handle(char_uuid)}")
#check if both write and write without response are in permissions list
if "write-without-response" in permissions and "write" in permissions:
uiwidget.write_no_resp_toggle.setVisible(True)
uiwidget.write_no_resp_lbl.setVisible(True)
# set the toggle button to visible and only register one callback for both
uiwidget.char_write_btn.clicked.connect(lambda state : self.char_write_btn_handler(self.extract_uuid_hex(char_uuid),True))
else: #check if at least one of them is in permissions list
# check if permissions list ['write-without-response', 'write', 'notify' , 'read' ,indicate] adn enable disable buttons with same name
uiwidget.write_no_resp_toggle.setVisible(False)
uiwidget.write_no_resp_lbl.setVisible(False)
if "write-without-response" in permissions:
uiwidget.char_write_btn.clicked.connect(lambda state : self.char_write_btn_handler(self.extract_uuid_hex(char_uuid), False))
pass
else:
uiwidget.permission_write_wo_resp.setEnabled(False)
#change background color of permissons label
uiwidget.permission_write_wo_resp.setStyleSheet("background-color: rgb(52, 59, 72);color:rgb(205,205,205);padding:5px;border-radius: 12px;")
if "write" in permissions: # this is write with response
uiwidget.char_write_btn.clicked.connect(lambda state : self.char_write_btn_handler(self.extract_uuid_hex(char_uuid),True))
pass
else:
uiwidget.char_write_txt.setMaximumWidth(0)
uiwidget.char_write_txt.setMinimumWidth(0)
uiwidget.char_write_btn.setMaximumWidth(0)
uiwidget.char_write_btn.setMinimumWidth(0)
#change background color of permissons label
uiwidget.permission_write.setStyleSheet("background-color: rgb(52, 59, 72);color:rgb(205,205,205);padding:5px;border-radius: 12px;")
if "notify" in permissions:
# regiter callback for notification toggle "notify_toggle" state change in uiwidget
uiwidget.notify_toggle.stateChanged.connect(lambda state : self.notify_toggle_handler(self.extract_uuid_hex(char_uuid),state))
pass
else:
uiwidget.notify_toggle.setEnabled(False)
#change background color of permissons label
uiwidget.permission_notify.setStyleSheet("background-color: rgb(52, 59, 72);color:rgb(205,205,205);padding:5px;border-radius: 12px;")
#make invisible
#uiwidget.permission_notify.setVisible(False)
if "read" in permissions:
# regiter callback for read button
uiwidget.char_read_btn.clicked.connect(lambda state : self.char_read_btn_handler(self.extract_uuid_hex(char_uuid)))
pass
else:
uiwidget.char_read_btn.setMaximumWidth(0)
uiwidget.char_read_btn.setMinimumWidth(0)
#change background color of permissons label
uiwidget.permission_read.setStyleSheet("background-color: rgb(52, 59, 72);color:rgb(205,205,205);padding:5px;border-radius: 12px;")
if "indicate" in permissions:
# regiter callback for indications
pass
else:
uiwidget.permission_indicate.setEnabled(False)
#change background color to light gray
uiwidget.permission_indicate.setStyleSheet("background-color: rgb(52, 59, 72);color:rgb(205,205,205);padding:5px;border-radius: 12px;")
#make invisible
#uiwidget.permission_indicate.setVisible(False)
# if no read and no write or write without response then hide read_write_frame
if "read" not in permissions and "write" not in permissions and "write-without-response" not in permissions:
uiwidget.read_write_frame.setMaximumHeight(0)
uiwidget.read_write_frame.setMinimumHeight(0)
widget.setLayout(self.chars_vbox)
widget.setStyleSheet("""
border: 0px solid rgb(52, 59, 72);
border-radius: 5px;
margin: 0px;
padding: 0px;""")
# add to vertical layout row,column
self.chars_vbox.addWidget(tempWidget,self.charCount,0)
self.chars_vbox.setSpacing(10)
self.chars_vbox.setContentsMargins(QMargins(20, 0, 0, 0))
self.charCount += 1
self.ui.scrollArea_2.setStyleSheet("""
border: 0px solid rgb(52, 59, 72);
border-radius: 0px;
margin: 0px;
padding: 0px;""")
self.ui.scrollArea_2.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.ui.scrollArea_2.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.ui.scrollArea_2.setWidgetResizable(True)
self.ui.scrollArea_2.setWidget(widget)
# Storing widgetsin dictionary for future reference, for accessing elements of the widget
# and removing it from the scroll area
uuid_raw = self.extract_uuid_hex(char_uuid)
self.char_dict[uuid_raw] = {
"char name":char_name,
"uiWidget":uiwidget,
"widgetlocation":tempWidget,
"permissions":permissions}
def stacked_widget_show_connected(self):
# change stacked widget to connections page
self.ui.btn_gatt_explorer.click()
def char_write_btn_handler(self, UUID , resp : bool = False ):
data_to_write = self.char_dict[UUID]["uiWidget"].char_write_txt.toPlainText()
# get this widget from char_dict
# check if write with response or write without response by checking toggle state
# only if the toggle button is visible otherwise it is disabled, dont override rap
if self.char_dict[UUID]["uiWidget"].write_no_resp_toggle.isVisible():
if self.char_dict[UUID]["uiWidget"].write_no_resp_toggle.isChecked():
resp = False
else:
resp = True
self.connectedDevice.device_char_write.emit(UUID,data_to_write,resp,False)
def char_read_btn_handler(self, UUID):
# get this widget from char_dict
self.connectedDevice.device_char_read.emit(UUID)
def notify_toggle_handler(self, UUID, state):
self.connectedDevice.device_char_notify.emit(UUID,state)
def char_notification_handler(self, uuid, payload):
char_uuid = self.extract_uuid_hex(uuid)
# find uuid in char dict and update the uiwidget, append text to char_read_txt
self.char_dict[char_uuid]["uiWidget"].char_read_txt.append(payload)
def char_read_response_handler(self, uuid, payload):
char_uuid = self.extract_uuid_hex(uuid)
# find uuid in char dict and update the uiwidget, append text to char_read_txt
self.char_dict[char_uuid]["uiWidget"].char_read_txt.append(payload)
def buttonClick(self):
# GET BUTTON CLICKED
btn = self.sender()
btnName = btn.objectName()
# SHOW HOME PAGE
if btnName == "btn_home":
self.ui.stackedWidget.setCurrentWidget(self.ui.home)
UIFunctions.resetStyle(self, btnName)
btn.setStyleSheet(UIFunctions.selectMenu(btn.styleSheet()))
# hide elfSettings frame
self.ui.ota_frame.hide()
self.ui.ota_frame.setMaximumHeight(0)
self.ui.elfSettings.hide()
self.ui.elfSettings.setMaximumHeight(0)
self.ui.scannerSettigns.setMaximumHeight(1000000)
self.ui.scannerSettigns.show()
# SHOW WIDGETS PAGE
if btnName == "btn_widgets":
self.ui.stackedWidget.setCurrentWidget(self.ui.widgets)
UIFunctions.resetStyle(self, btnName)
btn.setStyleSheet(UIFunctions.selectMenu(btn.styleSheet()))
# SHOW NEW PAGE
if btnName == "btn_gatt_explorer":
self.ui.stackedWidget.setCurrentWidget(self.ui.connections_page) # SET PAGE
UIFunctions.resetStyle(self, btnName) # RESET ANOTHERS BUTTONS SELECTED
btn.setStyleSheet(UIFunctions.selectMenu(btn.styleSheet())) # SELECT MENU
self.ui.elfSettings.hide()
self.ui.elfSettings.setMaximumHeight(0)
self.ui.scannerSettigns.setMaximumHeight(0)
self.ui.scannerSettigns.hide()
self.ui.ota_frame.show()
self.ui.ota_frame.setMaximumHeight(1000000)
if btnName == "btn_save":
pass
if btnName == "btn_insights":
self.ui.stackedWidget.setCurrentWidget(self.ui.insights)
UIFunctions.resetStyle(self, btnName) # RESET ANOTHERS BUTTONS SELECTED
btn.setStyleSheet(UIFunctions.selectMenu(btn.styleSheet())) # SELECT MENU
self.ui.scannerSettigns.hide()
self.ui.scannerSettigns.setMaximumHeight(0)
self.ui.ota_frame.hide()
self.ui.ota_frame.setMaximumHeight(0)
self.ui.elfSettings.setMaximumHeight(1000000)
self.ui.elfSettings.show()
# PRINT BTN NAME
#print(f'Button "{btnName}" pressed!')
def resizeEvent(self, event):
# Update Size Grips
UIFunctions.resize_grips(self)
def otas_progress_update(self,value):
self.ui.otasProgress.setValue(value)
def ota_reset(self):
self.fileName = None
self.fileLen = None
self.fileCrc32 = None
#------------------------ clean up fuctions ------------------------
def clean_up(self):
try:
container = self.ui.scrollArea_2.widget()
layout = container.layout()
if layout is not None:
for i in reversed(range(layout.count())):
widget_to_remove = layout.itemAt(i).widget()
# remove it from the layout list
layout.removeWidget(widget_to_remove)
# remove it from the gui
widget_to_remove.setParent(None)
except:
pass
# Clear the dictionary storing widget references
self.char_dict.clear()
# Clear treeview if required
self.ui.gatt_treeView.clear()
# Add any other cleanup operations here
def closeEvent(self, event):
# Kill on going threads
self.update_rssi_thread.GraphActive = False # Request the thread to stop
self.update_rssi_thread.quit() # Request the thread to stop
self.update_rssi_thread.wait() # Wait until the thread has actually stopped
self.stop_graphing()
self.stop_scanner()
self.stop_connection()
event.accept() # Accept the close event and let the window close
def stop_scanner(self):
self.ui.btn_scan.setText("Scan")
#self.ui.btn_scan.setStyleSheet("background-color: rgba(33, 37, 43, 180); border: 4px solid rgb(255, 59, 72);border-radius: 5px;")
self.ui.btn_scan.setStyleSheet(self.btn_stylesheet)
# self.ui.btn_scan.setStyleSheet("")
self.bleScanner.is_scanning = False
self.bleScanner.quit()
self.bleScanner.wait()
self.stop_graphing()
def stop_connection(self):
self.ui.btn_connect.setText("Connect")
self.ui.btn_disconnect.setText("Disconnect")
self.ui.btn_connect.setStyleSheet(self.btn_stylesheet)
self.connectedDevice.is_connected = False
self.connectedDevice.ble_address = None
self.connectedDevice.quit()
self.connectedDevice.wait()
self.ui.btn_connect.setEnabled(True)
def stop_graphing(self):
self.update_rssi_thread.GraphActive = False # Request the thread to stop
self.update_rssi_thread.quit() # Request the thread to stop
self.update_rssi_thread.wait() # Wait until the thread has actually stopped
if __name__ == "__main__":
app = QApplication(sys.argv)
# app.setWindowIcon(QIcon("icon.ico"))
window = MainWindow()
sys.exit(app.exec())