not1mm 25.6.10__py3-none-any.whl → 25.6.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
not1mm/__main__.py CHANGED
@@ -69,6 +69,7 @@ import not1mm.fsutils as fsutils
69
69
  from not1mm.logwindow import LogWindow
70
70
  from not1mm.checkwindow import CheckWindow
71
71
  from not1mm.dxcc_tracker import DXCCWindow
72
+ from not1mm.rotator import RotatorWindow
72
73
  from not1mm.bandmap import BandMapWindow
73
74
  from not1mm.vfo import VfoWindow
74
75
  from not1mm.ratewindow import RateWindow
@@ -187,6 +188,8 @@ class MainWindow(QtWidgets.QMainWindow):
187
188
  rate_window = None
188
189
  statistics_window = None
189
190
  dxcc_window = None
191
+ rotator_window = None
192
+ voice_window = None
190
193
  settings = None
191
194
  lookup_service = None
192
195
  fldigi_util = None
@@ -311,6 +314,7 @@ class MainWindow(QtWidgets.QMainWindow):
311
314
  self.actionStatistics.triggered.connect(self.launch_stats_window)
312
315
  self.actionVFO.triggered.connect(self.launch_vfo)
313
316
  self.actionDXCC.triggered.connect(self.launch_dxcc_window)
317
+ self.actionRotator.triggered.connect(self.launch_rotator_window)
314
318
  self.actionRecalculate_Mults.triggered.connect(self.recalculate_mults)
315
319
  self.actionLoad_Call_History_File.triggered.connect(self.load_call_history)
316
320
 
@@ -679,6 +683,8 @@ class MainWindow(QtWidgets.QMainWindow):
679
683
  self.station = self.database.fetch_station()
680
684
  if self.station is None:
681
685
  self.station = {}
686
+ if self.rotator_window is not None:
687
+ self.rotator_window.set_mygrid(self.station.get("GridSquare", ""))
682
688
  self.contact = self.database.empty_contact.copy()
683
689
  self.current_op = self.station.get("Call", "")
684
690
  self.voice_process.current_op = self.current_op
@@ -750,6 +756,16 @@ class MainWindow(QtWidgets.QMainWindow):
750
756
  self.dxcc_window.hide()
751
757
  self.dxcc_window.message.connect(self.dockwidget_message)
752
758
 
759
+ self.show_splash_msg("Setting up RotatorWindow.")
760
+ self.rotator_window = RotatorWindow()
761
+ self.rotator_window.setObjectName("rotator-window")
762
+ if os.environ.get("WAYLAND_DISPLAY") and old_Qt is True:
763
+ self.rotator_window.setFeatures(dockfeatures)
764
+ self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.rotator_window)
765
+ self.rotator_window.hide()
766
+ self.rotator_window.message.connect(self.dockwidget_message)
767
+ self.rotator_window.set_mygrid(self.station.get("GridSquare", ""))
768
+
753
769
  self.show_splash_msg("Setting up VFOWindow.")
754
770
  self.vfo_window = VfoWindow()
755
771
  self.vfo_window.setObjectName("vfo-window")
@@ -823,11 +839,18 @@ class MainWindow(QtWidgets.QMainWindow):
823
839
  if self.actionDXCC.isChecked():
824
840
  self.dxcc_window.show()
825
841
  self.dxcc_window.setActive(True)
826
- # self.dxcc_window.get_run_and_total_qs()
827
842
  else:
828
843
  self.dxcc_window.hide()
829
844
  self.dxcc_window.setActive(False)
830
845
 
846
+ self.actionRotator.setChecked(self.pref.get("rotatorwindow", False))
847
+ if self.actionRotator.isChecked():
848
+ self.rotator_window.show()
849
+ self.rotator_window.setActive(True)
850
+ else:
851
+ self.rotator_window.hide()
852
+ self.rotator_window.setActive(False)
853
+
831
854
  self.actionVFO.setChecked(self.pref.get("vfowindow", False))
832
855
  if self.actionVFO.isChecked():
833
856
  self.vfo_window.show()
@@ -1236,7 +1259,10 @@ class MainWindow(QtWidgets.QMainWindow):
1236
1259
  self.heading_distance.setText(
1237
1260
  f"{grid} Hdg {heading}° LP {reciprocol(heading)}° / "
1238
1261
  f"distance {int(kilometers*0.621371)}mi {kilometers}km"
1262
+ f" {msg.get('result', {}).get('name_fmt', '')}"
1239
1263
  )
1264
+ print("Setting heading")
1265
+ self.rotator_window.set_requested_azimuth(float(heading))
1240
1266
 
1241
1267
  def cluster_expire_updated(self, number):
1242
1268
  """signal from bandmap"""
@@ -1631,6 +1657,8 @@ class MainWindow(QtWidgets.QMainWindow):
1631
1657
  self.station = self.database.fetch_station()
1632
1658
  if self.station is None:
1633
1659
  self.station = {}
1660
+ if self.rotator_window is not None:
1661
+ self.rotator_window.set_mygrid(self.station.get("GridSquare", ""))
1634
1662
  self.current_op = self.station.get("Call", "")
1635
1663
  self.voice_process.current_op = self.current_op
1636
1664
  self.make_op_dir()
@@ -1673,6 +1701,8 @@ class MainWindow(QtWidgets.QMainWindow):
1673
1701
  self.station = self.database.fetch_station()
1674
1702
  if self.station is None:
1675
1703
  self.station = {}
