Add urh
This commit is contained in:
120
Software/Universal Radio Hacker/src/urh/util/Errors.py
Normal file
120
Software/Universal Radio Hacker/src/urh/util/Errors.py
Normal 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))
|
247
Software/Universal Radio Hacker/src/urh/util/FileOperator.py
Normal file
247
Software/Universal Radio Hacker/src/urh/util/FileOperator.py
Normal 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
|
61
Software/Universal Radio Hacker/src/urh/util/Formatter.py
Normal file
61
Software/Universal Radio Hacker/src/urh/util/Formatter.py
Normal 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
|
514
Software/Universal Radio Hacker/src/urh/util/GenericCRC.py
Normal file
514
Software/Universal Radio Hacker/src/urh/util/GenericCRC.py
Normal 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
|
@@ -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))
|
73
Software/Universal Radio Hacker/src/urh/util/Logger.py
Normal file
73
Software/Universal Radio Hacker/src/urh/util/Logger.py
Normal 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)
|
582
Software/Universal Radio Hacker/src/urh/util/ProjectManager.py
Normal file
582
Software/Universal Radio Hacker/src/urh/util/ProjectManager.py
Normal 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()
|
126
Software/Universal Radio Hacker/src/urh/util/RingBuffer.py
Normal file
126
Software/Universal Radio Hacker/src/urh/util/RingBuffer.py
Normal 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
|
117
Software/Universal Radio Hacker/src/urh/util/WSPChecksum.py
Normal file
117
Software/Universal Radio Hacker/src/urh/util/WSPChecksum.py
Normal 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")])
|
1
Software/Universal Radio Hacker/src/urh/util/__init__.py
Normal file
1
Software/Universal Radio Hacker/src/urh/util/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'joe'
|
474
Software/Universal Radio Hacker/src/urh/util/util.py
Normal file
474
Software/Universal Radio Hacker/src/urh/util/util.py
Normal 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
|
Reference in New Issue
Block a user