This commit is contained in:
RocketGod
2022-09-22 13:46:47 -07:00
parent f65104c2ab
commit e7667c1d93
565 changed files with 165005 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import sys
import traceback
from PyQt5.QtWidgets import QMessageBox, QWidget
from urh.util.Formatter import Formatter
from urh.util.Logger import logger
class Errors:
@staticmethod
def generic_error(title: str, msg: str, detailed_msg: str = None):
w = QWidget()
if detailed_msg:
msg = "Error: <b>" + msg.replace("\n",
"<br>") + "</b>" + "<br><br>----------<br><br>" + detailed_msg.replace(
"\n", "<br>")
QMessageBox.critical(w, title, msg)
@staticmethod
def exception(exception: Exception):
logger.exception(exception)
w = QWidget()
msg = "Error: <b>" + str(exception).replace("\n", "<br>") + "</b><hr>"
msg += traceback.format_exc().replace("\n", "<br>")
QMessageBox.critical(w, "An error occurred", msg)
@staticmethod
def no_device():
w = QWidget()
QMessageBox.critical(w, w.tr("No devices"),
w.tr("You have to choose at least one available "
"device in Edit->Options->Device."))
@staticmethod
def empty_selection():
w = QWidget()
QMessageBox.critical(w, w.tr("No selection"),
w.tr("Your selection is empty!"))
@staticmethod
def write_error(msg):
w = QWidget()
QMessageBox.critical(w, w.tr("Write error"),
w.tr("There was a error writing this file! {0}".format(msg)))
@staticmethod
def usrp_found():
w = QWidget()
QMessageBox.critical(w, w.tr("USRP not found"),
w.tr("USRP could not be found . Is the IP "
"correct?"))
@staticmethod
def hackrf_not_found():
w = QWidget()
if sys.platform == "win32":
msg = "Could not connect to HackRF. Try these solutions:" \
"<br/><br/> 1. Ensure HackRF is plugged in." \
"<br/> 2. <b>Install HackRF USB driver</b> with <a href='http://zadig.akeo.ie/'>Zadig</a>."
else:
msg = "Could not connect to HackRF. Try these solutions:" \
"<br/><br/> 1. Ensure HackRF is plugged in." \
"<br/> 2. Run the command <b>hackrf_info</b> in terminal as root." \
"<br/> 3. If 2. works for you, follow the instructions " \
"<a href='https://github.com/mossmann/hackrf/wiki/FAQ'>here</a>."
QMessageBox.critical(w, w.tr("HackRF not found"),
w.tr(msg))
@staticmethod
def gnuradio_not_installed():
w = QWidget()
QMessageBox.critical(w, w.tr("GNU Radio not found"),
w.tr("You need to install GNU Radio for this "
"feature."))
@staticmethod
def rtlsdr_sdr_driver():
if sys.platform == "win32":
w = QWidget()
QMessageBox.critical(w, w.tr("Could not access RTL-SDR device"),
w.tr("You may need to reinstall the driver with Zadig for 'Composite' device.<br>"
"See <a href='https://github.com/jopohl/urh/issues/389'>here</a> "
"for more information."))
@staticmethod
def empty_group():
w = QWidget()
QMessageBox.critical(w, w.tr("Empty group"),
w.tr("The group may not be empty."))
@staticmethod
def invalid_path(path: str):
w = QWidget()
QMessageBox.critical(w, w.tr("Invalid Path"),
w.tr("The path {0} is invalid.".format(path)))
@staticmethod
def network_sdr_send_is_elsewhere():
w = QWidget()
QMessageBox.information(w, "This feature is elsewhere", "You can send your data with the network SDR by "
"using the button below the generator table.")
@staticmethod
def not_enough_ram_for_sending_precache(memory_size_bytes):
w = QWidget()
if memory_size_bytes:
msg = "Precaching all your modulated data would take <b>{0}B</b> of memory, " \
"which does not fit into your RAM.<br>".format(Formatter.big_value_with_suffix(memory_size_bytes))
else:
msg = ""
msg += "Sending will be done in <b>continuous mode</b>.<br><br>" \
"This means, modulation will be performed live during sending.<br><br>" \
"If you experience problems, " \
"consider sending less messages or upgrade your RAM."
QMessageBox.information(w, w.tr("Entering continuous send mode"), w.tr(msg))

View File

@@ -0,0 +1,247 @@
import os
import shutil
import tarfile
import tempfile
import zipfile
import numpy as np
from PyQt5.QtCore import QDir
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from urh.signalprocessing.IQArray import IQArray
archives = {}
""":type: dict of [str, str]
:param: archives[extracted_filename] = filename"""
RECENT_PATH = QDir.homePath()
SIGNAL_FILE_EXTENSIONS_BY_TYPE = {
np.int8: ".complex16s",
np.uint8: ".complex16u",
np.int16: ".complex32s",
np.uint16: ".complex32u",
np.float32: ".complex",
np.complex64: ".complex"
}
SIGNAL_NAME_FILTERS_BY_TYPE = {
np.int8: "Complex16 signed (*.complex16s *.cs8)",
np.uint8: "Complex16 unsigned (*.complex16u *.cu8)",
np.uint16: "Complex32 unsigned (*.complex32u *.cu16)",
np.int16: "Complex32 signed (*.complex32s *.cs16)",
np.float32: "Complex (*.complex)",
np.complex64: "Complex (*.complex)"
}
EVERYTHING_FILE_FILTER = "All Files (*)"
SIGNAL_NAME_FILTERS = list(sorted(set(SIGNAL_NAME_FILTERS_BY_TYPE.values())))
COMPRESSED_COMPLEX_FILE_FILTER = "Compressed Complex File (*.coco)"
WAV_FILE_FILTER = "Waveform Audio File Format (*.wav *.wave)"
PROTOCOL_FILE_FILTER = "Protocol (*.proto.xml *.proto)"
BINARY_PROTOCOL_FILE_FILTER = "Binary Protocol (*.bin)"
PLAIN_BITS_FILE_FILTER = "Plain Bits (*.txt)"
FUZZING_FILE_FILTER = "Fuzzing Profile (*.fuzz.xml *.fuzz)"
SIMULATOR_FILE_FILTER = "Simulator Profile (*.sim.xml *.sim)"
TAR_FILE_FILTER = "Tar Archive (*.tar *.tar.gz *.tar.bz2)"
ZIP_FILE_FILTER = "Zip Archive (*.zip)"
SUB_FILE_FILTER = "Flipper SubGHz RAW (*.sub)"
def __get__name_filter_for_signals() -> str:
return ";;".join([EVERYTHING_FILE_FILTER] + SIGNAL_NAME_FILTERS + [COMPRESSED_COMPLEX_FILE_FILTER, WAV_FILE_FILTER])
def get_open_dialog(directory_mode=False, parent=None, name_filter="full") -> QFileDialog:
dialog = QFileDialog(parent=parent, directory=RECENT_PATH)
if directory_mode:
dialog.setFileMode(QFileDialog.Directory)
dialog.setWindowTitle("Open Folder")
else:
dialog.setFileMode(QFileDialog.ExistingFiles)
dialog.setWindowTitle("Open Files")
if name_filter == "full":
name_filter = __get__name_filter_for_signals() + ";;" \
+ ";;".join([PROTOCOL_FILE_FILTER, BINARY_PROTOCOL_FILE_FILTER, PLAIN_BITS_FILE_FILTER,
FUZZING_FILE_FILTER, SIMULATOR_FILE_FILTER, TAR_FILE_FILTER, ZIP_FILE_FILTER])
elif name_filter == "signals_only":
name_filter = __get__name_filter_for_signals()
elif name_filter == "proto":
name_filter = ";;".join([PROTOCOL_FILE_FILTER, BINARY_PROTOCOL_FILE_FILTER])
elif name_filter == "fuzz":
name_filter = FUZZING_FILE_FILTER
elif name_filter == "simulator":
name_filter = SIMULATOR_FILE_FILTER
dialog.setNameFilter(name_filter)
return dialog
def ask_save_file_name(initial_name: str, caption="Save signal", selected_name_filter=None):
global RECENT_PATH
if caption == "Save signal":
name_filter = __get__name_filter_for_signals()
elif caption == "Save fuzzing profile":
name_filter = FUZZING_FILE_FILTER
elif caption == "Save encoding":
name_filter = ""
elif caption == "Save simulator profile":
name_filter = SIMULATOR_FILE_FILTER
elif caption == "Export spectrogram":
name_filter = "Frequency Time (*.ft);;Frequency Time Amplitude (*.fta)"
elif caption == "Save protocol":
name_filter = ";;".join([PROTOCOL_FILE_FILTER, BINARY_PROTOCOL_FILE_FILTER])
elif caption == "Export demodulated":
name_filter = ";;".join([WAV_FILE_FILTER, SUB_FILE_FILTER])
else:
name_filter = EVERYTHING_FILE_FILTER
filename = None
dialog = QFileDialog(directory=RECENT_PATH, caption=caption, filter=name_filter)
dialog.setFileMode(QFileDialog.AnyFile)
dialog.setLabelText(QFileDialog.Accept, "Save")
dialog.setAcceptMode(QFileDialog.AcceptSave)
if selected_name_filter is not None:
dialog.selectNameFilter(selected_name_filter)
dialog.selectFile(initial_name)
if dialog.exec():
filename = dialog.selectedFiles()[0]
if filename:
RECENT_PATH = os.path.split(filename)[0]
return filename
def ask_signal_file_name_and_save(signal_name: str, data, sample_rate=1e6, wav_only=False, parent=None) -> str:
if wav_only:
if not signal_name.endswith(".wav") and not signal_name.endswith(".wave"):
signal_name += ".wav"
selected_name_filter = WAV_FILE_FILTER
else:
if not any(signal_name.endswith(e) for e in SIGNAL_NAME_FILTERS_BY_TYPE.values()):
try:
dtype = next(d for d in SIGNAL_FILE_EXTENSIONS_BY_TYPE.keys() if d == data.dtype)
signal_name += SIGNAL_FILE_EXTENSIONS_BY_TYPE[dtype]
selected_name_filter = SIGNAL_NAME_FILTERS_BY_TYPE[dtype]
except StopIteration:
selected_name_filter = None
else:
selected_name_filter = None
filename = ask_save_file_name(signal_name, selected_name_filter=selected_name_filter)
if filename:
try:
save_data(data, filename, sample_rate=sample_rate)
except Exception as e:
QMessageBox.critical(parent, "Error saving signal", e.args[0])
filename = None
else:
filename = None
return filename
def save_data(data, filename: str, sample_rate=1e6, num_channels=2):
if not isinstance(data, IQArray):
data = IQArray(data)
if filename.endswith(".wav"):
data.export_to_wav(filename, num_channels, sample_rate)
elif filename.endswith(".coco"):
data.save_compressed(filename)
elif filename.endswith(".sub"):
data.export_to_sub(filename, 433920000, "FuriHalSubGhzPresetOok650Async")
else:
data.tofile(filename)
if filename in archives.keys():
archive = archives[filename]
if archive.endswith("zip"):
rewrite_zip(archive)
elif archive.endswith("tar") or archive.endswith("bz2") or archive.endswith("gz"):
rewrite_tar(archive)
def save_signal(signal):
save_data(signal.iq_array.data, signal.filename, signal.sample_rate)
def rewrite_zip(zip_name):
tempdir = tempfile.mkdtemp()
try:
temp_name = os.path.join(tempdir, 'new.zip')
files_in_archive = [f for f in archives.keys() if archives[f] == zip_name]
with zipfile.ZipFile(temp_name, 'w') as zip_write:
for filename in files_in_archive:
zip_write.write(filename)
shutil.move(temp_name, zip_name)
finally:
shutil.rmtree(tempdir)
def rewrite_tar(tar_name: str):
tempdir = tempfile.mkdtemp()
compression = ""
if tar_name.endswith("gz"):
compression = "gz"
elif tar_name.endswith("bz2"):
compression = "bz2"
try:
ext = "" if len(compression) == 0 else "." + compression
temp_name = os.path.join(tempdir, 'new.tar' + ext)
files_in_archive = [f for f in archives.keys() if archives[f] == tar_name]
with tarfile.open(temp_name, 'w:' + compression) as tar_write:
for file in files_in_archive:
tar_write.add(file)
shutil.move(temp_name, tar_name)
finally:
shutil.rmtree(tempdir)
def uncompress_archives(file_names, temp_dir):
"""
Extract each archive from the list of filenames.
Normal files stay untouched.
Add all files to the Recent Files.
:type file_names: list of str
:type temp_dir: str
:rtype: list of str
"""
result = []
for filename in file_names:
if filename.endswith(".tar") or filename.endswith(".tar.gz") or filename.endswith(".tar.bz2"):
obj = tarfile.open(filename, "r")
extracted_file_names = []
for j, member in enumerate(obj.getmembers()):
obj.extract(member, temp_dir)
extracted_filename = os.path.join(temp_dir, obj.getnames()[j])
extracted_file_names.append(extracted_filename)
archives[extracted_filename] = filename
result.extend(extracted_file_names[:])
elif filename.endswith(".zip"):
obj = zipfile.ZipFile(filename)
extracted_file_names = []
for j, info in enumerate(obj.infolist()):
obj.extract(info, path=temp_dir)
extracted_filename = os.path.join(temp_dir, obj.namelist()[j])
extracted_file_names.append(extracted_filename)
archives[extracted_filename] = filename
result.extend(extracted_file_names[:])
else:
result.append(filename)
return result
def get_directory():
directory = QFileDialog.getExistingDirectory(None, "Choose Directory", QDir.homePath(),
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)
return directory

