├── .gitignore
├── LICENSE
├── README.md
├── images
├── colorized_wsjtx.PNG
└── packet_exchange.PNG
├── pywsjtx
├── __init__.py
├── extra
│ ├── __init__.py
│ ├── latlong_to_grid_square.py
│ └── simple_server.py
├── qcolor.py
└── wsjtx_packets.py
├── requirements.txt
├── samples
├── color_wsjtx_packets.py
├── dump_wsjtx_packets.py
├── grid_from_gps.py
├── n1mm_arrl_ru.py
└── wsjtx_packet_exchanger.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | # Jetbrains IDE
104 | .idea
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Brian Moran
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
4 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
5 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
10 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
11 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
12 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A python module to interpret and create WSJT-X UDP packets
2 |
3 | Developed and tested with Python >= 3.6
4 |
5 | Sample programs for:
6 | * Setting the WSJT-X Grid Square from an external GPS
7 | * Coloring callsigns in WSJT-X based on N1MM Logger+ dupe and multiplier status
8 | * JTAlert-X, N1MM Logger+, WSJT-X packet exchange
9 |
10 |
11 | Example screen display for colorized callsigns, showing a new country multiplier in red:
12 |
13 |
14 |
15 | Example screen display for N1MM Logger+, WSJT-X, JTAlert-X in simultaneous use with wsjtx-packet-exchanger.py
16 |
17 |
18 |
--------------------------------------------------------------------------------
/images/colorized_wsjtx.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmo/py-wsjtx/e725eb689314eff97298ccc87e84bcc5b8bc1ed9/images/colorized_wsjtx.PNG
--------------------------------------------------------------------------------
/images/packet_exchange.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmo/py-wsjtx/e725eb689314eff97298ccc87e84bcc5b8bc1ed9/images/packet_exchange.PNG
--------------------------------------------------------------------------------
/pywsjtx/__init__.py:
--------------------------------------------------------------------------------
1 | from pywsjtx.wsjtx_packets import *
2 | from pywsjtx.qcolor import *
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/pywsjtx/extra/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmo/py-wsjtx/e725eb689314eff97298ccc87e84bcc5b8bc1ed9/pywsjtx/extra/__init__.py
--------------------------------------------------------------------------------
/pywsjtx/extra/latlong_to_grid_square.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | class GPSException(Exception):
4 | def __init__(self,*args):
5 | super(GPSException, self).__init__(*args)
6 |
7 | # From K6WRU via stackexchange : see https://ham.stackexchange.com/questions/221/how-can-one-convert-from-lat-long-to-grid-square/244#244
8 | # Convert latitude and longitude to Maidenhead grid locators.
9 | #
10 | # Arguments are in signed decimal latitude and longitude. For example,
11 | # the location of my QTH Palo Alto, CA is: 37.429167, -122.138056 or
12 | # in degrees, minutes, and seconds: 37° 24' 49" N 122° 6' 26" W
13 | class LatLongToGridSquare(object):
14 | upper = 'ABCDEFGHIJKLMNOPQRSTUVWX'
15 | lower = 'abcdefghijklmnopqrstuvwx'
16 |
17 | @classmethod
18 | def to_grid(cls,dec_lat, dec_lon):
19 |
20 | if not (-180<=dec_lon<180):
21 | raise GPSException('longitude must be -180<=lon<180, given %f\n'%dec_lon)
22 | if not (-90<=dec_lat<90):
23 | raise GPSException('latitude must be -90<=lat<90, given %f\n'%dec_lat)
24 |
25 | adj_lat = dec_lat + 90.0
26 | adj_lon = dec_lon + 180.0
27 |
28 | grid_lat_sq = LatLongToGridSquare.upper[int(adj_lat/10)]
29 | grid_lon_sq = LatLongToGridSquare.upper[int(adj_lon/20)]
30 |
31 | grid_lat_field = str(int(adj_lat%10))
32 | grid_lon_field = str(int((adj_lon/2)%10))
33 |
34 | adj_lat_remainder = (adj_lat - int(adj_lat)) * 60
35 | adj_lon_remainder = ((adj_lon) - int(adj_lon/2)*2) * 60
36 |
37 | grid_lat_subsq = LatLongToGridSquare.lower[int(adj_lat_remainder/2.5)]
38 | grid_lon_subsq = LatLongToGridSquare.lower[int(adj_lon_remainder/5)]
39 |
40 | return grid_lon_sq + grid_lat_sq + grid_lon_field + grid_lat_field + grid_lon_subsq + grid_lat_subsq
41 |
42 | # GPS sentences are encoded
43 | @classmethod
44 | def convert_to_degrees(cls, gps_value, direction):
45 | if direction not in ['N','S','E','W']:
46 | raise GPSException("Invalid direction specifier for lat/long: {}".format(direction))
47 |
48 | dir_mult = 1
49 | if direction in ['S','W']:
50 | dir_mult = -1
51 |
52 | if len(gps_value) < 3:
53 | raise GPSException("Invalid Value for lat/long: {}".format(gps_value))
54 |
55 | dot_posn = gps_value.index('.')
56 |
57 | if dot_posn < 0:
58 | raise GPSException("Invalid Format for lat/long: {}".format(gps_value))
59 |
60 | degrees = gps_value[0:dot_posn-2]
61 | mins = gps_value[dot_posn-2:]
62 |
63 | f_degrees = dir_mult * (float(degrees) + (float(mins) / 60.0))
64 | return f_degrees
65 |
66 | @classmethod
67 | def GPGLL_to_grid(cls, GPSLLText):
68 | # example: $GPGLL,4740.99254,N,12212.31179,W,223311.00,A,A*70\r\n
69 | try:
70 | components = GPSLLText.split(",")
71 | if components[0]=='$GPGLL':
72 | del components[0]
73 | if components[5] != 'A':
74 | raise GPSException("Not a valid GPS fix")
75 | lat = LatLongToGridSquare.convert_to_degrees(components[0], components[1])
76 | long = LatLongToGridSquare.convert_to_degrees(components[2], components [3])
77 | grid = LatLongToGridSquare.to_grid(lat, long)
78 | except GPSException:
79 | grid = ""
80 | return grid
81 |
82 | @classmethod
83 | def GPGGA_to_grid(cls, GPSGAText):
84 | # example: $GPGGA, 161229.487, 3723.2475, N, 12158.3416, W, 1, 07, 1.0, 9.0, M, , , , 0000*18
85 | try:
86 | components = GPSGAText.split(",")
87 | if components[0]=='$GPGGA':
88 | del components[0]
89 | if components[5] == '0':
90 | raise GPSException("Not a valid GPS fix")
91 | lat = LatLongToGridSquare.convert_to_degrees(components[1], components[2])
92 | long = LatLongToGridSquare.convert_to_degrees(components[3], components [4])
93 | grid = LatLongToGridSquare.to_grid(lat, long)
94 | except GPSException:
95 | grid = ""
96 | return grid
97 |
--------------------------------------------------------------------------------
/pywsjtx/extra/simple_server.py:
--------------------------------------------------------------------------------
1 | #
2 | # In WSJTX parlance, the 'network server' is a program external to the wsjtx.exe program that handles packets emitted by wsjtx
3 | #
4 | # TODO: handle multicast groups.
5 | #
6 | # see dump_wsjtx_packets.py example for some simple usage
7 | #
8 | import socket
9 | import struct
10 | import pywsjtx
11 | import logging
12 | import ipaddress
13 |
14 | class SimpleServer(object):
15 | logger = logging.getLogger()
16 | MAX_BUFFER_SIZE = pywsjtx.GenericWSJTXPacket.MAXIMUM_NETWORK_MESSAGE_SIZE
17 | DEFAULT_UDP_PORT = 2237
18 | #
19 | #
20 | def __init__(self, ip_address='127.0.0.1', udp_port=DEFAULT_UDP_PORT, **kwargs):
21 | self.timeout = None
22 | self.verbose = kwargs.get("verbose",False)
23 |
24 | if kwargs.get("timeout") is not None:
25 | self.timeout = kwargs.get("timeout")
26 |
27 | the_address = ipaddress.ip_address(ip_address)
28 | if not the_address.is_multicast:
29 | self.sock = socket.socket(socket.AF_INET, # Internet
30 | socket.SOCK_DGRAM) # UDP
31 |
32 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
33 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
34 | self.sock.bind((ip_address, int(udp_port)))
35 | else:
36 | self.multicast_setup(ip_address, udp_port)
37 |
38 | if self.timeout is not None:
39 | self.sock.settimeout(self.timeout)
40 |
41 | def multicast_setup(self, group, port=''):
42 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
43 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
44 | self.sock.bind(('', port))
45 | mreq = struct.pack("4sl", socket.inet_aton(group), socket.INADDR_ANY)
46 | self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
47 |
48 | def rx_packet(self):
49 | try:
50 | pkt, addr_port = self.sock.recvfrom(self.MAX_BUFFER_SIZE) # buffer size is 1024 bytes
51 | return(pkt, addr_port)
52 | except socket.timeout:
53 | if self.verbose:
54 | logging.debug("rx_packet: socket.timeout")
55 | return (None, None)
56 |
57 | def send_packet(self, addr_port, pkt):
58 | bytes_sent = self.sock.sendto(pkt,addr_port)
59 | self.logger.debug("send_packet: Bytes sent {} ".format(bytes_sent))
60 |
61 | def demo_run(self):
62 | while True:
63 | (pkt, addr_port) = self.rx_packet()
64 | if (pkt != None):
65 | the_packet = pywsjtx.WSJTXPacketClassFactory.from_udp_packet(addr_port, pkt)
66 | print(the_packet)
67 |
--------------------------------------------------------------------------------
/pywsjtx/qcolor.py:
--------------------------------------------------------------------------------
1 | #
2 | # Utility class to help out with Qt Color Values.
3 | #
4 | class QCOLOR:
5 | SPEC_RGB = 1
6 | SPEC_INVALID = 0
7 |
8 | def __init__(self, spec, alpha, red, green, blue):
9 | self.spec = spec
10 | self.red = red
11 | self.green = green
12 | self.blue = blue
13 | self.alpha = alpha
14 |
15 | @classmethod
16 | def Black(cls):
17 | return QCOLOR(QCOLOR.SPEC_RGB, 255, 0, 0, 0)
18 |
19 | @classmethod
20 | def Red(cls):
21 | return QCOLOR(QCOLOR.SPEC_RGB, 255, 255, 0, 0)
22 |
23 | @classmethod
24 | def RGBA(cls, alpha, red, green, blue):
25 | return QCOLOR(QCOLOR.SPEC_RGB, alpha, red, green, blue)
26 |
27 | @classmethod
28 | def White(cls):
29 | return QCOLOR(QCOLOR.SPEC_RGB, 255,255,255,255)
30 |
31 | @classmethod
32 | def Uncolor(cls):
33 | return QCOLOR(QCOLOR.SPEC_INVALID, 0,0,0,0)
34 |
35 |
36 |
--------------------------------------------------------------------------------
/pywsjtx/wsjtx_packets.py:
--------------------------------------------------------------------------------
1 |
2 | import struct
3 | import datetime
4 | import math
5 |
6 |
7 | class PacketUtil:
8 | @classmethod
9 | # this hexdump brought to you by Stack Overflow
10 | def hexdump(cls, src, length=16):
11 | FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
12 | lines = []
13 | for c in range(0, len(src), length):
14 | chars = src[c:c + length]
15 | hex = ' '.join(["%02x" % x for x in chars])
16 | printable = ''.join(["%s" % ((x <= 127 and FILTER[x]) or '.') for x in chars])
17 | lines.append("%04x %-*s %s\n" % (c, length * 3, hex, printable))
18 | return ''.join(lines)
19 |
20 | # timezone tomfoolery
21 | @classmethod
22 | def midnight_utc(cls):
23 | utcnow = datetime.datetime.utcnow()
24 | utcmidnight = datetime.datetime(utcnow.year, utcnow.month, utcnow.day, 0, 0)
25 | return utcmidnight
26 |
27 | #converts a Julian day to a calendar Date
28 | @classmethod
29 | def JDToDateMeeus(cls,jDNum):
30 | F=0.0
31 |
32 | jDNum += 0.5
33 | Z = jDNum #Z == int so I = int part
34 | F = jDNum - Z #F = fractional part
35 | if(Z < 2299161): #Julian?
36 | A = Z
37 | else: #Gregorian
38 | alpha = math.floor((Z - 1867216.25) / 36524.25)
39 | A = Z + 1 + alpha - math.floor(alpha / 4.0)
40 | B = A + 1524
41 | C = math.floor((B - 122.1) /365.25)
42 | D = math.floor(365.25 * C)
43 | E = math.floor((B - D) /30.6001)
44 | day = int(B - D - math.floor(30.6001 * E) + F)
45 | if( E < 14):
46 | month = E - 1
47 | else:
48 | month = E - 13
49 | if(month > 2):
50 | year = C - 4716
51 | else:
52 | year = C - 4715
53 | return (year,month,day)
54 |
55 |
56 | class PacketWriter(object):
57 | def __init__(self ):
58 | self.ptr_pos = 0
59 | self.packet = bytearray()
60 | # self.max_ptr_pos
61 | self.write_header()
62 |
63 | def write_header(self):
64 | self.write_QUInt32(GenericWSJTXPacket.MAGIC_NUMBER)
65 | self.write_QInt32(GenericWSJTXPacket.SCHEMA_VERSION)
66 |
67 | def write_QInt8(self, val):
68 | self.packet.extend(struct.pack('>b', val))
69 |
70 | def write_QUInt8(self, val):
71 | self.packet.extend(struct.pack('>B', val))
72 |
73 | def write_QBool(self, val):
74 | self.packet.extend(struct.pack('>?', val))
75 |
76 | def write_QInt16(self, val):
77 | self.packet.extend(struct.pack('>h', val))
78 |
79 | def write_QUInt16(self, val):
80 | self.packet.extend(struct.pack('>H', val))
81 |
82 | def write_QInt32(self, val):
83 | self.packet.extend(struct.pack('>l',val))
84 |
85 | def write_QUInt32(self, val):
86 | self.packet.extend(struct.pack('>L', val))
87 |
88 | def write_QInt64(self, val):
89 | self.packet.extend(struct.pack('>q',val))
90 |
91 | def write_QFloat(self, val):
92 | self.packet.extend(struct.pack('>d', val))
93 |
94 | def write_QString(self, str_val):
95 |
96 | b_values = str_val
97 | if type(str_val) != bytes:
98 | b_values = str_val.encode()
99 | length = len(b_values)
100 | self.write_QInt32(length)
101 | self.packet.extend(b_values)
102 |
103 | def write_QColor(self, color_val):
104 | # see Qt serialization for QColor format; unfortunately thes serialization is nothing like what's in that.
105 | # It's not correct. Look instead at the wsjt-x configuration settings, where
106 | # color values have been serialized.
107 | #
108 | self.write_QInt8(color_val.spec)
109 | self.write_QUInt8(color_val.alpha)
110 | self.write_QUInt8(color_val.alpha)
111 |
112 | self.write_QUInt8(color_val.red)
113 | self.write_QUInt8(color_val.red)
114 |
115 | self.write_QUInt8(color_val.green)
116 | self.write_QUInt8(color_val.green)
117 |
118 | self.write_QUInt8(color_val.blue)
119 | self.write_QUInt8(color_val.blue)
120 | self.write_QUInt16(0)
121 |
122 | class PacketReader(object):
123 | def __init__(self, packet):
124 | self.ptr_pos = 0
125 | self.packet = packet
126 | self.max_ptr_pos = len(packet)-1
127 | self.skip_header()
128 |
129 | def at_eof(self):
130 | return self.ptr_pos > self.max_ptr_pos
131 |
132 | def skip_header(self):
133 | if self.max_ptr_pos < 8:
134 | raise Exception('Not enough data to skip header')
135 | self.ptr_pos = 8
136 |
137 | def check_ptr_bound(self,field_type, length):
138 | if self.ptr_pos + length > self.max_ptr_pos+1:
139 | raise Exception('Not enough data to extract {}'.format(field_type))
140 |
141 | ## grab data from the packet, incrementing the ptr_pos on the basis of the data we've gleaned
142 | def QInt32(self):
143 | self.check_ptr_bound('QInt32', 4) # sure we could inspect that, but that is slow.
144 | (the_int32,) = struct.unpack('>l',self.packet[self.ptr_pos:self.ptr_pos+4])
145 | self.ptr_pos += 4
146 | return the_int32
147 |
148 |
149 | def QInt8(self):
150 | self.check_ptr_bound('QInt8', 1)
151 | (the_int8,) = struct.unpack('>b', self.packet[self.ptr_pos:self.ptr_pos+1])
152 | self.ptr_pos += 1
153 | return the_int8
154 |
155 | def QInt64(self):
156 | self.check_ptr_bound('QInt64', 8)
157 | (the_int64,) = struct.unpack('>q', self.packet[self.ptr_pos:self.ptr_pos+8])
158 | self.ptr_pos += 8
159 | return the_int64
160 |
161 | def QFloat(self):
162 | self.check_ptr_bound('QFloat', 8)
163 | (the_double,) = struct.unpack('>d', self.packet[self.ptr_pos:self.ptr_pos+8])
164 | self.ptr_pos += 8
165 | return the_double
166 |
167 | def QString(self):
168 | str_len = self.QInt32()
169 | if str_len == -1:
170 | return None
171 | self.check_ptr_bound('QString[{}]'.format(str_len),str_len)
172 | (str,) = struct.unpack('{}s'.format(str_len), self.packet[self.ptr_pos:self.ptr_pos + str_len])
173 | self.ptr_pos += str_len
174 | return str.decode('utf-8')
175 |
176 | def QDateTime(self):
177 | jdnum = self.QInt64()
178 | millis_since_midnight = self.QInt32()
179 | spec = self.QInt8()
180 | offset = 0
181 | if spec == 2:
182 | offset = self.QInt32()
183 | date = PacketUtil.JDToDateMeeus(jdnum)
184 | time = PacketUtil.midnight_utc() + datetime.timedelta(milliseconds=millis_since_midnight)
185 | return QDateTime(date,time,spec,offset)
186 |
187 | class QDateTime(object):
188 | def __init__(self,date,time,spec,offset):
189 | self.date=date
190 | self.time=time
191 | self.spec=spec
192 | self.offset=offset
193 |
194 | def __repr__(self):
195 | return "date {}\n\ttime {}\n\tspec {}\n\toffset {}".format(self.date,self.time,self.spec,self.offset)
196 |
197 | class GenericWSJTXPacket(object):
198 | SCHEMA_VERSION = 3
199 | MINIMUM_SCHEMA_SUPPORTED = 2
200 | MAXIMUM_SCHEMA_SUPPORTED = 3
201 | MINIMUM_NETWORK_MESSAGE_SIZE = 8
202 | MAXIMUM_NETWORK_MESSAGE_SIZE = 2048
203 | MAGIC_NUMBER = 0xadbccbda
204 |
205 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
206 | self.addr_port = addr_port
207 | self.magic = magic
208 | self.schema = schema
209 | self.pkt_type = pkt_type
210 | self.id = id
211 | self.pkt = pkt
212 |
213 | class InvalidPacket(GenericWSJTXPacket):
214 | TYPE_VALUE = -1
215 | def __init__(self, addr_port, packet, message):
216 | self.packet = packet
217 | self.message = message
218 | self.addr_port = addr_port
219 |
220 | def __repr__(self):
221 | return 'Invalid Packet: %s from %s:%s\n%s' % (self.message, self.addr_port[0], self.addr_port[1], PacketUtil.hexdump(self.packet))
222 |
223 | class HeartBeatPacket(GenericWSJTXPacket):
224 | TYPE_VALUE = 0
225 |
226 | def __init__(self, addr_port: object, magic: object, schema: object, pkt_type: object, id: object, pkt: object) -> object:
227 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
228 | ps = PacketReader(pkt)
229 | the_type = ps.QInt32()
230 | self.wsjtx_id = ps.QString()
231 | self.max_schema = ps.QInt32()
232 | self.version = ps.QInt8()
233 | self.revision = ps.QInt8()
234 |
235 | def __repr__(self):
236 | return 'HeartBeatPacket: from {}:{}\n\twsjtx id:{}\tmax_schema:{}\tschema:{}\tversion:{}\trevision:{}' .format(self.addr_port[0], self.addr_port[1],
237 | self.wsjtx_id, self.max_schema, self.schema, self.version, self.revision)
238 | @classmethod
239 | # make a heartbeat packet (a byte array) we can send to a 'client'. This should be it's own class.
240 | def Builder(cls,wsjtx_id='pywsjtx', max_schema=2, version=1, revision=1):
241 | # build the packet to send
242 | pkt = PacketWriter()
243 | pkt.write_QInt32(HeartBeatPacket.TYPE_VALUE)
244 | pkt.write_QString(wsjtx_id)
245 | pkt.write_QInt32(max_schema)
246 | pkt.write_QInt32(version)
247 | pkt.write_QInt32(revision)
248 | return pkt.packet
249 |
250 | class StatusPacket(GenericWSJTXPacket):
251 | TYPE_VALUE = 1
252 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
253 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
254 | ps = PacketReader(pkt)
255 | the_type = ps.QInt32()
256 | self.wsjtx_id = ps.QString()
257 | self.dial_frequency = ps.QInt64()
258 |
259 | self.mode = ps.QString()
260 | self.dx_call = ps.QString()
261 |
262 | self.report = ps.QString()
263 | self.tx_mode = ps.QString()
264 |
265 | self.tx_enabled = ps.QInt8()
266 | self.transmitting = ps.QInt8()
267 | self.decoding = ps.QInt8()
268 | self.rx_df = ps.QInt32()
269 | self.tx_df = ps.QInt32()
270 |
271 |
272 | self.de_call = ps.QString()
273 |
274 | self.de_grid = ps.QString()
275 | self.dx_grid = ps.QString()
276 |
277 | self.tx_watchdog = ps.QInt8()
278 | self.sub_mode = ps.QString()
279 | self.fast_mode = ps.QInt8()
280 |
281 | # new in wsjtx-2.0.0
282 | self.special_op_mode = ps.QInt8()
283 |
284 | def __repr__(self):
285 | str = 'StatusPacket: from {}:{}\n\twsjtx id:{}\tde_call:{}\tde_grid:{}\n'.format(self.addr_port[0], self.addr_port[1],self.wsjtx_id,
286 | self.de_call, self.de_grid)
287 | str += "\tfrequency:{}\trx_df:{}\ttx_df:{}\tdx_call:{}\tdx_grid:{}\treport:{}\n".format(self.dial_frequency, self.rx_df, self.tx_df, self.dx_call, self.dx_grid, self.report)
288 | str += "\ttransmitting:{}\t decoding:{}\ttx_enabled:{}\ttx_watchdog:{}\tsub_mode:{}\tfast_mode:{}\tspecial_op_mode:{}".format(self.transmitting, self.decoding, self.tx_enabled, self.tx_watchdog,
289 | self.sub_mode, self.fast_mode, self.special_op_mode)
290 | return str
291 |
292 |
293 | class DecodePacket(GenericWSJTXPacket):
294 | TYPE_VALUE = 2
295 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
296 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
297 | # handle packet-specific stuff.
298 | ps = PacketReader(pkt)
299 | the_type = ps.QInt32()
300 | self.wsjtx_id = ps.QString()
301 | self.new_decode = ps.QInt8()
302 | self.millis_since_midnight = ps.QInt32()
303 | self.time = PacketUtil.midnight_utc() + datetime.timedelta(milliseconds=self.millis_since_midnight)
304 | self.snr = ps.QInt32()
305 | self.delta_t = ps.QFloat()
306 | self.delta_f = ps.QInt32()
307 | self.mode = ps.QString()
308 | self.message = ps.QString()
309 | self.low_confidence = ps.QInt8()
310 | self.off_air = ps.QInt8()
311 |
312 | def __repr__(self):
313 | str = 'DecodePacket: from {}:{}\n\twsjtx id:{}\tmessage:{}\n'.format(self.addr_port[0],
314 | self.addr_port[1],
315 | self.wsjtx_id,
316 | self.message)
317 | str += "\tdelta_f:{}\tnew:{}\ttime:{}\tsnr:{}\tdelta_f:{}\tmode:{}".format(self.delta_f,
318 | self.new_decode,
319 | self.time,
320 | self.snr,
321 | self.delta_f,
322 | self.mode)
323 | return str
324 |
325 | class ClearPacket(GenericWSJTXPacket):
326 | TYPE_VALUE = 3
327 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
328 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
329 | # handle packet-specific stuff.
330 |
331 | class ReplyPacket(GenericWSJTXPacket):
332 | TYPE_VALUE = 4
333 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
334 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
335 | # handle packet-specific stuff.
336 |
337 | @classmethod
338 | def Builder(cls, decode_packet):
339 | # build the packet to send
340 | pkt = PacketWriter()
341 | pkt.write_QInt32(ReplyPacket.TYPE_VALUE)
342 | pkt.write_QString(decode_packet.wsjtx_id)
343 | pkt.write_QInt32(decode_packet.millis_since_midnight)
344 | pkt.write_QInt32(decode_packet.snr)
345 | pkt.write_QFloat(decode_packet.delta_t)
346 | pkt.write_QInt32(decode_packet.delta_f)
347 | pkt.write_QString(decode_packet.mode)
348 | pkt.write_QString(decode_packet.message)
349 | pkt.write_QInt8(decode_packet.low_confidence)
350 | pkt.write_QInt8(0)
351 | return pkt.packet
352 |
353 |
354 | class QSOLoggedPacket(GenericWSJTXPacket):
355 | TYPE_VALUE = 5
356 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
357 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
358 | # handle packet-specific stuff.
359 | ps = PacketReader(pkt)
360 | the_type = ps.QInt32()
361 | self.wsjtx_id = ps.QString()
362 | self.datetime_off = ps.QDateTime()
363 | self.call = ps.QString()
364 | self.grid = ps.QString()
365 | self.frequency = ps.QInt64()
366 | self.mode = ps.QString()
367 | self.report_sent = ps.QString()
368 | self.report_recv = ps.QString()
369 | self.tx_power = ps.QString()
370 | self.comments = ps.QString()
371 | self.name = ps.QString()
372 | self.datetime_on = ps.QDateTime()
373 | self.op_call = ps.QString()
374 | self.my_call = ps.QString()
375 | self.my_grid = ps.QString()
376 | self.exchange_sent = ps.QString()
377 | self.exchange_recv = ps.QString()
378 |
379 | def __repr__(self):
380 | str = 'QSOLoggedPacket: call {} @ {}\n\tdatetime:{}\tfreq:{}\n'.format(self.call,
381 | self.grid,
382 | self.datetime_off,
383 | self.frequency)
384 | str += "\tmode:{}\tsent:{}\trecv:{}".format(self.mode,
385 | self.report_sent,
386 | self.report_recv)
387 | return str
388 |
389 |
390 | class ClosePacket(GenericWSJTXPacket):
391 | TYPE_VALUE = 6
392 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
393 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
394 | # handle packet-specific stuff.
395 |
396 | class ReplayPacket(GenericWSJTXPacket):
397 | TYPE_VALUE = 7
398 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
399 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
400 | # handle packet-specific stuff.
401 |
402 | class HaltTxPacket(GenericWSJTXPacket):
403 | TYPE_VALUE = 8
404 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
405 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
406 | # handle packet-specific stuff.
407 |
408 | class FreeTextPacket(GenericWSJTXPacket):
409 | TYPE_VALUE = 9
410 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
411 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
412 | # handle packet-specific stuff.
413 |
414 | @classmethod
415 | def Builder(cls,to_wsjtx_id='WSJT-X', text="", send=False):
416 | # build the packet to send
417 | pkt = PacketWriter()
418 | print('To_wsjtx_id ',to_wsjtx_id,' text ',text, 'send ',send)
419 | pkt.write_QInt32(FreeTextPacket.TYPE_VALUE)
420 | pkt.write_QString(to_wsjtx_id)
421 | pkt.write_QString(text)
422 | pkt.write_QInt8(send)
423 | return pkt.packet
424 |
425 | class WSPRDecodePacket(GenericWSJTXPacket):
426 | TYPE_VALUE = 10
427 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
428 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
429 | # handle packet-specific stuff.
430 |
431 | class LocationChangePacket(GenericWSJTXPacket):
432 | TYPE_VALUE = 11
433 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
434 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
435 | # handle packet-specific stuff.
436 |
437 | @classmethod
438 | def Builder(cls, to_wsjtx_id='WSJT-X', new_grid=""):
439 | # build the packet to send
440 | pkt = PacketWriter()
441 | pkt.write_QInt32(LocationChangePacket.TYPE_VALUE)
442 | pkt.write_QString(to_wsjtx_id)
443 | pkt.write_QString(new_grid)
444 | return pkt.packet
445 |
446 | class LoggedADIFPacket(GenericWSJTXPacket):
447 | TYPE_VALUE = 12
448 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
449 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
450 | # handle packet-specific stuff.
451 |
452 | @classmethod
453 | def Builder(cls, to_wsjtx_id='WSJT-X', adif_text=""):
454 | # build the packet to send
455 | pkt = PacketWriter()
456 | pkt.write_QInt32(LoggedADIFPacket.TYPE_VALUE)
457 | pkt.write_QString(to_wsjtx_id)
458 | pkt.write_QString(adif_text)
459 | return pkt.packet
460 |
461 | class HighlightCallsignPacket(GenericWSJTXPacket):
462 | TYPE_VALUE = 13
463 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
464 | GenericWSJTXPacket.__init__(self, addr_port, magic, schema, pkt_type, id, pkt)
465 | # handle packet-specific stuff.
466 |
467 | # the callsign field can contain text, callsigns, entire lines.
468 |
469 | @classmethod
470 | def Builder(cls, to_wsjtx_id='WSJT-X', callsign="K1JT", background_color=None, foreground_color=None, highlight_last_only=True ):
471 | # build the packet to send
472 | pkt = PacketWriter()
473 | pkt.write_QInt32(HighlightCallsignPacket.TYPE_VALUE)
474 | pkt.write_QString(to_wsjtx_id)
475 | pkt.write_QString(callsign)
476 | pkt.write_QColor(background_color)
477 | pkt.write_QColor(foreground_color)
478 | pkt.write_QBool(highlight_last_only)
479 | return pkt.packet
480 |
481 | class WSJTXPacketClassFactory(GenericWSJTXPacket):
482 |
483 | PACKET_TYPE_TO_OBJ_MAP = {
484 | HeartBeatPacket.TYPE_VALUE: HeartBeatPacket,
485 | StatusPacket.TYPE_VALUE: StatusPacket,
486 | DecodePacket.TYPE_VALUE: DecodePacket,
487 | ClearPacket.TYPE_VALUE: ClearPacket,
488 | ReplyPacket.TYPE_VALUE: ReplyPacket,
489 | QSOLoggedPacket.TYPE_VALUE: QSOLoggedPacket,
490 | ClosePacket.TYPE_VALUE: ClosePacket,
491 | ReplayPacket.TYPE_VALUE: ReplayPacket,
492 | HaltTxPacket.TYPE_VALUE: HaltTxPacket,
493 | FreeTextPacket.TYPE_VALUE: FreeTextPacket,
494 | WSPRDecodePacket.TYPE_VALUE: WSPRDecodePacket
495 | }
496 | def __init__(self, addr_port, magic, schema, pkt_type, id, pkt):
497 | self.addr_port = addr_port
498 | self.magic = magic
499 | self.schema = schema
500 | self.pkt_type = pkt_type
501 | self.pkt_id = id
502 | self.pkt = pkt
503 |
504 | def __repr__(self):
505 | return 'WSJTXPacketFactory: from {}:{}\n{}' .format(self.addr_port[0], self.addr_port[1], PacketUtil.hexdump(self.pkt))
506 |
507 | # Factory-like method
508 | @classmethod
509 | def from_udp_packet(cls, addr_port, udp_packet):
510 | if len(udp_packet) < GenericWSJTXPacket.MINIMUM_NETWORK_MESSAGE_SIZE:
511 | return InvalidPacket( addr_port, udp_packet, "Packet too small")
512 |
513 | if len(udp_packet) > GenericWSJTXPacket.MAXIMUM_NETWORK_MESSAGE_SIZE:
514 | return InvalidPacket( addr_port, udp_packet, "Packet too large")
515 |
516 | (magic, schema, pkt_type, id_len) = struct.unpack('>LLLL', udp_packet[0:16])
517 |
518 | if magic != GenericWSJTXPacket.MAGIC_NUMBER:
519 | return InvalidPacket( addr_port, udp_packet, "Invalid Magic Value")
520 |
521 | if schema < GenericWSJTXPacket.MINIMUM_SCHEMA_SUPPORTED or schema > GenericWSJTXPacket.MAXIMUM_SCHEMA_SUPPORTED:
522 | return InvalidPacket( addr_port, udp_packet, "Unsupported schema value {}".format(schema))
523 | klass = WSJTXPacketClassFactory.PACKET_TYPE_TO_OBJ_MAP.get(pkt_type)
524 |
525 | if klass is None:
526 | return InvalidPacket( addr_port, udp_packet, "Unknown packet type {}".format(pkt_type))
527 |
528 | return klass(addr_port, magic, schema, pkt_type, id, udp_packet)
529 |
530 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmo/py-wsjtx/e725eb689314eff97298ccc87e84bcc5b8bc1ed9/requirements.txt
--------------------------------------------------------------------------------
/samples/color_wsjtx_packets.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
4 | import pywsjtx.extra.simple_server
5 |
6 | import re
7 | import random
8 |
9 | TEST_MULTICAST = True
10 |
11 | if TEST_MULTICAST:
12 | IP_ADDRESS = '224.1.1.1'
13 | PORT = 5007
14 | else:
15 | IP_ADDRESS = '127.0.0.1'
16 | PORT = 2237
17 |
18 | MY_MAX_SCHEMA = 3
19 |
20 | s = pywsjtx.extra.simple_server.SimpleServer(IP_ADDRESS, PORT, timeout=2.0)
21 |
22 | while True:
23 |
24 | (pkt, addr_port) = s.rx_packet()
25 | if (pkt != None):
26 | the_packet = pywsjtx.WSJTXPacketClassFactory.from_udp_packet(addr_port, pkt)
27 |
28 | if type(the_packet) == pywsjtx.HeartBeatPacket:
29 | max_schema = max(the_packet.max_schema, MY_MAX_SCHEMA)
30 | reply_beat_packet = pywsjtx.HeartBeatPacket.Builder(the_packet.wsjtx_id,max_schema)
31 | s.send_packet(addr_port, reply_beat_packet)
32 | if type(the_packet) == pywsjtx.DecodePacket:
33 | m = re.match(r"^CQ\s+(\S+)\s+", the_packet.message)
34 | if m:
35 | print("Callsign {}".format(m.group(1)))
36 | callsign = m.group(1)
37 |
38 | color_pkt = pywsjtx.HighlightCallsignPacket.Builder(the_packet.wsjtx_id, callsign,
39 |
40 | pywsjtx.QCOLOR.White(),
41 | pywsjtx.QCOLOR.Red(),
42 | True)
43 |
44 | normal_pkt = pywsjtx.HighlightCallsignPacket.Builder(the_packet.wsjtx_id, callsign,
45 | pywsjtx.QCOLOR.Uncolor(),
46 | pywsjtx.QCOLOR.Uncolor(),
47 | True)
48 | s.send_packet(addr_port, color_pkt)
49 | #print(pywsjtx.PacketUtil.hexdump(color_pkt))
50 | print(the_packet)
51 |
52 |
53 |
--------------------------------------------------------------------------------
/samples/dump_wsjtx_packets.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
4 | import pywsjtx.extra.simple_server
5 |
6 | #IP_ADDRESS = '224.1.1.1'
7 | #PORT = 5007
8 |
9 | IP_ADDRESS = '127.0.0.1'
10 | PORT = 2237
11 |
12 | s = pywsjtx.extra.simple_server.SimpleServer(IP_ADDRESS, PORT, timeout=2.0)
13 |
14 | while True:
15 |
16 | (pkt, addr_port) = s.rx_packet()
17 | if (pkt != None):
18 | the_packet = pywsjtx.WSJTXPacketClassFactory.from_udp_packet(addr_port, pkt)
19 | if type(the_packet) == pywsjtx.StatusPacket:
20 | pass
21 | print(the_packet)
22 |
23 |
24 |
--------------------------------------------------------------------------------
/samples/grid_from_gps.py:
--------------------------------------------------------------------------------
1 |
2 | # simple example to open a com port (using pyserial) to read NMEA sentences from an attached GPS,
3 | # and supply the grid value to wsjtx.
4 |
5 | # using standard NMEA sentences
6 | import os
7 | import sys
8 | import threading
9 | from datetime import datetime
10 | import serial
11 | import logging
12 |
13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
14 | import pywsjtx
15 | import pywsjtx.extra.simple_server
16 | import pywsjtx.extra.latlong_to_grid_square
17 |
18 | # Windows communications port we're using.
19 | # TODO: gpsd on linux
20 |
21 | COMPORT = 'COM7'
22 | # Set 'COM7' to the com port number of your GPS. Example 'COM9'
23 | COMPORTBAUDRATE = '9600'
24 | # Set '9600' to the baud rate for your GPS com port. Usually 4800, 9600, 19200, 38400, etc. Example '19200'
25 | IP_ADDRESS = '127.0.0.1'
26 | # Set '127.0.0.1' to the IP address where this script should inject the grid. Example: If you are running both this script and WSJT-X on the same machine, set this to '127.0.0.1' both here AND in WSJT-X Settings>Reporting>UDP Server>UDP Server (In the WSJT-X UDP Server settings, there will be no quotes ' around the IP address.)
27 | PORT = 2237
28 | # Set '2237' to the IP address where this script should inject the grid. Example: If you are running both this script and WSJT-X on the same machine, set this to '2237' both here AND in WSJT-X Settings>Reporting>UDP Server>UDP Server Port Number (In the WSJT-X UDP Server settings, there will be no quotes ' around the port number.)
29 | logging.basicConfig(level=logging.DEBUG)
30 |
31 | class NMEALocation(object):
32 | # parse the NMEA message for location into a grid square
33 |
34 | def __init__(self, grid_changed_callback = None):
35 | self.valid = False
36 | self.grid = "" # this is an attribute, so allegedly doesn't need locking when used with multiple threads.
37 | self.last_fix_at = None
38 | self.grid_changed_callback = grid_changed_callback
39 |
40 | def handle_serial(self,text):
41 | if text.startswith('$GPGLL'):
42 | logging.debug('nmea sentence: {}'.format(text.rstrip()))
43 | grid = pywsjtx.extra.latlong_to_grid_square.LatLongToGridSquare.GPGLL_to_grid(text)
44 |
45 | if grid != "":
46 | self.valid = True
47 | self.last_fix_at = datetime.utcnow()
48 | else:
49 | self.valid = False
50 |
51 | if grid != "" and self.grid != grid:
52 | logging.debug("NMEALocation - grid mismatch old: {} new: {}".format(self.grid,grid))
53 | self.grid = grid
54 | if (self.grid_changed_callback):
55 | c_thr = threading.Thread(target=self.grid_changed_callback, args=(grid,), kwargs={})
56 | c_thr.start()
57 | elif text.startswith('$GPGGA'):
58 | logging.debug('nmea sentence: {}'.format(text.rstrip()))
59 | grid = pywsjtx.extra.latlong_to_grid_square.LatLongToGridSquare.GPGGA_to_grid(text)
60 |
61 | if grid != "":
62 | self.valid = True
63 | self.last_fix_at = datetime.utcnow()
64 | else:
65 | self.valid = False
66 |
67 | if grid != "" and self.grid != grid:
68 | logging.debug("NMEALocation - grid mismatch old: {} new: {}".format(self.grid,grid))
69 | self.grid = grid
70 | if (self.grid_changed_callback):
71 | c_thr = threading.Thread(target=self.grid_changed_callback, args=(grid,), kwargs={})
72 | c_thr.start()
73 |
74 | class SerialGPS(object):
75 |
76 | def __init__(self):
77 | self.line_handlers = []
78 | self.comm_thread = None
79 | self.comm_device = None
80 | self.stop_signalled = False
81 |
82 | def add_handler(self, line_handler):
83 | if (not (line_handler is None)) and (not (line_handler in self.line_handlers)):
84 | self.line_handlers.append(line_handler)
85 |
86 | def open(self, comport, baud, line_handler, **serial_kwargs):
87 | if self.comm_device is not None:
88 | self.close()
89 | self.stop_signalled = False
90 | self.comm_device = serial.Serial(comport, baud, **serial_kwargs)
91 | if self.comm_device is not None:
92 | self.add_handler(line_handler)
93 | self.comm_thread = threading.Thread(target=self.serial_worker, args=())
94 | self.comm_thread.start()
95 |
96 | def close(self):
97 | self.stop_signalled = True
98 | self.comm_thread.join()
99 |
100 | self.comm_device.close()
101 | self.line_handlers = []
102 | self.comm_device = None
103 | self.stop_signalled = False
104 |
105 | def remove_handler(self, line_handler):
106 | self.line_handlers.remove(line_handler)
107 |
108 | def serial_worker(self):
109 | while (True):
110 | if self.stop_signalled:
111 | return # terminate
112 | line = self.comm_device.readline()
113 | # dispatch the line
114 | # note that bytes are used for readline, vs strings after the decode to utf-8
115 | if line.startswith(b'$'):
116 | try:
117 | str_line = line.decode("utf-8")
118 | for p in self.line_handlers:
119 | p(str_line)
120 | except UnicodeDecodeError as ex:
121 | logging.debug("serial_worker: {} - line: {}".format(ex,[hex(c) for c in line]))
122 |
123 | @classmethod
124 | def example_line_handler(cls, text):
125 | print('serial: ',text)
126 |
127 | # set up the serial_gps to run
128 | # get location data from the GPS, update the grid
129 | # get the grid out of the status message from the WSJT-X instance
130 |
131 | # if we have a grid, and it's not the same as GPS, then make it the same by sending the message.
132 | # But only do that if triggered by a status message.
133 |
134 |
135 | wsjtx_id = None
136 | nmea_p = None
137 | gps_grid = ""
138 |
139 | def example_callback(new_grid):
140 | global gps_grid
141 | print("New Grid! {}".format(new_grid))
142 | # this sets the
143 | gps_grid = new_grid
144 |
145 | sgps = SerialGPS()
146 |
147 | s = pywsjtx.extra.simple_server.SimpleServer(IP_ADDRESS,PORT)
148 |
149 | print("Starting wsjt-x message server")
150 |
151 | while True:
152 |
153 | (pkt, addr_port) = s.rx_packet()
154 | if (pkt != None):
155 | the_packet = pywsjtx.WSJTXPacketClassFactory.from_udp_packet(addr_port, pkt)
156 | if wsjtx_id is None and (type(the_packet) == pywsjtx.HeartBeatPacket):
157 | # we have an instance of WSJTX
158 | print("wsjtx detected, id is {}".format(the_packet.wsjtx_id))
159 | print("starting gps monitoring")
160 | wsjtx_id = the_packet.wsjtx_id
161 | # start up the GPS reader
162 | nmea_p = NMEALocation(example_callback)
163 | sgps.open(COMPORT, COMPORTBAUDRATE, nmea_p.handle_serial, timeout=1.2)
164 |
165 | if type(the_packet) == pywsjtx.StatusPacket:
166 | if gps_grid != "" and the_packet.de_grid != gps_grid:
167 | print("Sending Grid Change to wsjtx-x, old grid:{} new grid: {}".format(the_packet.de_grid, gps_grid))
168 | grid_change_packet = pywsjtx.LocationChangePacket.Builder(wsjtx_id, "GRID:"+gps_grid)
169 | logging.debug(pywsjtx.PacketUtil.hexdump(grid_change_packet))
170 | s.send_packet(the_packet.addr_port, grid_change_packet)
171 | # for fun, change the TX5 message to our grid square, so we don't have to call CQ again
172 | # this only works if the length of the free text message is less than 13 characters.
173 | # if len(the_packet.de_call <= 5):
174 | # free_text_packet = pywsjtx.FreeTextPacket.Builder(wsjtx_id,"73 {} {}".format(the_packet.de_call, the_packet[0:4]),False)
175 | # s.send_packet(addr_port, free_text_packet)
176 |
177 | print(the_packet)
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/samples/n1mm_arrl_ru.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 | #
4 | # Proof of concept -- colorize wsjt-x callsigns on the basis of dupe status in N1MM.
5 | #
6 | # Set up these variables:
7 | # The N1MM Database file - this is a SQLite DB file that has an "s3db" file extension
8 | N1MM_DB_FILE = "C:\\Users\\brian\\Documents\\N1MM Logger+\\Databases\\n9adg_2018.s3db"
9 |
10 | # Download the CTY file from the same place that WL_CTY comes from. This is the country prefix lookup file.
11 | CTY_FILE = "C:\\Users\\brian\\Documents\\N1MM Logger+\\SupportFiles\\CTY.DAT"
12 |
13 | # Make sure CONTESTNR correstponds to the ARRL RU contest in your N1MM Database.
14 | CONTESTNR = 1
15 |
16 | # TODO: Find via Contest_name and START_DATE -- if multiple, show the CONTESTNR for all and have to use that strategy
17 | #CONTEST_NAME="ARRLRTTY"
18 | #START_DATE="2018-12-08 00:0:00"
19 |
20 | TEST_MULTICAST = True
21 | RETRANSMIT_UDP = True
22 |
23 | if TEST_MULTICAST:
24 | IP_ADDRESS = '224.1.1.1'
25 | PORT = 5007
26 | else:
27 | IP_ADDRESS = '127.0.0.1'
28 | PORT = 2238
29 |
30 | RETRANSMIT_IP_ADDRESS = '127.0.0.1'
31 | RETRANSMIT_IP_BOUND_PORT = 0 # 0 means pick an unused port
32 | RETRANSMIT_IP_PORT = 2237
33 |
34 | MY_MAX_SCHEMA = 2
35 |
36 | LOOKUP_THREADS = 4
37 | STP_MAX_SECONDS = 20 # how often do we hit N1MM to see what sections we've worked?
38 |
39 | import sqlite3
40 | import os
41 | import sys
42 | import re
43 | import queue
44 | import threading
45 | #from datetime import datetime
46 | #import serial
47 | import logging
48 | import time
49 |
50 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
51 | import pywsjtx
52 | import pywsjtx.extra.simple_server
53 |
54 | #
55 | # DONE! - listen for UDP packets from N1MM for logged QSOs to use as source of packets; process those and add to the queue, so colorization happens in somewhat realtime
56 | # DONE! - Interpret callsigns calling me to look up & colorize
57 | # K1ABC W9XYZ EN42
58 | # DONE! - Watch exchanges so we can color mults that way
59 | # K1ABC W9XYZ 579 IL
60 | # K1ABC W9XYZ R 559 IL
61 |
62 |
63 | # ------------------- File picker instead of constant?
64 | #import tkinter as tk
65 | #from tkinter import filedialog
66 |
67 | #root = tk.Tk()
68 | #root.withdraw()
69 |
70 | #file_path = filedialog.askopenfilename(title = "Select N1MM Database file to use",filetypes = (("N1MM Database File","*.s3db"),("all files","*.*")))
71 |
72 | # -- TODO put these in separate files.
73 | #--- N1MM-related
74 | class N1MMLoggerPlus():
75 | database_path = None
76 | n1mm_sql_connection = None
77 |
78 | def __init__(self, n1mm_database_path, logger=None, **kwargs):
79 | self.verbose_logging = False
80 | self.logger = logger or logging.getLogger(__name__)
81 | self.database_path = n1mm_database_path
82 | #self.n1mm_sql_connection = self.open_n1mm_database(n1mm_database_path)
83 | logging.debug("n1mm database name {}".format(self.database_path))
84 |
85 | def open_db(self):
86 | if self.n1mm_sql_connection is not None:
87 | logging.error("Database is already open [{}]".format(self.db_file_name))
88 | raise Exception("Database is already open [{}]".format(self.db_file_name))
89 |
90 | cx = sqlite3.connect(self.database_path)
91 | cx.row_factory = sqlite3.Row
92 | self.n1mm_sql_connection = cx
93 |
94 | def close_db(self):
95 | if self.n1mm_sql_connection is not None:
96 | self.n1mm_sql_connection.close()
97 | self.database_path = None
98 | self.n1mm_sql_connection = None
99 | else:
100 | logging.info("close called, but connection not open.")
101 |
102 | def get_contest(self, **kwargs):
103 | contest_nr = kwargs.get("contestnr",None)
104 | if contest_nr is not None:
105 | c = self.n1mm_sql_connection.cursor()
106 | c.execute('SELECT * FROM ContestInstance where ContestID = ?', (contest_nr,))
107 | rows = c.fetchall()
108 | if len(rows) == 0:
109 | raise Exception("Cannot find ContestNR {} in N1MM Database".format(contest_nr))
110 | self.contestnr = contest_nr
111 | logging.debug("ContestNR {} found".format(contest_nr))
112 | else:
113 | pass
114 |
115 | def simple_dupe_status(self,callsign):
116 | if self.n1mm_sql_connection is None:
117 | raise Exception("N1MM database is not open")
118 |
119 | c = self.n1mm_sql_connection.cursor()
120 | c.execute('SELECT * FROM DXLOG where ContestNR = ? AND Call = ?', (self.contestnr, callsign,))
121 | rows = c.fetchall()
122 | logging.debug("dupe_status for {}: worked {} times".format(callsign, len(rows)))
123 | return len(rows)>0
124 |
125 | def prefix_worked_count(self, prefix):
126 | if self.n1mm_sql_connection is None:
127 | raise Exception("N1MM database is not open")
128 |
129 | c = self.n1mm_sql_connection.cursor()
130 | c.execute('SELECT COUNT(1) as C FROM DXLOG where ContestNR = ? AND CountryPrefix=?', (self.contestnr, prefix,))
131 | rows = c.fetchall()
132 | return rows[0]['C']
133 |
134 | def sections_for_prefixes(self, country_prefix_list):
135 | from itertools import chain
136 | if self.n1mm_sql_connection is None:
137 | raise Exception("N1MM database is not open")
138 |
139 | c = self.n1mm_sql_connection.cursor()
140 | placeholder = '?'
141 | placeholders = ', '.join(placeholder * len(country_prefix_list))
142 | query = 'select DISTINCT sect from dxlog where ContestNR = {} AND CountryPrefix in ({}) AND IsMultiplier1<>0 ORDER BY sect ASC' .format(self.contestnr, placeholders)
143 | print(query)
144 | c.execute(query, country_prefix_list)
145 | rows = c.fetchall()
146 | sections = [ item['sect'] for item in rows ]
147 | return sections
148 |
149 |
150 | class Cty:
151 | cty_file_location = None
152 | table = {}
153 | def __init__(self, cty_file_location, **kwargs):
154 | # read the file
155 | self.logger = kwargs.get("logger",logging.getLogger(__name__))
156 | self.cty_file_location = cty_file_location
157 |
158 | def load(self):
159 | if os.path.isfile(self.cty_file_location)==False:
160 | raise Exception("CTY file [{}] doesn't exist".format(self.cty_file_location))
161 | fh = open(self.cty_file_location, 'r')
162 |
163 | while True:
164 | # read lines
165 |
166 | first_line = fh.readline()
167 | if first_line is None or len(first_line)==0:
168 | break
169 | first_line = first_line.rstrip()
170 | fields = first_line.split(':')
171 | #logging.debug("Split into fields {}".format(fields))
172 | if len(fields) != 9:
173 | logging.warning("Wrong number of fields for line [{}] - {}".format(first_line, len(fields)))
174 | raise Exception("Can't parse CTY file - wrong number of fields on line {}".format(first_line))
175 | continent = fields[3].strip()
176 | primary_prefix = fields[7].strip()
177 | # now read the prefixes
178 | last_line = first_line
179 | prefix_lines = ""
180 | while not last_line.endswith(";"):
181 | last_line = fh.readline().rstrip()
182 | prefix_lines += last_line
183 |
184 | prefix_lines = prefix_lines.rstrip(";")
185 |
186 | prefix_or_callsign = prefix_lines.split(",")
187 |
188 | for p in prefix_or_callsign:
189 | p = p.strip()
190 | if p is None:
191 | continue
192 |
193 | # handle special suffixes
194 | # The following special characters can be applied after an alias prefix:
195 | # (#) Override CQ Zone
196 | # [#] Override ITU Zone
197 | # <#/#> Override latitude/longitude
198 | # {aa} Override Continent
199 | # ~#~ Override local time offset from GMT
200 | p = re.sub(r'\{\d+\}', '', p)
201 | p = re.sub(r'\[\d+\]', '', p)
202 | p = re.sub(r'<\d+/\d+>', '', p)
203 | p = re.sub(r'\{\w\w\}', '', p)
204 | p = re.sub(r'~\d+~', '', p)
205 | exact_match = False
206 | #logging.debug("Adding prefix {} for {}".format(primary_prefix, p))
207 | self.table[p] = { 'primary_prefix' : primary_prefix, 'continent' : continent }
208 |
209 | fh.close()
210 |
211 | def prefix_for(self, callsign):
212 | # lookup the callsign in the CTY file data, return the prefix.
213 | prefixes = [k for k in self.table.keys() if k==callsign[0:len(k)] or k=="={}".format(callsign)]
214 | prefixes.sort(key = lambda s: 100-len(s))
215 | #print("Prefixes {}".format(prefixes))
216 | found_prefix = None
217 | # exact matches first
218 | unique_call = [ c for c in prefixes if c.startswith('=')]
219 | if unique_call:
220 | if len(unique_call) > 1:
221 | logging.warning("More than one UNIQUE call matched: {}".format(unique_call))
222 | found_prefix = self.table[unique_call[0]]['primary_prefix']
223 | logging.debug("Prefix for {} is [{}] ({})".format(callsign,found_prefix,unique_call[0]))
224 | return found_prefix
225 | #
226 | if len(prefixes) > 0:
227 | k = prefixes[0]
228 | found_prefix = self.table[k]['primary_prefix']
229 | logging.debug("Prefix for {} is [{}] ({})".format(callsign, found_prefix, k))
230 |
231 | return found_prefix
232 |
233 | from threading import RLock
234 | class StateProvinceKeeper:
235 | class __OnlyOne:
236 |
237 | def __init__(self):
238 | self.__already_worked = []
239 | self.logger = logging.getLogger(__name__)
240 | self.lock = RLock()
241 |
242 | def already_worked(self, section):
243 | self.lock.acquire()
244 | try:
245 | self.logger.debug("Checking section {}".format(section))
246 | rval = (section in self.__already_worked)
247 | finally:
248 | self.lock.release()
249 | return rval
250 |
251 | def update_already_worked(self, new_list):
252 | self.lock.acquire()
253 | try:
254 | self.logger.debug("Updating logged sections {}".format(new_list))
255 | self.__already_worked = new_list.copy()
256 | finally:
257 | self.lock.release()
258 |
259 | instance = None
260 |
261 | def __new__(cls):
262 | if not StateProvinceKeeper.instance:
263 | StateProvinceKeeper.instance = StateProvinceKeeper.__OnlyOne()
264 | return StateProvinceKeeper.instance
265 |
266 | def __init__(self,st_pr_db_file_name,**kwargs):
267 | self.db_file_name = st_pr_db_file_name
268 | self.logger = kwargs.get("logger", logging.getLogger(__name__))
269 | #self.create_db_if_needed()
270 | self.sql_connection = None
271 |
272 |
273 |
274 | class CallsignWorker:
275 | threads = []
276 | def __init__(self, threadcount, cty, n1mm_db_file_name, n1mm_args, **kwargs ):
277 | self.logger = kwargs.get("logger", logging.getLogger(__name__))
278 | self.input_queue = queue.Queue()
279 | self.output_queue = queue.Queue()
280 | self.threadcount = threadcount
281 | self.cty = cty
282 |
283 | self.n1mm_db_file_name = n1mm_db_file_name
284 | self.n1mm_args = n1mm_args
285 |
286 | self.threads = []
287 | self.internal_start_threads()
288 |
289 | def internal_start_threads(self):
290 | for i in range(self.threadcount):
291 | t = threading.Thread(target=self.callsign_lookup_worker)
292 | t.start()
293 | self.threads.append(t)
294 |
295 | def internal_stop_threads(self):
296 | for i in range(self.threadcount):
297 | self.input_queue.put(None)
298 | for t in self.threads:
299 | t.join()
300 |
301 | def stop_threads(self):
302 | self.internal_stop_threads()
303 |
304 | def callsign_lookup_worker(self):
305 | n1mm = N1MMLoggerPlus(self.n1mm_db_file_name,self.logger)
306 | n1mm.open_db()
307 | logging.warning(self.n1mm_args)
308 | n1mm.get_contest(**self.n1mm_args)
309 |
310 | stp = StateProvinceKeeper()
311 |
312 |
313 | while True:
314 | input_pkt = self.input_queue.get()
315 | is_section_mult = False
316 | if input_pkt is None:
317 | break
318 | prefix = self.cty.prefix_for(input_pkt['callsign'])
319 | dupe_status = n1mm.simple_dupe_status(input_pkt['callsign'])
320 | if not dupe_status and input_pkt.get('exchange') and prefix in ['K', 'VE']:
321 | is_section_mult = not stp.already_worked(input_pkt['exchange'])
322 | logging.info("Section multiplier {} {}".format(input_pkt['callsign'], input_pkt['exchange']))
323 | is_mult = not dupe_status and n1mm.prefix_worked_count(prefix)==0
324 | input_pkt['dupe'] = dupe_status
325 | input_pkt['is_mult'] = is_mult or is_section_mult
326 | input_pkt['is_section_mult'] = is_section_mult
327 | input_pkt['prefix'] = prefix
328 | logging.debug("Thread: {} - Callsign status {} prefix:{} dupe:{} mult:{}".format(threading.current_thread().name, input_pkt['callsign'], prefix, dupe_status, is_mult, ))
329 | self.output_queue.put(input_pkt)
330 |
331 | class Retransmitter:
332 | def __init__(self, ip_address, ip_port, upstream_server, **kwargs):
333 | self.server = pywsjtx.extra.simple_server.SimpleServer(ip_address, ip_port, **kwargs)
334 | self.upstream_server = upstream_server
335 | self.logger = kwargs.get("logger", logging.getLogger(__name__))
336 |
337 | # wait for packets from the ip_address:ip_port. Resend them to upstream_server
338 | # send packets to port 2237
339 | self._t = threading.Thread(target=self.packet_resender)
340 | self._t.start()
341 |
342 | def send(self, message, from_addr_port, to_addr_port):
343 | addr, port = from_addr_port
344 | self.from_addr_port = (addr,port) #from_addr_port
345 | self.server.send_packet(to_addr_port, message)
346 | logging.debug("sent packet from {} to N1MM Logger port {} {} bytes".format(from_addr_port, to_addr_port, len(message)))
347 |
348 |
349 | def stop_threads(self):
350 | self._t.join()
351 |
352 | def packet_resender(self):
353 | while True:
354 | input_pkt, addr_port = self.server.rx_packet()
355 | if (input_pkt != None and self.from_addr_port != None):
356 | logging.debug("RX from N1MM Logger {}, Sending {} bytes to upstream {}".format(addr_port,len(input_pkt), self.from_addr_port))
357 | self.upstream_server.send_packet(self.from_addr_port, input_pkt)
358 |
359 | def main():
360 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] %(message)s")
361 | consoleFormatter = logging.Formatter('%(asctime)s %(message)s')
362 | logging.basicConfig(level=logging.INFO,format='%(asctime)s %(message)s')
363 |
364 | # set up file logging, as well
365 | fileHandler = logging.FileHandler("n1mm_arrl_ru_run.log")
366 | fileHandler.setFormatter(logFormatter)
367 | logging.getLogger().addHandler(fileHandler)
368 | # log to console
369 | if False:
370 | consoleHandler = logging.StreamHandler()
371 | consoleHandler.setFormatter(consoleFormatter)
372 | logging.getLogger().addHandler(consoleHandler)
373 |
374 | logging.getLogger().setLevel(logging.DEBUG)
375 | cty = Cty(CTY_FILE, logger=logging.getLogger())
376 | cty.load()
377 |
378 | n1mm = N1MMLoggerPlus(N1MM_DB_FILE,logger=logging.getLogger())
379 | n1mm.open_db()
380 | n1mm.get_contest(contestnr=CONTESTNR)
381 | already_worked = n1mm.sections_for_prefixes(['K','VE'])
382 | print(already_worked)
383 |
384 | stp = StateProvinceKeeper()
385 | stp.update_already_worked(already_worked)
386 |
387 | #print("AZ already worked? {}".format(stp.already_worked("AZ")))
388 | #stp.open_db()
389 | #stp.add('N9ADG','K','WA')
390 | #stp.add('K9CT','K','IL')
391 | #print("K9CT's section {}".format(stp.section_for('K9CT')))
392 |
393 | retransmitter = None
394 |
395 | # take calls that are CQing, or replying, etc. and colorize them after the dupe check
396 |
397 | cw = CallsignWorker(LOOKUP_THREADS, cty, N1MM_DB_FILE,{'contestnr':CONTESTNR})
398 |
399 | # get a callsign
400 | # put on queue
401 |
402 | s = pywsjtx.extra.simple_server.SimpleServer(IP_ADDRESS, PORT, timeout=2.0)
403 |
404 | if RETRANSMIT_UDP:
405 | retransmitter = Retransmitter(RETRANSMIT_IP_ADDRESS,0,s,timeout=0.5)
406 |
407 | mult_foreground_color = pywsjtx.QCOLOR.White()
408 | mult_background_color = pywsjtx.QCOLOR.Red()
409 |
410 | dupe_background_color = pywsjtx.QCOLOR.RGBA(255,211,211,211) # light grey
411 | dupe_foreground_color = pywsjtx.QCOLOR.RGBA(255,169,169,169) # dark grey
412 |
413 | stp_age = time.time()
414 |
415 | while True:
416 |
417 | (pkt, addr_port) = s.rx_packet()
418 | if (pkt != None):
419 | if RETRANSMIT_UDP:
420 | # retransmit to someone else (e.g. N1MM Logger)
421 | retransmitter.send(pkt, addr_port, (RETRANSMIT_IP_ADDRESS, RETRANSMIT_IP_PORT))
422 |
423 | the_packet = pywsjtx.WSJTXPacketClassFactory.from_udp_packet(addr_port, pkt)
424 |
425 | if type(the_packet) == pywsjtx.HeartBeatPacket:
426 | max_schema = max(the_packet.max_schema, MY_MAX_SCHEMA)
427 | reply_beat_packet = pywsjtx.HeartBeatPacket.Builder(the_packet.wsjtx_id, max_schema)
428 | s.send_packet(addr_port, reply_beat_packet)
429 |
430 | while type(the_packet) == pywsjtx.DecodePacket:
431 | m = re.match(r"^CQ\s+(\S{3,}?)\s+", the_packet.message) or re.match(r"^CQ\s+\S{2}\s+(\S{3,}?)\s+", the_packet.message)
432 | if m:
433 | callsign = m.group(1)
434 | print("Callsign {}".format(callsign))
435 |
436 | cw.input_queue.put({'callsign':callsign, 'input':the_packet, 'addr_port':addr_port})
437 | break
438 | # K1ABC W9XYZ 579 IL
439 | # K1ABC W9XYZ R 559 IL
440 | m = re.match(r"(\S{3,}?)\s+(\S{3,}?)(\sR)?\s5\d\d\s([A-Z]{2,3})", the_packet.message)
441 | if m:
442 | callsign = m.group(2)
443 | section = m.group(4)
444 | #print("Callsign {} - section {}".format(callsign, section))
445 |
446 | cw.input_queue.put({'callsign':callsign, 'input':the_packet, 'addr_port':addr_port, 'exchange':section})
447 | break
448 | break
449 | print(the_packet)
450 |
451 | # service queue
452 |
453 | while not cw.output_queue.empty():
454 | resolved = cw.output_queue.get(False)
455 | print("Resolved packet available callsign:{}, dupe:{}, mult:{}".format(resolved['callsign'], resolved['dupe'], resolved['is_mult']))
456 | wsjtx_id = resolved['input'].wsjtx_id
457 |
458 | if resolved['dupe']:
459 | color_pkt = pywsjtx.HighlightCallsignPacket.Builder(wsjtx_id, resolved['callsign'],
460 | dupe_background_color,
461 | dupe_foreground_color,
462 | True)
463 | s.send_packet(resolved['addr_port'], color_pkt)
464 |
465 | if resolved['is_section_mult']: # color the whole thing
466 | color_pkt = pywsjtx.HighlightCallsignPacket.Builder(wsjtx_id, resolved['input'].message,
467 | mult_background_color,
468 | mult_foreground_color,
469 | True)
470 | s.send_packet(resolved['addr_port'], color_pkt)
471 |
472 | pass
473 | elif resolved['is_mult']:
474 | color_pkt = pywsjtx.HighlightCallsignPacket.Builder(wsjtx_id, resolved['callsign'],
475 | mult_background_color,
476 | mult_foreground_color,
477 | True)
478 | s.send_packet(resolved['addr_port'], color_pkt)
479 |
480 | if not resolved['is_mult'] and not resolved['dupe']:
481 | #color_pkt = pywsjtx.HighlightCallsignPacket.Builder(wsjtx_id, resolved['callsign'],
482 | # pywsjtx.QCOLOR.Uncolor(),
483 | # pywsjtx.QCOLOR.Uncolor(),
484 | # True)
485 | #s.send_packet(resolved['addr_port'], color_pkt)
486 | pass
487 |
488 | if (time.time() - stp_age) > STP_MAX_SECONDS:
489 | logging.debug("Updating sections from N1MM")
490 | already_worked = n1mm.sections_for_prefixes(['K', 'VE'])
491 | stp.update_already_worked(already_worked)
492 | stp_age = time.time()
493 |
494 | if __name__ == "__main__":
495 | main()
496 |
--------------------------------------------------------------------------------
/samples/wsjtx_packet_exchanger.py:
--------------------------------------------------------------------------------
1 | #
2 | # A rude work around to get JTAlert v.2, N1MM Logger+ 1.0.7422.0 (first version with WSJTX support), and WSJT-X 2.0 working
3 | # together.
4 | #
5 | # 0. Start this program (python 3.x)
6 | # 1. Start wsjt-x. Configure UDP server port to be 224.1.1.1, port 5007
7 | # 2. Start N1MM. No configuration possible for wsjtx window
8 | # 3. Verify that N1MM's wsjtx window is receiving CQ spots from wsjtx
9 | # 4. Edit the file C:\Users\\AppData\Local\WSJT-X\WSJT-X.ini
10 | # add these two lines above the UDPServer=224.1.1.1 line:
11 | # udpserver=127.0.0.1
12 | # udpserverport=2238
13 | # Put a comment in front of the UDPServer= and UDPServerPort= lines, like this:
14 | # #UDPServer=224.0.0.1
15 | # #UDPServerPort=5007
16 | # 5. Save the file.
17 | # 6. Start JT-Alert (JTAlert will read the .ini file with the 127.0.0.1:2238 values, and start receiving packets
18 | # 7. Comment out the 127.0.0.1:2238 UDPServer lines with #
19 | # 8. Save the file again.
20 |
21 | # You'll have to do this again each time you start up JTAlert to 'fool' it into using port 2238.
22 | #
23 | # Issues: JTALERT-X closes it's socket connection while the settings dialog is open. We do the brute force thing of trying to
24 | # reestablish the connection to the port
25 |
26 |
27 | # WSJT-X talks to a multicast port. This program sits on the multicast port, and forwards packets it receives to other Unicast sockets.
28 | # It also listens on the unicast sockets, and forwards received packets back to WSJT-X.
29 |
30 | # It assumes that there's only ONE other application sending packets to the multicast socket (WSJT-X)
31 | #
32 |
33 | MULTICAST_PORT = { 'ip_address':'224.1.1.1', 'port':5007, 'timeout':0.2 }
34 | FORWARDING_PORTS = [
35 | { 'ip_address': '127.0.0.1', 'port': 2237, 'timeout':0.2 },
36 | { 'ip_address': '127.0.0.1', 'port': 2238, 'timeout':0.2 }
37 | ]
38 |
39 | import os
40 | import sys
41 | import logging
42 |
43 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
44 | import pywsjtx
45 | import pywsjtx.extra.simple_server
46 |
47 | #
48 | # The RIGHT way to do a packet redistributor like this is to select or wait on the sockets.
49 | # This one trades a little latency (~.50 seconds for two unicast and one multicast socket) for
50 | # not changing some other code I had around
51 | #
52 | def main():
53 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] %(message)s")
54 | consoleFormatter = logging.Formatter('%(asctime)s %(message)s')
55 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
56 | wsjtx_instance_addr_port = None
57 | servers = []
58 | master_server = pywsjtx.extra.simple_server.SimpleServer(MULTICAST_PORT['ip_address'], MULTICAST_PORT['port'],
59 | timeout=MULTICAST_PORT['timeout'])
60 | for port_spec in FORWARDING_PORTS:
61 | servers.append({"instance":pywsjtx.extra.simple_server.SimpleServer(port_spec.get('server_ip_address','127.0.0.1'),
62 | port_spec.get('server_port',0), timeout=port_spec['timeout']),
63 | "to_addr_port": (port_spec['ip_address'], port_spec['port']),
64 | "server_spec": port_spec })
65 |
66 | while (True):
67 | pkt, wsjtx_instance_addr_port = master_server.rx_packet()
68 | if pkt is not None:
69 | logging.debug('RX packet from wsjtx')
70 | for s in servers:
71 | s['instance'].send_packet(s['to_addr_port'],pkt )
72 | for s in servers:
73 | try:
74 | pkt, srv_instance_addr_port = s['instance'].rx_packet()
75 | if pkt is not None:
76 | logging.debug('RX packet from {}'.format(s['to_addr_port']))
77 | master_server.send_packet(wsjtx_instance_addr_port,pkt)
78 | except ConnectionResetError:
79 | # reopen the port.
80 | logging.debug("ERROR - Restarting port {}".format(s['to_addr_port']))
81 | s["instance"] = pywsjtx.extra.simple_server.SimpleServer(port_spec.get('server_ip_address','127.0.0.1'),
82 | port_spec.get('server_port',0), timeout=port_spec['timeout'])
83 |
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmo/py-wsjtx/e725eb689314eff97298ccc87e84bcc5b8bc1ed9/setup.py
--------------------------------------------------------------------------------