1704
+ if self.rotator_window is not None:
1705
+ self.rotator_window.set_mygrid(self.station.get("GridSquare", ""))
1676
1706
  if self.station.get("Call", "") == "":
1677
1707
  self.edit_station_settings()
1678
1708
  self.current_op = self.station.get("Call", "")
@@ -2245,11 +2275,21 @@ class MainWindow(QtWidgets.QMainWindow):
2245
2275
  if self.actionDXCC.isChecked():
2246
2276
  self.dxcc_window.show()
2247
2277
  self.dxcc_window.setActive(True)
2248
- # self.dxcc_window.get_run_and_total_qs()
2249
2278
  else:
2250
2279
  self.dxcc_window.hide()
2251
2280
  self.dxcc_window.setActive(False)
2252
2281
 
2282
+ def launch_rotator_window(self) -> None:
2283
+ """Launch the rotator window"""
2284
+ self.pref["rotatorwindow"] = self.actionRotator.isChecked()
2285
+ self.write_preference()
2286
+ if self.actionRotator.isChecked():
2287
+ self.rotator_window.show()
2288
+ self.rotator_window.setActive(True)
2289
+ else:
2290
+ self.rotator_window.hide()
2291
+ self.rotator_window.setActive(False)
2292
+
2253
2293
  def launch_vfo(self) -> None:
2254
2294
  """Launch the VFO window"""
2255
2295
  self.pref["vfowindow"] = self.actionVFO.isChecked()
@@ -3099,6 +3139,8 @@ class MainWindow(QtWidgets.QMainWindow):
3099
3139
  self.station["Club"] = self.settings_dialog.Club.text()
3100
3140
  self.station["Email"] = self.settings_dialog.Email.text()
3101
3141
  self.database.add_station(self.station)
3142
+ if self.rotator_window is not None:
3143
+ self.rotator_window.set_mygrid(self.settings_dialog.GridSquare.text())
3102
3144
  self.settings_dialog.close()
3103
3145
  if self.current_op == "":
3104
3146
  self.current_op = self.station.get("Call", "")
@@ -4060,6 +4102,8 @@ class MainWindow(QtWidgets.QMainWindow):
4060
4102
  f"Regional Hdg {heading}° LP {reciprocol(heading)}° / "
4061
4103
  f"distance {int(kilometers*0.621371)}mi {kilometers}km"
4062
4104
  )
4105
+ if self.rotator_window is not None:
4106
+ self.rotator_window.set_requested_azimuth(float(heading))
4063
4107
  self.contact["CountryPrefix"] = primary_pfx
4064
4108
  self.contact["ZN"] = int(cq)
4065
4109
  if self.contest:
not1mm/data/main.ui CHANGED
@@ -1573,6 +1573,7 @@
1573
1573
  <addaction name="actionStatistics"/>
1574
1574
  <addaction name="actionVFO"/>
1575
1575
  <addaction name="actionDXCC"/>
1576
+ <addaction name="actionRotator"/>
1576
1577
  </widget>
1577
1578
  <widget class="QMenu" name="menuOther">
1578
1579
  <property name="title">
@@ -2144,6 +2145,17 @@
2144
2145
  <string>Alt+D</string>
2145
2146
  </property>
2146
2147
  </action>
2148
+ <action name="actionRotator">
2149
+ <property name="checkable">
2150
+ <bool>true</bool>
2151
+ </property>
2152
+ <property name="text">
2153
+ <string>Rotator</string>
2154
+ </property>
2155
+ <property name="shortcut">
2156
+ <string>Alt+P</string>
2157
+ </property>
2158
+ </action>
2147
2159
  </widget>
2148
2160
  <resources/>
2149
2161
  <connections/>