View File

@@ -0,0 +1,61 @@
import locale
from urh.util.Logger import logger
class Formatter:
@staticmethod
def local_decimal_seperator():
return locale.localeconv()["decimal_point"]
@staticmethod
def science_time(time_in_seconds: float, decimals=2, append_seconds=True, remove_spaces=False) -> str:
if time_in_seconds < 1e-6:
suffix = "n"
value = time_in_seconds * 1e9
elif time_in_seconds < 1e-3:
suffix = "µ"
value = time_in_seconds * 1e6
elif time_in_seconds < 1:
suffix = "m"
value = time_in_seconds * 1e3
else:
suffix = ""
value = time_in_seconds
result = locale.format_string("%.{0}f ".format(decimals) + suffix, value)
if append_seconds:
result += "s"
if remove_spaces:
result = result.replace(" ", "")
return result
@staticmethod
def big_value_with_suffix(value: float, decimals=3, strip_zeros=True) -> str:
fmt_str = "%.{0:d}f".format(decimals)
suffix = ""
if abs(value) >= 1e9:
suffix = "G"
result = locale.format_string(fmt_str, value / 1e9)
elif abs(value) >= 1e6:
suffix = "M"
result = locale.format_string(fmt_str, value / 1e6)
elif abs(value) >= 1e3:
suffix = "K"
result = locale.format_string(fmt_str, value / 1e3)
else:
result = locale.format_string(fmt_str, value)
if strip_zeros:
result = result.rstrip("0").rstrip(Formatter.local_decimal_seperator())
return result + suffix
@staticmethod
def str2val(str_val, dtype, default=0):
try:
return dtype(str_val)
except (ValueError, TypeError):
logger.warning("The {0} is not a valid {1}, assuming {2}".format(str_val, str(dtype), str(default)))
return default

View File

