IDRB/Client/client.py
damp11113 1e77eae2d8 update 1.6.2
this update is update on server side
new
Buffer

client side
fix buffer
2024-02-27 20:48:19 +07:00

668 lines
31 KiB
Python

"""
This file is part of IDRB Project.
IDRB Project is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
IDRB Project is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with IDRB Project. If not, see <https://www.gnu.org/licenses/>.
"""
import queue
import time
from datetime import datetime
import cv2
import dearpygui.dearpygui as dpg
import threading
import socket
import requests
import pickle
import pyaudio
import zmq
from pyogg import OpusDecoder
import configparser
import ctypes
import zlib
from utils import *
import appcomponent
class App:
def __init__(self):
self.RDS = None
self.config = configparser.ConfigParser()
self.config.read("config.ini")
self.device_name_output = self.config["audio"]["device"]
self.buffersize = self.config["network"]["buffersize"]
self.working = False
self.readchannel = 1
self.firstrun = True
self.firststart = True
self.device_index_output = 0
self.ccdecryptpassword = None
self.ccisencrypt = None
self.ccisdecrypt = None
self.ccisdecryptpassword = None
self.paudio = pyaudio.PyAudio()
self.cprotocol = None
self.cciswaitlogoim = True
self.ccthreadlogorecisworking = False
self.ccserversecount = 0
self.lsitem = None
self.ccconwithpubselect = False
self.buffer = queue.Queue(maxsize=self.buffersize)
def connecttoserverwithpubselect(self, sender, data):
self.ccconwithpubselect = True
dpg.configure_item("pubserverselectwindow", show=False)
dpg.configure_item("connectbuttonpubserverselect", show=False)
self.connecttoserver(None, None)
def connecttoserver(self, sender, data):
dpg.configure_item("connectservergroup", show=False)
dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0))
if self.ccconwithpubselect:
serverlabel = str(dpg.get_item_configuration(self.lsitem)["label"])
protocol = serverlabel.split("|")[0].strip()
ip = serverlabel.split("|")[1].split(":")[0].strip()
port = serverlabel.split("|")[1].split(":")[1].strip()
self.ccconwithpubselect = False
else:
protocol = dpg.get_value("serverprotocol").strip()
ip = dpg.get_value("serverip").strip()
port = dpg.get_value("serverport")
self.cprotocol = protocol
if protocol == "TCP":
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
except:
dpg.configure_item("connectbutton", show=True)
elif protocol == "ZeroMQ":
context = zmq.Context()
s = context.socket(zmq.SUB)
s.connect(f"tcp://{ip}:{port}")
s.setsockopt_string(zmq.SUBSCRIBE, "")
elif protocol == "ZeroMQ (WS)":
context = zmq.Context()
s = context.socket(zmq.SUB)
s.connect(f"ws://{ip}:{port}")
s.setsockopt_string(zmq.SUBSCRIBE, "")
self.cprotocol = "ZeroMQ"
self.working = True
self.device_index_output = 0
for i in range(self.paudio.get_device_count()):
dev = self.paudio.get_device_info_by_index(i)
if dev['name'] == self.device_name_output:
self.device_index_output = dev['index']
break
thread = threading.Thread(target=self.stream, args=(s, ))
thread.start()
def disconnectserver(self, sender=None, data=None):
dpg.configure_item("disconnectbutton", show=False)
dpg.configure_item("serverstatus", default_value='disconnecting...', color=(255, 255, 0))
self.working = False
dpg.configure_item("serverinfobutton", show=False)
dpg.configure_item("mediachannelselect", show=False)
dpg.configure_item("morerdsbutton", show=False)
dpg.configure_item("station_logo_config", show=False)
dpg.configure_item("RDSinfo", show=False)
dpg.configure_item("disconnectbutton", show=False)
dpg.configure_item("logostatus", show=False)
self.firstrun = True
self.firststart = True
self.ccdecryptpassword = None
self.ccisencrypt = None
self.ccisdecrypt = None
self.ccisdecryptpassword = None
self.cciswaitlogoim = True
self.ccthreadlogorecisworking = False
# clear buffer
def RDSshow(self):
try:
dpg.configure_item("RDSinfo",
default_value=f'{self.RDS["PS"]} ({self.RDS["ContentInfo"]["Codec"].upper()} {self.RDS["ContentInfo"]["bitrate"] / 1000}Kbps {self.RDS["AudioMode"]})',
show=True)
dpg.configure_item("RDSPS", default_value="PS: " + self.RDS["PS"])
dpg.configure_item("RDSRT", default_value="RT: " + limit_string_in_line(self.RDS["RT"], 120))
dpg.configure_item("RDSCTlocal", default_value="Time Local: " + datetime.fromtimestamp(self.RDS["CT"]["Local"]).strftime('%H:%M:%S'))
dpg.configure_item("RDSCTUTC", default_value="Time UTC: " + datetime.fromtimestamp(self.RDS["CT"]["UTC"]).strftime('%H:%M:%S'))
try:
if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking:
if not self.RDS["images"]["logo"]["contents"] == b'':
print(self.RDS["images"]["logo"]["contents"] == b'',
self.RDS["images"]["logo"]["part"]["total"] == \
self.RDS["images"]["logo"]["part"]["current"],
self.RDS["images"]["logo"]["part"]["current"] > 0)
self.ccthreadlogorecisworking = True
logoreciveprocessingthread = threading.Thread(target=self.RDSlogorecivelazy)
logoreciveprocessingthread.start()
else:
if not self.RDS["images"]["logo"]["lazy"]:
dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8), cv2.IMREAD_COLOR)))
dpg.configure_item("logostatus", show=False)
except Exception as e:
dpg.configure_item("station_logo_config", show=False)
#print(e)
except Exception as e:
pass
def RDSlogorecivelazy(self):
try:
received_data = b""
received_data_current_past = b""
received_data_current = b""
try:
print(self.RDS["images"]["logo"]["part"]["current"], self.RDS["images"]["logo"]["part"]["total"])
while not self.RDS["images"]["logo"]["part"]["current"] == self.RDS["images"]["logo"]["part"]["total"]:
currentprocess = self.RDS["images"]["logo"]["part"]["current"]
totalprocess = self.RDS["images"]["logo"]["part"]["total"]
received_data_current = self.RDS["images"]["logo"]["contents"]
if received_data_current != received_data_current_past:
print(received_data_current)
received_data_current_past = received_data_current
received_data += received_data_current
dpg.configure_item("logostatus", color=(255, 255, 0), default_value=f"Receiving... ({currentprocess}/{totalprocess})")
dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(received_data_current, np.uint8), cv2.IMREAD_COLOR)))
dpg.configure_item("logostatus", color=(0, 255, 0), default_value=f"Received logo! waiting for new image...")
dpg.configure_item("station_logo_config", show=True)
self.ccthreadlogorecisworking = False
except Exception as e:
print("receive error", e)
dpg.configure_item("logostatus", color=(255, 0, 0), default_value=f"Receive logo error! waiting for new image...")
dpg.configure_item("station_logo_config", show=False)
self.ccthreadlogorecisworking = False
except Exception as e:
pass
def changechannel(self, sender, data):
dpg.configure_item("serverstatus", default_value='please wait...', color=(255, 255, 0))
dpg.configure_item("station_logo_config", show=False)
self.readchannel = int(dpg.get_value(sender).split(" ")[0])
self.firstrun = True
self.ccdecryptpassword = None
self.ccisencrypt = None
self.ccisdecrypt = None
self.ccisdecryptpassword = None
self.device_index_output = 0
for i in range(self.paudio.get_device_count()):
dev = self.paudio.get_device_info_by_index(i)
if dev['name'] == self.device_name_output:
self.device_index_output = dev['index']
break
def submitpassworddecrypt(self, sender, data):
dpg.configure_item("requestpasswordpopup", show=False)
self.ccdecryptpassword = dpg.get_value("requestpasswordinputpopup")
self.ccisdecryptpassword = True
def changeaudiodevice(self, sender, data):
self.device_name_output = dpg.get_value(sender)
self.config["audio"]["device"] = dpg.get_value(sender)
self.config.write(open('config.ini', 'w'))
def changebuffersize(self, sender, data):
self.buffersize = int(dpg.get_value(sender))
self.config["network"]["buffersize"] = str(dpg.get_value(sender))
self.config.write(open('config.ini', 'w'))
def pubserverselectone(self, sender, data):
if data == False:
dpg.configure_item("connectbuttonpubserverselect", show=False)
return
else:
dpg.configure_item("connectbuttonpubserverselect", show=True)
if self.lsitem == None:
self.lsitem = sender
if self.lsitem != sender:
dpg.set_value(self.lsitem, False)
self.lsitem = sender
if dpg.get_item_configuration(self.lsitem)["label"] == "N/A":
dpg.configure_item("connectbuttonpubserverselect", show=False)
def pubserverselectsearch(self, serversearch="", limit=10):
self.lsitem = None
if serversearch == "":
dpg.configure_item("pubserverselectstatus", default_value="Please wait...", color=(255, 255, 0))
else:
dpg.configure_item("pubserverselectstatus", default_value="Searching...", color=(255, 255, 0))
if self.ccserversecount != 0:
# clear list
for i in range(self.ccserversecount):
dpg.delete_item(f"pubserverid{i}")
self.ccserversecount = 0
if serversearch == "":
response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}")
else:
response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}&serversearch={serversearch}")
# Check if the request was successful (status code 200)
if response.status_code == 200:
# Parse JSON data
allstationdata = response.json()
# Iterate over each station
for server_id, server_details in allstationdata.items():
with dpg.table_row(parent="pubserverlist", tag=f"pubserverid{self.ccserversecount}"):
if server_details['ServerURL'] == "" or server_details['ServerPort'] == "" or server_details['ServerProtocol'] == "":
dpg.add_selectable(label=f"N/A", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone)
else:
dpg.add_selectable(label=f"{server_details['ServerProtocol']} | {server_details['ServerURL']}:{server_details['ServerPort']}", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone)
dpg.add_selectable(label=server_details["ServerName"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone)
dpg.add_selectable(label=server_details["ServerDesc"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone)
dpg.add_selectable(label=server_details["ConnectionUser"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone)
self.ccserversecount += 1
dpg.configure_item("pubserverselectstatus", default_value=f"Founded {self.ccserversecount} server", color=(0, 255, 0))
def pubserverselectopen(self, sender, data):
dpg.configure_item("pubserverselectwindow", show=True)
self.pubserverselectsearch()
def streambuffer(self, socket):
while self.working:
try:
if self.cprotocol == "TCP":
tempdata = b''
# data = socket.recv(1580152)
while True:
part = socket.recv(1024)
tempdata += part
if len(part) < 1024:
# either 0 or end of data
break
self.buffer.put(tempdata, timeout=0.1)
elif self.cprotocol == "ZeroMQ":
self.buffer.put(socket.recv(), timeout=0.1)
else:
self.buffer.put(b"")
dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(0, 255, 0))
except queue.Full:
dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(255, 0, 0))
while not self.buffer.empty():
self.buffer.get()
dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(255, 255, 0))
dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0))
dpg.configure_item("connectservergroup", show=True)
def stream(self, socket):
opus_decoder = None
streamoutput = None
tfrpx = list(range(250))
altfrpy = [0] * 250
adcctfrpy = [0] * 250
imcctfrpy = [0] * 2500
bytesconunt = 0
bytesconunt_frame = 0
codecbytesconunt = 0
start_time = time.time()
evaluation_audio_X = None
decodecodec = None
SBT = threading.Thread(target=self.streambuffer, args=(socket,))
SBT.start()
while True:
try:
if self.working:
if self.buffer.not_empty:
data = self.buffer.get()
else:
dpg.configure_item("serverstatus", default_value='Buffering...', color=(255, 255, 0))
continue
bytesconunt += len(data)
if bytesconunt_frame >= 10:
stoptime = time.time()
speed_kbps = calculate_speed(start_time, stoptime, bytesconunt)
dpg.configure_item("serverstatus", default_value=f'connected {int(speed_kbps)}Kbps ({len(data)})', color=(0, 255, 0))
codec_kbps = calculate_throughput(start_time, stoptime, codecbytesconunt)
dpg.configure_item("codecbitratestatus", default_value=f'{self.RDS["ContentInfo"]["Codec"].upper()} {self.RDS["ContentInfo"]["bitrate"] / 1000}/{codec_kbps:.2f} Kbps')
dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}')
start_time = time.time()
bytesconunt_frame = 0
bytesconunt = 0
codecbytesconunt = 0
if len(altfrpy) > 250:
altfrpy.pop(0)
altfrpy.append(len(data))
dpg.set_value('transferatealldataplot', [tfrpx, altfrpy])
if len(data) == 0:
dpg.configure_item("serverstatus", default_value='lost connected', color=(255, 0, 0))
socket.close()
if self.cprotocol == "TCP":
socket.close()
elif self.cprotocol == "ZeroMQ":
try:
message = socket.recv(zmq.NOBLOCK)
if message is None:
break # No more messages
# Process the received message if needed
print(f"Received message: {message.decode()}")
except zmq.error.ZMQError as e:
if e.errno == zmq.EAGAIN:
break # No more messages
else:
raise
socket.close()
else:
socket.close()
self.disconnectserver()
break
try:
decompressed_data = zlib.decompress(data)
datadecoded = pickle.loads(decompressed_data)
except:
pass
if len(imcctfrpy) > 250:
imcctfrpy.pop(0)
imcctfrpy.append(len(str(datadecoded["channel"][self.readchannel]["RDS"]["images"])))
dpg.set_value('transferateimagesoncchannelplot', [tfrpx, imcctfrpy])
try:
if datadecoded["channel"][self.readchannel]["RDS"] != self.RDS:
self.RDS = datadecoded["channel"][self.readchannel]["RDS"]
rdshow = threading.Thread(target=self.RDSshow)
rdshow.start()
dpg.configure_item("ServerListener", default_value="Listener: " + str(datadecoded["serverinfo"]["Listener"]) + " Users")
dpg.configure_item("ServerNamedisp", default_value="Server: " + str(datadecoded["serverinfo"]["RDS"]["ServerName"]))
dpg.configure_item("ServerDescdisp", default_value="Description: " + str(datadecoded["serverinfo"]["RDS"]["ServerDesc"]))
except:
pass
if self.firstrun:
decodecodec = datadecoded["channel"][self.readchannel]["RDS"]["ContentInfo"]["Codec"]
if decodecodec.upper() == "OPUS":
opus_decoder = OpusDecoder()
opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"])
opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"])
streamoutput = self.paudio.open(format=pyaudio.paInt16, channels=self.RDS["ContentInfo"]["channel"], rate=self.RDS["ContentInfo"]["samplerates"], output=True, output_device_index=self.device_index_output)
evaluation_audio_X = np.fft.fftfreq(1024, 1.0 / self.RDS["ContentInfo"]["samplerates"])[:1024 // 2]
if len(datadecoded["channel"]) > 1:
channel_info = []
for i in range(1, len(datadecoded["channel"]) + 1):
channel_info.append(f'{i} {"[Encrypt]" if datadecoded["channel"][i]["Encrypt"] else "[No Encrypt]"} {datadecoded["channel"][i]["Station"]} ({datadecoded["channel"][i]["RDS"]["ContentInfo"]["Codec"]} {datadecoded["channel"][i]["RDS"]["ContentInfo"]["bitrate"] / 1000}Kbps {datadecoded["channel"][i]["RDS"]["AudioMode"]})')
dpg.configure_item("mediachannelselect", show=True, items=channel_info)
dpg.configure_item("morerdsbutton", show=True)
dpg.configure_item("serverinfobutton", show=True)
dpg.configure_item("logostatus", show=True)
try:
if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking:
if not self.RDS["images"]["logo"]["contents"] == b'' or \
self.RDS["images"]["logo"]["part"]["total"] == \
self.RDS["images"]["logo"]["part"]["current"] or \
self.RDS["images"]["logo"]["part"]["current"] > 0:
self.ccthreadlogorecisworking = True
logoreciveprocessingthread = threading.Thread(target=self.RDSlogorecivelazy)
logoreciveprocessingthread.start()
else:
if not self.RDS["images"]["logo"]["lazy"]:
dpg.set_value("station_logo", CV22DPG(
cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8),
cv2.IMREAD_COLOR)))
dpg.configure_item("station_logo_config", show=True)
dpg.configure_item("logostatus", show=False)
except:
dpg.configure_item("station_logo_config", show=False)
dpg.configure_item("disconnectbutton", show=True)
dpg.configure_item("RDSPI", default_value=f"PI: {hex(self.RDS['PI'])[2:].upper()}")
if self.firststart and len(datadecoded["channel"]) > 1:
self.readchannel = datadecoded["mainchannel"]
dpg.configure_item("mediachannelselect", show=True, default_value=channel_info[self.readchannel - 1])
elif self.firststart:
self.readchannel = datadecoded["mainchannel"]
# check if channel is encrypted
if datadecoded["channel"][self.readchannel]["Encrypt"]:
dpg.configure_item("requestpasswordpopup", show=True)
dpg.configure_item("serverstatus", default_value='connected', color=(0, 255, 0))
self.ccisencrypt = True
else:
dpg.configure_item("serverstatus", default_value='connected --Kbps (----)', color=(0, 255, 0))
self.firstrun = False
self.firststart = False
if not self.firstrun:
data = datadecoded["channel"][self.readchannel]["Content"]
if len(data) == 0:
dpg.configure_item("serverstatus", default_value=f'connected but no audio', color=(255, 0, 0))
if self.ccisdecryptpassword and self.ccisencrypt:
try:
# decrypt data
encryptdata = data.split(b'|||||')[0]
salt = data.split(b'|||||')[1]
iv = data.split(b'|||||')[2]
data = decrypt_data(encryptdata, self.ccdecryptpassword, salt, iv)
if data == b'':
self.ccisdecrypt = False
self.ccdecryptpassword = None
else:
self.ccisdecrypt = True
except:
dpg.configure_item("serverstatus", default_value="Decrypt Error", color=(255, 0, 0))
if self.ccisdecrypt or not self.ccisencrypt:
codecbytesconunt += len(data)
if decodecodec.upper() == "OPUS":
decoded_pcm = opus_decoder.decode(memoryview(bytearray(data)))
else: # pcm
decoded_pcm = data
# Check if the decoded PCM is empty or not
if len(decoded_pcm) > 0:
pcm_to_write = np.frombuffer(decoded_pcm, dtype=np.int16)
streamoutput.write(pcm_to_write.tobytes())
audioL = pcm_to_write[::2]
audioR = pcm_to_write[1::2]
Lnormalized_data = audioL * np.hanning(len(audioL))
Lfft_data = np.abs(np.fft.fft(Lnormalized_data))[:1024 // 2]
Rnormalized_data = audioR * np.hanning(len(audioR))
Rfft_data = np.abs(np.fft.fft(Rnormalized_data))[:1024 // 2]
dpg.set_value('audioinfoleftplot', [evaluation_audio_X, Lfft_data])
dpg.set_value('audioinforightplot', [evaluation_audio_X, Rfft_data])
else:
print("Decoded PCM is empty")
if len(adcctfrpy) > 250:
adcctfrpy.pop(0)
adcctfrpy.append(len(datadecoded["channel"][self.readchannel]["Content"]))
dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy])
bytesconunt_frame += 1
else:
SBT.join()
streamoutput.close()
if self.cprotocol == "TCP":
socket.close()
elif self.cprotocol == "ZeroMQ":
try:
message = socket.recv(zmq.NOBLOCK)
if message is None:
break # No more messages
# Process the received message if needed
print(f"Received message: {message.decode()}")
except zmq.error.ZMQError as e:
if e.errno == zmq.EAGAIN:
break # No more messages
else:
raise
socket.close()
else:
socket.close()
self.disconnectserver()
break
except Exception as e:
if str(e) == "An error occurred while decoding an Opus-encoded packet: corrupted stream":
dpg.configure_item("serverstatus", default_value="Unable to decode audio data", color=(255, 0, 0))
else:
print("connection lost", e)
try:
streamoutput.close()
except:
pass
socket.close()
if self.cprotocol == "TCP":
socket.close()
elif self.cprotocol == "ZeroMQ":
try:
message = socket.recv(zmq.NOBLOCK)
if message is None:
break # No more messages
# Process the received message if needed
print(f"Received message: {message.decode()}")
except zmq.error.ZMQError as e:
if e.errno == zmq.EAGAIN:
break # No more messages
else:
raise
socket.close()
else:
socket.close()
self.disconnectserver()
break
def init(self):
if self.config["debug"]["hideconsole"] == "true":
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
ctypes.CDLL("opus.dll")
dpg.create_context()
dpg.create_viewport(title=f'IDRB Client v1.6.2 Beta', width=1280, height=720, large_icon="IDRBfavicon.ico", clear_color=(43, 45, 48)) # set viewport window
dpg.setup_dearpygui()
# -------------- add code here --------------
noimage_texture_data = []
for i in range(0, 128 * 128):
noimage_texture_data.append(20 / 255)
noimage_texture_data.append(0)
noimage_texture_data.append(20 / 255)
noimage_texture_data.append(20 / 255)
with dpg.texture_registry():
dpg.add_raw_texture(128, 128, noimage_texture_data, tag="station_logo", format=dpg.mvFormat_Float_rgb)
width, height, channels, data = dpg.load_image("IDRBlogo.png")
dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo")
dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo_background")
with dpg.window(no_background=True, no_title_bar=True, no_move=True, no_resize=True, tag="backgroundviewportlogo"):
dpg.add_image("app_logo_background")
dpg.add_text("ThaiSDR Solutions", pos=(230, 230))
appcomponent.window(self)
appcomponent.menubar(self)
num_devices = self.paudio.get_device_count()
output_devices = []
for i in range(num_devices):
device_info = self.paudio.get_device_info_by_index(i)
if device_info['maxOutputChannels'] > 0:
output_devices.append(device_info['name'])
dpg.configure_item("selectaudiooutputdevicecombo", items=output_devices, default_value=self.config["audio"]["device"])
dpg.configure_item("buffersizeintinput", default_value=int(self.config["network"]["buffersize"]))
# -------------------------------------------
dpg.show_viewport()
# Start a separate thread for a task
self.thread_stop_event = threading.Event()
while dpg.is_dearpygui_running():
self.render()
dpg.render_dearpygui_frame()
# Signal the thread to stop and wait for it to finish
self.thread_stop_event.set()
dpg.destroy_context()
def render(self):
# insert here any code you would like to run in the render loop
# you can manually stop by using stop_dearpygui() or self.exit()
try:
dpg.fit_axis_data("x_axis")
dpg.fit_axis_data("y_axis1")
dpg.fit_axis_data("y_axis2")
dpg.fit_axis_data("y_axis3")
dpg.fit_axis_data("x_axis_1")
dpg.configure_item("backgroundviewportlogo", pos=(dpg.get_viewport_width() - 550, dpg.get_viewport_height() - 300))
except:
pass
def exit(self):
dpg.destroy_context()
app = App()
app.init()