not1mm/data/rotator.ui ADDED
@@ -0,0 +1,144 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ui version="4.0">
3
+ <class>RotatorWidget</class>
4
+ <widget class="QDockWidget" name="RotatorWidget">
5
+ <property name="geometry">
6
+ <rect>
7
+ <x>0</x>
8
+ <y>0</y>
9
+ <width>352</width>
10
+ <height>248</height>
11
+ </rect>
12
+ </property>
13
+ <property name="windowTitle">
14
+ <string>Rotator</string>
15
+ </property>
16
+ <widget class="QWidget" name="centralwidget">
17
+ <layout class="QVBoxLayout" name="verticalLayout">
18
+ <property name="leftMargin">
19
+ <number>0</number>
20
+ </property>
21
+ <property name="topMargin">
22
+ <number>0</number>
23
+ </property>
24
+ <property name="rightMargin">
25
+ <number>0</number>
26
+ </property>
27
+ <property name="bottomMargin">
28
+ <number>0</number>
29
+ </property>
30
+ <item>
31
+ <layout class="QHBoxLayout" name="horizontalLayout">
32
+ <item>
33
+ <widget class="QPushButton" name="move_button">
34
+ <property name="focusPolicy">
35
+ <enum>Qt::FocusPolicy::NoFocus</enum>
36
+ </property>
37
+ <property name="text">
38
+ <string>Move</string>
39
+ </property>
40
+ </widget>
41
+ </item>
42
+ </layout>
43
+ </item>
44
+ <item>
45
+ <widget class="QGraphicsView" name="compassView">
46
+ <property name="focusPolicy">
47
+ <enum>Qt::FocusPolicy::ClickFocus</enum>
48
+ </property>
49
+ <property name="frameShape">
50
+ <enum>QFrame::Shape::NoFrame</enum>
51
+ </property>
52
+ <property name="frameShadow">
53
+ <enum>QFrame::Shadow::Plain</enum>
54
+ </property>
55
+ <property name="renderHints">
56
+ <set>QPainter::RenderHint::Antialiasing|QPainter::RenderHint::SmoothPixmapTransform|QPainter::RenderHint::TextAntialiasing</set>
57
+ </property>
58
+ </widget>
59
+ </item>
60
+ <item>
61
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
62
+ <item>
63
+ <spacer name="horizontalSpacer">
64
+ <property name="orientation">
65
+ <enum>Qt::Orientation::Horizontal</enum>
66
+ </property>
67
+ <property name="sizeHint" stdset="0">
68
+ <size>
69
+ <width>40</width>
70
+ <height>20</height>
71
+ </size>
72
+ </property>
73
+ </spacer>
74
+ </item>
75
+ <item>
76
+ <widget class="QPushButton" name="north_button">
77
+ <property name="focusPolicy">
78
+ <enum>Qt::FocusPolicy::NoFocus</enum>
79
+ </property>
80
+ <property name="text">
81
+ <string>N</string>
82
+ </property>
83
+ <property name="iconSize">
84
+ <size>
85
+ <width>16</width>
86
+ <height>16</height>
87
+ </size>
88
+ </property>
89
+ <property name="flat">
90
+ <bool>false</bool>
91
+ </property>
92
+ </widget>
93
+ </item>
94
+ <item>
95
+ <widget class="QPushButton" name="south_button">
96
+ <property name="focusPolicy">
97
+ <enum>Qt::FocusPolicy::NoFocus</enum>
98
+ </property>
99
+ <property name="text">
100
+ <string>S</string>
101
+ </property>
102
+ </widget>
103
+ </item>
104
+ <item>
105
+ <widget class="QPushButton" name="west_button">
106
+ <property name="focusPolicy">
107
+ <enum>Qt::FocusPolicy::NoFocus</enum>
108
+ </property>
109
+ <property name="text">
110
+ <string>W</string>
111
+ </property>
112
+ </widget>
113
+ </item>
114
+ <item>
115
+ <widget class="QPushButton" name="east_button">
116
+ <property name="focusPolicy">
117
+ <enum>Qt::FocusPolicy::NoFocus</enum>
118
+ </property>
119
+ <property name="text">
120
+ <string>E</string>
121
+ </property>
122
+ </widget>
123
+ </item>
124
+ <item>
125
+ <spacer name="horizontalSpacer_2">
126
+ <property name="orientation">
127
+ <enum>Qt::Orientation::Horizontal</enum>
128
+ </property>
129
+ <property name="sizeHint" stdset="0">
130
+ <size>
131
+ <width>40</width>
132
+ <height>20</height>
133
+ </size>
134
+ </property>
135
+ </spacer>
136
+ </item>
137
+ </layout>
138
+ </item>
139
+ </layout>
140
+ </widget>
141
+ </widget>
142
+ <resources/>
143
+ <connections/>
144
+ </ui>
@@ -0,0 +1,101 @@
1
+ import socket
2
+ import logging
3
+
4
+ if __name__ == "__main__":
5
+ print("I'm not the program you are looking for.")
6
+
7
+ logger = logging.getLogger("rot_interface")
8
+
9
+
10
+ class RotatorInterface:
11
+ """
12
+ A class to interface with a rotator control program (like rotctld).
13
+ """
14
+
15
+ def __init__(self, host="127.0.0.1", port=4533):
16
+ self.host = host
17
+ self.port = port
18
+ self.socket = None
19
+ self.connected = False
20
+ self.connect()
21
+
22
+ def connect(self):
23
+ """Connect to the rotator control program."""
24
+ try:
25
+ self.socket = socket.create_connection((self.host, self.port), timeout=1)
26
+ self.connected = True
27
+ logger.info(f"Connected to rotator at {self.host}:{self.port}")
28
+ except (socket.timeout, ConnectionRefusedError, OSError) as e:
29
+ self.connected = False
30
+ logger.warning(
31
+ f"Failed to connect to rotator at {self.host}:{self.port}: {e}"
32
+ )
33
+ self.socket = None
34
+
35
+ def disconnect(self):
36
+ """Disconnect from the rotator control program."""
37
+ if self.socket:
38
+ try:
39
+ self.socket.close()
40
+ logger.info("Disconnected from rotator")
41
+ except OSError as e:
42
+ logger.warning(f"Error closing rotator socket: {e}")
43
+ self.socket = None
44
+ self.connected = False
45
+
46
+ def send_command(self, command):
47
+ """Send a command to the rotator control program and return the response."""
48
+ if not self.connected or not self.socket:
49
+ self.connect()
50
+ if not self.connected or not self.socket:
51
+ logger.warning("Not connected to rotator. Command not sent.")
52
+ return None
53
+
54
+ try:
55
+ self.socket.sendall((command + "\n").encode())
56
+ response = self.socket.recv(1024).decode().strip()
57
+ logger.debug(f"Sent: {command}, Received: {response}")
58
+ return response
59
+ except OSError as e:
60
+ logger.warning(f"Error sending command to rotator: {e}")
61
+ self.disconnect()
62
+ return None
63
+
64
+ def get_position(self):
65
+ """Get the current azimuth and elevation from the rotator."""
66
+ response = self.send_command("p")
67
+ logger.debug(f"get_position response: {response}")
68
+ if response:
69
+ if response == "RPRT -1":
70
+ return None, None
71
+ try:
72
+ azimuth, elevation = map(float, response.split("\n"))
73
+ return azimuth, elevation
74
+ except ValueError:
75
+ logger.warning(f"Invalid response from rotator: {response}")
76
+ return None, None
77
+
78
+ def set_position(self, azimuth, elevation=0.0) -> bool:
79
+ """Set the azimuth and elevation on the rotator."""
80
+ response = self.send_command(f"P {azimuth} {elevation}")
81
+ return response is not None
82
+
83
+ def park_rotator(self) -> bool:
84
+ """Park the rotator."""
85
+ response = self.send_command("K")
86
+ return response is not None
87
+
88
+ def reset_rotator(self) -> bool:
89
+ """Reset the rotator."""
90
+ response = self.send_command("R")
91
+ return response is not None
92
+
93
+ def move_rotator(self, direction, speed) -> bool:
94
+ """Move the rotator in the specified direction at the specified speed."""
95
+ response = self.send_command(f"M {direction} {speed}")
96
+ return response is not None
97
+
98
+ def stop_rotator(self) -> bool:
99
+ """Stop the rotator."""
100
+ response = self.send_command("S")
101
+ return response is not None
not1mm/lib/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """It's the version"""
2
2
 