@@ -0,0 +1,514 @@
import array
import copy
from collections import OrderedDict
from xml.etree import ElementTree as ET
from urh.cythonext import util as c_util
from urh.util import util
class GenericCRC(object):
# https://en.wikipedia.org/wiki/Polynomial_representations_of_cyclic_redundancy_checks
DEFAULT_POLYNOMIALS = OrderedDict([
# x^8 + x^7 + x^6 + x^4 + x^2 + 1
("8_standard", array.array("B", [1,
1, 1, 0, 1, 0, 1, 0, 1])),
# x^16+x^15+x^2+x^0
("16_standard", array.array("B", [1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1])),
# x^16+x^12+x^5+x^0
("16_ccitt", array.array("B", [1,
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])),
# x^16+x^13+x^12+x^11+x^10+x^8+x^6+x^5+x^2+x^0
("16_dnp", array.array("B", [1,
0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1])),
# x^8 + x^2 + x + 1
("8_ccitt", array.array("B", [1,
0, 0, 0, 0, 0, 1, 1, 1]))
])
STANDARD_CHECKSUMS = OrderedDict([
# see method guess_standard_parameters_and_datarange for default parameters
# Links:
# - https://en.wikipedia.org/wiki/Cyclic_redundancy_check
# - http://reveng.sourceforge.net/crc-catalogue/1-15.htm
# - https://crccalc.com/
("CRC8 (default)", dict(polynomial="0xD5")),
("CRC8 CCITT", dict(polynomial="0x07")),
("CRC8 Bluetooth", dict(polynomial="0xA7", ref_in=True, ref_out=True)),
("CRC8 DARC", dict(polynomial="0x39", ref_in=True, ref_out=True)),
("CRC8 NRSC-5", dict(polynomial="0x31", start_value=1)),
("CRC16 (default)", dict(polynomial="0x8005", ref_in=True, ref_out=True)),
("CRC16 CCITT", dict(polynomial="0x1021", ref_in=True, ref_out=True)),
("CRC16 NRSC-5", dict(polynomial="0x080B", start_value=1, ref_in=True, ref_out=True)),
("CRC16 CC1101", dict(polynomial="0x8005", start_value=1)),
("CRC16 CDMA2000", dict(polynomial="0xC867", start_value=1)),
("CRC32 (default)", dict(polynomial="0x04C11DB7", start_value=1, final_xor=1, ref_in=True, ref_out=True)),
])
def __init__(self, polynomial="16_standard", start_value=False, final_xor=False, reverse_polynomial=False,
reverse_all=False, little_endian=False, lsb_first=False):
if isinstance(polynomial, str):
self.caption = polynomial
else:
self.caption = ""
self.polynomial = self.choose_polynomial(polynomial)
self.reverse_polynomial = reverse_polynomial
self.reverse_all = reverse_all
self.little_endian = little_endian
self.lsb_first = lsb_first
self.start_value = self.__read_parameter(start_value)
self.final_xor = self.__read_parameter(final_xor)
self.cache = []
self.__cache_bits = 8
def __read_parameter(self, value):
if isinstance(value, bool) or isinstance(value, int):
return array.array('B', [value] * (self.poly_order - 1))
else:
if len(value) == self.poly_order - 1:
return value
else:
return array.array('B', value[0] * (self.poly_order - 1))
def __eq__(self, other):
if not isinstance(other, GenericCRC):
return False
return all(getattr(self, attrib) == getattr(other, attrib) for attrib in (
"polynomial", "reverse_polynomial", "reverse_all", "little_endian", "lsb_first", "start_value",
"final_xor"))
def __hash__(self):
return hash((self.polynomial.tobytes(), self.reverse_polynomial, self.reverse_all, self.little_endian,
self.lsb_first, self.start_value.tobytes(), self.final_xor.tobytes()))
@property
def poly_order(self):
return len(self.polynomial)
@property
def polynomial_as_bit_str(self) -> str:
return "".join("1" if p else "0" for p in self.polynomial)
@property
def polynomial_as_hex_str(self) -> str:
return util.bit2hex(self.polynomial[1:]) # do not show leading one
@property
def polynomial_to_html(self) -> str:
result = ""
for i in range(self.poly_order):
index = self.poly_order - 1 - i
if self.polynomial[i] > 0:
if index > 1:
result += "x<sup>{0}</sup> + ".format(index)
elif index == 1:
result += "x + "
elif index == 0:
result += "1"
result = result.rstrip(" + ")
return result
def set_polynomial_from_hex(self, hex_str: str):
old = self.polynomial
self.polynomial = array.array("B", [1]) + util.hex2bit(hex_str)
if self.polynomial != old:
self.cache = []
self.__cache_bits = 8
def choose_polynomial(self, polynomial):
if isinstance(polynomial, str):
return self.DEFAULT_POLYNOMIALS[polynomial]
elif isinstance(polynomial, int):
return list(self.DEFAULT_POLYNOMIALS.items())[polynomial][1]
else:
return polynomial
def get_parameters(self):
return self.polynomial, self.start_value, self.final_xor, \
self.lsb_first, self.reverse_polynomial, self.reverse_all, self.little_endian
def crc(self, inpt):
result = c_util.crc(array.array("B", inpt),
array.array("B", self.polynomial),
array.array("B", self.start_value),
array.array("B", self.final_xor),
self.lsb_first, self.reverse_polynomial, self.reverse_all, self.little_endian)
return util.number_to_bits(result, self.poly_order - 1)
def cached_crc(self, inpt, bits=8):
if len(self.cache) == 0:
self.calculate_cache(bits)
result = c_util.cached_crc(self.cache,
self.__cache_bits,
array.array("B", inpt),
array.array("B", self.polynomial),
array.array("B", self.start_value),
array.array("B", self.final_xor),
self.lsb_first, self.reverse_polynomial, self.reverse_all, self.little_endian)
return util.number_to_bits(result, self.poly_order - 1)
def calculate_cache(self, bits=8):
if 0 < bits < self.poly_order:
self.__cache_bits = bits
else:
self.__cache_bits = 8 if self.poly_order > 8 else self.poly_order - 1
self.cache = c_util.calculate_cache(array.array("B", self.polynomial), self.reverse_polynomial,
self.__cache_bits)
def get_crc_datarange(self, inpt, vrfy_crc_start):
return c_util.get_crc_datarange(array.array("B", inpt),
array.array("B", self.polynomial),
vrfy_crc_start,
array.array("B", self.start_value),
array.array("B", self.final_xor),
self.lsb_first, self.reverse_polynomial, self.reverse_all, self.little_endian)
def reference_crc(self, inpt):
len_inpt = len(inpt)
if len(self.start_value) < self.poly_order - 1:
return False
crc = copy.copy(self.start_value[0:(self.poly_order - 1)])
for i in range(0, len_inpt + 7, 8):
for j in range(0, 8):
if self.lsb_first:
idx = i + (7 - j)
else:
idx = i + j
if idx >= len_inpt:
break
if crc[0] != inpt[idx]:
crc[0:self.poly_order - 2] = crc[1:self.poly_order - 1] # crc = crc << 1
crc[self.poly_order - 2] = False
for x in range(0, self.poly_order - 1):
if self.reverse_polynomial:
crc[x] ^= self.polynomial[self.poly_order - 1 - x]
else:
crc[x] ^= self.polynomial[x + 1]
else:
crc[0:self.poly_order - 2] = crc[1:self.poly_order - 1] # crc = crc << 1
crc[self.poly_order - 2] = False
for i in range(0, self.poly_order - 1):
if self.final_xor[i]:
crc[i] = not crc[i]
if self.reverse_all:
crc_old = []
for i in range(0, self.poly_order - 1):
crc_old.append(crc[self.poly_order - 2 - i])
crc = crc_old
if self.poly_order - 1 == 16 and self.little_endian:
self.__swap_bytes(crc, 0, 1)
elif self.poly_order - 1 == 32 and self.little_endian:
self.__swap_bytes(crc, 0, 3)
self.__swap_bytes(crc, 1, 2)
elif self.poly_order - 1 == 64 and self.little_endian:
for pos1, pos2 in [(0, 7), (1, 6), (2, 5), (3, 4)]:
self.__swap_bytes(crc, pos1, pos2)
# return crc
return array.array("B", crc)
def calculate(self, bits: array.array):
return self.crc(bits)
@staticmethod
def __swap_bytes(array, pos1: int, pos2: int):
array[pos1 * 8:pos1 * 8 + 8], array[pos2 * 8:pos2 * 8 + 8] = \
array[pos2 * 8: pos2 * 8 + 8], array[pos1 * 8:pos1 * 8 + 8]
@staticmethod
def from_standard_checksum(name: str):
result = GenericCRC()
result.set_individual_parameters(**GenericCRC.STANDARD_CHECKSUMS[name])
result.caption = name
return result
def set_individual_parameters(self, polynomial, start_value=0, final_xor=0, ref_in=False, ref_out=False,
little_endian=False, reverse_polynomial=False):
# Set polynomial from hex or bit array
old = self.polynomial
if isinstance(polynomial, str):
self.set_polynomial_from_hex(polynomial)
else:
self.polynomial = polynomial
# Clear cache if polynomial changes
if self.polynomial != old:
self.cache = []
self.__cache_bits = 8
# Set start value completely or 0000/FFFF
if isinstance(start_value, int):
self.start_value = array.array("B", [start_value] * (self.poly_order - 1))
elif isinstance(start_value, array.array) and len(start_value) == self.poly_order - 1:
self.start_value = start_value
else:
raise ValueError("Invalid start value length")
# Set final xor completely or 0000/FFFF
if isinstance(final_xor, int):
self.final_xor = array.array("B", [final_xor] * (self.poly_order - 1))
elif isinstance(final_xor, array.array) and len(final_xor) == self.poly_order - 1:
self.final_xor = final_xor
else:
raise ValueError("Invalid final xor length")
# Set boolean parameters
old_reverse = self.reverse_polynomial
self.reverse_polynomial = reverse_polynomial
if self.reverse_polynomial != old_reverse:
self.cache = []
self.__cache_bits = 8
self.reverse_all = ref_out
self.little_endian = little_endian
self.lsb_first = ref_in
def set_crc_parameters(self, i):
# Bit 0,1 = Polynomial
val = (i >> 0) & 3
old = self.polynomial
self.polynomial = self.choose_polynomial(val)
poly_order = len(self.polynomial)
if (self.polynomial != old):
self.cache = []
self.__cache_bits = 8
# Bit 2 = Start Value
val = (i >> 2) & 1
self.start_value = [val != 0] * (poly_order - 1)
# Bit 3 = Final XOR
val = (i >> 3) & 1
self.final_xor = [val != 0] * (poly_order - 1)
# Bit 4 = Reverse Polynomial
val = (i >> 4) & 1
old_reverse = self.reverse_polynomial
if val == 0:
self.reverse_polynomial = False
else:
self.reverse_polynomial = True
if (self.reverse_polynomial != old_reverse):
self.cache = []
self.__cache_bits = 8
# Bit 5 = Reverse (all) Result
val = (i >> 5) & 1
if val == 0:
self.reverse_all = False
else:
self.reverse_all = True
# Bit 6 = Little Endian
val = (i >> 6) & 1
if val == 0:
self.little_endian = False
else:
self.little_endian = True
# Bit 7 = Least Significant Bit (LSB) first
val = (i >> 7) & 1
if val == 0:
self.lsb_first = False
else:
self.lsb_first = True
@classmethod
def __initialize_standard_checksums(cls):
for name in cls.STANDARD_CHECKSUMS:
polynomial = cls.STANDARD_CHECKSUMS[name]["polynomial"]
if isinstance(polynomial, str):
polynomial = array.array("B", [1]) + util.hex2bit(polynomial)
cls.STANDARD_CHECKSUMS[name]["polynomial"] = polynomial
n = len(polynomial) - 1
try:
start_val = cls.STANDARD_CHECKSUMS[name]["start_value"]
except KeyError:
start_val = 0
if isinstance(start_val, int):
cls.STANDARD_CHECKSUMS[name]["start_value"] = array.array("B", [start_val] * n)
try:
final_xor = cls.STANDARD_CHECKSUMS[name]["final_xor"]
except KeyError:
final_xor = 0
if isinstance(final_xor, int):
cls.STANDARD_CHECKSUMS[name]["final_xor"] = array.array("B", [final_xor] * n)
def guess_all(self, bits, trash_max=7, ignore_positions: set = None):
"""
:param bits:
:param trash_max:
:param ignore_positions: columns to ignore (e.g. if already another label on them)
:return: a CRC object, data_range_start, data_range_end, crc_start, crc_end
"""
self.__initialize_standard_checksums()
ignore_positions = set() if ignore_positions is None else ignore_positions
for i in range(0, trash_max):
ret = self.guess_standard_parameters_and_datarange(bits, i)
if ret == (0, 0, 0):
continue # nothing found
crc_start, crc_end = len(bits) - i - ret[0].poly_order + 1, len(bits) - i
if not any(i in ignore_positions for i in range(crc_start, crc_end)):
return ret[0], ret[1], ret[2], crc_start, crc_end
return 0, 0, 0, 0, 0
def bruteforce_all(self, inpt, trash_max=7):
polynomial_sizes = [16, 8]
len_input = len(inpt)
for s in polynomial_sizes:
for i in range(len_input - s - trash_max, len_input - s):
ret = self.bruteforce_parameters_and_data_range(inpt, i)
if ret != (0, 0, 0):
return ret[0], ret[1], ret[2], i, i + s
return 0, 0, 0, 0, 0
def guess_standard_parameters(self, inpt, vrfy_crc):
# Tests all standard parameters and return parameter_value (else False), if a valid CRC could be computed.
# Note: vfry_crc is included inpt!
for i in range(0, 2 ** 8):
self.set_crc_parameters(i)
if len(vrfy_crc) == self.poly_order and self.crc(inpt) == vrfy_crc:
return i
return False
def guess_standard_parameters_and_datarange(self, inpt, trash):
"""
Tests standard parameters from dict and return polynomial object, if a valid CRC could be computed
and determines start and end of crc datarange (end is set before crc)
Note: vfry_crc is included inpt!
"""
# Test longer polynomials first, because smaller polynomials have higher risk of false positive
for name, parameters in sorted(self.STANDARD_CHECKSUMS.items(),
key=lambda x: len(x[1]["polynomial"]),
reverse=True):
self.caption = name
data_begin, data_end = c_util.get_crc_datarange(inpt,
parameters["polynomial"],
max(0,
len(inpt) - trash - len(parameters["polynomial"])) + 1,
parameters["start_value"],
parameters["final_xor"],
parameters.get("ref_in", False),
parameters.get("reverse_polynomial", False),
parameters.get("ref_out", False),
parameters.get("little_endian", False))
if (data_begin, data_end) != (0, 0):
self.set_individual_parameters(**parameters)
return self, data_begin, data_end
return 0, 0, 0
def bruteforce_parameters_and_data_range(self, inpt, vrfy_crc_start):
# Tests all standard parameters and return parameter_value (else False), if a valid CRC could be computed
# and determines start and end of crc datarange (end is set before crc)
# Note: vfry_crc is included inpt!
for i in range(0, 2 ** 8):
self.set_crc_parameters(i)
data_begin, data_end = self.get_crc_datarange(inpt, vrfy_crc_start)
if (data_begin, data_end) != (0, 0):
return i, data_begin, data_end
return 0, 0, 0
def reverse_engineer_polynomial(self, dataset, crcset):
# Sets must be of equal size and > 2
setlen = len(dataset)
if setlen != len(crcset) or setlen < 3:
return False
# XOR each data string with every other string and find strings that only differ in one bit
one_bitter = []
one_bitter_crc = []
for i in range(0, setlen):
for j in range(i + 1, setlen):
if len(dataset[i]) == len(dataset[j]) and len(crcset[i]) == len(crcset[j]):
count = 0
tmp = -1
for x in range(0, len(dataset[i])):
if dataset[i][x] != dataset[j][x]:
tmp = x
count += 1
if count > 1:
break
if count == 1:
one_bitter.append(tmp)
tmp_crc = []
for x in range(0, len(crcset[i])):
tmp_crc.append(crcset[i][x] ^ crcset[j][x])
one_bitter_crc.extend([tmp_crc])
# Find two CRCs from one bit sequences with position i and i+1. CRC from one bit sequence with position i+1 must have MSB=1
setlen = len(one_bitter)
for i in range(0, setlen):
for j in range(0, setlen):
if i != j and one_bitter[i] + 1 == one_bitter[j] and one_bitter_crc[j][0] == True:
# Compute Polynomial
polynomial = one_bitter_crc[i].copy()
for x in range(0, len(one_bitter_crc[i]) - 1):
polynomial[x] ^= one_bitter_crc[j][x + 1]
return polynomial
return False
def to_xml(self):
root = ET.Element("crc")
root.set("polynomial", util.convert_bits_to_string(self.polynomial, 0))
root.set("start_value", util.convert_bits_to_string(self.start_value, 0))
root.set("final_xor", util.convert_bits_to_string(self.final_xor, 0))
root.set("ref_in", str(int(self.lsb_first)))
root.set("ref_out", str(int(self.reverse_all)))
return root
@classmethod
def from_xml(cls, tag: ET.Element):
polynomial = tag.get("polynomial", "1010")
start_value = tag.get("start_value", "0000")
final_xor = tag.get("final_xor", "0000")
ref_in = bool(int(tag.get("ref_in", "0")))
ref_out = bool(int(tag.get("ref_out", "0")))
return GenericCRC(polynomial=util.string2bits(polynomial),
start_value=util.string2bits(start_value), final_xor=util.string2bits(final_xor),
lsb_first=ref_in, reverse_all=ref_out)
@staticmethod
def bit2str(inpt):
return "".join(["1" if x else "0" for x in inpt])
@staticmethod
def str2bit(inpt):
return [True if x == "1" else False for x in inpt]
@staticmethod
def int2bit(inpt):
return [True if x == "1" else False for x in '{0:08b}'.format(inpt)]
@staticmethod
def str2arr(inpt):
return array.array("B", GenericCRC.str2bit(inpt))
@staticmethod
def bit2int(inpt):
return int(GenericCRC.bit2str(inpt), 2)
@staticmethod
def hex2str(inpt):
bitstring = bin(int(inpt, base=16))[2:]
return "0" * (4 * len(inpt.lstrip('0x')) - len(bitstring)) + bitstring

