diff --git a/pytools/app_rel.py b/pytools/app_rel.py
index 5627763..4bb57ba 100644
--- a/pytools/app_rel.py
+++ b/pytools/app_rel.py
@@ -9,21 +9,28 @@
telemetry_dir_name = 'telemetry'
mon = Monitor(monitor_bus_name=mon_bus_name, argv=sys.argv)
+zero = DataSourceConst('zero', value=0)
rp = EwRelativePosition([
+ DataSourceConst("airplane", value="airplane-white"),
DataSourceSinus('plane_phi', mult=360),
DataSourceConst('plane_r', value=60),
+ DataSourceConst('zero', value=20), # alt
DataSourceSinus('plane_course', iphase=0.0, mult=360),
+ DataSourceConst("aim-yellow", value="aim-green"),
DataSourceSinus('base_phi', iphase=0.5, mult=360),
DataSourceConst('base_r', value=120),
+ zero, # alt
+ zero, # course
+ DataSourceConst("aim-green", value="aim-yellow"), # icon
DataSourceSinus('target_phi', iphase=-0.5, mult=360),
DataSourceConst('target_r', value=160),
+ zero, # alt
+ zero, # course
])
-
-
mon.add_widget(EwGroup([rp]))
mon.app_window.resize(800, 800)
diff --git a/pytools/ds/__init__.py b/pytools/ds/__init__.py
index 4d6eddc..eac977a 100644
--- a/pytools/ds/__init__.py
+++ b/pytools/ds/__init__.py
@@ -5,9 +5,9 @@
DataSourceTimeline,
DataSourceSum,
NoDataStub,
- DataSourceCalcFilteredRate
+ DataSourceCalcFilteredRate,
)
-from .qt import DataSourceWidget
+from .qt import DataSourceLineEdit, DataSourceButtons
__all__ = [
"DataSourceBasic",
@@ -17,5 +17,6 @@
"DataSourceSum",
"DataSourceCalcFilteredRate",
"DataSourceTimeline",
- "DataSourceWidget"
+ "DataSourceLineEdit",
+ "DataSourceButtons"
]
diff --git a/pytools/ds/common.py b/pytools/ds/common.py
new file mode 100644
index 0000000..fe2c41c
--- /dev/null
+++ b/pytools/ds/common.py
@@ -0,0 +1,39 @@
+from typing import Union
+
+from mymath import to_cartesian, to_polar
+from .datasources import DataSourceBasic, NoDataStub
+
+
+class DataSourceCoordsCartesian(DataSourceBasic):
+ def __init__(self, name, phi, r, scale=1.0, decimals=3, **kwargs):
+ super().__init__(name, **kwargs)
+ self._phi = phi
+ self._r = r
+ self._scale = scale
+ self._decimals = decimals
+
+ def connect(self):
+ pass
+
+ def read(self) -> Union[float, int, str, NoDataStub]:
+ return to_cartesian(phi=self._phi,
+ r=self._r,
+ scale=self._scale,
+ decimals=self._decimals)
+
+
+class DataSourceCoordsPolar(DataSourceBasic):
+ def __init__(self, name, x, y, scale=1.0, decimals=3, **kwargs):
+ super().__init__(name, **kwargs)
+ self._x = x
+ self._y = y
+ self._scale = scale
+ self._decimals = decimals
+
+ def connect(self):
+ pass
+
+ def read(self) -> Union[float, int, str, NoDataStub]:
+ return to_polar(x=self._x, y=self._y,
+ scale=self._scale,
+ decimals=self._decimals)
diff --git a/pytools/ds/datasources.py b/pytools/ds/datasources.py
index 339c52e..4694907 100644
--- a/pytools/ds/datasources.py
+++ b/pytools/ds/datasources.py
@@ -1,9 +1,7 @@
+import math
+import time
from abc import abstractmethod
from typing import List, Union
-import time
-import math
-
-from PyQt5.QtWidgets import QLineEdit
class NoDataStub:
@@ -107,6 +105,7 @@ def read(self) -> Union[float, int, str, NoDataStub]:
self.time += self.delta_time
return self.time
+
class DataSourceSinus(DataSourceBasic):
def __init__(self, name, *, omega=1.0, iphase=0.0, bias=0.0, **kwargs):
super().__init__(name, **kwargs)
diff --git a/pytools/ds/qt.py b/pytools/ds/qt.py
index 6a5761b..688253f 100644
--- a/pytools/ds/qt.py
+++ b/pytools/ds/qt.py
@@ -1,24 +1,53 @@
-from typing import Union
+from ctypes import c_uint16
+from typing import Union, List
-from PyQt5.QtWidgets import QLineEdit
+from PyQt5.QtWidgets import QLineEdit, QPushButton
from . import NoDataStub
from .datasources import DataSourceBasic
+ButtonValuesT = tuple[QPushButton, int]
+
+
+class DataSourceButtons(DataSourceBasic):
+ """
+ Example:
+ `cmd_ds = DataSourceButtons("buttons", [(cmd.take_off_btn, 1), (cmd.land_btn, 2)])`
+ """
+ value: int = 0
+
+ def __init__(self, name, cmd_tuples: List[ButtonValuesT]):
+ super().__init__(name)
+ for c in cmd_tuples:
+ [btn, v] = c
+ btn.clicked.connect(self.set_value_fn(v))
+
+ def set_value_fn(self, v):
+ def _set_val():
+ self.value = v
+
+ return _set_val
-class DataSourceWidget(DataSourceBasic):
def connect(self):
pass
def read(self) -> Union[float, int, str, NoDataStub]:
return self.value
+
+class DataSourceLineEdit(DataSourceBasic):
+ def connect(self):
+ pass
+
def __init__(self, name, widget: QLineEdit):
super().__init__(name)
self.widget = widget
self.value = 0.0
self.widget.textChanged.connect(self._widget_edited)
+ def read(self) -> Union[float, int, str, NoDataStub]:
+ return self.value
+
def _widget_edited(self, val):
self.set_value(self.float(val))
diff --git a/pytools/ds/tests/__init__.py b/pytools/ds/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pytools/ds/tests/test_coords_cartesian.py b/pytools/ds/tests/test_coords_cartesian.py
new file mode 100644
index 0000000..456677c
--- /dev/null
+++ b/pytools/ds/tests/test_coords_cartesian.py
@@ -0,0 +1,38 @@
+import math
+from unittest import TestCase
+
+from ds.common import DataSourceCoordsCartesian
+
+
+# Sample coords:
+#
+# •4 | •1
+# |
+# ----------+-----------
+# |
+# •3 | •2
+
+class TestDataSourceCartesian(TestCase):
+ def test_pt1(self):
+ ds = DataSourceCoordsCartesian("cartesian", phi=45, r=math.hypot(10, 10), decimals=2)
+ [x, y] = ds.read()
+ self.assertEqual(10.0, x)
+ self.assertEqual(10.0, y)
+
+ def test_pt2(self):
+ ds = DataSourceCoordsCartesian("cartesian", phi=45+90, r=math.hypot(10, 10), decimals=2)
+ [x, y] = ds.read()
+ self.assertEqual(10.0, x)
+ self.assertEqual(-10.0, y)
+
+ def test_pt3(self):
+ ds = DataSourceCoordsCartesian("cartesian", phi=45 + 180, r=math.hypot(10, 10), decimals=2)
+ [x, y] = ds.read()
+ self.assertEqual(-10.0, x)
+ self.assertEqual(-10.0, y)
+
+ def test_pt4(self):
+ ds = DataSourceCoordsCartesian("cartesian", phi=45 + 270, r=math.hypot(10, 10), decimals=2)
+ [x, y] = ds.read()
+ self.assertEqual(-10.0, x)
+ self.assertEqual(10.0, y)
diff --git a/pytools/ds/tests/test_coords_polar.py b/pytools/ds/tests/test_coords_polar.py
new file mode 100644
index 0000000..3a62c42
--- /dev/null
+++ b/pytools/ds/tests/test_coords_polar.py
@@ -0,0 +1,44 @@
+import math
+
+import mymath
+from unittest import TestCase
+
+from ds.common import DataSourceCoordsPolar
+
+
+# Sample coords:
+#
+# •4 | •1
+# |
+# ----------+-----------
+# |
+# •3 | •2
+
+class TestDataSourcePolar(TestCase):
+ def test_pt1(self):
+ decimals = 3
+ polar_ds = DataSourceCoordsPolar("to_polar", 10, 10, decimals=decimals)
+ [phi, r] = polar_ds.read()
+ self.assertEqual(45.0, phi)
+ self.assertEqual(round(math.hypot(10, 10), decimals), r)
+
+ def test_pt2(self):
+ decimals = 3
+ polar_ds = DataSourceCoordsPolar("to_polar", x=10, y=-10, decimals=decimals)
+ [phi, r] = polar_ds.read()
+ self.assertEqual(phi, 45.0 + 90.0)
+ self.assertEqual(round(math.hypot(10, 10), decimals), r)
+
+ def test_pt3(self):
+ decimals = 3
+ polar_ds = DataSourceCoordsPolar("to_polar", x=-10, y=-10, decimals=decimals)
+ [phi, r] = polar_ds.read()
+ self.assertEqual(phi, 45.0 + 180.0)
+ self.assertEqual(r, round(math.hypot(10, 10), decimals))
+
+ def test_pt4(self):
+ decimals = 3
+ polar_ds = DataSourceCoordsPolar("to_polar", x=-10, y=10, decimals=decimals)
+ [phi, r] = polar_ds.read()
+ self.assertEqual(phi, 45.0 + 270.0)
+ self.assertEqual(round(math.hypot(10, 10), decimals), r)
diff --git a/pytools/ew/relative_position.py b/pytools/ew/relative_position.py
index 603b47c..9307b80 100644
--- a/pytools/ew/relative_position.py
+++ b/pytools/ew/relative_position.py
@@ -1,11 +1,12 @@
-import math
from typing import List, Union, Dict
from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QPointF, pyqtSignal
from PyQt5.QtGui import QPainter, QPen, QPixmap, QBrush, QFont
+from mymath import to_polar, to_cartesian, translate_point, rev_translate_point
from ds import DataSourceBasic, NoDataStub
+
from .common import MyQtWidget, rel_path
from .widgets import EwBasic
@@ -15,31 +16,6 @@
ds_tuple_size = 5
-def to_polar(x, y, scale=1.0):
- pt = QPointF(x, y)
- r = math.sqrt(pt.x() * pt.x() + pt.y() * pt.y()) / scale
- degrees = math.degrees(math.atan2(-y, x)) + 90
- phi = (degrees + 360) % 360
- return [round(phi, 2), round(r, 3)]
-
-
-def to_cartesian(r, phi, scale=1.0):
- _phi_rad = math.radians(phi)
- _x = scale * r * math.sin(_phi_rad)
- _y = -(scale * r * math.cos(_phi_rad))
- return [_x, _y]
-
-
-def rtranslate_point(width, height, pt: QPointF, zero_offset_x=0.0, zero_offset_y=0.0):
- dx, dy = width / 2 + zero_offset_x, height / 2 + zero_offset_y
- return QPointF(pt.x() - dx, - (pt.y() - dy))
-
-
-def translate_point(width, height, pt: QPointF, zero_offset_x=0.0, zero_offset_y=0.0):
- dx, dy = width / 2 + zero_offset_x, height / 2 + zero_offset_y
- return QPointF(pt.x() + dx, pt.y() + dy)
-
-
class EwRelativePosition(MyQtWidget, EwBasic):
target_changed = pyqtSignal(float, float, name='targetChanged')
scale_changed = pyqtSignal(float, name='zoomChanged')
@@ -55,16 +31,18 @@ def __init__(self, icon='aim-yellow'):
self.azimuth = 0.0
def draw(self, canvas, center, scale):
- [_x, _y] = to_cartesian(self.r, self.phi, scale)
+ [_x, _y] = to_cartesian(r=self.r, phi=self.phi, scale=scale)
self.svg_icon.setX(_x + center[0])
- self.svg_icon.setY(_y + center[1])
+ self.svg_icon.setY(-_y + center[1])
self.svg_icon.setRotation(self.azimuth)
canvas.setPen(QPen(Qt.darkGray, 1, Qt.DotLine, Qt.RoundCap))
- canvas.drawLine(translate_point(canvas.device().width(), canvas.device().height(),
+ canvas.drawLine(translate_point(canvas.device().width(),
+ canvas.device().height(),
QPointF(0, 0),
center[0], center[1]),
- translate_point(canvas.device().width(), canvas.device().height(),
- QPointF(_x, _y),
+ translate_point(canvas.device().width(),
+ canvas.device().height(),
+ QPointF(_x, -_y),
center[0], center[1]))
def __init__(self, data_sources: List[DataSourceBasic], fixed_size=None, **kwargs):
@@ -117,7 +95,8 @@ def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
self.zoom_out()
def _emit_target_chg(self, x, y):
- pt = rtranslate_point(self.rect().width(), self.rect().height(), QPointF(x, y), self.center[0], self.center[1])
+ pt = rev_translate_point(self.rect().width(), self.rect().height(), QPointF(x, y), self.center[0],
+ self.center[1])
[phi, r] = to_polar(pt.x(), pt.y(), scale=self.scale_m)
# noinspection PyUnresolvedReferences
self.target_changed.emit(phi, r)
@@ -175,6 +154,7 @@ def paintEvent(self, event):
def radraw_handler(self, vals: List[Union[float, int, str, NoDataStub]], vals_map: Dict):
count = len(vals) // ds_tuple_size
+
def check_stub(d):
return d if not isinstance(d, NoDataStub) else 0.0
diff --git a/pytools/ew/widgets.py b/pytools/ew/widgets.py
index f3c7206..92338c7 100644
--- a/pytools/ew/widgets.py
+++ b/pytools/ew/widgets.py
@@ -1,4 +1,4 @@
-import math
+import mymath
from abc import abstractmethod
from typing import List, Union, Dict, Tuple
@@ -456,11 +456,11 @@ def paintEvent(self, event):
_scaleX = _width / _originalWidth
_scaleY = _height / _originalHeight
- roll_rad = math.pi * self._roll / 180.0
+ roll_rad = mymath.pi * self._roll / 180.0
delta = 1.7 * self._pitch
- _faceDeltaX_new = _scaleX * delta * math.sin(roll_rad)
- _faceDeltaY_new = _scaleY * delta * math.cos(roll_rad)
+ _faceDeltaX_new = _scaleX * delta * mymath.sin(roll_rad)
+ _faceDeltaY_new = _scaleY * delta * mymath.cos(roll_rad)
offs_x = _faceDeltaX_new - self._faceDeltaX_old
offs_y = _faceDeltaY_new - self._faceDeltaY_old
diff --git a/pytools/images/vehicle/boat-white.svg b/pytools/images/vehicle/boat-white.svg
new file mode 100644
index 0000000..5dea1ca
--- /dev/null
+++ b/pytools/images/vehicle/boat-white.svg
@@ -0,0 +1,3 @@
+
diff --git a/pytools/images/vehicle/boat.svg b/pytools/images/vehicle/boat.svg
new file mode 100644
index 0000000..a65a5b5
--- /dev/null
+++ b/pytools/images/vehicle/boat.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/pytools/monitor.py b/pytools/monitor.py
index aa01b85..e864057 100644
--- a/pytools/monitor.py
+++ b/pytools/monitor.py
@@ -1,9 +1,8 @@
import sys
-import time
from typing import List
from PyQt5 import QtWidgets, QtCore
-from PyQt5.QtCore import QThread, QObject, pyqtSignal, QThreadPool, QRunnable, pyqtSlot
+from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QRunnable, pyqtSlot
from PyQt5.QtWidgets import QPushButton, QApplication
@@ -90,7 +89,8 @@ def run(self):
Your code goes in this function
"""
print("Thread start")
- self.job(self.process_events)
+ self.job()
+ self.process_events()
print("Thread finished")
diff --git a/pytools/monitor.py.orig b/pytools/monitor.py.orig
new file mode 100644
index 0000000..e1ca0a5
--- /dev/null
+++ b/pytools/monitor.py.orig
@@ -0,0 +1,184 @@
+import sys
+import time
+from typing import List
+
+from PyQt5 import QtWidgets, QtCore
+from PyQt5.QtCore import QThread, QObject, pyqtSignal, QThreadPool, QRunnable, pyqtSlot
+from PyQt5.QtWidgets import QPushButton, QApplication
+
+
+class ApplicationWindow(QtWidgets.QMainWindow):
+ def __init__(self, *, title="ESWB display", tabs=False):
+ super().__init__()
+
+ self.widgets = []
+
+ # self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self.setWindowTitle(title)
+
+ self.main_widget = QtWidgets.QWidget(self) if not tabs else QtWidgets.QTabWidget()
+ self.main_widget.setFocus()
+ self.setCentralWidget(self.main_widget)
+
+ self.main_layout = QtWidgets.QVBoxLayout(self.main_widget)
+
+ self.timer = QtCore.QTimer()
+ self.timer.setInterval(20)
+ self.timer.timeout.connect(self.redraw)
+ self.timer.start()
+
+ def add_ew(self, widget, stretch=0):
+ self.main_layout.addWidget(widget, stretch=stretch)
+
+ def reg_widget(self, widget):
+ self.widgets.append(widget)
+
+ def connect(self):
+ for w in self.widgets:
+ try:
+ w.connect()
+ except:
+ pass
+
+ def redraw(self):
+ for w in self.widgets:
+ w.redraw()
+
+
+class Tab(QtWidgets.QWidget):
+ def __init__(self, *, parent):
+ super().__init__()
+ self.parent = parent
+ self.main_layout = QtWidgets.QVBoxLayout(self)
+
+ def add_widget(self, w, stretch=0):
+ self.main_layout.addWidget(w, stretch=stretch)
+ self.parent.app_window.reg_widget(w)
+
+
+class WorkerSignals(QObject):
+ close = pyqtSignal()
+ error = pyqtSignal(tuple)
+ result = pyqtSignal(object)
+
+
+class Worker(QRunnable):
+ """
+ Worker thread
+ """
+ def __init__(self, job, sleep=0.1):
+ super().__init__()
+ self.job = job
+ self.sleep = sleep if sleep > 0.01 else 0.01
+ self.signals = WorkerSignals()
+ self.signals.close.connect(self.terminate)
+ self.finished = False
+
+ def terminate(self):
+ self.finished = True
+
+<<<<<<< HEAD
+=======
+ def process_events(self):
+ QApplication.processEvents()
+ if self.finished:
+ return False
+
+ return True
+
+>>>>>>> origin/dev
+ @pyqtSlot()
+ def run(self):
+ """
+ Your code goes in this function
+ """
+ print("Thread start")
+<<<<<<< HEAD
+ while True:
+ time.sleep(self.sleep)
+ self.job()
+ QApplication.processEvents()
+ if self.finished:
+ print("Thread finished")
+ break
+=======
+ self.job(self.process_events)
+ print("Thread finished")
+>>>>>>> origin/dev
+
+
+class Monitor:
+ def __init__(self, *, monitor_bus_name='monitor', argv=None, tabs=False):
+ super().__init__()
+ self.app = QtWidgets.QApplication(argv)
+ self.app_window = ApplicationWindow(tabs=tabs)
+ self.service_bus_name = monitor_bus_name
+ self.tab_widget = None
+ self.app.setStyleSheet("QLineEdit[readOnly=\"true\"] {color: #808080; background-color: #F0F0F0; padding: 0}")
+ self.app.aboutToQuit.connect(self.on_quit)
+ self.runnables: List[Worker] = []
+
+ def on_quit(self):
+ for r in self.runnables:
+ r.signals.close.emit()
+
+ def run_task(self, runnable: QRunnable):
+<<<<<<< HEAD
+ self.runnables.append(runnable);
+=======
+ self.runnables.append(runnable)
+>>>>>>> origin/dev
+ pool = QThreadPool.globalInstance()
+ pool.start(runnable)
+
+ def report_progress(self):
+ pass
+
+ def connect(self):
+ self.app_window.connect()
+
+ def show(self):
+ self.app_window.show()
+
+ def run(self):
+ self.connect()
+ self.show()
+ sys.exit(self.app.exec_())
+
+ def add_widget(self, w, stretch=0):
+ self.app_window.add_ew(w, stretch=stretch)
+ self.app_window.reg_widget(w)
+
+ def add_button(self, btn: QPushButton):
+ btn.setParent(self.app_window.main_widget)
+
+ def add_tab(self, name):
+ tab = Tab(parent=self)
+ self.app_window.main_widget.addTab(tab, name)
+ return tab
+
+
+class ArgParser:
+ def __init__(self):
+ import argparse
+
+ self.parser = argparse.ArgumentParser('ESWB monitor tool')
+
+ self.parser.add_argument(
+ '--serdev',
+ action='store',
+ default='/dev/ttyUSB0',
+ type=str,
+ help='Serial interface device path',
+ )
+
+ self.parser.add_argument(
+ '--serbaud',
+ action='store',
+ default='115200',
+ type=int,
+ help='Serial interface baudrate',
+
+ )
+
+ self.args = self.parser.parse_args(sys.argv[1:])
diff --git a/pytools/mymath/__init__.py b/pytools/mymath/__init__.py
new file mode 100644
index 0000000..1bb0f1f
--- /dev/null
+++ b/pytools/mymath/__init__.py
@@ -0,0 +1,8 @@
+from .core import *
+
+__all__ = [
+ "to_polar",
+ "to_cartesian",
+ "translate_point",
+ "rev_translate_point"
+]
diff --git a/pytools/mymath/core.py b/pytools/mymath/core.py
new file mode 100644
index 0000000..2b5ed07
--- /dev/null
+++ b/pytools/mymath/core.py
@@ -0,0 +1,28 @@
+import math
+
+from PyQt5.QtCore import QPointF
+
+
+def to_polar(x, y, scale=1.0, decimals=3):
+ pt = QPointF(x, y)
+ r = math.sqrt(pt.x() * pt.x() + pt.y() * pt.y()) / scale
+ degrees = math.degrees(math.atan2(-y, x)) + 90
+ phi = (degrees + 360) % 360
+ return [round(phi, decimals), round(r, decimals)]
+
+
+def to_cartesian(r, phi, scale=1.0, decimals=6):
+ _phi_rad = math.radians(phi)
+ _x = scale * r * math.sin(_phi_rad)
+ _y = scale * r * math.cos(_phi_rad)
+ return [round(_x, decimals), round(_y, decimals)]
+
+
+def translate_point(width, height, pt: QPointF, zero_offset_x=0.0, zero_offset_y=0.0):
+ dx, dy = width / 2 + zero_offset_x, height / 2 + zero_offset_y
+ return QPointF(pt.x() + dx, pt.y() + dy)
+
+
+def rev_translate_point(width, height, pt: QPointF, zero_offset_x=0.0, zero_offset_y=0.0):
+ dx, dy = width / 2 + zero_offset_x, height / 2 + zero_offset_y
+ return QPointF(pt.x() - dx, - (pt.y() - dy))
diff --git a/requirements.txt b/requirements.txt
index 7b92fe3..18e7b52 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,14 +1,19 @@
aiofiles==22.1.0
+attrs==22.2.0
branca==0.6.0
certifi==2022.12.7
charset-normalizer==3.0.1
+exceptiongroup==1.1.0
folium==0.14.0
httptools==0.5.0
idna==3.4
+iniconfig==2.0.0
Jinja2==3.1.2
MarkupSafe==2.1.2
multidict==6.0.4
numpy==1.24.1
+packaging==23.0
+pluggy==1.0.0
PyOpenGL==3.1.6
PyOpenGL-accelerate==3.1.6
PyQt5==5.15.7
@@ -18,9 +23,11 @@ PyQt5-stubs==5.15.6.0
pyqtgraph==0.13.1
PyQtWebEngine==5.15.6
PyQtWebEngine-Qt5==5.15.2
+pytest==7.2.1
requests==2.28.2
sanic==22.12.0
sanic-routing==22.8.0
+tomli==2.0.1
ujson==5.7.0
urllib3==1.26.14
uvloop==0.17.0
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29