3
- __version__ = "25.6.10"
3
+ __version__ = "25.6.11"
not1mm/rotator.py ADDED
@@ -0,0 +1,323 @@
1
+ from PyQt6.QtWidgets import QDockWidget, QGraphicsScene
2
+
3
+ from PyQt6.QtGui import (
4
+ QImage,
5
+ QColor,
6
+ QPixmap,
7
+ QPen,
8
+ QBrush,
9
+ QPainterPath,
10
+ QShowEvent,
11
+ QResizeEvent,
12
+ )
13
+
14
+ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
15
+ from PyQt6 import uic
16
+
17
+ from not1mm.lib.rot_interface import RotatorInterface
18
+ import not1mm.fsutils as fsutils
19
+ import math
20
+ import logging
21
+ import os
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class RotatorWindow(QDockWidget):
27
+ message = pyqtSignal(dict)
28
+ pref = {}
29
+ MAP_RESOLUTION = 600
30
+ GLOBE_RADIUS = 100.0
31
+ AZIMUTH_DEAD_BAND = 2
32
+ requestedAzimuthNeedle = None
33
+ antennaNeedle = None
34
+
35
+ def __init__(self):
36
+ super().__init__()
37
+ self.active = False
38
+ self.compassScene = None
39
+ self.mygrid = "DM13at"
40
+ self.requestedAzimuth = None
41
+ self.antennaAzimuth = None
42
+ uic.loadUi(fsutils.APP_DATA_PATH / "rotator.ui", self)
43
+ self.north_button.clicked.connect(self.set_north_azimuth)
44
+ self.south_button.clicked.connect(self.set_south_azimuth)
45
+ self.east_button.clicked.connect(self.set_east_azimuth)
46
+ self.west_button.clicked.connect(self.set_west_azimuth)
47
+ self.move_button.clicked.connect(self.the_eye_of_sauron)
48
+ self.redrawMap()
49
+ self.rotator = RotatorInterface()
50
+ self.antennaAzimuth, _ = self.rotator.get_position()
51
+ self.set_antenna_azimuth(self.antennaAzimuth)
52
+ self.watch_timer = QTimer()
53
+ self.watch_timer.timeout.connect(self.check_rotator)
54
+ self.watch_timer.start(1000)
55
+
56
+ def msg_from_main(self, msg: dict) -> None:
57
+ """"""
58
+ if self.active is True and isinstance(msg, dict):
59
+ if msg.get("cmd", "") in ("UPDATELOG", "CONTACTCHANGED", "DELETED"):
60
+ ...
61
+ if msg.get("cmd", "") == "NEWDB":
62
+ ...
63
+
64
+ def set_mygrid(self, mygrid: str) -> None:
65
+ """"""
66
+ if isinstance(mygrid, str):
67
+ self.mygrid = mygrid
68
+ self.redrawMap()
69
+
70
+ def setActive(self, active: bool) -> None:
71
+ """"""
72
+ if isinstance(active, bool):
73
+ self.active = active
74
+
75
+ def set_requested_azimuth(self, azimuth: float) -> None:
76
+ if isinstance(azimuth, float):
77
+ self.requestedAzimuth = azimuth
78
+ self.requestedAzimuthNeedle.setRotation(self.requestedAzimuth)
79
+ self.requestedAzimuthNeedle.show()
80
+ else:
81
+ self.requestedAzimuthNeedle.hide()
82
+
83
+ def set_antenna_azimuth(self, azimuth: float) -> None:
84
+ if isinstance(azimuth, float):
85
+ self.antennaAzimuth = azimuth
86
+ self.antennaNeedle.setRotation(self.antennaAzimuth)
87
+ self.antennaNeedle.show()
88
+ else:
89
+ self.antennaNeedle.hide()
90
+
91
+ def set_north_azimuth(self) -> None:
92
+ """Point the antenna North."""
93
+ if self.rotator.connected:
94
+ self.rotator.set_position(0.0)
95
+
96
+ def set_south_azimuth(self) -> None:
97
+ """Point the antenna South."""
98
+ if self.rotator.connected:
99
+ self.rotator.set_position(180.0)
100
+
101
+ def set_east_azimuth(self) -> None:
102
+ """Point the antenna East."""
103
+ if self.rotator.connected:
104
+ self.rotator.set_position(90.0)
105
+
106
+ def set_west_azimuth(self) -> None:
107
+ """Point the antenna West."""
108
+ if self.rotator.connected:
109
+ self.rotator.set_position(270.0)
110
+
111
+ def the_eye_of_sauron(self) -> None:
112
+ """Move the antennas azimuth to match the contacts."""
113
+ if self.rotator.connected:
114
+ self.rotator.set_position(self.requestedAzimuth)
115
+
116
+ def redrawMap(self) -> None:
117
+ """"""
118
+ self.compassScene = QGraphicsScene()
119
+ self.compassView.setScene(self.compassScene)
120
+ self.compassView.setStyleSheet("background-color: transparent;")
121
+ file = fsutils.APP_DATA_PATH / "nasabluemarble.jpg"
122
+ source = QImage()
123
+ source.load(str(file))
124
+ the_map = QImage(
125
+ self.MAP_RESOLUTION, self.MAP_RESOLUTION, QImage.Format.Format_ARGB32
126
+ )
127
+ the_map.fill(QColor(0, 0, 0, 0))
128
+ lat, lon = self.gridtolatlon(self.mygrid)
129
+
130
+ if os.path.exists(f"{fsutils.USER_DATA_PATH}/{self.mygrid}.png"):
131
+ the_map.load(f"{fsutils.USER_DATA_PATH}/{self.mygrid}.png")
132
+ else:
133
+ the_map = self.equirectangular_to_azimuthal_equidistant(
134
+ source, lat, lon, output_size=self.MAP_RESOLUTION
135
+ )
136
+
137
+ pixMapItem = self.compassScene.addPixmap(QPixmap.fromImage(the_map))
138
+ pixMapItem.moveBy(-self.MAP_RESOLUTION / 2, -self.MAP_RESOLUTION / 2)
139
+ pixMapItem.setTransformOriginPoint(
140
+ self.MAP_RESOLUTION / 2, self.MAP_RESOLUTION / 2
141
+ )
142
+ pixMapItem.setScale(self.GLOBE_RADIUS * 2 / self.MAP_RESOLUTION)
143
+ self.compassScene.addEllipse(
144
+ -100,
145
+ -100,
146
+ self.GLOBE_RADIUS * 2,
147
+ self.GLOBE_RADIUS * 2,
148
+ QPen(QColor(100, 100, 100)),
149
+ QBrush(QColor(0, 0, 0), Qt.BrushStyle.NoBrush),
150
+ )
151
+ self.compassScene.addEllipse(
152
+ -1,
153
+ -1,
154
+ 2,
155
+ 2,
156
+ QPen(Qt.PenStyle.NoPen),
157
+ QBrush(QColor(0, 0, 0), Qt.BrushStyle.SolidPattern),
158
+ )
159
+ path = QPainterPath()
160
+ path.lineTo(-2, 0)
161
+ path.lineTo(0, -90)
162
+ path.lineTo(2, 0)
163
+ path.closeSubpath()
164
+
165
+ path2 = QPainterPath()
166
+ path2.lineTo(-1, 0)
167
+ path2.lineTo(0, -90)
168
+ path2.lineTo(1, 0)
169
+ path2.closeSubpath()
170
+
171
+ self.requestedAzimuthNeedle = self.compassScene.addPath(
172
+ path,
173
+ QPen(QColor(0, 0, 0, 150)),
174
+ QBrush(QColor(255, 0, 0), Qt.BrushStyle.SolidPattern),
175
+ )
176
+
177
+ if isinstance(self.requestedAzimuth, float):
178
+ self.requestedAzimuthNeedle.setRotation(self.requestedAzimuth)
179
+ self.requestedAzimuthNeedle.show()
180
+ else:
181
+ self.requestedAzimuthNeedle.hide()
182
+
183
+ self.antennaNeedle = self.compassScene.addPath(
184
+ path,
185
+ QPen(QColor(0, 0, 0, 150)),
186
+ QBrush(QColor(255, 191, 0), Qt.BrushStyle.SolidPattern),
187
+ )
188
+ if isinstance(self.antennaAzimuth, float):
189
+ self.antennaNeedle.setRotation(self.antennaAzimuth)
190
+ self.antennaNeedle.show()
191
+ else:
192
+ self.antennaNeedle.hide()
193
+
194
+ def gridtolatlon(self, maiden: str) -> tuple[float, float]:
195
+ """
196
+ Converts a maidenhead gridsquare to a latitude longitude pair.
197
+ """
198
+ try:
199
+ maiden = str(maiden).strip().upper()
200
+
201
+ chars_in_grid_square = len(maiden)
202
+ if not 8 >= chars_in_grid_square >= 2 and chars_in_grid_square % 2 == 0:
203
+ return 0, 0
204
+ lon = (ord(maiden[0]) - 65) * 20 - 180
205
+ lat = (ord(maiden[1]) - 65) * 10 - 90
206
+ if chars_in_grid_square >= 4:
207
+ lon += (ord(maiden[2]) - 48) * 2
208
+ lat += (ord(maiden[3]) - 48) * 1
209
+ if chars_in_grid_square >= 6:
210
+ lon += (ord(maiden[4]) - 65) * (5.0 / 60.0)
211
+ lat += (ord(maiden[5]) - 65) * (2.5 / 60.0)
212
+ if chars_in_grid_square >= 8:
213
+ lon += (ord(maiden[6]) - 48) * (30.0 / 3600.0)
214
+ lat += (ord(maiden[7]) - 48) * (15.0 / 3600.0)
215
+ lon += 15.0 / 3600.0
216
+ lat += 7.5 / 3600.0
217
+ else:
218
+ lon += 2.5 / 60.0
219
+ lat += 1.25 / 60.0
220
+ else:
221
+ lon += 1
222
+ lat += 0.5
223
+ else:
224
+ lon += 10
225
+ lat += 5
226
+
227
+ return lat, lon
228
+ except IndexError:
229
+ return 0.0, 0.0
230
+
231
+ def equirectangular_to_azimuthal_equidistant(
232
+ self,
233
+ source_img: QImage,
234
+ center_lat_deg: float,
235
+ center_lon_deg: float,
236
+ output_size: int = 600,
237
+ ) -> QImage:
238
+ """This does some super magic"""
239
+
240
+ width, height = source_img.width(), source_img.height()
241
+ dest_img = QImage(output_size, output_size, QImage.Format.Format_ARGB32)
242
+
243
+ # Convert center point to radians
244
+ lat0 = math.radians(center_lat_deg)
245
+ lon0 = math.radians(center_lon_deg)
246
+
247
+ sin_lat = math.sin(lat0)
248
+ cos_lat = math.cos(lat0)
249
+
250
+ R = output_size / 2
251
+ for y in range(output_size):
252
+ for x in range(output_size):
253
+ # Convert pixel coordinate to normalized cartesian coordinate in range [-1, 1]
254
+ dx = (x - R) / R
255
+ dy = (R - y) / R # Invert Y axis to have +Y upwards
256
+
257
+ rho = math.sqrt(dx * dx + dy * dy)
258
+ if rho > 1.0:
259
+ # Outside the projection circle
260
+ dest_img.setPixelColor(x, y, QColor(0, 0, 0, 0))
261
+ continue
262
+
263
+ c = (
264
+ rho * math.pi
265
+ ) # scale rho to angular distance (c = great-circle distance)
266
+
267
+ if rho == 0:
268
+ lat = lat0
269
+ lon = lon0
270
+ else:
271
+ sin_c = math.sin(c)
272
+ cos_c = math.cos(c)
273
+
274
+ lat = math.asin(cos_c * sin_lat + (dy * sin_c * cos_lat / rho))
275
+
276
+ lon = lon0 + math.atan2(
277
+ dx * sin_c,
278
+ rho * cos_lat * cos_c - dy * sin_lat * sin_c,
279
+ )
280
+
281
+ # Map lat/lon to source image pixel coordinates (equirectangular)
282
+ lon_deg = math.degrees(lon)
283
+ lat_deg = math.degrees(lat)
284
+
285
+ # Wrap longitude to [-180, 180]
286
+ if lon_deg < -180:
287
+ lon_deg += 360
288
+ elif lon_deg > 180:
289
+ lon_deg -= 360
290
+
291
+ # Map to source image coordinates
292
+ src_x = int(((lon_deg + 180.0) / 360.0) * width)
293
+ src_y = int(((90 - lat_deg) / 180.0) * height)
294
+
295
+ # Clamp coordinates
296
+ src_x = max(0, min(width - 1, src_x))
297
+ src_y = max(0, min(height - 1, src_y))
298
+
299
+ color = source_img.pixelColor(src_x, src_y)
300
+ dest_img.setPixelColor(x, y, color)
301
+
302
+ dest_img.save(f"{fsutils.USER_DATA_PATH}/{self.mygrid}.png", "PNG")
303
+ return dest_img
304
+
305
+ def showEvent(self, event: QShowEvent) -> None:
306
+ """Make the globe fit in the widget when widget is shown."""
307
+ self.compassView.fitInView(
308
+ self.compassScene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio
309
+ )
310
+
311
+ def resizeEvent(self, event: QResizeEvent) -> None:
312
+ """Make globe fit in widget when widget is resized."""
313
+ self.compassView.fitInView(
314
+ self.compassScene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio
315
+ )
316
+
317
+ def check_rotator(self) -> None:
318
+ """Check the rotator"""
319
+ if self.rotator.connected:
320
+ self.antennaAzimuth, _ = self.rotator.get_position()
321
+ self.set_antenna_azimuth(self.antennaAzimuth)
322
+ else:
323
+ self.rotator.connect()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: not1mm
3
- Version: 25.6.10
3
+ Version: 25.6.11
4
4
  Summary: NOT1MM Logger