View File

@@ -0,0 +1,32 @@
INDENT_WIDTH_PX = 20
def monospace(string):
return "<samp>" + string + "</samp>"
def indent_string(string, depth=1):
width = depth * INDENT_WIDTH_PX
return '<table style="margin-left: {0}px;" border=0><tr><td>{1}</td></tr></table>'.format(width, string)
def mark_differences(value: str, compare_against: str):
result = []
for i, char in enumerate(value):
try:
if char != compare_against[i]:
result.append('<font color="red">{}</font>'.format(char))
else:
result.append(char)
except IndexError:
result.append(char)
return "".join(result)
def align_expected_and_got_value(expected: str, got: str, align_depth=1):
width = align_depth * INDENT_WIDTH_PX
got_marked = mark_differences(got, expected)
return '<table style="margin-left: {0}px;" border=0>' \
'<tr><td>Expected: </td><td>{1}</td></tr><tr><td>Got: </td><td>{2}</td> </tr>' \
'</table>'.format(width, monospace(expected), monospace(got_marked))

View File

@@ -0,0 +1,73 @@
import logging
import os
import sys
import tempfile
class Color:
PURPLE = '\033[95m'
CYAN = '\033[96m'
DARKCYAN = '\033[36m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
END = '\033[0m'
TMP = "/tmp" if sys.platform == "darwin" else tempfile.gettempdir()
LOG_LEVEL_PATH = os.path.join(TMP, "urh_log_level")
def read_log_level(default):
try:
with open(LOG_LEVEL_PATH, "r") as f:
return int(f.readlines()[0].strip())
except:
return default
def save_log_level():
try:
with open(LOG_LEVEL_PATH, "w") as f:
f.write(str(logger.level))
except:
pass
logger_conf = {
"level": read_log_level(default=logging.DEBUG),
"format": '[%(levelname)s::%(filename)s::%(funcName)s] %(message)s'
}
log_file_handler = None
if hasattr(sys, "frozen"):
try:
sys.stdin.isatty()
except:
# STDIN is not usable, so we are running in GUI mode
logfile_name = os.path.join(TMP, "urh.log")
# Add the log message handler to the logger
import logging.handlers
log_file_handler = logging.handlers.RotatingFileHandler(logfile_name, maxBytes=2e6, backupCount=5)
logging.basicConfig(**logger_conf)
logging_colors_per_level = {
logging.WARNING: Color.YELLOW,
logging.ERROR: Color.RED,
logging.CRITICAL: Color.RED
}
for level, level_color in logging_colors_per_level.items():
if sys.platform != "win32":
logging.addLevelName(level, "{0}{1}{2}".format(level_color, logging.getLevelName(level), Color.END))
logger = logging.getLogger("urh")
if log_file_handler is not None:
logger.addHandler(log_file_handler)

View File