5
5
  Author-email: Michael Bridak <michael.bridak@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -108,6 +108,7 @@ Dynamic: license-file
108
108
  - [The Check Partial Window](#the-check-partial-window)
109
109
  - [The Rate Window](#the-rate-window)
110
110
  - [The DXCC window](#the-dxcc-window)
111
+ - [The Rotator Window (Work In Progress)](#the-rotator-window-work-in-progress)
111
112
  - [The Remote VFO Window](#the-remote-vfo-window)
112
113
  - [Cabrillo](#cabrillo)
113
114
  - [ADIF](#adif)
@@ -245,6 +246,7 @@ generated, 'cause I'm lazy, list of those who've submitted PR's.
245
246
 
246
247
  ## Recent Changes
247
248
 
249
+ - [25-6-11] Added a rotator control widget.
248
250
  - [25-6-10] Merged PR from @dj1yfk correcting WPX prefix calculation.
249
251
  - [25-6-8] Revmoved SQLite WAL mode.
250
252
  - Rewrote DXCC tracker.
@@ -385,8 +387,8 @@ Under the `CW` tab, there are three options: i) `cwdaemon` that normally uses IP
385
387
  iii) `CAT` that sends Morse characters via rigctld if your radio supports it.
386
388
 
387
389
  For contests that require a serial number as part of the exchange, there is an option to pad it with leading zeroes,
388
- typically represented by the cut number "T". For example, serial number "001" can be sent as "TT1". The user can
389
- configure the `CW Sent Nr Padding` character (default: T) and `CW Sent Nr Padding
390
+ typically represented by the cut number "T". For example, serial number "001" can be sent as "TT1". The user can
391
+ configure the `CW Sent Nr Padding` character (default: T) and `CW Sent Nr Padding
390
392
  Length` (default: 3) or specify no padding by entering length "0".
391
393
 
392
394
  ### Cluster
@@ -696,6 +698,18 @@ This window shows you a grid of DXCC entities you've aquired and on what bands.
696
698
 
697
699
  ![DXCC Window](https://github.com/mbridak/not1mm/raw/master/pic/dxcc_window.png)
698
700
 
701
+ ### The Rotator Window (Work In Progress)
702
+
703
+ `Window`>`Rotator`
704
+
705
+ The Rotator window is a work in progress. Currently it blindly connects to a rigctld
706
+ instance on it's default port of 4533.
707
+
708
+ ![Rotator Window](https://github.com/mbridak/not1mm/raw/master/pic/rotator_window.png)
709
+
710
+ I myself don't have a rotator to test with. I'm a QRP wires in the trees, if only I
711
+ had a tree, kind of guy. So we're kind of hoping this works. If not, don't use it.
712
+
699
713
  ### The Remote VFO Window
700
714
 
701
715
  You can control the VFO on a remote rig by following the directions listed in
@@ -1,5 +1,5 @@
1
1
  not1mm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- not1mm/__main__.py,sha256=IHFJFKGAgKyPZUdi-aRDFIS--XJPdoPiV6utXx31_-I,172631
2
+ not1mm/__main__.py,sha256=9ogJCPQExyZeOH0uNXmDbo9TSRp5YL_M5XqRm6z2vEs,174810
3
3
  not1mm/bandmap.py,sha256=0JmZ32UvkaPjXs2xTgowX1GLvZo5zHU_Zo9y_GL-On4,31139
4
4
  not1mm/checkwindow.py,sha256=zEHlw40j6Wr3rvKbCQf2lcezCoiZqaBqEvBjQU5aKW0,7630
5
5
  not1mm/dxcc_tracker.py,sha256=LUTt538mBjOr590WMby-hmRz47XG8xUCkU2x0j4oB4w,4306
@@ -8,6 +8,7 @@ not1mm/logwindow.py,sha256=O2dMaT_BYWsXA_dxsEHN92JwN-qVGy9nmH0MCMaG9gY,42830
8
8
  not1mm/lookupservice.py,sha256=GkY_qHZfrW6XHf8upIoaG4hCFqm0fg6Ganu9ConGrIc,2628
9
9
  not1mm/radio.py,sha256=4Lysf9BY3vdtYCHwKfzO5WN7IGyh4_lKSVuQ6F4Z08g,5536
10
10
  not1mm/ratewindow.py,sha256=iBjqdOetIEX0wSwdGM89Ibt4gVlFdE-K8HQPnkVPVOg,6965
11
+ not1mm/rotator.py,sha256=iQ3baA7pJog0NLRAvc7gBWCFRpGRckFu9p97AJIcBl4,11204
11
12
  not1mm/rtc_service.py,sha256=axAwnCBuTr-QL0YwXtWvg9tjwhcFsiiEZFgFjOofX6k,2816
12
13
  not1mm/statistics.py,sha256=eOmUvbbYdbbIYHmaEhtBGab1IxAf2RQYV1q9MItpqEM,7969
13
14
  not1mm/test.py,sha256=WhL0DLlJTD15aON8Dkf2q_tlP_X1juxKZJh0jEC99tU,154
@@ -37,7 +38,7 @@ not1mm/data/k6gte.not1mm-32.png,sha256=XdTsCa3xqwTfn26Ga7RwO_Vlbg_77RKkSc8bMxVcC
37
38
  not1mm/data/k6gte.not1mm-64.png,sha256=6ku45Gq1g5ezh04F07osoKRtanb3e4kbx5XdIEh3N90,2925
38
39
  not1mm/data/logwindow.ui,sha256=vfkNdzJgFs3tTOBKLDavF2zVMvNHWOZ82fAErRi6pQY,1436
39
40
  not1mm/data/logwindowx.ui,sha256=9FzDJtLRpagvAWcDjFdB9NnvNZ4bVxdTNHy1Jit2ido,1610
40
- not1mm/data/main.ui,sha256=lr3_rwmyywfYQtw9GNf1leeJ0fe6Y2Xy3-cdV4kmKi4,65425
41
+ not1mm/data/main.ui,sha256=2cGrhepmcTylTWJ2LwisW6q9nZsReOH38yThvUn_31g,65717
41
42
  not1mm/data/new_contest.ui,sha256=i-WThxa9IBstyAMCRtNMB4NVPpV3fekUIX1YDYPyuOQ,25521
42
43
  not1mm/data/not1mm.html,sha256=c9-mfjMwDt4f5pySUruz2gREW33CQ2_rCddM2z5CZQo,23273
43
44
  not1mm/data/opon.ui,sha256=mC4OhoVIfR1H9IqHAKXliPMm8VOVmxSEadpsFQ7XnS4,2247
@@ -47,6 +48,7 @@ not1mm/data/radio_grey.png,sha256=9eOtMHDpQvRYY29D7_vPeacWbwotRXZTMm8EiHE9TW0,12
47
48
  not1mm/data/radio_red.png,sha256=QvkMk7thd_hCEIyK5xGAG4xVVXivl39nwOfD8USDI20,957
48
49
  not1mm/data/ratewindow.ui,sha256=c0gikcZQYWuGwWdFE1PGcRbeJ9nTUPIRkOozWo2FQw8,11541
49
50
  not1mm/data/reddot.png,sha256=M33jEMoU8W4rQ4_MVyzzKxDPDte1ypKBch5VnUMNLKE,565
51
+ not1mm/data/rotator.ui,sha256=GcyEsb1TOQwp4g02jfCxsQRr8WkC7L4Bm5TTyvkk7hE,3935
50
52
  not1mm/data/rttymacros.txt,sha256=FQ2BnAChXF5w-tzmMnBOE8IgvviAEsd3cmmz4b8GOPk,467
51
53
  not1mm/data/settings.ui,sha256=0nAdg4hDv37x0RL5G0Cc1L8p-F95YtkOjIGqlCGu0Fs,40073
52
54
  not1mm/data/splash.png,sha256=85_BQotR1q24uCthrKm4SB_6ZOMwRjR-Jdp1XBHSTyg,5368
@@ -119,10 +121,11 @@ not1mm/lib/multicast.py,sha256=KJcruI-bOuHfHXPjl3SGQhL6I9sKrygy-sdFSvxffUM,3255
119
121
  not1mm/lib/n1mm.py,sha256=H54mpgJF0GAmKavM-nb5OAq2SJFWYkux4eMWWiSRxJc,6288
120
122
  not1mm/lib/new_contest.py,sha256=IznTDMq7yXHB6zBoGUEC_WDYPCPpsSZW4wwMJi16zK0,816
121
123
  not1mm/lib/plugin_common.py,sha256=nqiUq11T9Wz8RDrRen4Zvp-KXVWUYcIp5JPZwqmu2Oo,13913
124
+ not1mm/lib/rot_interface.py,sha256=kGhpRsxPa6MYDaHd7gLJfzgsJKiJ-qHyx8tJ2tWwwlk,3589
122
125
  not1mm/lib/select_contest.py,sha256=WsptLuwkouIHeocJL3oZ6-eUfEnhpwdc-x7eMZ_TIVM,359
123
126
  not1mm/lib/settings.py,sha256=5xnsagH48qGeCDhfxPWW9yaXtv8wT13yoIVvYt8h_Qs,16023
124
127
  not1mm/lib/super_check_partial.py,sha256=jX7DjHesEV4KNVQbddJui0wAsYHerikH7W0iPv7PXQw,3110
125
- not1mm/lib/version.py,sha256=sXRrPfTyvfpaX9z-c1mGF5B_4fqe5Zv7Ppj4gk6oCG4,48
128
+ not1mm/lib/version.py,sha256=gBX35fdqUs73XC5HMduJfXsYIvynpAIxe65kdBhhSEE,48
126
129
  not1mm/lib/versiontest.py,sha256=8vDNptuBBunn-1IGkjNaquehqBYUJyjrPSF8Igmd4_Y,1286
127
130
  not1mm/plugins/10_10_fall_cw.py,sha256=oJh3JKqjOpnWElSlZpiQ631UnaOd8qra5s9bl_QoInk,14783
128
131
  not1mm/plugins/10_10_spring_cw.py,sha256=p7dSDtbFK0e6Xouw2V6swYn3VFVgHKyx4IfRWyBjMZY,14786
@@ -186,9 +189,9 @@ not1mm/plugins/ukeidx.py,sha256=ZsIFXgOSwjuKNmN4W_C0TAgGqgnabJGNLMHwGkl3_bk,1910
186
189
  not1mm/plugins/vhf_sprint.py,sha256=a9QFTpv8XUbZ_GLjdVCh7svykFa-gXOWwKFZ6MD3uQM,19289
187
190
  not1mm/plugins/weekly_rtty.py,sha256=C8Xs3Q5UgSYx-mFFar8BVARWtmqlyrbeC98Ubzb4UN8,20128
188
191
  not1mm/plugins/winter_field_day.py,sha256=hmAMgkdqIXtnCNyUp8J9Bb8liN8wj10wps6ROuG-Bok,15284
189
- not1mm-25.6.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
190
- not1mm-25.6.10.dist-info/METADATA,sha256=PgwRNo6RTuKpAR6IKbPGFhVQ4CGN14wxhPu_480m7MU,35108
191
- not1mm-25.6.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
192
- not1mm-25.6.10.dist-info/entry_points.txt,sha256=pMcZk_0dxFgLkcUkF0Q874ojpwOmF3OL6EKw9LgvocM,47
193
- not1mm-25.6.10.dist-info/top_level.txt,sha256=0YmTxEcDzQlzXub-lXASvoLpg_mt1c2thb5cVkDf5J4,7
194
- not1mm-25.6.10.dist-info/RECORD,,
192
+ not1mm-25.6.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
193
+ not1mm-25.6.11.dist-info/METADATA,sha256=qI3TGssUIO-II6kzwzQDNbcWJjvhikRRwt6oUXA-5CA,35678
194
+ not1mm-25.6.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
195
+ not1mm-25.6.11.dist-info/entry_points.txt,sha256=pMcZk_0dxFgLkcUkF0Q874ojpwOmF3OL6EKw9LgvocM,47
196
+ not1mm-25.6.11.dist-info/top_level.txt,sha256=0YmTxEcDzQlzXub-lXASvoLpg_mt1c2thb5cVkDf5J4,7
197
+ not1mm-25.6.11.dist-info/RECORD,,