@@ -0,0 +1,582 @@
import os
import xml.etree.ElementTree as ET
from PyQt5.QtCore import QDir, Qt, QObject, pyqtSignal
from PyQt5.QtWidgets import QMessageBox, QApplication
from urh import settings
from urh.dev import config
from urh.models.ProtocolTreeItem import ProtocolTreeItem
from urh.signalprocessing.Encoding import Encoding
from urh.signalprocessing.FieldType import FieldType
from urh.signalprocessing.MessageType import MessageType
from urh.signalprocessing.Modulator import Modulator
from urh.signalprocessing.Participant import Participant
from urh.signalprocessing.Signal import Signal
from urh.util import FileOperator, util
from urh.util.Logger import logger
class ProjectManager(QObject):
NEWLINE_CODE = "###~~~***~~~###_--:;;-__***~~~###" # Newlines don't get loaded from xml properly
AUTOSAVE_INTERVAL_MINUTES = 5
project_loaded_status_changed = pyqtSignal(bool)
project_updated = pyqtSignal()
def __init__(self, main_controller):
super().__init__()
self.main_controller = main_controller
self.device_conf = dict(frequency=config.DEFAULT_FREQUENCY,
sample_rate=config.DEFAULT_SAMPLE_RATE,
bandwidth=config.DEFAULT_BANDWIDTH,
name="USRP")
self.simulator_rx_conf = dict()
self.simulator_tx_conf = dict()
self.simulator_num_repeat = 1
self.simulator_retries = 10
self.simulator_timeout_ms = 2500
self.simulator_error_handling_index = 2
self.__project_file = None
self.__modulators = [Modulator("Modulator")] # type: list[Modulator]
self.__decodings = [] # type: list[Encoding]
self.load_decodings()
self.modulation_was_edited = False
self.description = ""
self.project_path = ""
self.broadcast_address_hex = "ffff"
self.participants = []
self.field_types = [] # type: list[FieldType]
self.field_types_by_caption = dict()
self.reload_field_types()
@property
def modulators(self):
return self.__modulators
@modulators.setter
def modulators(self, value):
if value:
self.__modulators[:] = value
if hasattr(self.main_controller, "generator_tab_controller"):
self.main_controller.generator_tab_controller.refresh_modulators()
@property
def decodings(self):
return self.__decodings
@decodings.setter
def decodings(self, value):
if value:
self.__decodings[:] = value
if hasattr(self.main_controller, "compare_frame_controller"):
self.main_controller.compare_frame_controller.fill_decoding_combobox()
@property
def project_loaded(self) -> bool:
return self.project_file is not None
@property
def project_file(self):
return self.__project_file
@project_file.setter
def project_file(self, value):
self.__project_file = value
self.project_loaded_status_changed.emit(self.project_loaded)
def reload_field_types(self):
self.field_types = FieldType.load_from_xml()
self.field_types_by_caption = {field_type.caption: field_type for field_type in self.field_types}
def set_device_parameters(self, kwargs: dict):
for key, value in kwargs.items():
self.device_conf[key] = value
def on_simulator_rx_parameters_changed(self, kwargs: dict):
for key, value in kwargs.items():
self.simulator_rx_conf[key] = value
def on_simulator_tx_parameters_changed(self, kwargs: dict):
for key, value in kwargs.items():
self.simulator_tx_conf[key] = value
def on_simulator_sniff_parameters_changed(self, kwargs: dict):
for key, value in kwargs.items():
# Save sniff values in common device conf
self.device_conf[key] = value
def load_decodings(self):
if self.project_file:
return
else:
prefix = os.path.realpath(os.path.join(settings.get_qt_settings_filename(), ".."))
fallback = [Encoding(["Non Return To Zero (NRZ)"]),
Encoding(["Non Return To Zero + Invert",
settings.DECODING_INVERT]),
Encoding(["Manchester I",
settings.DECODING_EDGE]),
Encoding(["Manchester II",
settings.DECODING_EDGE,
settings.DECODING_INVERT]),
Encoding(["Differential Manchester",
settings.DECODING_EDGE,
settings.DECODING_DIFFERENTIAL])
]
try:
f = open(os.path.join(prefix, settings.DECODINGS_FILE), "r")
except FileNotFoundError:
self.decodings = fallback
return
decodings = []
for line in map(str.strip, f):
tmp_conf = []
for j in map(str.strip, line.split(",")):
tmp_conf.append(j.replace("'", ""))
decodings.append(Encoding(tmp_conf))
f.close()
self.decodings = decodings if decodings else fallback
@staticmethod
def read_device_conf_dict(tag: ET.Element, target_dict):
if tag is None:
return
for dev_tag in tag:
if dev_tag.text is None:
logger.warn("{} has None text".format(str(dev_tag)))
continue
try:
try:
value = int(dev_tag.text)
except ValueError:
value = float(dev_tag.text)
except ValueError:
value = dev_tag.text
if dev_tag.tag == "bit_len":
target_dict["samples_per_symbol"] = value # legacy
else:
target_dict[dev_tag.tag] = value
@staticmethod
def __device_conf_dict_to_xml(key_name: str, device_conf: dict):
result = ET.Element(key_name)
for key in sorted(device_conf):
device_val_tag = ET.SubElement(result, key)
device_val_tag.text = str(device_conf[key])
return result
def simulator_rx_conf_to_xml(self) -> ET.Element:
return self.__device_conf_dict_to_xml("simulator_rx_conf", self.simulator_rx_conf)
def simulator_tx_conf_to_xml(self) -> ET.Element:
return self.__device_conf_dict_to_xml("simulator_tx_conf", self.simulator_tx_conf)
def read_parameters(self, root):
self.read_device_conf_dict(root.find("device_conf"), target_dict=self.device_conf)
self.read_device_conf_dict(root.find("simulator_rx_conf"), target_dict=self.simulator_rx_conf)
self.read_device_conf_dict(root.find("simulator_tx_conf"), target_dict=self.simulator_tx_conf)
self.description = root.get("description", "").replace(self.NEWLINE_CODE, "\n")
self.broadcast_address_hex = root.get("broadcast_address_hex", "ffff")
def read_message_types(self):
if self.project_file is None:
return None
tree = ET.parse(self.project_file)
root = tree.getroot()
result = []
for msg_type_tag in root.find("protocol").find("message_types").findall("message_type"):
result.append(MessageType.from_xml(msg_type_tag))
return result
def set_project_folder(self, path, ask_for_new_project=True, close_all=True):
if self.project_file is not None or close_all:
# Close existing project (if any) or existing files if requested
self.main_controller.close_all_files()
FileOperator.RECENT_PATH = path
util.PROJECT_PATH = path
self.project_path = path
self.project_file = os.path.join(self.project_path, settings.PROJECT_FILE)
collapse_project_tabs = False
if not os.path.isfile(self.project_file):
if ask_for_new_project:
reply = QMessageBox.question(self.main_controller, "Project File",
"Do you want to create a Project File for this folder?\n"
"If you chose No, you can do it later via File->Convert Folder to Project.",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.main_controller.show_project_settings()
else:
self.project_file = None
if self.project_file is not None:
root = ET.Element("UniversalRadioHackerProject")
tree = ET.ElementTree(root)
tree.write(self.project_file)
self.modulation_was_edited = False
else:
tree = ET.parse(self.project_file)
root = tree.getroot()
collapse_project_tabs = bool(int(root.get("collapse_project_tabs", 0)))
self.modulation_was_edited = bool(int(root.get("modulation_was_edited", 0)))
cfc = self.main_controller.compare_frame_controller
self.read_parameters(root)
self.participants[:] = Participant.read_participants_from_xml_tag(xml_tag=root.find("protocol"))
self.main_controller.add_files(self.read_opened_filenames())
self.read_compare_frame_groups(root)
self.decodings = Encoding.read_decoders_from_xml_tag(root.find("protocol"))
cfc.proto_analyzer.message_types[:] = self.read_message_types()
cfc.message_type_table_model.update()
cfc.proto_analyzer.from_xml_tag(root=root.find("protocol"), participants=self.participants,
decodings=cfc.decodings)
cfc.updateUI()
try:
for message_type in cfc.proto_analyzer.message_types:
for lbl in filter(lambda x: not x.show, message_type):
cfc.set_protocol_label_visibility(lbl)
except Exception as e:
logger.exception(e)
self.modulators = self.read_modulators_from_project_file()
self.main_controller.simulator_tab_controller.load_config_from_xml_tag(root.find("simulator_config"))
if len(self.project_path) > 0 and self.project_file is None:
self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled(True)
else:
self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled(False)
self.main_controller.adjust_for_current_file(path)
self.main_controller.filemodel.setRootPath(path)
self.main_controller.ui.fileTree.setRootIndex(
self.main_controller.file_proxy_model.mapFromSource(self.main_controller.filemodel.index(path)))
self.main_controller.ui.fileTree.setToolTip(path)
self.main_controller.ui.splitter.setSizes([1, 1])
if collapse_project_tabs:
self.main_controller.collapse_project_tab_bar()
else:
self.main_controller.expand_project_tab_bar()
self.main_controller.setWindowTitle("Universal Radio Hacker [" + path + "]")
self.project_loaded_status_changed.emit(self.project_loaded)
self.project_updated.emit()
def convert_folder_to_project(self):
self.project_file = os.path.join(self.project_path, settings.PROJECT_FILE)
self.main_controller.show_project_settings()
def write_signal_information_to_project_file(self, signal: Signal, tree=None):
if self.project_file is None or signal is None or len(signal.filename) == 0:
return
if tree is None:
tree = ET.parse(self.project_file)
root = tree.getroot()
existing_filenames = {}
for signal_tag in root.iter("signal"):
existing_filenames[signal_tag.attrib["filename"]] = signal_tag
try:
file_path = os.path.relpath(signal.filename, self.project_path)
except ValueError:
# Can happen e.g. on Windows when Project is in C:\ and signal on D:\
file_path = signal.filename
if file_path in existing_filenames.keys():
signal_tag = existing_filenames[file_path]
else:
# Create new tag
signal_tag = ET.SubElement(root, "signal")
signal_tag.set("name", signal.name)
signal_tag.set("filename", file_path)
signal_tag.set("samples_per_symbol", str(signal.samples_per_symbol))
signal_tag.set("center", str(signal.center))
signal_tag.set("center_spacing", str(signal.center_spacing))
signal_tag.set("tolerance", str(signal.tolerance))
signal_tag.set("noise_threshold", str(signal.noise_threshold))
signal_tag.set("noise_minimum", str(signal.noise_min_plot))
signal_tag.set("noise_maximum", str(signal.noise_max_plot))
signal_tag.set("modulation_type", str(signal.modulation_type))
signal_tag.set("sample_rate", str(signal.sample_rate))
signal_tag.set("pause_threshold", str(signal.pause_threshold))
signal_tag.set("message_length_divisor", str(signal.message_length_divisor))
signal_tag.set("bits_per_symbol", str(signal.bits_per_symbol))
signal_tag.set("costas_loop_bandwidth", str(signal.costas_loop_bandwidth))
messages = ET.SubElement(signal_tag, "messages")
for message in messages:
messages.append(message.to_xml())
tree.write(self.project_file)
def write_modulators_to_project_file(self, tree=None):
"""
:type modulators: list of Modulator
:return:
"""
if self.project_file is None or not self.modulators:
return
if tree is None:
tree = ET.parse(self.project_file)
root = tree.getroot()
root.append(Modulator.modulators_to_xml_tag(self.modulators))
tree.write(self.project_file)
def read_modulators_from_project_file(self):
"""
:rtype: list of Modulator
"""
return ProjectManager.read_modulators_from_file(self.project_file)
@staticmethod
def read_modulators_from_file(filename: str):
if not filename:
return []
tree = ET.parse(filename)
root = tree.getroot()
return Modulator.modulators_from_xml_tag(root)
def save_project(self, simulator_config=None):
if self.project_file is None or not os.path.isfile(self.project_file):
return
# Recreate file
open(self.project_file, 'w').close()
root = ET.Element("UniversalRadioHackerProject")
tree = ET.ElementTree(root)
tree.write(self.project_file)
# self.write_labels(self.maincontroller.compare_frame_controller.proto_analyzer)
self.write_modulators_to_project_file(tree=tree)
tree = ET.parse(self.project_file)
root = tree.getroot()
root.append(self.__device_conf_dict_to_xml("device_conf", self.device_conf))
root.append(self.simulator_rx_conf_to_xml())
root.append(self.simulator_tx_conf_to_xml())
root.set("description", str(self.description).replace("\n", self.NEWLINE_CODE))
root.set("collapse_project_tabs", str(int(not self.main_controller.ui.tabParticipants.isVisible())))
root.set("modulation_was_edited", str(int(self.modulation_was_edited)))
root.set("broadcast_address_hex", str(self.broadcast_address_hex))
open_files = []
for i, sf in enumerate(self.main_controller.signal_tab_controller.signal_frames):
self.write_signal_information_to_project_file(sf.signal, tree=tree)
try:
pf = self.main_controller.signal_protocol_dict[sf]
filename = pf.filename
if filename in FileOperator.archives.keys():
open_filename = FileOperator.archives[filename]
else:
open_filename = filename
if not open_filename or open_filename in open_files:
continue
open_files.append(open_filename)
file_tag = ET.SubElement(root, "open_file")
try:
file_path = os.path.relpath(open_filename, self.project_path)
except ValueError:
file_path = open_filename
file_tag.set("name", file_path)
file_tag.set("position", str(i))
except Exception:
pass
for group_tag in root.findall("group"):
root.remove(group_tag)
cfc = self.main_controller.compare_frame_controller
for i, group in enumerate(cfc.groups):
group_tag = ET.SubElement(root, "group")
group_tag.set("name", str(group.name))
group_tag.set("id", str(i))
for proto_frame in cfc.protocols[i]:
if proto_frame.filename:
proto_tag = ET.SubElement(group_tag, "cf_protocol")
try:
rel_file_name = os.path.relpath(proto_frame.filename, self.project_path)
except ValueError:
rel_file_name = proto_frame.filename
proto_tag.set("filename", rel_file_name)
root.append(cfc.proto_analyzer.to_xml_tag(decodings=cfc.decodings, participants=self.participants,
messages=[msg for proto in cfc.full_protocol_list for msg in
proto.messages]))
if simulator_config is not None:
root.append(simulator_config.save_to_xml())
util.write_xml_to_file(root, self.project_file)
def read_participants_for_signal(self, signal: Signal, messages):
if self.project_file is None or len(signal.filename) == 0:
return False
tree = ET.parse(self.project_file)
root = tree.getroot()
try:
signal_filename = os.path.relpath(signal.filename, self.project_path)
except ValueError:
signal_filename = signal.filename
for sig_tag in root.iter("signal"):
if sig_tag.attrib["filename"] == signal_filename:
messages_tag = sig_tag.find("messages")
try:
if messages_tag:
for i, message_tag in enumerate(messages_tag.iter("message")):
messages[i].from_xml(message_tag, self.participants)
except IndexError:
return False
return True
return False
def read_project_file_for_signal(self, signal: Signal):
if self.project_file is None or len(signal.filename) == 0:
return False
tree = ET.parse(self.project_file)
root = tree.getroot()
try:
signal_filename = os.path.relpath(signal.filename, self.project_path)
except ValueError:
signal_filename = signal.filename
for sig_tag in root.iter("signal"):
if sig_tag.attrib["filename"] == signal_filename:
signal.name = sig_tag.attrib["name"]
center = sig_tag.get("qad_center", None) # legacy support
signal.center = float(sig_tag.get("center", 0)) if center is None else float(center)
signal.center_spacing = float(sig_tag.get("center_spacing", 0.1))
signal.tolerance = int(sig_tag.get("tolerance", 5))
signal.bits_per_symbol = int(sig_tag.get("bits_per_symbol", 1))
signal.costas_loop_bandwidth = float(sig_tag.get("costas_loop_bandwidth", 0.1))
signal.noise_threshold = float(sig_tag.get("noise_threshold", 0.1))
signal.sample_rate = float(sig_tag.get("sample_rate", 1e6))
signal.samples_per_symbol = int(sig_tag.get("bit_length", 0)) # Legacy for old project files
if signal.samples_per_symbol == 0:
signal.samples_per_symbol = int(sig_tag.get("samples_per_symbol", 100))
try:
# Legacy support when modulation type was integer
signal.modulation_type = Signal.MODULATION_TYPES[int(sig_tag.get("modulation_type", 0))]
except (ValueError, IndexError):
signal.modulation_type = sig_tag.get("modulation_type", "ASK")
signal.pause_threshold = int(sig_tag.get("pause_threshold", 8))
signal.message_length_divisor = int(sig_tag.get("message_length_divisor", 1))
break
return True
def read_opened_filenames(self):
if self.project_file is not None:
tree = ET.parse(self.project_file)
root = tree.getroot()
file_names = []
for file_tag in root.findall("open_file"):
pos = int(file_tag.attrib["position"])
filename = file_tag.attrib["name"]
if not os.path.isfile(filename):
filename = os.path.normpath(os.path.join(self.project_path, filename))
file_names.insert(pos, filename)
QApplication.setOverrideCursor(Qt.WaitCursor)
file_names = FileOperator.uncompress_archives(file_names, QDir.tempPath())
QApplication.restoreOverrideCursor()
return file_names
return []
def read_compare_frame_groups(self, root):
proto_tree_model = self.main_controller.compare_frame_controller.proto_tree_model
tree_root = proto_tree_model.rootItem
pfi = proto_tree_model.protocol_tree_items
proto_frame_items = [item for item in pfi[0]] # type: list[ProtocolTreeItem]
for group_tag in root.iter("group"):
name = group_tag.attrib["name"]
id = group_tag.attrib["id"]
if id == "0":
tree_root.child(0).setData(name)
else:
tree_root.addGroup(name=name)
group = tree_root.child(int(id))
for proto_tag in group_tag.iter("cf_protocol"):
filename = proto_tag.attrib["filename"]
if not os.path.isfile(filename):
filename = os.path.normpath(os.path.join(self.project_path, filename))
try:
proto_frame_item = next((p for p in proto_frame_items if p.protocol.filename == filename))
except StopIteration:
proto_frame_item = None
if proto_frame_item is not None:
group.appendChild(proto_frame_item)
self.main_controller.compare_frame_controller.expand_group_node(int(id))
self.main_controller.compare_frame_controller.refresh()
def from_dialog(self, dialog):
if dialog.committed:
if dialog.new_project or not os.path.isfile(os.path.join(dialog.path, settings.PROJECT_FILE)):
self.set_project_folder(dialog.path, ask_for_new_project=False, close_all=False)
self.device_conf["frequency"] = dialog.freq
self.device_conf["sample_rate"] = dialog.sample_rate
self.device_conf["gain"] = dialog.gain
self.device_conf["bandwidth"] = dialog.bandwidth
self.description = dialog.description
self.broadcast_address_hex = dialog.broadcast_address_hex.lower().replace(" ", "")
if dialog.new_project:
self.participants[:] = dialog.participants
self.project_updated.emit()

View File

@@ -0,0 +1,126 @@
import numpy as np
from multiprocessing import Value, Array
from urh.signalprocessing.IQArray import IQArray
class RingBuffer(object):
"""
A RingBuffer containing complex values.
"""
def __init__(self, size: int, dtype=np.float32):
self.dtype = dtype
types = {np.uint8: "B", np.int8: "b", np.int16: "h", np.uint16: "H", np.float32: "f", np.float64: "d"}
self.__data = Array(types[self.dtype], 2*size)
self.size = size
self.__left_index = Value("L", 0)
self.__right_index = Value("L", 0)
self.__length = Value("L", 0)
def __len__(self):
return self.__length.value
@property
def left_index(self):
return self.__left_index.value
@left_index.setter
def left_index(self, value):
self.__left_index.value = value % self.size
@property
def right_index(self):
return self.__right_index.value
@right_index.setter
def right_index(self, value):
self.__right_index.value = value % self.size
@property
def is_empty(self) -> bool:
return len(self) == 0
@property
def space_left(self):
return self.size - len(self)
@property
def data(self):
return np.frombuffer(self.__data.get_obj(), dtype=self.dtype).reshape(len(self.__data) // 2, 2)
@property
def view_data(self):
"""
Get a representation of the ring buffer for plotting. This is expensive, so it should only be used in frontend
:return:
"""
left, right = self.left_index, self.left_index + len(self)
if left > right:
left, right = right, left
data = self.data.flatten()
return np.concatenate((data[left:right], data[right:], data[:left]))
def clear(self):
self.left_index = 0
self.right_index = 0
def will_fit(self, number_values: int) -> bool:
return number_values <= self.space_left
def push(self, values: IQArray):
"""
Push values to buffer. If buffer can't store all values a ValueError is raised
"""
n = len(values)
if len(self) + n > self.size:
raise ValueError("Too much data to push to RingBuffer")
slide_1 = np.s_[self.right_index:min(self.right_index + n, self.size)]
slide_2 = np.s_[:max(self.right_index + n - self.size, 0)]
with self.__data.get_lock():
data = np.frombuffer(self.__data.get_obj(), dtype=self.dtype).reshape(len(self.__data) // 2, 2)
data[slide_1] = values[:slide_1.stop - slide_1.start]
data[slide_2] = values[slide_1.stop - slide_1.start:]
self.right_index += n
self.__length.value += n
def pop(self, number: int, ensure_even_length=False) -> np.ndarray:
"""
Pop number of elements. If there are not enough elements, all remaining elements are returned and the
buffer is cleared afterwards. If buffer is empty, an empty numpy array is returned.
If number is -1 (or any other value below zero) than complete buffer is returned
"""
if ensure_even_length:
number -= number % 2
if len(self) == 0 or number == 0:
return np.array([], dtype=self.dtype)
if number < 0:
# take everything
number = len(self)
else:
number = min(number, len(self))
with self.__data.get_lock():
result = np.ones(2*number, dtype=self.dtype).reshape(number, 2)
data = np.frombuffer(self.__data.get_obj(), dtype=self.dtype).reshape(len(self.__data) // 2, 2)
if self.left_index + number > len(data):
end = len(data) - self.left_index
else:
end = number
result[:end] = data[self.left_index:self.left_index + end]
if end < number:
result[end:] = data[:number-end]
self.left_index += number
self.__length.value -= number
return result

View File

@@ -0,0 +1,117 @@
import array
import copy
from enum import Enum
from xml.etree import ElementTree as ET
from urh.util import util
from urh.util.GenericCRC import GenericCRC
class WSPChecksum(object):
"""
This class implements the three checksums from Wireless Short Packet (WSP) standard
http://hes-standards.org/doc/SC25_WG1_N1493.pdf
"""
class ChecksumMode(Enum):
auto = 0
checksum4 = 1
checksum8 = 2
crc8 = 3
CRC_8_POLYNOMIAL = array.array("B", [1,
0, 0, 0, 0, 0, 1, 1, 1]) # x^8+x^2+x+1
def __init__(self, mode=ChecksumMode.auto):
self.mode = mode
self.caption = str(mode)
def __eq__(self, other):
if not isinstance(other, WSPChecksum):
return False
return self.mode == other.mode
def __hash__(self):
return hash(self.mode)
def calculate(self, msg: array.array) -> array.array:
"""
Get the checksum for a WSP message. There are three hashes possible:
1) 4 Bit Checksum - For Switch Telegram (RORG=5 or 6 and STATUS = 0x20 or 0x30)
2) 8 Bit Checksum: STATUS bit 2^7 = 0
3) 8 Bit CRC: STATUS bit 2^7 = 1
:param msg: the message without Preamble/SOF and EOF. Message starts with RORG and ends with CRC
"""
try:
if self.mode == self.ChecksumMode.auto:
if msg[0:4] == util.hex2bit("5") or msg[0:4] == util.hex2bit("6"):
# Switch telegram
return self.checksum4(msg)
status = msg[-16:-8]
if status[0]:
return self.crc8(msg[:-8]) # ignore trailing hash
else:
return self.checksum8(msg[:-8]) # ignore trailing hash
elif self.mode == self.ChecksumMode.checksum4:
return self.checksum4(msg)
elif self.mode == self.ChecksumMode.checksum8:
return self.checksum8(msg[:-8])
elif self.mode == self.ChecksumMode.crc8:
return self.crc8(msg[:-8])
except IndexError:
return None
@classmethod
def search_for_wsp_checksum(cls, bits_behind_sync):
data_start, data_stop, crc_start, crc_stop = 0, 0, 0, 0
if bits_behind_sync[-4:].tobytes() != array.array("B", [1, 0, 1, 1]).tobytes():
return 0, 0, 0, 0 # Check for EOF
rorg = bits_behind_sync[0:4].tobytes()
if rorg == array.array("B", [0, 1, 0, 1]).tobytes() or rorg == array.array("B", [0, 1, 1, 0]).tobytes():
# Switch telegram
if cls.checksum4(bits_behind_sync[-8:]).tobytes() == bits_behind_sync[-8:-4].tobytes():
crc_start = len(bits_behind_sync) - 8
crc_stop = len(bits_behind_sync) - 4
data_stop = crc_start
return data_start, data_stop, crc_start, crc_stop
# todo: Find crc8 and checksum8
return 0, 0, 0, 0
@classmethod
def checksum4(cls, bits: array.array) -> array.array:
hash = 0
val = copy.copy(bits)
val[-4:] = array.array("B", [False, False, False, False])
for i in range(0, len(val), 8):
hash += int("".join(map(str, map(int, val[i:i + 8]))), 2)
hash = (((hash & 0xf0) >> 4) + (hash & 0x0f)) & 0x0f
return array.array("B", list(map(bool, map(int, "{0:04b}".format(hash)))))
@classmethod
def checksum8(cls, bits: array.array) -> array.array:
hash = 0
for i in range(0, len(bits) - 8, 8):
hash += int("".join(map(str, map(int, bits[i:i + 8]))), 2)
return array.array("B", list(map(bool, map(int, "{0:08b}".format(hash % 256)))))
@classmethod
def crc8(cls, bits: array.array):
return array.array("B", GenericCRC(polynomial=cls.CRC_8_POLYNOMIAL).crc(bits))
def to_xml(self) -> ET.Element:
root = ET.Element("wsp_checksum")
root.set("mode", str(self.mode.name))
return root
@classmethod
def from_xml(cls, tag: ET.Element):
return WSPChecksum(mode=WSPChecksum.ChecksumMode[tag.get("mode", "auto")])

View File

@@ -0,0 +1 @@
__author__ = 'joe'

View File

@@ -0,0 +1,474 @@
import array
import os
import shlex
import shutil
import subprocess
import sys
import time
from xml.dom import minidom
from xml.etree import ElementTree as ET
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontDatabase, QFont
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QSplitter
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QPlainTextEdit, QTableWidgetItem
from urh import settings
from urh.util.Logger import logger
PROJECT_PATH = None # for referencing in external program calls
BCD_ERROR_SYMBOL = "?"
BCD_LUT = {"{0:04b}".format(i): str(i) if i < 10 else BCD_ERROR_SYMBOL for i in range(16)}
BCD_REVERSE_LUT = {str(i): "{0:04b}".format(i) for i in range(10)}
BCD_REVERSE_LUT[BCD_ERROR_SYMBOL] = "0000"
DEFAULT_PROGRAMS_WINDOWS = {}
def profile(func):
def func_wrapper(*args):
t = time.perf_counter()
result = func(*args)
print("{} took {:.2f}ms".format(func, 1000 * (time.perf_counter() - t)))
return result
return func_wrapper
def set_icon_theme():
if sys.platform != "linux" or settings.read("icon_theme_index", 0, int) == 0:
# noinspection PyUnresolvedReferences
import urh.ui.xtra_icons_rc
QIcon.setThemeName("oxy")
else:
QIcon.setThemeName("")
def get_free_port():
import socket
s = socket.socket()
s.bind(("", 0))
port = s.getsockname()[1]
s.close()
return port
def set_shared_library_path():
shared_lib_dir = get_shared_library_path()
if shared_lib_dir:
if sys.platform == "win32":
current_path = os.environ.get("PATH", '')
if not current_path.startswith(shared_lib_dir):
os.environ["PATH"] = shared_lib_dir + os.pathsep + current_path
else:
# LD_LIBRARY_PATH will not be considered at runtime so we explicitly load the .so's we need
exts = [".so"] if sys.platform == "linux" else [".so", ".dylib"]
import ctypes
libs = sorted(os.listdir(shared_lib_dir))
libusb = next((lib for lib in libs if "libusb" in lib), None)
if libusb:
# Ensure libusb is loaded first
libs.insert(0, libs.pop(libs.index(libusb)))
for lib in libs:
if lib.lower().startswith("lib") and any(ext in lib for ext in exts):
lib_path = os.path.join(shared_lib_dir, lib)
if os.path.isfile(lib_path):
try:
ctypes.cdll.LoadLibrary(lib_path)
except Exception as e:
logger.exception(e)
def get_shared_library_path():
if hasattr(sys, "frozen"):
return os.path.dirname(sys.executable)
util_dir = os.path.dirname(os.path.realpath(__file__)) if not os.path.islink(__file__) \
else os.path.dirname(os.path.realpath(os.readlink(__file__)))
urh_dir = os.path.realpath(os.path.join(util_dir, ".."))
assert os.path.isdir(urh_dir)
shared_lib_dir = os.path.realpath(os.path.join(urh_dir, "dev", "native", "lib", "shared"))
if os.path.isdir(shared_lib_dir):
return shared_lib_dir
else:
return ""
def convert_bits_to_string(bits, output_view_type: int, pad_zeros=False, lsb=False, lsd=False, endianness="big"):
"""
Convert bit array to string
:param endianness: Endianness little or big
:param bits: Bit array
:param output_view_type: Output view type index
0 = bit, 1=hex, 2=ascii, 3=decimal 4=binary coded decimal (bcd)
:param pad_zeros:
:param lsb: Least Significant Bit -> Reverse bits first
:param lsd: Least Significant Digit -> Reverse result at end
:return:
"""
bits_str = "".join(["1" if b else "0" for b in bits])
if output_view_type == 4:
# For BCD we need to enforce padding
pad_zeros = True
if pad_zeros and output_view_type in (1, 2, 4):
n = 4 if output_view_type in (1, 4) else 8 if output_view_type == 2 else 1
bits_str += "0" * ((n - (len(bits_str) % n)) % n)
if lsb:
# Reverse bit string
bits_str = bits_str[::-1]
if endianness == "little":
# reverse byte wise
bits_str = "".join(bits_str[max(i - 8, 0):i] for i in range(len(bits_str), 0, -8))
if output_view_type == 0: # bit
result = bits_str
elif output_view_type == 1: # hex
result = "".join(["{0:x}".format(int(bits_str[i:i + 4], 2)) for i in range(0, len(bits_str), 4)])
elif output_view_type == 2: # ascii
result = "".join(map(chr,
[int("".join(bits_str[i:i + 8]), 2) for i in range(0, len(bits_str), 8)]))
elif output_view_type == 3: # decimal
try:
result = str(int(bits_str, 2))
except ValueError:
return None
elif output_view_type == 4: # bcd
result = "".join([BCD_LUT[bits_str[i:i + 4]] for i in range(0, len(bits_str), 4)])
else:
raise ValueError("Unknown view type")
if lsd:
# reverse result
return result[::-1]
else:
return result
def hex2bit(hex_str: str) -> array.array:
if not isinstance(hex_str, str):
return array.array("B", [])
if hex_str[:2] == "0x":
hex_str = hex_str[2:]
try:
bitstring = "".join("{0:04b}".format(int(h, 16)) for h in hex_str)
return array.array("B", [True if x == "1" else False for x in bitstring])
except (TypeError, ValueError) as e:
logger.error(e)
result = array.array("B", [])
return result
def ascii2bit(ascii_str: str) -> array.array:
if not isinstance(ascii_str, str):
return array.array("B", [])
try:
bitstring = "".join("{0:08b}".format(ord(c)) for c in ascii_str)
return array.array("B", [True if x == "1" else False for x in bitstring])
except (TypeError, ValueError) as e:
logger.error(e)
result = array.array("B", [])
return result
def decimal2bit(number: str, num_bits: int) -> array.array:
try:
number = int(number)
except ValueError as e:
logger.error(e)
return array.array("B", [])
fmt_str = "{0:0" + str(num_bits) + "b}"
return array.array("B", map(int, fmt_str.format(number)))
def bcd2bit(value: str) -> array.array:
try:
return array.array("B", map(int, "".join(BCD_REVERSE_LUT[c] for c in value)))
except Exception as e:
logger.error(e)
return array.array("B", [])
def convert_string_to_bits(value: str, display_format: int, target_num_bits: int) -> array.array:
if display_format == 0:
result = string2bits(value)
elif display_format == 1:
result = hex2bit(value)
elif display_format == 2:
result = ascii2bit(value)
elif display_format == 3:
result = decimal2bit(value, target_num_bits)
elif display_format == 4:
result = bcd2bit(value)
else:
raise ValueError("Unknown display format {}".format(display_format))
if len(result) == 0:
raise ValueError("Error during conversion.")
if len(result) < target_num_bits:
# pad with zeros
return result + array.array("B", [0] * (target_num_bits - len(result)))
else:
return result[:target_num_bits]
def create_textbox_dialog(content: str, title: str, parent) -> QDialog:
d = QDialog(parent)
d.resize(800, 600)
d.setWindowTitle(title)
layout = QVBoxLayout(d)
text_edit = QPlainTextEdit(content)
text_edit.setReadOnly(True)
layout.addWidget(text_edit)
d.setLayout(layout)
return d
def string2bits(bit_str: str) -> array.array:
return array.array("B", map(int, bit_str))
def bit2hex(bits: array.array, pad_zeros=False) -> str:
return convert_bits_to_string(bits, 1, pad_zeros)
def number_to_bits(n: int, length: int) -> array.array:
fmt = "{0:0" + str(length) + "b}"
return array.array("B", map(int, fmt.format(n)))
def bits_to_number(bits: array.array) -> int:
return int("".join(map(str, bits)), 2)
def aggregate_bits(bits: array.array, size=4) -> array.array:
result = array.array("B", [])
for i in range(0, len(bits), size):
h = 0
for k in range(size):
try:
h += (2 ** (size - 1 - k)) * bits[i + k]
except IndexError:
# Implicit padding with zeros
continue
result.append(h)
return result
def convert_numbers_to_hex_string(arr: np.ndarray):
"""
Convert an array like [0, 1, 10, 2] to string 012a2
:param arr:
:return:
"""
lut = {i: "{0:x}".format(i) for i in range(16)}
return "".join(lut[x] if x in lut else " {} ".format(x) for x in arr)
def clip(value, minimum, maximum):
return max(minimum, min(value, maximum))
def file_can_be_opened(filename: str):
try:
open(filename, "r").close()
return True
except Exception as e:
if not isinstance(e, FileNotFoundError):
logger.debug(str(e))
return False
def create_table_item(content):
item = QTableWidgetItem(str(content))
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
return item
def write_xml_to_file(xml_tag: ET.Element, filename: str):
xml_str = minidom.parseString(ET.tostring(xml_tag)).toprettyxml(indent=" ")
with open(filename, "w") as f:
for line in xml_str.split("\n"):
if line.strip():
f.write(line + "\n")
def get_monospace_font() -> QFont:
fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
fixed_font.setPointSize(QApplication.instance().font().pointSize())
return fixed_font
def get_name_from_filename(filename: str):
if not isinstance(filename, str):
return "No Name"
return os.path.basename(filename).split(".")[0]
def get_default_windows_program_for_extension(extension: str):
if os.name != "nt":
return None
if not extension.startswith("."):
extension = "." + extension
if extension in DEFAULT_PROGRAMS_WINDOWS:
return DEFAULT_PROGRAMS_WINDOWS[extension]
try:
assoc = subprocess.check_output("assoc " + extension, shell=True, stderr=subprocess.PIPE).decode().split("=")[1]
ftype = subprocess.check_output("ftype " + assoc, shell=True).decode().split("=")[1].split(" ")[0]
ftype = ftype.replace('"', '')
assert shutil.which(ftype) is not None
except Exception:
return None
DEFAULT_PROGRAMS_WINDOWS[extension] = ftype
return ftype
def parse_command(command: str):
try:
posix = os.name != "nt"
splitted = shlex.split(command, posix=posix)
# strip quotations
if not posix:
splitted = [s.replace('"', '').replace("'", "") for s in splitted]
except ValueError:
splitted = [] # e.g. when missing matching "
if len(splitted) == 0:
return "", []
cmd = splitted.pop(0)
if PROJECT_PATH is not None and not os.path.isabs(cmd) and shutil.which(cmd) is None:
# Path relative to project path
cmd = os.path.normpath(os.path.join(PROJECT_PATH, cmd))
cmd = [cmd]
# This is for legacy support, if you have filenames with spaces and did not quote them
while shutil.which(" ".join(cmd)) is None and len(splitted) > 0:
cmd.append(splitted.pop(0))
return " ".join(cmd), splitted
def run_command(command, param: str = None, use_stdin=False, detailed_output=False, return_rc=False):
cmd, arg = parse_command(command)
if shutil.which(cmd) is None:
logger.error("Could not find {}".format(cmd))
return ""
startupinfo = None
if os.name == 'nt':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
if "." in cmd:
default_app = get_default_windows_program_for_extension(cmd.split(".")[-1])
if default_app:
arg.insert(0, cmd)
cmd = default_app
call_list = [cmd] + arg
try:
if detailed_output:
if param is not None:
call_list.append(param)
p = subprocess.Popen(call_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
out, err = p.communicate()
result = "{} exited with {}".format(" ".join(call_list), p.returncode)
if out.decode():
result += " stdout: {}".format(out.decode())
if err.decode():
result += " stderr: {}".format(err.decode())
if return_rc:
return result, p.returncode
else:
return result
elif use_stdin:
p = subprocess.Popen(call_list, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
startupinfo=startupinfo)
param = param.encode() if param is not None else None
out, _ = p.communicate(param)
if return_rc:
return out.decode(), p.returncode
else:
return out.decode()
else:
if param is not None:
call_list.append(param)
if return_rc:
raise ValueError("Return Code not supported for this configuration")
return subprocess.check_output(call_list, stderr=subprocess.PIPE, startupinfo=startupinfo).decode()
except Exception as e:
msg = "Could not run {} ({})".format(cmd, e)
logger.error(msg)
if detailed_output:
return msg
else:
return ""
def validate_command(command: str):
if not isinstance(command, str):
return False
cmd, _ = parse_command(command)
return shutil.which(cmd) is not None
def set_splitter_stylesheet(splitter: QSplitter):
splitter.setHandleWidth(4)
bgcolor = settings.BGCOLOR.lighter(150)
r, g, b, a = bgcolor.red(), bgcolor.green(), bgcolor.blue(), bgcolor.alpha()
splitter.setStyleSheet("QSplitter::handle:vertical {{margin: 4px 0px; "
"background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0.2 rgba(255, 255, 255, 0),"
"stop:0.5 rgba({0}, {1}, {2}, {3}),"
"stop:0.8 rgba(255, 255, 255, 0));"
"image: url(:/icons/icons/splitter_handle_horizontal.svg);}}"
"QSplitter::handle:horizontal {{margin: 4px 0px; "
"background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
"stop:0.2 rgba(255, 255, 255, 0),"
"stop:0.5 rgba({0}, {1}, {2}, {3}),"
"stop:0.8 rgba(255, 255, 255, 0));"
"image: url(:/icons/icons/splitter_handle_vertical.svg);}}".format(r, g, b, a))
def calc_x_y_scale(rect, parent):
view_rect = parent.view_rect() if hasattr(parent, "view_rect") else rect
parent_width = parent.width() if hasattr(parent, "width") else 750
parent_height = parent.height() if hasattr(parent, "height") else 300
scale_x = view_rect.width() / parent_width
scale_y = view_rect.height() / parent_height
return scale_x, scale_y