├── LICENSE.txt ├── README.md ├── aprsmon.py ├── aprsrecv.py ├── eb200.py ├── fmdemod.py ├── ft4.py ├── ft8.py ├── ft8i.py ├── jt65.py ├── jt65i.py ├── jt65mon.py ├── jt65prefixes.dat ├── libfano ├── Makefile ├── README ├── README-rtm ├── fano.c ├── fano.h ├── metrics.c ├── seqtest.c ├── sim.c ├── tab.c └── wrapfano.c ├── libldpc ├── Makefile ├── arrays.h └── libldpc.c ├── librs ├── Makefile ├── README-rtm ├── char.h ├── decode_rs.c ├── encode_rs.c ├── init_rs.c ├── int.h ├── rs.h └── wrapkarn.c ├── notes.txt ├── pskreport.py ├── sdrip.py ├── sdriq.py ├── sdrplay.py ├── weak.cfg.example ├── weakargs.py ├── weakaudio.py ├── weakcat.py ├── weakdriver.py ├── weakutil.py ├── wspr.py ├── wsprmon.py ├── wwvbmon.py └── wwvmon.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 1021 Robert T. Morris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weakmon 2 | 3 | This is a set of Python/numpy/scipy terminal-window programs for HF 4 | JT65 (jt65i.py) and FT8 (ft8i.py). With an Elecraft K3, both can 5 | listen for CQs on multiple receivers simultaneously, and switch the 6 | receiver(s) among bands each minute. There are also demodulators for 7 | WSPR, APRS, WWV, and WWVB. The software works on Macs, Linux, and 8 | FreeBSD. For an FT8 demodulator in C++, see 9 | [ft8mon](https://github.com/rtmrtmrtmrtm/ft8mon). 10 | 11 | While these programs don't use Joe Taylor's WSJT software, they do 12 | incorporate ideas and protocol details derived from that software. 13 | These programs use Phil Karn's Reed-Solomon and convolutional 14 | decoders. 15 | 16 | The software depends on a few packages. Here's how to install them 17 | on Ubuntu Linux: 18 | ``` 19 | sudo apt-get install python2.7 20 | sudo apt-get install python-six 21 | sudo apt-get install gcc-5 22 | sudo apt-get install python-numpy 23 | sudo apt-get install python-scipy 24 | sudo apt-get install python-pyaudio 25 | sudo apt-get install python-serial 26 | ``` 27 | 28 | If you have a Mac with macports: 29 | ``` 30 | sudo port install python27 31 | sudo port install py27-numpy 32 | sudo port install py27-scipy 33 | sudo port install py27-pyaudio 34 | sudo port install py27-serial 35 | ``` 36 | 37 | Now compile the LDPC decoder, and Phil Karn's Reed-Solomon and convolutional decoders: 38 | ``` 39 | (cd libldpc ; make) 40 | (cd libfano ; make) 41 | (cd librs ; make) 42 | ``` 43 | 44 | At this point you should be able to run ft8i.py, jt65i.py, wsprmon.py, etc. with 45 | no arguments, and see lists of available sound cards and serial ports, 46 | like this: 47 | 48 | ``` 49 | % python2.7 ft8i.py 50 | usage: ft8i.py [-h] [-card CARD CHAN] [-cat TYPE DEV] [-levels] [-v] 51 | [-band BAND] [-bands BANDS] [-card2 CARD CHAN] 52 | [-card3 CARD CHAN] [-card4 CARD CHAN] [-out CARD] [-test] 53 | sound card numbers for -card and -out: 54 | 0: Built-in Output, channels=0 55 | 1: USB Audio CODEC , channels=0 56 | 2: USB Audio CODEC , channels=2 11025 12000 16000 22050 44100 48000 57 | or -card sdrip IPADDR 58 | or -card sdriq /dev/SERIALPORT 59 | or -card eb200 IPADDR 60 | or -card sdrplay sdrplay 61 | serial devices for -cat: 62 | /dev/cu.usbserial-A503XT23 63 | /dev/cu.Bluetooth-Incoming-Port 64 | radio types for -cat: k3 rx340 8711 sdrip sdriq r75 r8500 ar5000 eb200 sdrplay prc138 65 | ``` 66 | 67 | If you've hooked up a transceiver with VOX to your computer's sound 68 | card, and set it to 14.074 and USB, you can use ft8i.py like 69 | this: 70 | 71 | ``` 72 | % python2.7 ft8i.py -card 2 0 -out 1 -band 20 73 | 74 | ``` 75 | 76 | The "-card 2 0" means the left (0) channel of sound card number 2 (as 77 | listed in ft8i.py's "usage" output). The "-out 1" means sound card 1. 78 | 79 | ft8i.py will display decoded messages, and mark each received CQ 80 | with a letter; type the letter to respond to the CQ. 81 | 82 | ft8i.py can automatically switch among a set of bands, once per 83 | minute. For example, this command will tell a K3 to scan 30, 20, and 84 | 17 meters for JT65. 85 | 86 | ``` 87 | % python2.7 ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -bands "30 20 17" 88 | ``` 89 | 90 | ft8i.py can listen to multiple receivers at the same time, so that 91 | you can look for CQs on more than one band simultaneously. For 92 | example, for a K3 with a sub-receiver: 93 | 94 | ``` 95 | % python2.7 ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -bands "40 30 20" 96 | ``` 97 | 98 | For a K3 (without sub-receiver) and an RFSpace NetSDR/CloudIQ/SDR-IP: 99 | 100 | ``` 101 | % python2.7 ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 sdrip 192.168.3.130 -bands "40 30 20" 102 | ``` 103 | 104 | For a K3 with sub-receiver and an RFSpace NetSDR/CloudIQ/SDR-IP (i.e. three receivers): 105 | 106 | ``` 107 | % python2.7 ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -card3 sdrip 192.168.3.130 -bands "40 30 20" 108 | ``` 109 | 110 | You may need to take steps to give yourself permission to use the 111 | serial device (change its mode or put yourself in the appropriate 112 | group). 113 | 114 | The aprsmon.py, jt65mon.py, wsprmon.py, wwvbmon.py, and wwvmon.py 115 | programs each decode and display receptions for the respective format. 116 | They use argument conventions similar to those of ft8i.py. 117 | 118 | These programs can switch among bands on a number of radio types: 119 | Elecraft K3, Ten-Tec RX-340, Watkins Johnson WJ-8711, RFSpace SDR-IP, 120 | RFSpace CloudIQ, RFSpace NetSDR, RFSpace SDR-IQ, Icom R75, Icom R8500, 121 | AOR AR5000, Rohde and Schwarz EB200, SDRplay, and Harris PRC-138. 122 | 123 | Use the -levels flag to help adjust the audio level from the radio. 124 | Peaks of a few thousand are good. 125 | 126 | You must set your call sign and grid to send with jt65i.py or ft8i.py, 127 | or to report to wsprnet and pskreporter. Do this by copying 128 | weak.cfg.example to weak.cfg, un-commenting the mycall and mygrid 129 | lines, and changing them to your callsign and grid. 130 | 131 | Your computer's clock must be correct to within about second for WSPR, 132 | JT65, and FT8; try ntp. 133 | 134 | This software surely contains errors, particularly since I'm no expert 135 | at signal processing. I'd appreciate fixes for any bugs you discover. 136 | 137 | Robert Morris, AB1HL 138 | -------------------------------------------------------------------------------- /aprsmon.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # decode APRS packets. 5 | # 6 | # needs a sound card connected to a radio 7 | # tuned to 144.390 in FM. 8 | # 9 | # to read from a .wav file, use ./aprsrecv.py -file xxx.wav 10 | # 11 | 12 | import aprsrecv 13 | import weakargs 14 | import weakaudio 15 | import weakcat 16 | import sys 17 | 18 | def cb(fate, msg, start, space_to_mark, snr): 19 | # fate=0 -- unlikely to be correct. 20 | # fate=1 -- CRC failed but syntax look OK. 21 | # fate=2 -- CRC is correct. 22 | if fate >= 2: 23 | print("%.1f %s" % (snr, msg)) 24 | 25 | def main(): 26 | parser = weakargs.stdparse('Decode APRS.') 27 | args = weakargs.parse_args(parser) 28 | 29 | if args.cat != None: 30 | cat = weakcat.open(args.cat) 31 | cat.set_fm_data() 32 | # cat.sdr.setgain(0) 33 | cat.setf(0, 144390000) 34 | 35 | if args.card == None: 36 | parser.error("aprsmon requires -card") 37 | 38 | ar = aprsrecv.APRSRecv() 39 | ar.callback = cb 40 | ar.opencard(args.card) 41 | ar.gocard() 42 | 43 | sys.exit(0) 44 | 45 | main() 46 | -------------------------------------------------------------------------------- /eb200.py: -------------------------------------------------------------------------------- 1 | # 2 | # Control a Rohde & Schwarz EB200 receiver through its ethernet interface. 3 | # 4 | # Robert Morris, AB1HL 5 | # 6 | 7 | import socket 8 | import sys 9 | import threading 10 | import time 11 | import struct 12 | import numpy 13 | import weakutil 14 | 15 | # 16 | # if already connected, return existing EB200, 17 | # otherwise a new one. 18 | # 19 | eb200s = { } 20 | mu = threading.Lock() 21 | def open(dev): 22 | global eb200s, mu 23 | mu.acquire() 24 | if not (dev in eb200s): 25 | eb200s[dev] = EB200(dev) 26 | eb = eb200s[dev] 27 | mu.release() 28 | return eb 29 | 30 | class EB200: 31 | 32 | def __init__(self, ipaddr, port=5555): 33 | self.ipaddr = ipaddr 34 | self.port = port 35 | 36 | self.tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 37 | self.tcp.connect((ipaddr, port)) 38 | myhost = self.tcp.getsockname()[0] 39 | 40 | self.udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 41 | self.udp.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024) 42 | self.udp.bind(('', 0)) # ask kernel for a port 43 | myport = self.udp.getsockname()[1] 44 | 45 | # set audio format to 8000/sec, 16 bits/sample, one channel. 46 | self.rate = 8000 47 | self.tcp.send("SYSTem:AUDio:REMote:MODe 10\n") 48 | 49 | # set up a UdpPath pointing at our UDP port. 50 | # use TRACE:UDP? to read this back. 51 | cmd = "TRACE:UDP:DEFAULT:TAG:ON \"%s\", %d, AUDIO" % (myhost, 52 | myport) 53 | self.tcp.send("%s\n" % (cmd)) 54 | 55 | # input buffer of buffers filled by reader() thread. 56 | self.bufs = [ ] 57 | self.bufs_mu = threading.Lock() 58 | 59 | # last sequence number seen from EB200 in a UDP packet. 60 | self.seq = None 61 | 62 | # separate reader thread to keep reading the UDP 63 | # socket so it doesn't overflow. 64 | self.th = threading.Thread(target=lambda : self.reader()) 65 | self.th.daemon = True 66 | self.th.start() 67 | 68 | def parse(self, buf): 69 | if len(buf) < 16+4+8: 70 | sys.stderr.write("eb200: too short\n") 71 | return 72 | 73 | # EB200 header. 74 | h = struct.unpack(">IHHH", buf[0:10]) 75 | if h[0] != 0x000EB200: 76 | sys.stderr.write("eb200: bad magic\n") 77 | return 78 | seq = h[3] 79 | 80 | if self.seq != None: 81 | if seq != self.seq + 1 and seq > self.seq: 82 | sys.stderr.write("eb200: missed %d packets (%d %d)\n" % (seq - self.seq, 83 | self.seq, seq)) 84 | # XXX should fake the right number of samples. 85 | self.seq = seq 86 | 87 | # GenericAttribute 88 | ga = struct.unpack(">HH", buf[16:20]) 89 | if ga[0] != 401: 90 | sys.stderr.write("eb200: not an AUDIO tag\n") 91 | return 92 | galen = ga[1] # the whole rest of the packet 93 | 94 | # TraceAttribute == AudioAttribute 95 | ta = struct.unpack(">HBBI", buf[20:28]) 96 | 97 | # ta[0] is the number of 16-bit samples. 98 | # ta[3] is selectorFlags, always 0x40000. 99 | if ta[2] != 0: 100 | sys.stderr.write("eb200: unexpected optional header\n") 101 | return 102 | if ta[3] != 0x40000: 103 | sys.stderr.write("eb200: unexpected flag %x\n" % (ta[3])) 104 | return 105 | 106 | samples = buf[28:] 107 | 108 | # samples is now a string, containing big-endian shorts. 109 | # convert to a numpy array. 110 | # this code assumes the EB200 sends signed samples. 111 | # samples = numpy.fromstring(samples, dtype=numpy.int16) 112 | samples = numpy.fromstring(samples, dtype='>i2') 113 | 114 | self.bufs_mu.acquire() 115 | self.bufs.append(samples) 116 | self.bufs_mu.release() 117 | 118 | 119 | def reader(self): 120 | while True: 121 | buf = self.udp.recv(8192) 122 | self.parse(buf) 123 | 124 | # blocks until some samples are available. 125 | def readaudio(self): 126 | while True: 127 | self.bufs_mu.acquire() 128 | bufs = self.bufs 129 | self.bufs = [ ] 130 | self.bufs_mu.release() 131 | 132 | if len(bufs) > 0: 133 | buf = numpy.concatenate(bufs) 134 | return buf 135 | 136 | time.sleep(0.2) 137 | 138 | def getrate(self): 139 | return self.rate 140 | 141 | def setfreq(self, hz): 142 | self.tcp.send("freq %d\n" % (hz)) 143 | 144 | def set_usb_data(self): 145 | self.tcp.send(":freq:afc 0\n") 146 | self.tcp.send(":output:squelch 0\n") 147 | self.tcp.send(":input:att:auto 1\n") 148 | # set BW first, since otherwise can't change to USB. 149 | self.tcp.send("band 2400\n") 150 | self.tcp.send("demodulation usb\n") 151 | 152 | def set_fm_data(self): 153 | self.tcp.send(":freq:afc 0\n") 154 | self.tcp.send(":output:squelch 0\n") 155 | self.tcp.send(":input:att:auto 1\n") 156 | self.tcp.send("band 15000\n") 157 | self.tcp.send("demodulation fm\n") 158 | self.tcp.send("band 15000\n") 159 | -------------------------------------------------------------------------------- /fmdemod.py: -------------------------------------------------------------------------------- 1 | # 2 | # FM demodulation, from i/q to audio. 3 | # 4 | 5 | import numpy 6 | import scipy 7 | import scipy.signal 8 | 9 | def butter_lowpass(cut, samplerate, order=5): 10 | nyq = 0.5 * samplerate 11 | cut = cut / nyq 12 | b, a = scipy.signal.butter(order, cut, btype='lowpass') 13 | return b, a 14 | 15 | class FMDemod: 16 | 17 | def __init__(self, rate): 18 | self.rate = rate 19 | self.width = 8000 20 | self.order = 3 21 | self.filter = None 22 | self.zi = None 23 | 24 | # 25 | # input is complex i/q raw samples. 26 | # output is (audio, amplitudes). 27 | # audio is the demodulated audio. 28 | # amplitudes are the i/q amplitudes, so the caller 29 | # can compute SNR (if they can guess what is signal 30 | # and what is noise). 31 | # 32 | def demod(self, samples): 33 | # complex low-pass filter. 34 | # passes fmwidth on either side of zero, 35 | # so real width is 2*self.width. 36 | if self.filter == None: 37 | self.filter = butter_lowpass(self.width, self.rate, self.order) 38 | self.zi = scipy.signal.lfiltic(self.filter[0], 39 | self.filter[1], 40 | [0]) 41 | bzi = scipy.signal.lfilter(self.filter[0], 42 | self.filter[1], 43 | samples, 44 | zi=self.zi) 45 | cc2 = bzi[0] 46 | self.zi = bzi[1] 47 | 48 | # quadrature fm demodulation, as in gnuradio gr_quadrature_demod_cf. 49 | # seems to be the same as the Lyons scheme. 50 | product = numpy.multiply(cc2[1:], numpy.conjugate(cc2[0:-1])) 51 | diff = numpy.angle(product) 52 | diff = numpy.append(diff, diff[-1]) 53 | 54 | # don't de-emphasize, since intended for aprs. 55 | 56 | # calculate amplitude at each sample, 57 | # for later snr calculation. 58 | # calculated as post-filter length of complex vector. 59 | amp = numpy.sqrt(numpy.add(numpy.square(cc2.real), numpy.square(cc2.imag))) 60 | 61 | return (diff, amp) 62 | -------------------------------------------------------------------------------- /ft8i.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # interactive FT8. 5 | # runs in a Linux or Mac terminal window. 6 | # user can respond to CQs, though not send CQ. 7 | # optional automatic band switching when not in QSO. 8 | # 9 | # I use ft8i.py with a K3S via USB, automatically switching 10 | # among bands: 11 | # ./ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 12 | # 13 | # To switch among just a few bands: 14 | # ./ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -bands "30 20 17" 15 | # 16 | # To use on a single band without CAT (radio must be set up 17 | # correctly already, and have VOX if you want to transmit): 18 | # ./ft8i.py -card 2 0 -out 1 -band 30 19 | # 20 | # Select a CQ to reply to by typing the letter displayed 21 | # next to the CQ. ft8i.py automates the rest of the exchange. 22 | # 23 | # ft8i.py can use multiple receivers, listening to a different band on 24 | # each; when you reply to a CQ it automatically sets the transmitter to 25 | # the correct band. This works for a K3 with a sub-receiver, or a K3 26 | # with SDRs that ft8i.py knows how to control. 27 | # 28 | # For a K3 with a sub-receiver: 29 | # ./ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -bands "40 30 20" 30 | # 31 | # For a K3 without sub-receiver, and an RFSpace NetSDR/CloudIQ/SDR-IP: 32 | # ./ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 sdrip 192.168.3.130 -bands "40 30 20" 33 | # 34 | # For a K3 with sub-receiver and an RFSpace NetSDR/CloudIQ/SDR-IP (i.e. three receivers): 35 | # ./ft8i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -card3 sdrip 192.168.3.130 -bands "40 30 20" 36 | # 37 | # Robert Morris, AB1HL 38 | # 39 | 40 | #import fakeft8 as ft8 41 | import ft8 42 | import weakdriver 43 | 44 | # automatically switch only among these bands. 45 | # auto_bands = [ "160", "80", "60", "40", "30", "20", "17", "15", "12", "10" ] 46 | auto_bands = [ "40", "30", "20", "17", "15", "12", "10" ] 47 | 48 | frequencies = { "160" : 1.840, "80" : 3.573, "40" : 7.074, 49 | "30" : 10.136, "20" : 14.074, 50 | "17" : 18.100, "15" : 21.074, "12" : 24.915, 51 | "10" : 28.074, "6" : 50.313 } 52 | 53 | def main(): 54 | weakdriver.driver_main("ft8i", ft8.FT8, ft8.FT8Send, frequencies, 55 | auto_bands, "ft8", "FT") 56 | 57 | main() 58 | -------------------------------------------------------------------------------- /jt65i.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # interactive JT65. 5 | # runs in a Linux or Mac terminal window. 6 | # user can respond to CQs, but not send CQ. 7 | # optional automatic band switching when not in QSO. 8 | # 9 | # I use jt65i.py with a K3S via USB, automatically switching 10 | # among bands: 11 | # ./jt65i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 12 | # 13 | # To switch among just a few bands: 14 | # ./jt65i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -bands "30 20 17" 15 | # 16 | # To use on a single band without CAT (radio must be set up 17 | # correctly already, and have VOX if you want to transmit): 18 | # ./jt65i.py -card 2 0 -out 1 -band 30 19 | # 20 | # Select a CQ to reply to by typing the letter displayed 21 | # next to the CQ. jt65i.py automates the rest of the exchange. 22 | # 23 | # jt65i.py can use multiple receivers, listening to a different band on 24 | # each; when you reply to a CQ it automatically sets the transmitter to 25 | # the correct band. This works for a K3 with a sub-receiver, or a K3 26 | # with SDRs that jt65i.py knows how to control. 27 | # 28 | # For a K3 with a sub-receiver: 29 | # ./jt65i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -bands "40 30 20" 30 | # 31 | # For a K3 without sub-receiver, and an RFSpace NetSDR/CloudIQ/SDR-IP: 32 | # ./jt65i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 sdrip 192.168.3.130 -bands "40 30 20" 33 | # 34 | # For a K3 with sub-receiver and an RFSpace NetSDR/CloudIQ/SDR-IP (i.e. three receivers): 35 | # ./jt65i.py -card 2 0 -out 1 -cat k3 /dev/cu.usbserial-A503XT23 -card2 2 1 -cat2 k3 - -card3 sdrip 192.168.3.130 -bands "40 30 20" 36 | # 37 | # Robert Morris, AB1HL 38 | # 39 | 40 | #import fake65 as jt65 41 | import jt65 42 | import weakdriver 43 | 44 | # automatically switch only among these bands. 45 | # auto_bands = [ "160", "80", "60", "40", "30", "20", "17", "15", "12", "10" ] 46 | auto_bands = [ "40", "30", "20", "17" ] 47 | 48 | frequencies = { "160" : 1.838, "80" : 3.576, "60" : 5.357, "40" : 7.076, 49 | "30" : 10.138, "20" : 14.076, 50 | "17" : 18.102, "15" : 21.076, "12" : 24.917, 51 | "10" : 28.076, "6" : 50.276 } 52 | def main(): 53 | weakdriver.driver_main("jt65i", jt65.JT65, jt65.JT65Send, frequencies, 54 | auto_bands, "jt65", "FT") 55 | 56 | main() 57 | -------------------------------------------------------------------------------- /jt65mon.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # receive JT65. 5 | # 6 | # switches among bands if weakcat.py understands the radio. 7 | # reports to pskreporter.info if mycall/mygrid defined in weak.ini. 8 | # 9 | # Robert Morris, AB1HL 10 | # 11 | 12 | import jt65 13 | import sys 14 | import os 15 | import time 16 | import numpy 17 | import threading 18 | import re 19 | import random 20 | import copy 21 | import weakcat 22 | import weakaudio 23 | import weakutil 24 | import weakargs 25 | import pskreport 26 | 27 | # look only at these bands. 28 | plausible = [ "40", "30", "20", "17", "15" ] 29 | 30 | b2f = { "160" : 1.838, "80" : 3.576, "60" : 5.357, "40" : 7.076, 31 | "30" : 10.138, "20" : 14.076, 32 | "17" : 18.102, "15" : 21.076, "12" : 24.917, 33 | "10" : 28.076, "6" : 50.276 } 34 | 35 | def load_prefixes(): 36 | d = { } 37 | f = open("jt65prefixes.dat") 38 | for ln in f: 39 | ln = re.sub(r'\t', ' ', ln) 40 | ln = re.sub(r' *', ' ', ln) 41 | ln.strip() 42 | ln = re.sub(r' *\(.*\) *', '', ln) 43 | ln.strip() 44 | m = re.search(r'^([A-Z0-9]+) +(.*)', ln) 45 | if m != None: 46 | d[m.group(1)] = m.group(2) 47 | f.close() 48 | return d 49 | 50 | def look_prefix(call, d): 51 | if len(call) == 5 and call[0:3] == "KG4": 52 | # KG4xx is Guantanamo, KG4x and KG4xxx are not. 53 | return "Guantanamo Bay" 54 | 55 | while len(call) > 0: 56 | if call in d: 57 | return d[call] 58 | call = call[0:-1] 59 | return None 60 | 61 | # weighted choice (to pick bands). 62 | # a[i] = [ value, weight ] 63 | def wchoice(a, n): 64 | total = 0.0 65 | for e in a: 66 | total += e[1] 67 | 68 | ret = [ ] 69 | while len(ret) < n: 70 | x = random.random() * total 71 | for ai in range(0, len(a)): 72 | e = a[ai] 73 | if x <= e[1]: 74 | ret.append(e[0]) 75 | total -= e[1] 76 | a = a[0:ai] + a[ai+1:] 77 | break 78 | x -= e[1] 79 | 80 | return ret 81 | 82 | def wchoice_test(): 83 | a = [ [ "a", .1 ], [ "b", .1 ], [ "c", .4 ], [ "d", .3 ], [ "e", .1 ] ] 84 | counts = { } 85 | for iter in range(0, 500): 86 | x = wchoice(a, 2) 87 | for e in x: 88 | counts[e] = counts.get(e, 0) + 1 89 | print(counts) 90 | 91 | # listen for CQ, answer. 92 | class JT65Mon: 93 | def __init__(self, desc1, desc2, cat, oneband): 94 | self.mycall = weakutil.cfg("jt65mon", "mycall") 95 | self.mygrid = weakutil.cfg("jt65mon", "mygrid") 96 | 97 | self.oneband = oneband 98 | self.verbose = False 99 | self.rate = 11025 100 | self.allname = "jt65-all.txt" 101 | self.bandname = "jt65-band.txt" 102 | self.jtname = "jt65" 103 | 104 | self.incards = [ ] 105 | self.incards.append(desc1) 106 | if desc2 != None: 107 | self.incards.append(desc2) 108 | 109 | if cat != None: 110 | self.cat = weakcat.open(cat) 111 | self.cat.sync() 112 | self.cat.set_usb_data() 113 | else: 114 | self.cat = None 115 | 116 | # for each band, count of received signals last time we 117 | # looked at it, to guess most profitable band. 118 | self.bandinfo = { } 119 | 120 | self.prefixes = load_prefixes() 121 | 122 | if self.mycall != None and self.mygrid != None: 123 | # talk to pskreporter. 124 | print("reporting to pskreporter as %s at %s" % (self.mycall, self.mygrid)) 125 | self.pskr = pskreport.T(self.mycall, self.mygrid, "weakmon 0.2", False) 126 | else: 127 | print("not reporting to pskreporter since call/grid not in weak.cfg") 128 | self.pskr = None 129 | 130 | # read latest msgs from all cards. 131 | def readall(self, now, bands): 132 | minute = self.r[0].minute(now) 133 | 134 | # for each card, new msgs 135 | all = [ ] # all messages, with duplicates 136 | d = { } # for each text, [ card_index, msg ] 137 | for ci in range(0, len(self.r)): 138 | bandcount = 0 # all msgs 139 | bandcount1 = 0 # msgs with lowist reed-solomon error counts 140 | msgs = self.r[ci].get_msgs() 141 | # each msg is a jt65.Decode 142 | for m in msgs: 143 | m.card = ci 144 | if m.minute == minute: 145 | bandcount += 1 146 | if m.nerrs < 25: 147 | bandcount1 += 1 148 | all.append(m) 149 | z = d.get(m.msg, []) 150 | z.append([ ci, m ]) 151 | d[m.msg] = z 152 | else: 153 | print("LATE: %s %.1f %s" % (self.r[ci].ts(m.decode_time), m.hz(), m.msg)) 154 | x = self.bandinfo.get(bands[ci], 0) 155 | self.bandinfo[bands[ci]] = 0.5 * x + 0.5 * bandcount1 156 | 157 | f = open(self.bandname, "a") 158 | f.write("%s %s %d %2d %2d\n" % (self.r[ci].ts(now), 159 | bands[ci], 160 | ci, 161 | bandcount, 162 | bandcount1)) 163 | f.close() 164 | 165 | # append each msg to jt65-all.txt. 166 | for txt in d: 167 | band = None 168 | got = "" 169 | hz = None 170 | snr = None 171 | nerrs = [ -1, -1 ] # for each antenna 172 | for [ ci, m ] in d[txt]: 173 | band = bands[ci] 174 | if not (str(ci) in got): 175 | got += str(ci) 176 | hz = m.hz() 177 | nerrs[ci] = m.nerrs 178 | if snr == None or m.snr > snr: 179 | snr = m.snr 180 | 181 | info = "%s %s rcv %2s %2d %2d %3.0f %6.1f %s" % (self.r[ci].ts(m.decode_time), 182 | band, 183 | got, 184 | nerrs[0], 185 | nerrs[1], 186 | snr, 187 | m.hz(), 188 | m.msg) 189 | 190 | print(info) 191 | 192 | f = open(self.allname, "a") 193 | f.write(info + "\n") 194 | f.close() 195 | 196 | # send msgs that don't have too many errors to pskreporter. 197 | # the 30 here suppresses some good CQ receptions, but 198 | # perhaps better that than reporting erroneous decodes. 199 | if (nerrs[0] >= 0 and nerrs[0] < 30) or (nerrs[1] >= 0 and nerrs[1] < 30): 200 | txt = m.msg 201 | hz = m.hz() + int(b2f[band] * 1000000.0) 202 | tm = m.decode_time 203 | self.maybe_pskr(txt, hz, tm) 204 | 205 | return all 206 | 207 | # report to pskreporter if we can figure out the originating 208 | # call and grid. 209 | def maybe_pskr(self, txt, hz, tm): 210 | if self.pskr == None: 211 | return 212 | txt = txt.strip() 213 | txt = re.sub(r' *', ' ', txt) 214 | txt = re.sub(r'CQ DX ', 'CQ ', txt) 215 | txt = re.sub(r'CQDX ', 'CQ ', txt) 216 | mm = re.search(r'^CQ ([0-9A-Z/]+) ([A-R][A-R][0-9][0-9])$', txt) 217 | if mm != None and self.iscall(mm.group(1)): 218 | self.pskr.got(mm.group(1), hz, "JT65", mm.group(2), tm) 219 | return 220 | mm = re.search(r'^([0-9A-Z/]+) ([0-9A-Z/]+) ([A-R][A-R][0-9][0-9])$', txt) 221 | if mm != None and self.iscall(mm.group(1)) and self.iscall(mm.group(2)): 222 | self.pskr.got(mm.group(2), hz, "JT65", mm.group(3), tm) 223 | return 224 | 225 | # does call look syntactically correct? 226 | def iscall(self, call): 227 | if len(call) < 3: 228 | return False 229 | if re.search(r'[0-9][A-Z]', call) == None: 230 | # no digit 231 | return False 232 | return True 233 | 234 | # return two bands to listen for CQs on. 235 | # specialized to two receivers. doesn't work for 236 | # just one receiver. 237 | def rankbands(self): 238 | global plausible 239 | 240 | if len(plausible) == 1: 241 | return [ plausible[0] ] 242 | 243 | # are we missing bandinfo for any bands? 244 | missing = [ ] 245 | for b in plausible: 246 | if self.bandinfo.get(b) == None: 247 | missing.append(b) 248 | 249 | # most profitable bands, best first. 250 | best = sorted(plausible, key = lambda b : -self.bandinfo.get(b, -1)) 251 | 252 | # always explore missing bands first. 253 | if len(missing) >= len(self.r): 254 | return missing[0:len(self.r)] 255 | 256 | if len(missing) > 0: 257 | return [ best[0], missing[0] ] 258 | 259 | ret = [ ] 260 | if len(self.r) > 1: 261 | # two receivers. 262 | # always look at best band. 263 | ret.append(best[0]) 264 | best = best[1:] 265 | 266 | if len(best) == 0: 267 | pass 268 | elif random.random() < 0.3 or self.bandinfo[best[0]] <= 0.1: 269 | band = random.choice(best) 270 | ret.append(band) 271 | else: 272 | wa = [ [ b, self.bandinfo[b] ] for b in best ] 273 | band = wchoice(wa, 1)[0] 274 | ret.append(band) 275 | 276 | return ret 277 | 278 | def one(self): 279 | if self.oneband != None: 280 | bands = [ self.oneband ] * len(self.r) 281 | else: 282 | # choose a band per receiver. 283 | bands = self.rankbands() 284 | 285 | bands = bands[0:len(self.r)] 286 | while len(bands) < len(self.r): 287 | bands.append(bands[0]) 288 | 289 | # highest frequency on main receiver, so that low-pass ATU 290 | # will let us use main antenna on sub-receiver too. 291 | bands = sorted(bands, key = lambda b : int(b)) 292 | 293 | if self.verbose: 294 | sys.stdout.write("band ") 295 | for b in bands: 296 | sys.stdout.write("%s " % (b)) 297 | sys.stdout.write("; ") 298 | for b in self.bandinfo: 299 | sys.stdout.write("%s %.1f, " % (b, self.bandinfo[b])) 300 | sys.stdout.write("\n") 301 | sys.stdout.flush() 302 | 303 | if self.cat != None: 304 | for i in range(0, len(self.r)): 305 | self.cat.setf(i, int(b2f[bands[i]] * 1000000.0)) 306 | 307 | time.sleep(55) 308 | 309 | # wait for the 59th second. 310 | while True: 311 | now = time.time() 312 | second = self.r[0].second(now) 313 | if second >= 59.5: 314 | break 315 | time.sleep(0.2) 316 | 317 | self.readall(now, bands) 318 | 319 | def go(self): 320 | # receive card(s) 321 | self.r = [ ] 322 | self.rth = [ ] 323 | for c in self.incards: 324 | r = jt65.JT65() 325 | self.r.append(r) 326 | r.cardrate = self.rate 327 | r.opencard(c) 328 | th = threading.Thread(target=lambda : r.gocard()) 329 | th.daemon = True 330 | th.start() 331 | self.rth.append(th) 332 | 333 | while True: 334 | self.one() 335 | 336 | def close(self): 337 | for r in self.r: 338 | r.close() 339 | for th in self.rth: 340 | th.join() 341 | 342 | def main(): 343 | parser = weakargs.stdparse('Decode JT65A.') 344 | parser.add_argument("-band") 345 | 346 | args = weakargs.parse_args(parser) 347 | 348 | if args.card == None: 349 | parser.error("jt65mon requires -card") 350 | 351 | if args.cat == None and args.band == None: 352 | parser.error("jt65mon needs either -cat or -band") 353 | 354 | jt65mon = JT65Mon(args.card, args.card2, args.cat, args.band) 355 | jt65mon.verbose = args.v 356 | jt65mon.go() 357 | jt65mon.close() 358 | sys.exit(0) 359 | 360 | main() 361 | -------------------------------------------------------------------------------- /libfano/Makefile: -------------------------------------------------------------------------------- 1 | CC=cc 2 | 3 | all : 4 | $(CC) -O2 -std=gnu99 fano.c tab.c metrics.c wrapfano.c -shared -fPIC -o libfano.so 5 | -------------------------------------------------------------------------------- /libfano/README: -------------------------------------------------------------------------------- 1 | Fano decoder v1.1 2 | Copyright 1995 Phil Karn 3 | 4 | This package includes an encoder and a soft-decision sequential 5 | decoder for K=32, rate 1/2 convolutional codes. The decoder uses the 6 | Fano algorithm. 7 | 8 | Also included are support routines to generate metric tables that are 9 | optimized for gaussian noise with a specified Eb/N0 ratio, and a test 10 | driver that exercises the encoder/decoder routines and keeps statistics. 11 | 12 | The files are as follows: 13 | 14 | README this file 15 | Makefile Makefile for GCC under BSDI 2.0 (edit to taste) 16 | fano.h header file with declarations for fano.c 17 | fano.c the encoder and Fano decoder routines 18 | metrics.c metric table generator 19 | sim.c transmitter/channel simulator (including gaussian noise gen) 20 | seqtest.c driver program for testing 21 | tab.c parity lookup table 22 | 23 | The test program in seqtest.c creates a test frame, encodes it, adds 24 | gaussian noise and decodes it. It then repeats a specified number of 25 | times, keeping a histogram on the number of decoder cycles required 26 | per bit. By default, the program continuously displays the statistics 27 | using the UNIX "curses" package; this can be suppressed with the -q 28 | (quiet) option. 29 | 30 | The gaussian random number generator in sim.c uses the traditional 31 | "rejection" method. This requires slightly more than one floating 32 | point square root and log function per pair of gaussian numbers. This 33 | makes noise generation rather slow, much slower in fact than the 34 | actual sequential decoding process (except for very noisy 35 | packets). The BSDI 2.0 math library routines do not make use of the 36 | native 387 FPU instructions, and this made it even slower. So the 37 | makefile specifies a separate library that I built locally (-lm387) 38 | with versions of the log and sqrt functions that do use the FPU. Other 39 | 386/486 UNIX clones apparently do have updated math libraries, so this 40 | special library shouldn't be necessary. In that case, simply eliminate 41 | the reference to -lm387. If you do need my lm387 library, let me know 42 | and I can package it up for release. 43 | 44 | If you want to time the speed of the decoder, use the -t (timetest) 45 | option. This executes the decoder in a tight loop repeatedly decoding 46 | the same packet, allowing you to test just the decoder and not the 47 | noise generator or screen update routines. Use the UNIX "time" command 48 | to get your results. 49 | 50 | Three code polynomials are supported as described in fano.c. A #define 51 | statement at the top of fano.c selects the polynomial. 52 | 53 | The arguments to the encoder and decoder routines are documented in 54 | comments in fano.c. Note that the encoded symbols created by encode() 55 | take on the values 0 and 1, while fano() ordinarily expects 8-bit 56 | soft-decision receive symbols. The interpretation of these symbols by 57 | the decoder is completely determined by the metric table. 58 | 59 | The metric table generator in gen_met() in metrics.c assumes the 60 | channel is corrupted by gaussian noise at some specified level, that 61 | the received symbols from the modem are offset-128 binary with some 62 | specified amplitude, and that symbol value corresponding to a 63 | transmitted "1" is larger than for a "0". Given these parameters it 64 | builds the table from by computing the log-likelihood function for 65 | each possible received symbol value. 66 | 67 | The performance of a sequential decoder depends critically on the 68 | accuracy of its metric table. In some cases (e.g., the estimated noise 69 | level is incorrect) the degradation may be relatively minor. But if 70 | the table is way off, e.g., if the signal level is so low compared to 71 | the expected values that a negative metric results even on the correct 72 | path, then the decoder won't work even if the signal is otherwise 73 | completely clean. 74 | 75 | By generating the appropriate metric table, you could use the decoder 76 | on some other kind of channel. For example, on a binary (hard 77 | decision) channel there would only be four table entries corresponding 78 | to the four channel transition probabilities. And if the channel is 79 | symmetric (BSC), this 2x2 matrix would also be symmetric. 80 | 81 | If you use this decoder in an actual application, you won't need sim.c 82 | and seqtest.c. Also, instead of including metrics.c to compute the 83 | metric tables at application run time, you could build a set of metric 84 | tables into static tables, perhaps for several fixed Eb/N0s. This 85 | would avoid floating point math in your runtime package. 86 | 87 | Any real application will also probably require an interleaver, since 88 | convolutional coders (especially sequential decoders) are highly 89 | sensitive to burst errors. 90 | 91 | Phil Karn 92 | 93 | March 1995: version 1.0 94 | Original release for BSDI 1.1 95 | 96 | August 5, 1995: version 1.1 97 | Updated for BSDI 2.0 98 | - changes in curses/termcap 99 | Several minor bugs fixed: 100 | - fencepost error in cycle count returned by fano() 101 | - correct returned value of cumulative metric 102 | - clean up computation of noise level for gen_met() in seqtest.c, change 103 | convention to conform to viterbi 1.1 package 104 | 105 | 106 | -------------------------------------------------------------------------------- /libfano/README-rtm: -------------------------------------------------------------------------------- 1 | # this works on OSX, FreeBSD, and Linux: 2 | cc -O2 -std=gnu99 fano.c tab.c metrics.c wrapfano.c -shared -fPIC -o libfano.so 3 | 4 | import ctypes 5 | libfano = ctypes.cdll.LoadLibrary("./libfano.so") 6 | libfano.fano_encode(...) 7 | 8 | # this also works on OSX: 9 | clang -O -dynamiclib -std=gnu99 fano.c tab.c metrics.c wrapfano.c -current_version 1.0 -compatibility_version 1.0 -o libfano.A.dylib 10 | import ctypes 11 | libfano = ctypes.cdll.LoadLibrary("libfano.A.dylib") 12 | libfano.fano_encode(...) 13 | -------------------------------------------------------------------------------- /libfano/fano.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Soft decision Fano sequential decoder for K=32 r=1/2 convolutional code 3 | * Copyright 1994, Phil Karn, KA9Q 4 | */ 5 | #define LL 1 /* Select Layland-Lushbaugh code */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include "fano.h" 11 | 12 | struct node { 13 | unsigned long encstate; /* Encoder state of next node */ 14 | long gamma; /* Cumulative metric to this node */ 15 | int metrics[4]; /* Metrics indexed by all possible tx syms */ 16 | int tm[2]; /* Sorted metrics for current hypotheses */ 17 | int i; /* Current branch being tested */ 18 | }; 19 | 20 | /* Convolutional coding polynomials. All are rate 1/2, K=32 */ 21 | #ifdef NASA_STANDARD 22 | /* "NASA standard" code by Massey & Costello 23 | * Nonsystematic, quick look-in, dmin=11, dfree=23 24 | * used on Pioneer 10-12, Helios A,B 25 | */ 26 | #define POLY1 0xbbef6bb7 27 | #define POLY2 0xbbef6bb5 28 | #endif 29 | 30 | #ifdef MJ 31 | /* Massey-Johannesson code 32 | * Nonsystematic, quick look-in, dmin=13, dfree>=23 33 | * Purported to be more computationally efficient than Massey-Costello 34 | */ 35 | #define POLY1 0xb840a20f 36 | #define POLY2 0xb840a20d 37 | #endif 38 | 39 | #ifdef LL 40 | /* Layland-Lushbaugh code 41 | * Nonsystematic, non-quick look-in, dmin=?, dfree=? 42 | */ 43 | #define POLY1 0xf2d05351 44 | #define POLY2 0xe4613c47 45 | #endif 46 | 47 | /* Convolutional encoder macro. Takes the encoder state, generates 48 | * a rate 1/2 symbol pair and stores it in 'sym'. The symbol generated from 49 | * POLY1 goes into the 2-bit of sym, and the symbol generated from POLY2 50 | * goes into the 1-bit. 51 | */ 52 | #define ENCODE(sym,encstate){\ 53 | unsigned long _tmp;\ 54 | \ 55 | _tmp = (encstate) & POLY1;\ 56 | _tmp ^= _tmp >> 16;\ 57 | (sym) = Partab[(_tmp ^ (_tmp >> 8)) & 0xff] << 1;\ 58 | _tmp = (encstate) & POLY2;\ 59 | _tmp ^= _tmp >> 16;\ 60 | (sym) |= Partab[(_tmp ^ (_tmp >> 8)) & 0xff];\ 61 | } 62 | 63 | 64 | /* Convolutionally encode a packet. The input data bytes are read 65 | * high bit first and the encoded packet is written into 'symbols', 66 | * one symbol per byte. The first symbol is generated from POLY1, 67 | * the second from POLY2. 68 | * 69 | * Storing only one symbol per byte uses more space, but it is faster 70 | * and easier than trying to pack them more compactly. 71 | */ 72 | int 73 | encode( 74 | unsigned char *symbols, /* Output buffer, 2*nbytes */ 75 | unsigned char *data, /* Input buffer, nbytes */ 76 | unsigned int nbytes) /* Number of bytes in data */ 77 | { 78 | unsigned long encstate; 79 | int sym; 80 | int i; 81 | 82 | encstate = 0; 83 | while(nbytes-- != 0){ 84 | for(i=7;i>=0;i--){ 85 | encstate = (encstate << 1) | ((*data >> i) & 1); 86 | ENCODE(sym,encstate); 87 | *symbols++ = sym >> 1; 88 | *symbols++ = sym & 1; 89 | } 90 | data++; 91 | } 92 | return 0; 93 | } 94 | 95 | /* 96 | * like fano() but no metric table, instead sym0 and sym1 97 | * directly provide the probabilities. 98 | * metrics.c suggests e.g. in0[i] is floor(0.5+4*log2(2*p0)), where p0 99 | * is the probability that the i'th symbol represents a zero. 100 | */ 101 | int 102 | nfano( 103 | unsigned long *metric, /* Final path metric (returned value) */ 104 | unsigned long *cycles, /* Cycle count (returned value) */ 105 | unsigned char *data, /* Decoded output data */ 106 | int *sym0, /* log of prob of each symbol being 0 */ 107 | int *sym1, /* log of prob of each symbol being 1 */ 108 | unsigned int nbits, /* Number of output bits */ 109 | int delta, /* Threshold adjust parameter */ 110 | unsigned long maxcycles)/* Decoding timeout in cycles per bit */ 111 | { 112 | struct node *nodes; /* First node */ 113 | register struct node *np; /* Current node */ 114 | struct node *lastnode; /* Last node */ 115 | struct node *tail; /* First node of tail */ 116 | long t; /* Threshold */ 117 | long m0,m1; 118 | long ngamma; 119 | unsigned int lsym; 120 | unsigned long i; 121 | 122 | if((nodes = (struct node *)malloc(nbits*sizeof(struct node))) == NULL){ 123 | printf("alloc failed\n"); 124 | return 0; 125 | } 126 | lastnode = &nodes[nbits-1]; 127 | tail = &nodes[nbits-31]; // originally 33 128 | 129 | /* Compute all possible branch metrics for each symbol pair 130 | * This is the only place we actually look at the raw input symbols 131 | */ 132 | for(np=nodes;np <= lastnode;np++){ 133 | np->metrics[0] = sym0[0] + sym0[1]; 134 | np->metrics[1] = sym0[0] + sym1[1]; 135 | np->metrics[2] = sym1[0] + sym0[1]; 136 | np->metrics[3] = sym1[0] + sym1[1]; 137 | 138 | sym0 += 2; 139 | sym1 += 2; 140 | } 141 | np = nodes; 142 | np->encstate = 0; 143 | 144 | /* Compute and sort branch metrics from root node */ 145 | ENCODE(lsym,np->encstate); /* 0-branch (LSB is 0) */ 146 | m0 = np->metrics[lsym]; 147 | 148 | /* Now do the 1-branch. To save another ENCODE call here and 149 | * inside the loop, we assume that both polynomials are odd, 150 | * providing complementary pairs of branch symbols. 151 | 152 | * This code should be modified if a systematic code were used. 153 | */ 154 | m1 = np->metrics[3^lsym]; 155 | if(m0 > m1){ 156 | /* 0-branch has better metric */ 157 | np->tm[0] = m0; 158 | np->tm[1] = m1; 159 | } else { 160 | /* 1-branch is better */ 161 | np->tm[0] = m1; 162 | np->tm[1] = m0; 163 | np->encstate++; /* Set low bit */ 164 | } 165 | np->i = 0; /* Start with best branch */ 166 | maxcycles *= nbits; 167 | np->gamma = t = 0; 168 | 169 | /* Start the Fano decoder */ 170 | for(i=1;i <= maxcycles;i++){ 171 | #ifdef debug 172 | printf("k=%ld, g=%ld, t=%ld, m[%d]=%d\n", 173 | np-nodes,np->gamma,t,np->i,np->tm[np->i]); 174 | #endif 175 | /* Look forward */ 176 | ngamma = np->gamma + np->tm[np->i]; 177 | if(ngamma >= t){ 178 | /* Node is acceptable */ 179 | if(np->gamma < t + delta){ 180 | /* First time we've visited this node; 181 | * Tighten threshold. 182 | * 183 | * This loop could be replaced with 184 | * t += delta * ((ngamma - t)/delta); 185 | * but the multiply and divide are slower. 186 | */ 187 | while(ngamma >= t + delta) 188 | t += delta; 189 | } 190 | /* Move forward */ 191 | np[1].gamma = ngamma; 192 | np[1].encstate = np->encstate << 1; 193 | if(++np == lastnode) 194 | break; /* Done! */ 195 | 196 | /* Compute and sort metrics, starting with the 197 | * zero branch 198 | */ 199 | ENCODE(lsym,np->encstate); 200 | if(np >= tail){ 201 | /* The tail must be all zeroes, so don't even 202 | * bother computing the 1-branches there. 203 | */ 204 | np->tm[0] = np->metrics[lsym]; 205 | } else { 206 | m0 = np->metrics[lsym]; 207 | m1 = np->metrics[3^lsym]; 208 | if(m0 > m1){ 209 | /* 0-branch is better */ 210 | np->tm[0] = m0; 211 | np->tm[1] = m1; 212 | } else { 213 | /* 1-branch is better */ 214 | np->tm[0] = m1; 215 | np->tm[1] = m0; 216 | np->encstate++; /* Set low bit */ 217 | } 218 | } 219 | np->i = 0; /* Start with best branch */ 220 | continue; 221 | } 222 | /* Threshold violated, can't go forward */ 223 | for(;;){ 224 | /* Look backward */ 225 | if(np == nodes || np[-1].gamma < t){ 226 | /* Can't back up either. 227 | * Relax threshold and and look 228 | * forward again to better branch. 229 | */ 230 | t -= delta; 231 | if(np->i != 0){ 232 | np->i = 0; 233 | np->encstate ^= 1; 234 | } 235 | break; 236 | } 237 | /* Back up */ 238 | if(--np < tail && np->i != 1){ 239 | /* Search next best branch */ 240 | np->i++; 241 | np->encstate ^= 1; 242 | break; 243 | } /* else keep looking back */ 244 | } 245 | } 246 | *metric = np->gamma; /* Return final path metric */ 247 | 248 | /* Copy decoded data to user's buffer */ 249 | nbits >>= 3; 250 | np = &nodes[7]; 251 | while(nbits-- != 0){ 252 | *data++ = np->encstate; 253 | np += 8; 254 | } 255 | 256 | free(nodes); 257 | *cycles = i+1; 258 | if(i >= maxcycles) 259 | return -1; /* Decoder timed out */ 260 | return 0; /* Successful completion */ 261 | } 262 | -------------------------------------------------------------------------------- /libfano/fano.h: -------------------------------------------------------------------------------- 1 | int fano(unsigned long *metric, unsigned long *cycles, 2 | unsigned char *data,unsigned char *symbols, 3 | unsigned int nbits,int mettab[2][256],int delta, 4 | unsigned long maxcycles); 5 | int encode(unsigned char *symbols,unsigned char *data,unsigned int nbytes); 6 | double gen_met(int mettab[2][256],int amp,double noise,double bias,int scale); 7 | 8 | extern unsigned char Partab[]; 9 | 10 | int nfano(unsigned long *metric, unsigned long *cycles, 11 | unsigned char *data, int *sym0, int *sym1, 12 | unsigned int nbits,int delta, 13 | unsigned long maxcycles); 14 | -------------------------------------------------------------------------------- /libfano/metrics.c: -------------------------------------------------------------------------------- 1 | /* Generate metric tables for a soft-decision convolutional decoder 2 | * assuming gaussian noise on a PSK channel. 3 | * 4 | * Works from "first principles" by evaluating the normal probability 5 | * function and then computing the log-likelihood function 6 | * for every possible received symbol value 7 | * 8 | * Copyright 1995 Phil Karn, KA9Q 9 | */ 10 | 11 | /* Symbols are offset-binary, with 128 corresponding to an erased (no 12 | * information) symbol 13 | */ 14 | #define OFFSET 128 15 | 16 | #include 17 | #include 18 | 19 | /* Normal function integrated from -Inf to x. Range: 0-1 */ 20 | #define normal(x) (0.5 + 0.5*erf((x)/M_SQRT2)) 21 | 22 | /* Logarithm base 2 */ 23 | #define log2(x) (log(x)*M_LOG2E) 24 | 25 | /* Generate log-likelihood metrics for 8-bit soft quantized channel 26 | * assuming AWGN and BPSK 27 | */ 28 | void 29 | gen_met( 30 | int mettab[2][256], /* Metric table, [sent sym][rx symbol] */ 31 | int amp, /* Signal amplitude, units */ 32 | double noise, /* Relative noise voltage */ 33 | double bias, /* Metric bias; 0 for viterbi, rate for sequential */ 34 | int scale /* Scale factor */ 35 | ){ 36 | double n; 37 | int s,bit; 38 | double metrics[2][256]; 39 | double p0,p1; 40 | 41 | /* Zero is a special value, since this sample includes all 42 | * lower samples that were clipped to this value, i.e., it 43 | * takes the whole lower tail of the curve 44 | */ 45 | p1 = normal(((0-OFFSET+0.5)/amp - 1)/noise); /* P(s|1) */ 46 | 47 | /* Prob of this value occurring for a 0-bit */ /* P(s|0) */ 48 | p0 = normal(((0-OFFSET+0.5)/amp + 1)/noise); 49 | metrics[0][0] = log2(2*p0/(p1+p0)) - bias; 50 | metrics[1][0] = log2(2*p1/(p1+p0)) - bias; 51 | 52 | for(s=1;s<255;s++){ 53 | /* P(s|1), prob of receiving s given 1 transmitted */ 54 | p1 = normal(((s-OFFSET+0.5)/amp - 1)/noise) - 55 | normal(((s-OFFSET-0.5)/amp - 1)/noise); 56 | 57 | /* P(s|0), prob of receiving s given 0 transmitted */ 58 | p0 = normal(((s-OFFSET+0.5)/amp + 1)/noise) - 59 | normal(((s-OFFSET-0.5)/amp + 1)/noise); 60 | 61 | #ifdef notdef 62 | printf("P(%d|1) = %lg, P(%d|0) = %lg\n",s,p1,s,p0); 63 | #endif 64 | metrics[0][s] = log2(2*p0/(p1+p0)) - bias; 65 | metrics[1][s] = log2(2*p1/(p1+p0)) - bias; 66 | } 67 | /* 255 is also a special value */ 68 | /* P(s|1) */ 69 | p1 = 1 - normal(((255-OFFSET-0.5)/amp - 1)/noise); 70 | /* P(s|0) */ 71 | p0 = 1 - normal(((255-OFFSET-0.5)/amp + 1)/noise); 72 | 73 | metrics[0][255] = log2(2*p0/(p1+p0)) - bias; 74 | metrics[1][255] = log2(2*p1/(p1+p0)) - bias; 75 | #ifdef notdef 76 | /* The probability of a raw symbol error is the probability 77 | * that a 1-bit would be received as a sample with value 78 | * 0-128. This is the offset normal curve integrated from -Inf to 0. 79 | */ 80 | printf("symbol Pe = %lg\n",normal(-1/noise)); 81 | #endif 82 | for(bit=0;bit<2;bit++){ 83 | for(s=0;s<256;s++){ 84 | /* Scale and round to nearest integer */ 85 | mettab[bit][s] = floor(metrics[bit][s] * scale + 0.5); 86 | #ifdef notdef 87 | printf("metrics[%d][%d] = %lg, mettab = %d\n", 88 | bit,s,metrics[bit][s],mettab[bit][s]); 89 | #endif 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /libfano/seqtest.c: -------------------------------------------------------------------------------- 1 | /* Test a rate 1/2 soft decision sequential decoder 2 | * Copyright 1994 Phil Karn, KA9Q 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "fano.h" 11 | 12 | #define RATE 0.5 13 | #define OFFSET 128 14 | 15 | struct stat { 16 | unsigned long cycles; 17 | unsigned long count; 18 | }; 19 | 20 | main(argc,argv) 21 | int argc; 22 | char *argv[]; 23 | { 24 | double ebn0,mebn0,esn0,mesn0,noise,mnoise,s; 25 | int mettab[2][256]; 26 | int amp,nbits,i,bit; 27 | unsigned char *psymbols,*symbols; 28 | unsigned char *data,*decdata; 29 | unsigned long metric,cycles,totcycles; 30 | long t,ntrials; 31 | long seed; 32 | extern char *optarg; 33 | extern int optind; 34 | unsigned long limit; 35 | int delta; 36 | struct stat stats[25]; 37 | unsigned long errors; 38 | int range; 39 | FILE *output; 40 | int quiet; 41 | int timetest; 42 | 43 | timetest = 0; 44 | totcycles = 0; 45 | amp = 100; 46 | mebn0 = ebn0 = 5.0; 47 | nbits = 1152; 48 | ntrials = 10; 49 | delta = 17; 50 | limit = 10000; 51 | quiet = 0; 52 | time(&seed); 53 | while((i = getopt(argc,argv,"a:e:n:N:d:l:qs:tm:")) != EOF){ 54 | switch(i){ 55 | case 'a': /* Signal amplitude in units */ 56 | amp = atoi(optarg); 57 | break; 58 | case 'e': /* Eb/N0 in dB */ 59 | mebn0 = ebn0 = atof(optarg); 60 | break; 61 | case 'm': /* Metric table Eb/N0 in dB */ 62 | mebn0 = atof(optarg); 63 | break; 64 | case 'n': /* Number of data bits */ 65 | nbits = atoi(optarg); 66 | break; 67 | case 'N': /* Number of trials (frames) */ 68 | ntrials = atoi(optarg); 69 | break; 70 | case 'd': /* Decoder threshold adjustment (delta) */ 71 | delta = atoi(optarg); 72 | break; 73 | case 'l': /* Limit on decoder operations/bit */ 74 | limit = atoi(optarg); 75 | break; 76 | case 'q': /* Suppress curses update */ 77 | quiet++; 78 | break; 79 | case 's': /* Seed for random number generator */ 80 | seed = atoi(optarg); 81 | break; 82 | case 't': /* Timetest mode */ 83 | timetest = 1; 84 | break; 85 | } 86 | } 87 | if(optind >= argc){ 88 | usage(); 89 | exit(1); 90 | } 91 | srandom(seed); 92 | 93 | output = fopen(argv[optind],"w+"); 94 | 95 | esn0 = ebn0 + 10*log10(RATE); /* actual Es/N0 in dB */ 96 | mesn0 = mebn0 + 10*log10(RATE); /* metric table Es/N0 in dB */ 97 | 98 | /* Compute noise voltage. The 0.5 factor accounts for BPSK seeing 99 | * only half the noise power, and the sqrt() converts power to 100 | * voltage. 101 | */ 102 | noise = sqrt(0.5/pow(10.,esn0/10.)); 103 | mnoise = sqrt(0.5/pow(10.,mesn0/10.)); 104 | 105 | data = malloc(nbits/8); 106 | decdata = malloc(nbits/8); 107 | psymbols = malloc(nbits*2); 108 | symbols = malloc(nbits*2); 109 | 110 | /* Generate metrics analytically, with gaussian pdf */ 111 | gen_met(mettab,amp,mnoise,RATE,4); 112 | 113 | /* Generate data (all 0's) and encode */ 114 | memset(data,0,nbits/8); 115 | 116 | #ifndef notdef 117 | for(i=0;i= count fraction\n"); 174 | for(i=0;i<25;i++){ 175 | fprintf(output,"%6d %6ld %.2lg\n",stats[i].cycles, 176 | stats[i].count,(double)stats[i].count/t); 177 | if(stats[i].cycles >= limit || stats[i].count == 0) 178 | break; 179 | } 180 | 181 | } 182 | if(!quiet){ 183 | 184 | erase(); 185 | move(0,0); 186 | printw("Seed %ld Amplitude %d units, Eb/N0 = %lg dB metric table Eb/N0 = %lg dB\n",seed,amp,ebn0,mebn0); 187 | printw("Frame length = %d bits, delta = %d, cycle limit = %ld, #frames = %ld\n", 188 | nbits,delta,limit,ntrials); 189 | 190 | printw("decoding errors: %ld\n",errors); 191 | printw("Average N: %lf\n",(double)totcycles/stats[0].count/nbits); 192 | printw(" N >= count fraction\n"); 193 | for(i=0;i<25;i++){ 194 | printw("%6d %6ld %.2lg\n",stats[i].cycles, 195 | stats[i].count,(double)stats[i].count/t); 196 | if(stats[i].cycles >= limit || stats[i].count == 0) 197 | break; 198 | } 199 | 200 | refresh(); 201 | } 202 | } 203 | if(!quiet) 204 | endwin(); 205 | 206 | printf("Seed %ld Amplitude %d units, Eb/N0 = %lg dB metric table Eb/N0 = %lg dB\n",seed,amp,ebn0,mebn0); 207 | printf("Frame length = %d bits, delta = %d, cycle limit = %ld, #frames = %ld\n", 208 | nbits,delta,limit,ntrials); 209 | printf("decoding errors: %ld\n",errors); 210 | printf("Average N: %lf\n",(double)totcycles/stats[0].count/nbits); 211 | printf(" N >= count fraction\n"); 212 | for(i=0;i<25;i++){ 213 | printf("%6d %6ld %.2lg\n",stats[i].cycles, 214 | stats[i].count,(double)stats[i].count/ntrials); 215 | if(stats[i].cycles >= limit || stats[i].count == 0) 216 | break; 217 | } 218 | } 219 | 220 | usage() 221 | { 222 | printf("Usage: seqtest [options] output_file\n"); 223 | printf("Option&default meaning\n"); 224 | printf("-a 100 signal amplitude in units\n"); 225 | printf("-e 5.0 Signal Eb/N0 in dB (also sets -m)\n"); 226 | printf("-m 5.0 Eb/N0 in dB for metric table calc\n"); 227 | printf("-n 1152 Number of bits per frame\n"); 228 | printf("-N 10 Number of frames to simulate\n"); 229 | printf("-d 17 Decoder threshold (delta)\n"); 230 | printf("-l 10000 Decoder timeout, fwd motions per bit\n"); 231 | printf("-s [cur time] seed for random number generator\n\n"); 232 | 233 | printf("-q select quiet mode (default off)\n"); 234 | printf("-t select timetest mode (default off)\n"); 235 | } 236 | -------------------------------------------------------------------------------- /libfano/sim.c: -------------------------------------------------------------------------------- 1 | /* Simulate AWGN channel 2 | * Copyright 1994 Phil Karn, KA9Q 3 | */ 4 | #include 5 | 6 | #define OFFSET 128 7 | 8 | double normal_rand(double mean, double std_dev); 9 | 10 | /* Turn binary symbols into individual 8-bit channel symbols 11 | * with specified noise and gain 12 | */ 13 | void 14 | modnoise( 15 | unsigned char *symbols, /* Input and Output symbols, 8 bits each */ 16 | unsigned int nsyms, /* Symbol count */ 17 | double amp, /* Signal amplitude */ 18 | double noise /* Noise amplitude */ 19 | ){ 20 | double s; 21 | 22 | while(nsyms-- != 0){ 23 | s = normal_rand(*symbols ? 1.0 : -1.0,noise); 24 | s *= amp; 25 | s += OFFSET; 26 | if(s > 255) 27 | s = 255; /* Clip to 8-bit range */ 28 | if(s < 0) 29 | s = 0; 30 | *symbols++ = floor(s+0.5); /* Round to int */ 31 | } 32 | } 33 | 34 | #define MAX_RANDOM 0x7fffffff 35 | 36 | /* Generate gaussian random double with specified mean and std_dev */ 37 | double 38 | normal_rand(double mean, double std_dev) 39 | { 40 | double fac,rsq,v1,v2; 41 | static double gset; 42 | static int iset; 43 | 44 | if(iset){ 45 | /* Already got one */ 46 | iset = 0; 47 | return mean + std_dev*gset; 48 | } 49 | /* Generate two evenly distributed numbers between -1 and +1 50 | * that are inside the unit circle 51 | */ 52 | do { 53 | v1 = 2.0 * (double)random() / MAX_RANDOM - 1; 54 | v2 = 2.0 * (double)random() / MAX_RANDOM - 1; 55 | rsq = v1*v1 + v2*v2; 56 | } while(rsq >= 1.0 || rsq == 0.0); 57 | fac = sqrt(-2.0*log(rsq)/rsq); 58 | gset = v1*fac; 59 | iset++; 60 | return mean + std_dev*v2*fac; 61 | } 62 | -------------------------------------------------------------------------------- /libfano/tab.c: -------------------------------------------------------------------------------- 1 | /* 8-bit parity lookup table, generated by partab.c */ 2 | unsigned char Partab[] = { 3 | 0, 1, 1, 0, 1, 0, 0, 1, 4 | 1, 0, 0, 1, 0, 1, 1, 0, 5 | 1, 0, 0, 1, 0, 1, 1, 0, 6 | 0, 1, 1, 0, 1, 0, 0, 1, 7 | 1, 0, 0, 1, 0, 1, 1, 0, 8 | 0, 1, 1, 0, 1, 0, 0, 1, 9 | 0, 1, 1, 0, 1, 0, 0, 1, 10 | 1, 0, 0, 1, 0, 1, 1, 0, 11 | 1, 0, 0, 1, 0, 1, 1, 0, 12 | 0, 1, 1, 0, 1, 0, 0, 1, 13 | 0, 1, 1, 0, 1, 0, 0, 1, 14 | 1, 0, 0, 1, 0, 1, 1, 0, 15 | 0, 1, 1, 0, 1, 0, 0, 1, 16 | 1, 0, 0, 1, 0, 1, 1, 0, 17 | 1, 0, 0, 1, 0, 1, 1, 0, 18 | 0, 1, 1, 0, 1, 0, 0, 1, 19 | 1, 0, 0, 1, 0, 1, 1, 0, 20 | 0, 1, 1, 0, 1, 0, 0, 1, 21 | 0, 1, 1, 0, 1, 0, 0, 1, 22 | 1, 0, 0, 1, 0, 1, 1, 0, 23 | 0, 1, 1, 0, 1, 0, 0, 1, 24 | 1, 0, 0, 1, 0, 1, 1, 0, 25 | 1, 0, 0, 1, 0, 1, 1, 0, 26 | 0, 1, 1, 0, 1, 0, 0, 1, 27 | 0, 1, 1, 0, 1, 0, 0, 1, 28 | 1, 0, 0, 1, 0, 1, 1, 0, 29 | 1, 0, 0, 1, 0, 1, 1, 0, 30 | 0, 1, 1, 0, 1, 0, 0, 1, 31 | 1, 0, 0, 1, 0, 1, 1, 0, 32 | 0, 1, 1, 0, 1, 0, 0, 1, 33 | 0, 1, 1, 0, 1, 0, 0, 1, 34 | 1, 0, 0, 1, 0, 1, 1, 0, 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /libfano/wrapfano.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "fano.h" 8 | 9 | #define RATE 0.5 10 | 11 | #define LL 1 12 | 13 | #ifdef LL 14 | /* Layland-Lushbaugh code 15 | * Nonsystematic, non-quick look-in, dmin=?, dfree=? 16 | */ 17 | #define POLY1 0xf2d05351 18 | #define POLY2 0xe4613c47 19 | #endif 20 | 21 | /* Convolutional encoder macro. Takes the encoder state, generates 22 | * a rate 1/2 symbol pair and stores it in 'sym'. The symbol generated from 23 | * POLY1 goes into the 2-bit of sym, and the symbol generated from POLY2 24 | * goes into the 1-bit. 25 | */ 26 | #define ENCODE(sym,encstate){\ 27 | unsigned long _tmp;\ 28 | \ 29 | _tmp = (encstate) & POLY1;\ 30 | _tmp ^= _tmp >> 16;\ 31 | (sym) = Partab[(_tmp ^ (_tmp >> 8)) & 0xff] << 1;\ 32 | _tmp = (encstate) & POLY2;\ 33 | _tmp ^= _tmp >> 16;\ 34 | (sym) |= Partab[(_tmp ^ (_tmp >> 8)) & 0xff];\ 35 | } 36 | 37 | // in_bits should include 31 bits of zero padding at the end. 38 | // there will be 2x as many out_bits as in_bits. 39 | // out_bits[] are 0/1. 40 | // just do the encoding -- the encode() in fano.c really wants bytes. 41 | void 42 | fano_encode(unsigned char in_bits[], int n_in, unsigned char out_bits[]) 43 | { 44 | unsigned long encstate = 0; 45 | int j; 46 | 47 | j = 0; 48 | for(int i = 0; i < n_in; i++){ 49 | int sym; 50 | encstate = (encstate << 1) | in_bits[i]; 51 | ENCODE(sym, encstate); 52 | out_bits[j++] = sym >> 1; 53 | out_bits[j++] = sym & 1; 54 | } 55 | } 56 | 57 | // in0[i] is the log of the probability that the i'th symbol is a 0. 58 | // should be 2*n_out in0[] and in1[]. 59 | // n_out is number of bits in the decoded message, 60 | // same as n_in passed to fano_encode(), 61 | // presumably including the trailing 31 zeros. 62 | // out_bits are 0/1. 63 | // returns 1 if OK, 0 on error. 64 | int 65 | nfano_decode(int in0[], int in1[], int n_out, unsigned char out_bits[], 66 | int limit, int out_metric[]) 67 | { 68 | unsigned char decdata[1024]; 69 | unsigned long metric=0; 70 | unsigned long cycles=0; 71 | int delta; 72 | unsigned long maxcycles; 73 | int ret; 74 | 75 | delta = 17; 76 | maxcycles = limit; // how hard to work; 5000 to 100000 77 | 78 | ret = nfano(&metric, // output 79 | &cycles, // output 80 | decdata, // output, decoded msg, packed into bytes 81 | in0, 82 | in1, 83 | n_out, // bits expected in the decoded msg 84 | delta, // decoder threshold adjustment 85 | maxcycles); 86 | if(ret < 0) 87 | return 0; 88 | 89 | for(int i = 0; i < n_out; i++) 90 | out_bits[i] = (decdata[i/8] >> (7 - (i % 8))) & 1; 91 | 92 | out_metric[0] = metric; 93 | 94 | return 1; 95 | } 96 | -------------------------------------------------------------------------------- /libldpc/Makefile: -------------------------------------------------------------------------------- 1 | CC=cc 2 | 3 | all: 4 | $(CC) -O3 libldpc.c -shared -fPIC -o libldpc.so 5 | -------------------------------------------------------------------------------- /libldpc/arrays.h: -------------------------------------------------------------------------------- 1 | // this is the LDPC(174,91) parity check matrix. 2 | // each row describes one parity check. 3 | // 83 rows. 4 | // each number is an index into the codeword (1-origin). 5 | // the codeword bits mentioned in each row must xor to zero. 6 | // From WSJT-X's ldpc_174_91_c_reordered_parity.f90 7 | int Nm[][7] = { 8 | { 4, 31, 59, 91, 92, 96, 153 }, 9 | { 5, 32, 60, 93, 115, 146, 0 }, 10 | { 6, 24, 61, 94, 122, 151, 0 }, 11 | { 7, 33, 62, 95, 96, 143, 0 }, 12 | { 8, 25, 63, 83, 93, 96, 148 }, 13 | { 6, 32, 64, 97, 126, 138, 0 }, 14 | { 5, 34, 65, 78, 98, 107, 154 }, 15 | { 9, 35, 66, 99, 139, 146, 0 }, 16 | { 10, 36, 67, 100, 107, 126, 0 }, 17 | { 11, 37, 67, 87, 101, 139, 158 }, 18 | { 12, 38, 68, 102, 105, 155, 0 }, 19 | { 13, 39, 69, 103, 149, 162, 0 }, 20 | { 8, 40, 70, 82, 104, 114, 145 }, 21 | { 14, 41, 71, 88, 102, 123, 156 }, 22 | { 15, 42, 59, 106, 123, 159, 0 }, 23 | { 1, 33, 72, 106, 107, 157, 0 }, 24 | { 16, 43, 73, 108, 141, 160, 0 }, 25 | { 17, 37, 74, 81, 109, 131, 154 }, 26 | { 11, 44, 75, 110, 121, 166, 0 }, 27 | { 45, 55, 64, 111, 130, 161, 173 }, 28 | { 8, 46, 71, 112, 119, 166, 0 }, 29 | { 18, 36, 76, 89, 113, 114, 143 }, 30 | { 19, 38, 77, 104, 116, 163, 0 }, 31 | { 20, 47, 70, 92, 138, 165, 0 }, 32 | { 2, 48, 74, 113, 128, 160, 0 }, 33 | { 21, 45, 78, 83, 117, 121, 151 }, 34 | { 22, 47, 58, 118, 127, 164, 0 }, 35 | { 16, 39, 62, 112, 134, 158, 0 }, 36 | { 23, 43, 79, 120, 131, 145, 0 }, 37 | { 19, 35, 59, 73, 110, 125, 161 }, 38 | { 20, 36, 63, 94, 136, 161, 0 }, 39 | { 14, 31, 79, 98, 132, 164, 0 }, 40 | { 3, 44, 80, 124, 127, 169, 0 }, 41 | { 19, 46, 81, 117, 135, 167, 0 }, 42 | { 7, 49, 58, 90, 100, 105, 168 }, 43 | { 12, 50, 61, 118, 119, 144, 0 }, 44 | { 13, 51, 64, 114, 118, 157, 0 }, 45 | { 24, 52, 76, 129, 148, 149, 0 }, 46 | { 25, 53, 69, 90, 101, 130, 156 }, 47 | { 20, 46, 65, 80, 120, 140, 170 }, 48 | { 21, 54, 77, 100, 140, 171, 0 }, 49 | { 35, 82, 133, 142, 171, 174, 0 }, 50 | { 14, 30, 83, 113, 125, 170, 0 }, 51 | { 4, 29, 68, 120, 134, 173, 0 }, 52 | { 1, 4, 52, 57, 86, 136, 152 }, 53 | { 26, 51, 56, 91, 122, 137, 168 }, 54 | { 52, 84, 110, 115, 145, 168, 0 }, 55 | { 7, 50, 81, 99, 132, 173, 0 }, 56 | { 23, 55, 67, 95, 172, 174, 0 }, 57 | { 26, 41, 77, 109, 141, 148, 0 }, 58 | { 2, 27, 41, 61, 62, 115, 133 }, 59 | { 27, 40, 56, 124, 125, 126, 0 }, 60 | { 18, 49, 55, 124, 141, 167, 0 }, 61 | { 6, 33, 85, 108, 116, 156, 0 }, 62 | { 28, 48, 70, 85, 105, 129, 158 }, 63 | { 9, 54, 63, 131, 147, 155, 0 }, 64 | { 22, 53, 68, 109, 121, 174, 0 }, 65 | { 3, 13, 48, 78, 95, 123, 0 }, 66 | { 31, 69, 133, 150, 155, 169, 0 }, 67 | { 12, 43, 66, 89, 97, 135, 159 }, 68 | { 5, 39, 75, 102, 136, 167, 0 }, 69 | { 2, 54, 86, 101, 135, 164, 0 }, 70 | { 15, 56, 87, 108, 119, 171, 0 }, 71 | { 10, 44, 82, 91, 111, 144, 149 }, 72 | { 23, 34, 71, 94, 127, 153, 0 }, 73 | { 11, 49, 88, 92, 142, 157, 0 }, 74 | { 29, 34, 87, 97, 147, 162, 0 }, 75 | { 30, 50, 60, 86, 137, 142, 162 }, 76 | { 10, 53, 66, 84, 112, 128, 165 }, 77 | { 22, 57, 85, 93, 140, 159, 0 }, 78 | { 28, 32, 72, 103, 132, 166, 0 }, 79 | { 28, 29, 84, 88, 117, 143, 150 }, 80 | { 1, 26, 45, 80, 128, 147, 0 }, 81 | { 17, 27, 89, 103, 116, 153, 0 }, 82 | { 51, 57, 98, 163, 165, 172, 0 }, 83 | { 21, 37, 73, 138, 152, 169, 0 }, 84 | { 16, 47, 76, 130, 137, 154, 0 }, 85 | { 3, 24, 30, 72, 104, 139, 0 }, 86 | { 9, 40, 90, 106, 134, 151, 0 }, 87 | { 15, 58, 60, 74, 111, 150, 163 }, 88 | { 18, 42, 79, 144, 146, 152, 0 }, 89 | { 25, 38, 65, 99, 122, 160, 0 }, 90 | { 17, 42, 75, 129, 170, 172, 0 }, 91 | }; 92 | 93 | // Mn from WSJT-X's ldpc_174_91_c_reordered_parity.f90 94 | // each of the 174 rows corresponds to a codeword bit. 95 | // the numbers indicate which three parity 96 | // checks (rows in Nm) refer to the codeword bit. 97 | // 1-origin. 98 | int Mn[][3] = { 99 | { 16, 45, 73 }, 100 | { 25, 51, 62 }, 101 | { 33, 58, 78 }, 102 | { 1, 44, 45 }, 103 | { 2, 7, 61 }, 104 | { 3, 6, 54 }, 105 | { 4, 35, 48 }, 106 | { 5, 13, 21 }, 107 | { 8, 56, 79 }, 108 | { 9, 64, 69 }, 109 | { 10, 19, 66 }, 110 | { 11, 36, 60 }, 111 | { 12, 37, 58 }, 112 | { 14, 32, 43 }, 113 | { 15, 63, 80 }, 114 | { 17, 28, 77 }, 115 | { 18, 74, 83 }, 116 | { 22, 53, 81 }, 117 | { 23, 30, 34 }, 118 | { 24, 31, 40 }, 119 | { 26, 41, 76 }, 120 | { 27, 57, 70 }, 121 | { 29, 49, 65 }, 122 | { 3, 38, 78 }, 123 | { 5, 39, 82 }, 124 | { 46, 50, 73 }, 125 | { 51, 52, 74 }, 126 | { 55, 71, 72 }, 127 | { 44, 67, 72 }, 128 | { 43, 68, 78 }, 129 | { 1, 32, 59 }, 130 | { 2, 6, 71 }, 131 | { 4, 16, 54 }, 132 | { 7, 65, 67 }, 133 | { 8, 30, 42 }, 134 | { 9, 22, 31 }, 135 | { 10, 18, 76 }, 136 | { 11, 23, 82 }, 137 | { 12, 28, 61 }, 138 | { 13, 52, 79 }, 139 | { 14, 50, 51 }, 140 | { 15, 81, 83 }, 141 | { 17, 29, 60 }, 142 | { 19, 33, 64 }, 143 | { 20, 26, 73 }, 144 | { 21, 34, 40 }, 145 | { 24, 27, 77 }, 146 | { 25, 55, 58 }, 147 | { 35, 53, 66 }, 148 | { 36, 48, 68 }, 149 | { 37, 46, 75 }, 150 | { 38, 45, 47 }, 151 | { 39, 57, 69 }, 152 | { 41, 56, 62 }, 153 | { 20, 49, 53 }, 154 | { 46, 52, 63 }, 155 | { 45, 70, 75 }, 156 | { 27, 35, 80 }, 157 | { 1, 15, 30 }, 158 | { 2, 68, 80 }, 159 | { 3, 36, 51 }, 160 | { 4, 28, 51 }, 161 | { 5, 31, 56 }, 162 | { 6, 20, 37 }, 163 | { 7, 40, 82 }, 164 | { 8, 60, 69 }, 165 | { 9, 10, 49 }, 166 | { 11, 44, 57 }, 167 | { 12, 39, 59 }, 168 | { 13, 24, 55 }, 169 | { 14, 21, 65 }, 170 | { 16, 71, 78 }, 171 | { 17, 30, 76 }, 172 | { 18, 25, 80 }, 173 | { 19, 61, 83 }, 174 | { 22, 38, 77 }, 175 | { 23, 41, 50 }, 176 | { 7, 26, 58 }, 177 | { 29, 32, 81 }, 178 | { 33, 40, 73 }, 179 | { 18, 34, 48 }, 180 | { 13, 42, 64 }, 181 | { 5, 26, 43 }, 182 | { 47, 69, 72 }, 183 | { 54, 55, 70 }, 184 | { 45, 62, 68 }, 185 | { 10, 63, 67 }, 186 | { 14, 66, 72 }, 187 | { 22, 60, 74 }, 188 | { 35, 39, 79 }, 189 | { 1, 46, 64 }, 190 | { 1, 24, 66 }, 191 | { 2, 5, 70 }, 192 | { 3, 31, 65 }, 193 | { 4, 49, 58 }, 194 | { 1, 4, 5 }, 195 | { 6, 60, 67 }, 196 | { 7, 32, 75 }, 197 | { 8, 48, 82 }, 198 | { 9, 35, 41 }, 199 | { 10, 39, 62 }, 200 | { 11, 14, 61 }, 201 | { 12, 71, 74 }, 202 | { 13, 23, 78 }, 203 | { 11, 35, 55 }, 204 | { 15, 16, 79 }, 205 | { 7, 9, 16 }, 206 | { 17, 54, 63 }, 207 | { 18, 50, 57 }, 208 | { 19, 30, 47 }, 209 | { 20, 64, 80 }, 210 | { 21, 28, 69 }, 211 | { 22, 25, 43 }, 212 | { 13, 22, 37 }, 213 | { 2, 47, 51 }, 214 | { 23, 54, 74 }, 215 | { 26, 34, 72 }, 216 | { 27, 36, 37 }, 217 | { 21, 36, 63 }, 218 | { 29, 40, 44 }, 219 | { 19, 26, 57 }, 220 | { 3, 46, 82 }, 221 | { 14, 15, 58 }, 222 | { 33, 52, 53 }, 223 | { 30, 43, 52 }, 224 | { 6, 9, 52 }, 225 | { 27, 33, 65 }, 226 | { 25, 69, 73 }, 227 | { 38, 55, 83 }, 228 | { 20, 39, 77 }, 229 | { 18, 29, 56 }, 230 | { 32, 48, 71 }, 231 | { 42, 51, 59 }, 232 | { 28, 44, 79 }, 233 | { 34, 60, 62 }, 234 | { 31, 45, 61 }, 235 | { 46, 68, 77 }, 236 | { 6, 24, 76 }, 237 | { 8, 10, 78 }, 238 | { 40, 41, 70 }, 239 | { 17, 50, 53 }, 240 | { 42, 66, 68 }, 241 | { 4, 22, 72 }, 242 | { 36, 64, 81 }, 243 | { 13, 29, 47 }, 244 | { 2, 8, 81 }, 245 | { 56, 67, 73 }, 246 | { 5, 38, 50 }, 247 | { 12, 38, 64 }, 248 | { 59, 72, 80 }, 249 | { 3, 26, 79 }, 250 | { 45, 76, 81 }, 251 | { 1, 65, 74 }, 252 | { 7, 18, 77 }, 253 | { 11, 56, 59 }, 254 | { 14, 39, 54 }, 255 | { 16, 37, 66 }, 256 | { 10, 28, 55 }, 257 | { 15, 60, 70 }, 258 | { 17, 25, 82 }, 259 | { 20, 30, 31 }, 260 | { 12, 67, 68 }, 261 | { 23, 75, 80 }, 262 | { 27, 32, 62 }, 263 | { 24, 69, 75 }, 264 | { 19, 21, 71 }, 265 | { 34, 53, 61 }, 266 | { 35, 46, 47 }, 267 | { 33, 59, 76 }, 268 | { 40, 43, 83 }, 269 | { 41, 42, 63 }, 270 | { 49, 75, 83 }, 271 | { 20, 44, 48 }, 272 | { 42, 49, 57 }, 273 | }; 274 | -------------------------------------------------------------------------------- /librs/Makefile: -------------------------------------------------------------------------------- 1 | CC=cc 2 | 3 | all: 4 | $(CC) -O2 -DBIGSYM -std=gnu99 *.c -shared -fPIC -o librs.so 5 | -------------------------------------------------------------------------------- /librs/README-rtm: -------------------------------------------------------------------------------- 1 | Phil Karn's Reed-Solomon code, copied from wsjt-x source. 2 | 3 | this works on FreeBSD, Mac OSX, and Linux: 4 | cc -O2 -DBIGSYM -std=gnu99 *.c -shared -fPIC -o librs.so 5 | 6 | you may need to say gcc-4.8 or whatever instead of cc. 7 | 8 | you don't need this: 9 | clang -O -DBIGSYM -dynamiclib -std=gnu99 *.c -current_version 1.0 -compatibility_version 1.0 -o librs.A.dylib 10 | 11 | import ctypes 12 | librs = ctypes.cdll.LoadLibrary("librs.A.dylib") 13 | librs.rs_decode_(...) 14 | 15 | -------------------------------------------------------------------------------- /librs/char.h: -------------------------------------------------------------------------------- 1 | /* Include file to configure the RS codec for character symbols 2 | * 3 | * Copyright 2002, Phil Karn, KA9Q 4 | * May be used under the terms of the GNU General Public License (GPL) 5 | */ 6 | #define DTYPE unsigned char 7 | 8 | /* Reed-Solomon codec control block */ 9 | struct rs { 10 | int mm; /* Bits per symbol */ 11 | int nn; /* Symbols per block (= (1<= rs->nn) { 24 | x -= rs->nn; 25 | x = (x >> rs->mm) + (x & rs->nn); 26 | } 27 | return x; 28 | } 29 | #define MODNN(x) modnn(rs,x) 30 | 31 | #define MM (rs->mm) 32 | #define NN (rs->nn) 33 | #define ALPHA_TO (rs->alpha_to) 34 | #define INDEX_OF (rs->index_of) 35 | #define GENPOLY (rs->genpoly) 36 | #define NROOTS (rs->nroots) 37 | #define FCR (rs->fcr) 38 | #define PRIM (rs->prim) 39 | #define IPRIM (rs->iprim) 40 | #define PAD (rs->pad) 41 | #define A0 (NN) 42 | 43 | #define ENCODE_RS encode_rs_char 44 | #define DECODE_RS decode_rs_char 45 | #define INIT_RS init_rs_char 46 | #define FREE_RS free_rs_char 47 | 48 | void ENCODE_RS(void *p,DTYPE *data,DTYPE *parity); 49 | int DECODE_RS(void *p,DTYPE *data,int *eras_pos,int no_eras); 50 | void *INIT_RS(int symsize,int gfpoly,int fcr, 51 | int prim,int nroots,int pad); 52 | void FREE_RS(void *p); 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /librs/decode_rs.c: -------------------------------------------------------------------------------- 1 | /* Reed-Solomon decoder 2 | * Copyright 2002 Phil Karn, KA9Q 3 | * May be used under the terms of the GNU General Public License (GPL) 4 | */ 5 | 6 | #ifdef DEBUG 7 | #include 8 | #endif 9 | 10 | #include 11 | 12 | //#define NULL ((void *)0) 13 | #define min(a,b) ((a) < (b) ? (a) : (b)) 14 | 15 | #ifdef FIXED 16 | #include "fixed.h" 17 | #elif defined(BIGSYM) 18 | #include "int.h" 19 | #else 20 | #include "char.h" 21 | #endif 22 | 23 | int DECODE_RS( 24 | #ifdef FIXED 25 | DTYPE *data, int *eras_pos, int no_eras,int pad){ 26 | #else 27 | void *p,DTYPE *data, int *eras_pos, int no_eras){ 28 | struct rs *rs = (struct rs *)p; 29 | #endif 30 | int deg_lambda, el, deg_omega; 31 | int i, j, r,k; 32 | DTYPE u,q,tmp,num1,num2,den,discr_r; 33 | DTYPE lambda[NROOTS+1], s[NROOTS]; /* Err+Eras Locator poly 34 | * and syndrome poly */ 35 | DTYPE b[NROOTS+1], t[NROOTS+1], omega[NROOTS+1]; 36 | DTYPE root[NROOTS], reg[NROOTS+1], loc[NROOTS]; 37 | int syn_error, count; 38 | 39 | #ifdef FIXED 40 | /* Check pad parameter for validity */ 41 | if(pad < 0 || pad >= NN) 42 | return -1; 43 | #endif 44 | 45 | /* form the syndromes; i.e., evaluate data(x) at roots of g(x) */ 46 | for(i=0;i 0) { 77 | /* Init lambda to be the erasure locator polynomial */ 78 | lambda[1] = ALPHA_TO[MODNN(PRIM*(NN-1-eras_pos[0]))]; 79 | for (i = 1; i < no_eras; i++) { 80 | u = MODNN(PRIM*(NN-1-eras_pos[i])); 81 | for (j = i+1; j > 0; j--) { 82 | tmp = INDEX_OF[lambda[j - 1]]; 83 | if(tmp != A0) 84 | lambda[j] ^= ALPHA_TO[MODNN(u + tmp)]; 85 | } 86 | } 87 | 88 | #if DEBUG >= 1 89 | /* Test code that verifies the erasure locator polynomial just constructed 90 | Needed only for decoder debugging. */ 91 | 92 | /* find roots of the erasure location polynomial */ 93 | for(i=1;i<=no_eras;i++) 94 | reg[i] = INDEX_OF[lambda[i]]; 95 | 96 | count = 0; 97 | for (i = 1,k=IPRIM-1; i <= NN; i++,k = MODNN(k+IPRIM)) { 98 | q = 1; 99 | for (j = 1; j <= no_eras; j++) 100 | if (reg[j] != A0) { 101 | reg[j] = MODNN(reg[j] + j); 102 | q ^= ALPHA_TO[reg[j]]; 103 | } 104 | if (q != 0) 105 | continue; 106 | /* store root and error location number indices */ 107 | root[count] = i; 108 | loc[count] = k; 109 | count++; 110 | } 111 | if (count != no_eras) { 112 | printf("count = %d no_eras = %d\n lambda(x) is WRONG\n",count,no_eras); 113 | count = -1; 114 | goto finish; 115 | } 116 | #if DEBUG >= 2 117 | printf("\n Erasure positions as determined by roots of Eras Loc Poly:\n"); 118 | for (i = 0; i < count; i++) 119 | printf("%d ", loc[i]); 120 | printf("\n"); 121 | #endif 122 | #endif 123 | } 124 | for(i=0;i 0; j--){ 186 | if (reg[j] != A0) { 187 | reg[j] = MODNN(reg[j] + j); 188 | q ^= ALPHA_TO[reg[j]]; 189 | } 190 | } 191 | if (q != 0) 192 | continue; /* Not a root */ 193 | /* store root (index-form) and error location number */ 194 | #if DEBUG>=2 195 | printf("count %d root %d loc %d\n",count,i,k); 196 | #endif 197 | root[count] = i; 198 | loc[count] = k; 199 | /* If we've already found max possible roots, 200 | * abort the search to save time 201 | */ 202 | if(++count == deg_lambda) 203 | break; 204 | } 205 | if (deg_lambda != count) { 206 | /* 207 | * deg(lambda) unequal to number of roots => uncorrectable 208 | * error detected 209 | */ 210 | count = -1; 211 | goto finish; 212 | } 213 | /* 214 | * Compute err+eras evaluator poly omega(x) = s(x)*lambda(x) (modulo 215 | * x**NROOTS). in index form. Also find deg(omega). 216 | */ 217 | deg_omega = deg_lambda-1; 218 | for (i = 0; i <= deg_omega;i++){ 219 | tmp = 0; 220 | for(j=i;j >= 0; j--){ 221 | if ((s[i - j] != A0) && (lambda[j] != A0)) 222 | tmp ^= ALPHA_TO[MODNN(s[i - j] + lambda[j])]; 223 | } 224 | omega[i] = INDEX_OF[tmp]; 225 | } 226 | 227 | /* 228 | * Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = 229 | * inv(X(l))**(FCR-1) and den = lambda_pr(inv(X(l))) all in poly-form 230 | */ 231 | for (j = count-1; j >=0; j--) { 232 | num1 = 0; 233 | for (i = deg_omega; i >= 0; i--) { 234 | if (omega[i] != A0) 235 | num1 ^= ALPHA_TO[MODNN(omega[i] + i * root[j])]; 236 | } 237 | num2 = ALPHA_TO[MODNN(root[j] * (FCR - 1) + NN)]; 238 | den = 0; 239 | 240 | /* lambda[i+1] for i even is the formal derivative lambda_pr of lambda[i] */ 241 | for (i = min(deg_lambda,NROOTS-1) & ~1; i >= 0; i -=2) { 242 | if(lambda[i+1] != A0) 243 | den ^= ALPHA_TO[MODNN(lambda[i+1] + i * root[j])]; 244 | } 245 | #if DEBUG >= 1 246 | if (den == 0) { 247 | printf("\n ERROR: denominator = 0\n"); 248 | count = -1; 249 | goto finish; 250 | } 251 | #endif 252 | /* Apply error to data */ 253 | if (num1 != 0 && loc[j] >= PAD) { 254 | data[loc[j]-PAD] ^= ALPHA_TO[MODNN(INDEX_OF[num1] + INDEX_OF[num2] + NN - INDEX_OF[den])]; 255 | } 256 | } 257 | finish: 258 | if(eras_pos != NULL){ 259 | for(i=0;i 6 | 7 | #ifdef FIXED 8 | #include "fixed.h" 9 | #elif defined(BIGSYM) 10 | #include "int.h" 11 | #else 12 | #include "char.h" 13 | #endif 14 | 15 | void ENCODE_RS( 16 | #ifdef FIXED 17 | DTYPE *data, DTYPE *bb,int pad){ 18 | #else 19 | void *p,DTYPE *data, DTYPE *bb){ 20 | struct rs *rs = (struct rs *)p; 21 | #endif 22 | int i, j; 23 | DTYPE feedback; 24 | 25 | #ifdef FIXED 26 | /* Check pad parameter for validity */ 27 | if(pad < 0 || pad >= NN) 28 | return; 29 | #endif 30 | 31 | memset(bb,0,NROOTS*sizeof(DTYPE)); 32 | 33 | for(i=0;i 7 | 8 | #ifdef CCSDS 9 | #include "ccsds.h" 10 | #elif defined(BIGSYM) 11 | #include "int.h" 12 | #else 13 | #include "char.h" 14 | #endif 15 | 16 | //#define NULL ((void *)0) 17 | 18 | void FREE_RS(void *p){ 19 | struct rs *rs = (struct rs *)p; 20 | 21 | free(rs->alpha_to); 22 | free(rs->index_of); 23 | free(rs->genpoly); 24 | free(rs); 25 | } 26 | 27 | /* Initialize a Reed-Solomon codec 28 | * symsize = symbol size, bits (1-8) 29 | * gfpoly = Field generator polynomial coefficients 30 | * fcr = first root of RS code generator polynomial, index form 31 | * prim = primitive element to generate polynomial roots 32 | * nroots = RS code generator polynomial degree (number of roots) 33 | * pad = padding bytes at front of shortened block 34 | */ 35 | void *INIT_RS(int symsize,int gfpoly,int fcr,int prim, 36 | int nroots,int pad){ 37 | struct rs *rs; 38 | int i, j, sr,root,iprim; 39 | 40 | /* Check parameter ranges */ 41 | if(symsize < 0 || symsize > (int)(8*sizeof(DTYPE))) 42 | return NULL; /* Need version with ints rather than chars */ 43 | 44 | if(fcr < 0 || fcr >= (1<= (1<= (1<= ((1<mm = symsize; 55 | rs->nn = (1<pad = pad; 57 | 58 | rs->alpha_to = (DTYPE *)malloc(sizeof(DTYPE)*(rs->nn+1)); 59 | if(rs->alpha_to == NULL){ 60 | free(rs); 61 | return NULL; 62 | } 63 | rs->index_of = (DTYPE *)malloc(sizeof(DTYPE)*(rs->nn+1)); 64 | if(rs->index_of == NULL){ 65 | free(rs->alpha_to); 66 | free(rs); 67 | return NULL; 68 | } 69 | 70 | /* Generate Galois field lookup tables */ 71 | rs->index_of[0] = A0; /* log(zero) = -inf */ 72 | rs->alpha_to[A0] = 0; /* alpha**-inf = 0 */ 73 | sr = 1; 74 | for(i=0;inn;i++){ 75 | rs->index_of[sr] = i; 76 | rs->alpha_to[i] = sr; 77 | sr <<= 1; 78 | if(sr & (1<nn; 81 | } 82 | if(sr != 1){ 83 | /* field generator polynomial is not primitive! */ 84 | free(rs->alpha_to); 85 | free(rs->index_of); 86 | free(rs); 87 | return NULL; 88 | } 89 | 90 | /* Form RS code generator polynomial from its roots */ 91 | rs->genpoly = (DTYPE *)malloc(sizeof(DTYPE)*(nroots+1)); 92 | if(rs->genpoly == NULL){ 93 | free(rs->alpha_to); 94 | free(rs->index_of); 95 | free(rs); 96 | return NULL; 97 | } 98 | rs->fcr = fcr; 99 | rs->prim = prim; 100 | rs->nroots = nroots; 101 | 102 | /* Find prim-th root of 1, used in decoding */ 103 | for(iprim=1;(iprim % prim) != 0;iprim += rs->nn) 104 | ; 105 | rs->iprim = iprim / prim; 106 | 107 | rs->genpoly[0] = 1; 108 | for (i = 0,root=fcr*prim; i < nroots; i++,root += prim) { 109 | rs->genpoly[i+1] = 1; 110 | 111 | /* Multiply rs->genpoly[] by @**(root + x) */ 112 | for (j = i; j > 0; j--){ 113 | if (rs->genpoly[j] != 0) 114 | rs->genpoly[j] = rs->genpoly[j-1] ^ rs->alpha_to[modnn(rs,rs->index_of[rs->genpoly[j]] + root)]; 115 | else 116 | rs->genpoly[j] = rs->genpoly[j-1]; 117 | } 118 | /* rs->genpoly[0] can never be zero */ 119 | rs->genpoly[0] = rs->alpha_to[modnn(rs,rs->index_of[rs->genpoly[0]] + root)]; 120 | } 121 | /* convert rs->genpoly[] to index form for quicker encoding */ 122 | for (i = 0; i <= nroots; i++) 123 | rs->genpoly[i] = rs->index_of[rs->genpoly[i]]; 124 | 125 | return rs; 126 | } 127 | -------------------------------------------------------------------------------- /librs/int.h: -------------------------------------------------------------------------------- 1 | /* Include file to configure the RS codec for integer symbols 2 | * 3 | * Copyright 2002, Phil Karn, KA9Q 4 | * May be used under the terms of the GNU General Public License (GPL) 5 | */ 6 | #define DTYPE int 7 | 8 | /* Reed-Solomon codec control block */ 9 | struct rs { 10 | int mm; /* Bits per symbol */ 11 | int nn; /* Symbols per block (= (1<= rs->nn) { 24 | x -= rs->nn; 25 | x = (x >> rs->mm) + (x & rs->nn); 26 | } 27 | return x; 28 | } 29 | #define MODNN(x) modnn(rs,x) 30 | 31 | #define MM (rs->mm) 32 | #define NN (rs->nn) 33 | #define ALPHA_TO (rs->alpha_to) 34 | #define INDEX_OF (rs->index_of) 35 | #define GENPOLY (rs->genpoly) 36 | //#define NROOTS (rs->nroots) 37 | #define NROOTS (51) 38 | #define FCR (rs->fcr) 39 | #define PRIM (rs->prim) 40 | #define IPRIM (rs->iprim) 41 | #define PAD (rs->pad) 42 | #define A0 (NN) 43 | 44 | #define ENCODE_RS encode_rs_int 45 | #define DECODE_RS decode_rs_int 46 | #define INIT_RS init_rs_int 47 | #define FREE_RS free_rs_int 48 | 49 | void ENCODE_RS(void *p,DTYPE *data,DTYPE *parity); 50 | int DECODE_RS(void *p,DTYPE *data,int *eras_pos,int no_eras); 51 | void *INIT_RS(int symsize,int gfpoly,int fcr, 52 | int prim,int nroots,int pad); 53 | void FREE_RS(void *p); 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /librs/rs.h: -------------------------------------------------------------------------------- 1 | /* User include file for the Reed-Solomon codec 2 | * Copyright 2002, Phil Karn KA9Q 3 | * May be used under the terms of the GNU General Public License (GPL) 4 | */ 5 | 6 | /* General purpose RS codec, 8-bit symbols */ 7 | void encode_rs_char(void *rs,unsigned char *data,unsigned char *parity); 8 | int decode_rs_char(void *rs,unsigned char *data,int *eras_pos, 9 | int no_eras); 10 | void *init_rs_char(int symsize,int gfpoly, 11 | int fcr,int prim,int nroots, 12 | int pad); 13 | void free_rs_char(void *rs); 14 | 15 | /* General purpose RS codec, integer symbols */ 16 | void encode_rs_int(void *rs,int *data,int *parity); 17 | int decode_rs_int(void *rs,int *data,int *eras_pos,int no_eras); 18 | void *init_rs_int(int symsize,int gfpoly,int fcr, 19 | int prim,int nroots,int pad); 20 | void free_rs_int(void *rs); 21 | 22 | /* CCSDS standard (255,223) RS codec with conventional (*not* dual-basis) 23 | * symbol representation 24 | */ 25 | void encode_rs_8(unsigned char *data,unsigned char *parity,int pad); 26 | int decode_rs_8(unsigned char *data,int *eras_pos,int no_eras,int pad); 27 | 28 | /* CCSDS standard (255,223) RS codec with dual-basis symbol representation */ 29 | void encode_rs_ccsds(unsigned char *data,unsigned char *parity,int pad); 30 | int decode_rs_ccsds(unsigned char *data,int *eras_pos,int no_eras,int pad); 31 | 32 | /* Tables to map from conventional->dual (Taltab) and 33 | * dual->conventional (Tal1tab) bases 34 | */ 35 | extern unsigned char Taltab[],Tal1tab[]; 36 | -------------------------------------------------------------------------------- /librs/wrapkarn.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "rs.h" 7 | 8 | static void *rs; 9 | static int first=1; 10 | 11 | void rs_encode_(int *dgen, int *sent) 12 | // Encode JT65 data dgen[12], producing sent[63]. 13 | { 14 | int dat1[12]; 15 | int b[51]; 16 | int i; 17 | 18 | if(first) { 19 | // Initialize the JT65 codec 20 | rs=init_rs_int(6,0x43,3,1,51,0); 21 | first=0; 22 | } 23 | 24 | // Reverse data order for the Karn codec. 25 | for(i=0; i<12; i++) { 26 | dat1[i]=dgen[11-i]; 27 | } 28 | // Compute the parity symbols 29 | encode_rs_int(rs,dat1,b); 30 | 31 | // Move parity symbols and data into sent[] array, in reverse order. 32 | for (i = 0; i < 51; i++) sent[50-i] = b[i]; 33 | for (i = 0; i < 12; i++) sent[i+51] = dat1[11-i]; 34 | } 35 | 36 | void rs_decode_(int *recd0, int *era0, int *numera0, int *decoded, int *nerr) 37 | // Decode JT65 received data recd0[63], producing decoded[12]. 38 | // Erasures are indicated in era0[numera]. The number of corrected 39 | // errors is *nerr. If the data are uncorrectable, *nerr=-1 is returned. 40 | { 41 | int numera; 42 | int i; 43 | int era_pos[50]; 44 | int recd[63]; 45 | 46 | if(first) { 47 | rs=init_rs_int(6,0x43,3,1,51,0); 48 | first=0; 49 | } 50 | 51 | numera=*numera0; 52 | for(i=0; i<12; i++) recd[i]=recd0[62-i]; 53 | for(i=0; i<51; i++) recd[12+i]=recd0[50-i]; 54 | if(numera) 55 | for(i=0; iI", i) 26 | assert len(z) == 4 27 | return z 28 | 29 | # pack a 16-bit int. 30 | def p16(i): 31 | z = struct.pack(">H", i) 32 | assert len(z) == 2 33 | return z 34 | 35 | # pad to a multiple of four byte. 36 | def pad(s): 37 | while (len(s) % 4) != 0: 38 | s += chr(0) 39 | return s 40 | 41 | # 42 | # format and send reports. 43 | # 44 | class T: 45 | 46 | def __init__(self, mycall, mygrid, mysw, testing=False): 47 | self.testing = testing 48 | self.seq = 1 49 | self.sessionId = int(time.time()) 50 | self.mycall = mycall 51 | self.mygrid = mygrid 52 | self.mysw = mysw 53 | 54 | host = "pskreporter.info" 55 | if self.testing: 56 | port = 14739 # test server 57 | # test view: https://pskreporter.info/cgi-bin/psk-analysis.pl 58 | else: 59 | port = 4739 # production server 60 | 61 | self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 62 | self.s.connect((host, port)) 63 | 64 | # accumulate a list, send only every 5 minutes. 65 | self.pending = [ ] 66 | self.last_send = 0 67 | 68 | # seq should increment once per packet. 69 | # sessionId should stay the same. 70 | # mysw is the name of the software. 71 | # each senders element is [ call, freq, snr, grid, time ] 72 | # e.g. [ "KB1MBX", 14070987, "PSK31", "FN42", 1200960104 ] 73 | # modes: JT65, PSK31 74 | def fmt(self, senders): 75 | 76 | # receiver record format descriptor. 77 | # callsign, locator, s/w. 78 | rrf = hx([0x00, 0x03, 0x00, 0x24, 0x99, 0x92, 0x00, 0x03, 0x00, 0x00, 79 | 0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 80 | 0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 81 | 0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 82 | 0x00, 0x00,]) 83 | 84 | # sender record format descriptor. 85 | if False: 86 | # senderCallsign, frequency, sNR (1 byte), iMD (1 byte), mode (1 byte), informationSource, flowStartSeconds. 87 | srf = hx([ 0x00, 0x02, 0x00, 0x3C, 0x99, 0x93, 0x00, 0x07, 88 | 0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 89 | 0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F, 90 | 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 91 | 0x80, 0x07, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 92 | 0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 93 | 0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 94 | 0x00, 0x96, 0x00, 0x04,]) 95 | 96 | if True: 97 | # senderCallsign, frequency, mode, informationSource=1, senderLocator, flowStartSeconds 98 | srf = hx([ 0x00, 0x02, 0x00, 0x34, 0x99, 0x93, 0x00, 0x06, 99 | 0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 100 | 0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F, 101 | 0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 102 | 0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F, 103 | 0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F, 104 | 0x00, 0x96, 0x00, 0x04,]) 105 | 106 | # receiver record. 107 | # first cook up the data part of the record, since length comes first. 108 | rr = "" 109 | rr += pstr(self.mycall) 110 | rr += pstr(self.mygrid) 111 | rr += pstr(self.mysw) 112 | rr = pad(rr) 113 | # prepend rr's header. 114 | rr = hx([0x99, 0x92]) + p16(len(rr) + 4) + rr 115 | 116 | # sender records. 117 | # first the array of per-sender records, so we can find the length. 118 | sr = "" 119 | for snd in senders: 120 | # snd = [ "KB1MBX", 14070987, "PSK", "FN42", 1200960104 ] 121 | sr += pstr(snd[0]) # call sign 122 | sr += p32(snd[1]) # frequency 123 | sr += pstr(snd[2]) # "JT65" 124 | sr += chr(1) # informationSource 125 | sr += pstr(snd[3]) # grid 126 | sr += p32(int(snd[4])) 127 | sr = pad(sr) 128 | # prepend the sender records header, with length. 129 | sr = hx([0x99, 0x93]) + p16(len(sr) + 4) + sr 130 | 131 | # now the overall header (16 bytes long). 132 | h = "" 133 | h += hx([ 0x00, 0x0a ]) 134 | h += p16(len(rrf) + len(srf) + len(rr) + len(sr) + 16) 135 | h += p32(int(time.time())) 136 | h += p32(self.seq) 137 | self.seq += 1 138 | h += p32(self.sessionId) 139 | 140 | pkt = h + rrf + srf + rr + sr 141 | 142 | return pkt 143 | 144 | def dump(self, pkt): 145 | for i in range(0, 20): 146 | sys.stdout.write("%02x " % ord(pkt[i])) 147 | sys.stdout.write("\n") 148 | 149 | def send(self, pkt): 150 | self.s.send(pkt) 151 | 152 | # caller received something. buffer it until 5 minutes 153 | # since last send. 154 | # XXX what if packet would be > MTU but not yet 5 minutes? 155 | def got(self, call, hz, mode, grid, tm): 156 | info = [ call, int(hz), mode, grid, int(tm) ] 157 | self.pending.append(info) 158 | if time.time() - self.last_send >= 5*60: 159 | pkt = self.fmt(self.pending) 160 | self.send(pkt) 161 | self.pending = [ ] 162 | self.last_send = time.time() 163 | -------------------------------------------------------------------------------- /sdrip.py: -------------------------------------------------------------------------------- 1 | # 2 | # Control an RFSpace SDR-IP, NetSDR, or CloudIQ. 3 | # 4 | # Example: 5 | # sdr = sdrip.open("192.168.3.125") 6 | # sdr.setrate(32000) 7 | # sdr.setgain(-10) 8 | # sdr.setrun() 9 | # while True: 10 | # buf = sdr.readiq() 11 | # OR buf = sdr.readusb() 12 | # 13 | # Robert Morris, AB1HL 14 | # 15 | 16 | import socket 17 | import sys 18 | import os 19 | import numpy 20 | import scipy 21 | import scipy.signal 22 | import threading 23 | import time 24 | import struct 25 | import weakutil 26 | 27 | def x8(x): 28 | s = bytearray([x & 0xff]) 29 | return s 30 | 31 | def x16(x): 32 | # least-significant first 33 | s = bytearray([ 34 | x & 0xff, 35 | (x >> 8) & 0xff ]) 36 | return s 37 | 38 | def x32(x): 39 | # least-significant first 40 | s = bytearray([ 41 | x & 0xff, 42 | (x >> 8) & 0xff, 43 | (x >> 16) & 0xff, 44 | (x >> 24) & 0xff ]) 45 | return s 46 | 47 | # 40-bit frequency in Hz, lsb first 48 | # but argument must be an int 49 | def x40(hz): 50 | s = b"" 51 | for i in range(0, 5): 52 | s = s + bytearray([ hz & 0xff ]) 53 | hz >>= 8 54 | return s 55 | 56 | # turn a char into an int. 57 | # yord[s[i]] 58 | # in python27, s is str, s[i] is str, so call ord(). 59 | # in python3, s is bytes, s[i] is int, so no ord(). 60 | def yord(x): 61 | if type(x) == int: 62 | return x 63 | else: 64 | return ord(x) 65 | 66 | def y16(s): 67 | x = (yord(s[0]) + 68 | (yord(s[1]) << 8)) 69 | return x 70 | 71 | def y32(s): 72 | x = (yord(s[0]) + 73 | (yord(s[1]) << 8) + 74 | (yord(s[2]) << 16) + 75 | (yord(s[3]) << 24)) 76 | return x 77 | 78 | # turn 5 bytes from NetSDR into a 40-bit number. 79 | # LSB first. 80 | def y40(s): 81 | hz = (yord(s[0]) + 82 | (yord(s[1]) << 8) + 83 | (yord(s[2]) << 16) + 84 | (yord(s[3]) << 24) + 85 | (yord(s[4]) << 32)) 86 | return hz 87 | 88 | # turn a byte array into hex digits 89 | def hx(s): 90 | buf = "" 91 | for i in range(0, len(s)): 92 | buf += "%02x " % (yord(s[i])) 93 | return buf 94 | 95 | mu = threading.Lock() 96 | 97 | # 98 | # if already connected, return existing SDRIP, 99 | # otherwise a new one. 100 | # 101 | sdrips = { } 102 | def open(ipaddr): 103 | global sdrips, mu 104 | mu.acquire() 105 | if not (ipaddr in sdrips): 106 | sdrips[ipaddr] = SDRIP(ipaddr) 107 | sdr = sdrips[ipaddr] 108 | mu.release() 109 | return sdr 110 | 111 | class SDRIP: 112 | 113 | def __init__(self, ipaddr): 114 | # ipaddr is SDR-IP's IP address e.g. "192.168.3.123" 115 | self.mode = "usb" 116 | self.ipaddr = ipaddr 117 | self.mu = threading.Lock() 118 | self.lasthz = 0 119 | 120 | self.rate = None 121 | self.frequency = None 122 | self.running = False 123 | 124 | self.mhz_overload = { } 125 | self.mhz_gain = { } 126 | 127 | # 16 or 24 128 | # only 24 seems useful 129 | self.samplebits = 24 130 | 131 | # iq? i think only True works. 132 | self.iq = True 133 | 134 | self.nextseq = 0 135 | self.reader_pid = None 136 | 137 | self.connect() 138 | 139 | # "usb" or "fm" 140 | # maybe only here to be ready by weakaudio.py/SDRIP. 141 | def set_mode(self, mode): 142 | self.mode = mode 143 | 144 | def connect(self): 145 | # allocate a UDP socket and port for incoming data from the SDR-IP. 146 | self.ds = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 147 | self.ds.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024) 148 | self.ds.bind(('', 0)) # ask kernel to choose a free port 149 | hostport = self.ds.getsockname() # hostport[1] is port number 150 | 151 | # fork() a sub-process to read and buffer the data UDP socket, 152 | # since the Python thread scheduler doesn't run us often enough if 153 | # WSPR is compute-bound in numpy for tens of seconds. 154 | r, w = os.pipe() 155 | self.reader_pid = os.fork() 156 | if self.reader_pid == 0: 157 | os.close(r) 158 | self.reader(w) 159 | os._exit(0) 160 | else: 161 | self.pipe = r 162 | os.close(w) 163 | self.ds.close() 164 | 165 | # commands over TCP to port 50000 166 | self.cs = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 167 | self.cs.connect((self.ipaddr, 50000)) 168 | 169 | # only this thread reads from the control TCP socket, 170 | # and appends to self.replies. 171 | self.replies_mu = threading.Lock() 172 | self.replies = [ ] 173 | th = threading.Thread(target=lambda : self.drain_ctl()) 174 | th.daemon = True 175 | th.start() 176 | 177 | time.sleep(0.1) # CloudIQ 178 | 179 | # tell the SDR-IP where to send UDP packets 180 | self.setudp(hostport[1]) 181 | 182 | # boilerplate 183 | self.setad() 184 | self.setfilter(0) 185 | self.setgain(0) 186 | #self.setgain(-20) 187 | 188 | 189 | # "SDR-IP" 190 | #print("name: %s" % (self.getitem(0x0001))) 191 | 192 | # option 0x02 means reflock board is installed 193 | #oo = self.getitem(0x000A) # Options 194 | #oo0 = yord(oo[0]) 195 | #print("options: %02x" % (oo0)) 196 | 197 | if False: 198 | # set calibration. 199 | # 192.168.3.130 wants + 506 200 | # 192.168.3.131 wants + 525 201 | # (these are with the 10 mhz reflock ocxo, but not locked) 202 | data = b"" 203 | data += x8(0) # ignored 204 | if self.ipaddr == "192.168.3.130": 205 | data += x32(80000000 + 506) 206 | elif self.ipaddr == "192.168.3.131": 207 | data += x32(80000000 + 525) 208 | else: 209 | print("sdrip.py: unknown IP address %s for calibration" % (self.ipaddr)) 210 | # data += x32(80000000 + 0) 211 | data = None 212 | if data != None: 213 | self.setitem(0x00B0, data) 214 | 215 | # A/D Input Sample Rate Calibration 216 | # factory set to 80000000 217 | x = self.getitem(0x00B0) 218 | cal = y32(x[1:5]) 219 | print("sdrip %s cal: %s" % (self.ipaddr, cal)) 220 | 221 | # read the UDP socket from the SDR-IP. 222 | def reader1(self): 223 | while True: 224 | buf = self.ds.recv(4096) 225 | self.packets_mu.acquire() 226 | self.packets.append(buf) 227 | self.packets_mu.release() 228 | 229 | # read the data UDP socket in a separate process and 230 | # send the results on the pipe w. 231 | def reader(self, w): 232 | ww = os.fdopen(w, 'wb') 233 | 234 | # spawn a thread that just keeps reading from the socket 235 | # and appending packets to packets[]. 236 | 237 | self.packets = [ ] 238 | self.packets_mu = threading.Lock() 239 | 240 | th = threading.Thread(target=lambda : self.reader1()) 241 | th.daemon = True 242 | th.start() 243 | 244 | # move packets from packets[] to the UNIX pipe. 245 | # the pipe write() calls may block, but it's OK because 246 | # the reader1() thread keeps draining the UDP socket. 247 | while True: 248 | self.packets_mu.acquire() 249 | ppp = self.packets 250 | self.packets = [ ] 251 | self.packets_mu.release() 252 | 253 | if len(ppp) < 1: 254 | # we expect 100 pkts/second 255 | # but OSX seems to limit a process to 150 wakeups/second! 256 | # time.sleep(0.005) 257 | time.sleep(0.01) 258 | 259 | for pkt in ppp: 260 | try: 261 | ww.write(struct.pack('I', len(pkt))) 262 | ww.write(pkt) 263 | ww.flush() 264 | except: 265 | #sys.stderr.write("sdrip: pipe write failed\n") 266 | os._exit(1) 267 | 268 | # consume and record TCP control messages from the NetSDR, 269 | # and notice if it goes away. 270 | def drain_ctl(self): 271 | try: 272 | while True: 273 | reply = self.real_readreply() 274 | if reply != None: 275 | self.replies_mu.acquire() 276 | self.replies.append(reply) 277 | self.replies_mu.release() 278 | except: 279 | print("drain error:", sys.exc_info()[0]) 280 | sys.stdout.flush() 281 | pass 282 | 283 | sys.stderr.write("sdrip: control connection died\n") 284 | os.kill(self.reader_pid, 9) 285 | 286 | # read a 16-bit int from TCP control socket 287 | def read16(self): 288 | x0 = self.cs.recv(1) # least-significant byte 289 | x1 = self.cs.recv(1) # most-significant byte 290 | return (yord(x0) & 0xff) | ((yord(x1) << 8) & 0xff00) 291 | 292 | # read a reply from the TCP control socket 293 | # return [ type, item, data ] 294 | def readctl(self): 295 | len = self.read16() # overall length and msg type 296 | mtype = (len >> 13) & 0x7 297 | len &= 0x1fff 298 | if len == 2: 299 | # NAK -- but for what? 300 | sys.stderr.write("sdrip: NAK\n") 301 | return None 302 | item = self.read16() # control item 303 | data = b"" 304 | xlen = len - 4 305 | while xlen > 0: 306 | dd = self.cs.recv(1) 307 | data += dd 308 | xlen -= 1 309 | return [ mtype, item, data ] 310 | 311 | # read one reply from the tcp control socket. 312 | def real_readreply(self): 313 | reply = self.readctl() 314 | if reply == None: 315 | # NAK 316 | return None 317 | # print("reply: %d %04x %s" % (reply[0], reply[1], hx(reply[2]))) 318 | # reply[0] is mtype (0=set, 1=get) 319 | # reply[1] is item 320 | # reply[2] is date 321 | if reply[0] == 1 and reply[1] == 5: 322 | # A/D overload 323 | self.got_overload() 324 | return reply 325 | 326 | def got_overload(self): 327 | mhz = self.lasthz // 1000000 328 | self.mhz_overload[mhz] = time.time() 329 | ogain = self.mhz_gain.get(mhz, 0) 330 | gain = ogain - 10 331 | if gain < -30: 332 | gain = -30 333 | self.mhz_gain[mhz] = gain 334 | sys.stderr.write("sdrip: overload mhz=%d %d %d\n" % (mhz, ogain, gain)) 335 | 336 | # wait for drain thread to see the reply we want. 337 | def readreply(self, item): 338 | self.replies_mu.acquire() 339 | lasti = len(self.replies) - 10 # XXX 340 | lasti = max(0, lasti) 341 | self.replies_mu.release() 342 | 343 | while True: 344 | self.replies_mu.acquire() 345 | while lasti < len(self.replies): 346 | reply = self.replies[lasti] 347 | lasti = lasti + 1 348 | if reply[0] == 0 and reply[1] == item: 349 | self.replies_mu.release() 350 | return reply[2] 351 | if len(self.replies) > 20: 352 | self.replies = [ ] 353 | lasti = 0 354 | self.replies_mu.release() 355 | time.sleep(0.01) 356 | 357 | # send a Request Control Item, wait for and return the result 358 | def getitem(self, item, extra=None): 359 | try: 360 | self.mu.acquire() 361 | mtype = 1 # type=request control item 362 | buf = b"" 363 | buf += x8(4) # overall length, lsb 364 | buf += x8((mtype << 5) | 0) # 0 is len msb 365 | buf += x16(item) 366 | if extra != None: 367 | buf += extra 368 | self.cs.send(buf) 369 | ret = self.readreply(item) 370 | return ret 371 | finally: 372 | self.mu.release() 373 | 374 | def setitem(self, item, data): 375 | try: 376 | self.mu.acquire() 377 | mtype = 0 # set item 378 | lx = 4 + len(data) 379 | buf = b"" 380 | buf += x8(lx) 381 | buf += x8((mtype << 5) | 0) 382 | buf += x16(item) 383 | buf += data 384 | self.cs.send(buf) 385 | ret = self.readreply(item) 386 | return ret 387 | finally: 388 | self.mu.release() 389 | 390 | def print_setup(self): 391 | print(("freq 0: %d" % (self.getfreq(0)))) # 32770 if down-converting 392 | print(("name: %s" % (self.getname()))) 393 | print(("serial: %s" % (self.getserial()))) 394 | print(("interface: %d" % (self.getinterface()))) 395 | # print("boot version: %s" % (self.getversion(0))) 396 | # print("application firmware version: %s" % (self.getversion(1))) 397 | # print("hardware version: %s" % (self.getversion(2))) 398 | # print("FPGA config: %s" % (self.getversion(3))) 399 | print(("rate: %d" % (self.getrate()))) 400 | print(("freq 0: %d" % (self.getfreq(0)))) # 32770 if down-converting 401 | print(("A/D mode: %s" % (self.getad(0)))) 402 | print(("filter: %d" % (self.getfilter(0)))) 403 | print(("gain: %d" % (self.getgain(0)))) 404 | print(("fpga: %s" % (self.getfpga()))) 405 | print(("scale: %s" % (self.getscale(0)))) 406 | # print("downgain: %s" % (self.getdowngain())) 407 | 408 | # set Frequency 409 | def setfreq1(self, chan, hz): 410 | hz = int(hz) 411 | data = b"" 412 | data += bytearray([chan]) # 1=display, 0=actual receiver DDC 413 | data += x40(hz) 414 | self.setitem(0x0020, data) 415 | self.lasthz = hz 416 | 417 | def setfreq(self, hz): 418 | self.setfreq1(0, hz) # DDC 419 | self.setfreq1(1, hz) # display 420 | 421 | # a sleep seems to be needed for the case in which 422 | # a NetSDR is switching on the down-converter. 423 | if hz > 30000000 and (self.frequency == None or self.frequency < 30000000): 424 | time.sleep(0.5) 425 | 426 | self.frequency = hz 427 | 428 | # reduce gain if recently saw overload warning 429 | mhz = hz // 1000000 430 | gain = 0 431 | if mhz in self.mhz_gain: 432 | if time.time() - self.mhz_overload[mhz] > 5 * 60: 433 | self.mhz_overload[mhz] = time.time() 434 | self.mhz_gain[mhz] += 10 435 | if self.mhz_gain[mhz] > 0: 436 | self.mhz_gain[mhz] = 0 437 | gain = self.mhz_gain[mhz] 438 | if mhz <= 4 and gain > -10: 439 | gain = -10 440 | self.mhz_gain[mhz] = gain 441 | self.mhz_overload[mhz] = time.time() 442 | self.setgain(gain) 443 | 444 | def getfreq(self, chan): 445 | x = self.getitem(0x0020, x8(chan)) 446 | hz = y40(x[1:6]) 447 | return hz 448 | 449 | # set Receiver State to Run 450 | # only I/Q seems to work, not real. 451 | def setrun(self): 452 | self.running = True 453 | data = b"" 454 | if self.iq: 455 | data += x8(0x80) # 0x80=I/Q, 0x00=real 456 | else: 457 | data += x8(0x00) # 0x80=I/Q, 0x00=real 458 | data += x8(0x02) # 1=idle, 2=run 459 | if self.samplebits == 16: 460 | data += x8(0x00) # 80=24 bit continuous, 00=16 bit continuous 461 | else: 462 | data += x8(0x80) # 80=24 bit continuous, 00=16 bit continuous 463 | data += x8(0x00) # unused 464 | self.setitem(0x0018, data) 465 | self.nextseq = 0 466 | # self.print_setup() 467 | 468 | # stop receiver 469 | def stop(self): 470 | self.running = False 471 | data = b"" 472 | if self.iq: 473 | data += x8(0x80) # 0x80=I/Q, 0x00=real 474 | else: 475 | data += x8(0x00) # 0x80=I/Q, 0x00=real 476 | data += x8(0x01) # 1=idle, 2=run 477 | if self.samplebits == 16: 478 | data += x8(0x00) # 80=24 bit continuous, 00=16 bit continuous 479 | else: 480 | data += x8(0x80) # 80=24 bit continuous, 00=16 bit continuous 481 | data += x8(0x00) # unused 482 | self.setitem(0x0018, data) 483 | 484 | # DDC Output Sample Rate 485 | # rate is samples/second 486 | # must be an integer x4 division of 80 million. 487 | # the minimum is 32000. 488 | def setrate(self, rate): 489 | self.rate = rate 490 | data = b"" 491 | data += x8(0) # ignored 492 | data += x32(rate) 493 | self.setitem(0x00B8, data) 494 | 495 | def getrate(self): 496 | x = self.getitem(0x00B8, x8(0)) 497 | rate = y32(x[1:5]) 498 | return rate 499 | 500 | # A/D Modes 501 | # set dither and A/D gain 502 | def setad(self): 503 | data = b"" 504 | data += x8(0) # ignored 505 | # bit zero is dither, bit 1 is A/D gain 1.5 506 | #data += x8(0x3) 507 | data += x8(0x1) 508 | self.setitem(0x008A, data) 509 | 510 | # [ dither, A/D gain ] 511 | def getad(self, chan): 512 | x = self.getitem(0x008A, x8(0)) 513 | dither = (yord(x[1]) & 1) != 0 514 | gain = (yord(x[1]) & 2) != 0 515 | return [ dither, gain ] 516 | 517 | # RF Filter Select 518 | # 0=automatic 519 | # 11=bypass 520 | # 12=block everything (mute) 521 | def setfilter(self, f): 522 | data = b"" 523 | data += x8(0) # channel 524 | data += x8(f) 525 | self.setitem(0x0044, data) 526 | 527 | def getfilter(self, chan): 528 | x = self.getitem(0x0044, x8(chan)) 529 | return yord(x[1]) 530 | 531 | # RF Gain 532 | # gain is 0, -10, -20 -30 dB 533 | def setgain(self, gain): 534 | data = b"" 535 | data += x8(0) # channel 1 536 | data += x8(gain) 537 | self.setitem(0x0038, data) 538 | 539 | def getgain(self, chan): 540 | x = self.getitem(0x0038, x8(chan)) 541 | return yord(x[1]) 542 | 543 | # e.g. "NetSDR" 544 | def getname(self): 545 | x = self.getitem(0x0001) 546 | return x 547 | 548 | # e.g. "PS000553" 549 | def getserial(self): 550 | x = self.getitem(0x0002) 551 | return x 552 | 553 | # 123 means version 1.23 554 | # returns 10 for my NetSDR 555 | def getinterface(self): 556 | x = self.getitem(0x0003) 557 | return y16(x[0:2]) 558 | 559 | # ID=0 boot code 560 | # ID=1 application firmware 561 | # ID=2 hardware 562 | # ID=3 FPGA configuration 563 | # XXX seems to cause protocol problems, NetSDR sends NAKs or something. 564 | def getversion(self, id): 565 | x = self.getitem(0x0004, x8(id)) 566 | if x == None: 567 | # NAK 568 | return None 569 | if id == 3: 570 | return [ yord(x[1]), yord(x[2]) ] # ID, version 571 | else: 572 | return y16(x[1:3]) # version * 100 573 | 574 | # [ FPGA config number, FPGA config ID, FPGA revision, descr string ] 575 | # e.g. [1, 1, 7, 'Std FPGA Config \x00'] 576 | def getfpga(self): 577 | x = self.getitem(0x000C) 578 | return [ yord(x[0]), 579 | yord(x[1]), 580 | yord(x[2]), 581 | x[3:] ] 582 | 583 | # Receiver A/D Amplitude Scale 584 | def getscale(self, chan): 585 | x = self.getitem(0x0023, x8(chan)) 586 | return y16(x[1:3]) 587 | 588 | # VHF/UHF Down Converter Gain 589 | # XXX seems to yield a NAK 590 | def getdowngain(self): 591 | x = self.getitem(0x003A) 592 | auto = yord(x[0]) 593 | lna = yord(x[1]) 594 | mixer = yord(x[2]) 595 | ifout = yord(x[3]) 596 | return [ auto, lna, mixer, ifout ] 597 | 598 | # Data Output UDP IP and Port Address 599 | # just set the port, not the host address. 600 | def setudp(self, port): 601 | # find host's IP address. 602 | hostport = self.cs.getsockname() 603 | ipaddr = socket.inet_aton(hostport[0]) # yields a four-byte string, wrong order 604 | 605 | data = b"" 606 | data += bytearray([ 607 | ipaddr[3], 608 | ipaddr[2], 609 | ipaddr[1], 610 | ipaddr[0], ]) 611 | data += x16(port) 612 | 613 | self.setitem(0x00C5, data) 614 | 615 | # wait for and decode a UDP packet of I/Q samples. 616 | # returns a buffer with interleaved I and Q float64. 617 | # return an array of complex (real=I, imag=Q). 618 | def readiq(self): 619 | # read from the pipe; a 4-byte length, then the packet. 620 | x4 = os.read(self.pipe, 4) 621 | if len(x4) != 4: 622 | sys.stderr.write("sdrip read from child failed\n") 623 | os._exit(1) 624 | [plen] = struct.unpack("I", x4) 625 | assert plen > 0 and plen < 65536 626 | buf = b"" 627 | while len(buf) < plen: 628 | x = os.read(self.pipe, plen - len(buf)) 629 | buf = buf + x 630 | 631 | # parse SDR-IP header into length, msg type 632 | lx = yord(buf[0]) 633 | lx |= (yord(buf[1]) << 8) 634 | mtype = (lx >> 13) & 0x7 # 0x4 is data 635 | lx &= 0x1fff # should == len(buf) 636 | 637 | # packet sequence number (wraps to 1, not 0) 638 | seq = yord(buf[2]) | (yord(buf[3]) << 8) 639 | gap = 0 640 | if seq != self.nextseq and (seq != 1 or self.nextseq != 65536): 641 | # one or more packets were lost. 642 | # we'll fill the gap with zeros. 643 | sys.stderr.write("seq oops got=%d wanted=%d\n" % (seq, self.nextseq)) 644 | if seq > self.nextseq: 645 | gap = seq - self.nextseq 646 | self.nextseq = seq + 1 647 | 648 | if self.samplebits == 16: 649 | samples = numpy.fromstring(buf[4:], dtype=numpy.int16) 650 | else: 651 | s8 = numpy.fromstring(buf[4:], dtype=numpy.uint8) 652 | x0 = s8[0::3] 653 | x1 = s8[1::3] 654 | x2 = s8[2::3] 655 | # top 8 bits, sign-extended from x2 656 | high = numpy.greater(x2, 127) 657 | x3 = numpy.where(high, 658 | numpy.repeat(255, len(x2)), 659 | numpy.repeat(0, len(x2))) 660 | 661 | z = numpy.empty([len(x0)*4], dtype=numpy.uint8) 662 | z[0::4] = x0 663 | z[1::4] = x1 664 | z[2::4] = x2 665 | z[3::4] = x3 666 | zz = z.tostring() 667 | #s32 = numpy.fromstring(zz, dtype=numpy.int32) 668 | #samples = s32.astype(numpy.int16) 669 | samples = numpy.fromstring(zz, dtype=numpy.int32) 670 | 671 | samples = samples.astype(numpy.float64) 672 | 673 | if gap > 0: 674 | pad = numpy.zeros(len(samples)*gap, dtype=numpy.float64), 675 | samples = numpy.append(pad, samples) 676 | 677 | ii1 = samples[0::2] 678 | qq1 = samples[1::2] 679 | cc1 = ii1 + 1j*qq1 680 | return cc1 681 | 682 | # 683 | # read from SDR-IP, demodulate as USB. 684 | # 685 | def readusb(self): 686 | iq = self.readiq() 687 | usb = weakutil.iq2usb(iq) 688 | return usb 689 | -------------------------------------------------------------------------------- /sdriq.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # control an RFSpace SDR-IQ receiver. 4 | # 5 | # uses the kernel USB FTDI driver, which shows up as a serial device. 6 | # may need SDR-IQ firmware >= 1.07. 7 | # 8 | # works on Linux and Macs. 9 | # 10 | # Example: 11 | # sdr = sdriq.open("/dev/cu.usbserial-142") 12 | # sdr.setrate(8138) 13 | # sdr.setgain(-10) 14 | # sdr.setifgain(12) 15 | # sdr.setrun(True) 16 | # while True: 17 | # buf = sdr.readiq() 18 | # 19 | # Robert Morris, AB1HL 20 | # 21 | 22 | import sys 23 | import os 24 | import serial 25 | import time 26 | import threading 27 | import numpy 28 | import select 29 | 30 | # the SDR-IQ needs to be told how fast its sampling clock 31 | # actually runs, so it can set its down-sampling frequency 32 | # accurately. if frequencies seem a bit high, that means 33 | # the SDR-IQ is sampling to slow, so ppm here should be set 34 | # to a negative values (it's -23 for my SDR-IQ). if signals 35 | # are appearing at too-low frequencies, the SDR-IQ is sampling 36 | # too fast, and ppm should be set to a positive value. 37 | # ppm is parts per million, so if a 10 mhz signal shows up 38 | # at 10.000010, the SDR-IQ is sampling 1 PPM too slow 39 | # and ppm should be set to -1. 40 | ppm = -23 41 | 42 | # correspondence between rate codes and I/Q sample rate 43 | rates = [ 44 | [ 0x00001FCA, 8138 ], 45 | [ 0x00003F94, 16276 ], 46 | [ 0x000093A1, 37793 ], 47 | [ 0x0000D904, 55556 ], 48 | [ 0x0001B207, 111111 ], 49 | [ 0x00026C0A, 158730 ], 50 | [ 0x0002FDEE, 196078 ], 51 | ] 52 | 53 | def x16(x): 54 | return [ x & 0xff, (x >> 8) & 0xff ] 55 | 56 | def x32(x): 57 | data = [ ] 58 | data += [ x % 256 ] 59 | x /= 256 60 | data += [ x % 256 ] 61 | x /= 256 62 | data += [ x % 256 ] 63 | x /= 256 64 | data += [ x % 256 ] 65 | x /= 256 66 | assert x == 0 67 | return data 68 | 69 | def hx(s): 70 | buf = "" 71 | for i in range(0, len(s)): 72 | buf += "%02x " % (s[i]) 73 | return buf 74 | 75 | # 76 | # if already connected, return existing SDRIQ, 77 | # otherwise a new one. 78 | # 79 | sdriqs = { } 80 | mu = threading.Lock() 81 | def open(dev): 82 | global sdriqs, mu 83 | mu.acquire() 84 | if not (dev in sdriqs): 85 | sdriqs[dev] = SDRIQ(dev) 86 | sdr = sdriqs[dev] 87 | mu.release() 88 | return sdr 89 | 90 | class SDRIQ: 91 | def __init__(self, devname): 92 | self.running = False 93 | 94 | # only one request/response at a time. 95 | self.w_mu = threading.Lock() 96 | 97 | # protect self.ctl[]. 98 | self.ctl_mu = threading.Lock() 99 | 100 | # protect self.data[]. 101 | self.data_mu = threading.Lock() 102 | 103 | self.port = serial.Serial(devname, 104 | #timeout=2, 105 | baudrate=38400, 106 | parity=serial.PARITY_NONE, 107 | bytesize=serial.EIGHTBITS) 108 | 109 | # waiting type=4 data packets. 110 | # each entry is a 8192-entry array of ints. 111 | self.data = [ ] 112 | 113 | # waiting type=0 control replies. 114 | # each entry is [ mtype, mitem, data ]. 115 | self.ctl = [ ] 116 | 117 | # used only by reader() thread. 118 | self.reader_buf = [ ] 119 | 120 | # fork() a sub-process to read and buffer USB input, 121 | # since the Python thread scheduler doesn't run us often enough if 122 | # WSPR is compute-bound in numpy for tens of seconds. 123 | r, w = os.pipe() 124 | pid = os.fork() 125 | if pid == 0: 126 | os.close(r) 127 | self.child(w) 128 | os._exit(0) 129 | else: 130 | self.pipe = r 131 | os.close(w) 132 | 133 | # only one thread reads the serial port, appending 134 | # arriving control and data messages to ctl[] and data[]. 135 | self.th = threading.Thread(target=lambda : self.reader()) 136 | self.th.daemon = True 137 | self.th.start() 138 | 139 | self.port.write("\x04\x20\x01\x00") # get target name, basically a no-op 140 | 141 | # tell the SDR-IQ what its actual sampling frequency is. 142 | # this does not change the sampling frequency. 143 | nclock = int(66666667 * (1.0 + ppm/1000000.0)) 144 | self.setinputrate(nclock) 145 | sys.stderr.write("sdriq: DDC clock set to %d\n" % (self.getinputrate())) 146 | 147 | def child1(self): 148 | while True: 149 | # do our own read and buffer to avoid serial's internal 150 | # buffering and desire to return exactly the asked-for 151 | # number of bytes. 152 | timeout = 10 153 | select.select([self.port.fileno()], [], [], timeout) 154 | buf = os.read(self.port.fileno(), 8194) 155 | if len(buf) > 0: 156 | self.child_bufs_mu.acquire() 157 | self.child_bufs.append(buf) 158 | self.child_bufs_mu.release() 159 | 160 | # read USB data in a separate process, buffer it, 161 | # and send it on the pipe w. 162 | def child(self, w): 163 | ww = os.fdopen(w, 'wb') 164 | 165 | # spawn a thread that just keeps reading from USB 166 | # and appending buffers of bytes to child_bufs[]. 167 | 168 | self.child_bufs = [ ] 169 | self.child_bufs_mu = threading.Lock() 170 | 171 | th = threading.Thread(target=lambda : self.child1()) 172 | th.daemon = True 173 | th.start() 174 | 175 | # copy buffers from child_bufs[] to the UNIX pipe. 176 | # the pipe write() calls may block, but it's OK because 177 | # the child1() thread keeps draining the UDP socket. 178 | while True: 179 | self.child_bufs_mu.acquire() 180 | bufs = self.child_bufs 181 | self.child_bufs = [ ] 182 | self.child_bufs_mu.release() 183 | 184 | if len(bufs) < 1: 185 | time.sleep(0.1) 186 | 187 | for buf in bufs: 188 | try: 189 | ww.write(buf) 190 | except: 191 | os._exit(1) 192 | ww.flush() 193 | 194 | def reader(self): 195 | while True: 196 | [ mtype, mitem, data ] = self.readmsg() 197 | if mtype == 4: 198 | # data -- I/Q samples 199 | if len(data) != 8192: 200 | sys.stderr.write("sdriq: len(data) %d != 8192\n" % (len(data))) 201 | self.data_mu.acquire() 202 | self.data.append(data) 203 | self.data_mu.release() 204 | elif mtype > 4: 205 | sys.stderr.write("sdriq: unexpected type %d len=%d\n" % (mtype, 206 | len(data))) 207 | elif mtype == 0: 208 | # reply to a set/get control request. 209 | self.ctl_mu.acquire() 210 | self.ctl.append([ mtype, mitem, data ]) 211 | self.ctl_mu.release() 212 | elif mtype == 1 and mitem == 5: 213 | # probably A/D overload. 214 | pass 215 | else: 216 | sys.stderr.write("sdriq: unexpected type=%d item=%d len=%d\n" % (mtype, 217 | mitem, 218 | len(data))) 219 | pass 220 | 221 | # read data from the pipe from the child process. 222 | # absorb it into self.reader_buf[]. 223 | # only called by the reader thread. 224 | def rawread(self): 225 | buf = os.read(self.pipe, 10240) 226 | if len(buf) > 0: 227 | buf = [ ord(x) for x in buf ] 228 | self.reader_buf = self.reader_buf + buf 229 | 230 | # return exactly n bytes 231 | # only called by the reader thread. 232 | def readn(self, n): 233 | while len(self.reader_buf) < n: 234 | self.rawread() 235 | a = self.reader_buf[0:n] 236 | self.reader_buf = self.reader_buf[n:] 237 | return a 238 | 239 | # only called by the reader thread. 240 | def readmsg(self): 241 | x = self.readn(2) 242 | 243 | # 16-bit header 244 | mtype = (x[1] >> 5) & 7 245 | mlen = ((x[1] & 31) << 8) | x[0] 246 | 247 | if mtype >= 4: 248 | # data 249 | if mlen == 0: 250 | mlen = 8192 + 2 251 | data = self.readn(mlen - 2) 252 | return [ mtype, -1, data ] 253 | else: 254 | # response to control, or unsolicited control 255 | y = self.readn(2) 256 | mitem = y[0] | (y[1] << 8) 257 | data = self.readn(mlen - 4) 258 | return [ mtype, mitem, data ] 259 | 260 | def rawwrite(self, a): 261 | b = "" 262 | for aa in a: 263 | b = b + chr(aa) 264 | self.port.write(b) 265 | 266 | # look for the most recent reply to a control request 267 | # for item=mitem. 268 | def readreply(self, mitem): 269 | t0 = time.time() 270 | while True: 271 | if time.time() - t0 > 5: 272 | sys.stderr.write("sdriq: timed out waiting for reply to item=%x\n" % (mitem)) 273 | sys.exit(1) 274 | got = None 275 | self.ctl_mu.acquire() 276 | for m in self.ctl: 277 | if m[0] == 0 and m[1] == mitem: 278 | got = m 279 | self.ctl_mu.release() 280 | if got != None: 281 | return got[2] 282 | time.sleep(0.1) 283 | 284 | def clear_ctl(self): 285 | self.ctl_mu.acquire() 286 | self.ctl = [ ] 287 | self.ctl_mu.release() 288 | 289 | def getitem(self, mitem, param=None): 290 | buf = [ ] 291 | buf += [ 4 ] # overall length, lsb 292 | buf += [ 1 << 5 ] # mtype 1 293 | buf += x16(mitem) 294 | if param != None: 295 | buf += param 296 | # print "getitem writing %s" % (hx(buf)) 297 | 298 | self.w_mu.acquire() 299 | self.clear_ctl() 300 | self.rawwrite(buf) 301 | ret = self.readreply(mitem) 302 | self.w_mu.release() 303 | 304 | return ret 305 | 306 | def setitem(self, mitem, data): 307 | lx = 4 + len(data) 308 | buf = [ ] 309 | buf += [ lx ] # overall length, lsb 310 | buf += [ 0 << 5 ] # mtype 0 311 | buf += x16(mitem) 312 | buf += data 313 | # print "setitem writing %s" % (hx(buf)) 314 | 315 | self.w_mu.acquire() 316 | self.clear_ctl() 317 | self.rawwrite(buf) 318 | ret = self.readreply(mitem) 319 | self.w_mu.release() 320 | 321 | return ret 322 | 323 | def getfreq(self): 324 | x = self.getitem(0x0020, [0x00]) 325 | x = x[1:] 326 | # four bytes of frequency in hz, LSB first 327 | hz = x[0] 328 | hz += x[1]*256 329 | hz += x[2]*256*256 330 | hz += x[3]*256*256*256 331 | return hz 332 | 333 | def setfreq(self, hz): 334 | data = [ ] 335 | data += [ 0 ] 336 | data += x32(hz) 337 | data += [ 0 ] 338 | self.setitem(0x0020, data) 339 | 340 | # id=0 --> PIC boot code version 341 | # id=1 --> PIC firmware version 342 | # returns version * 100 343 | # but does not work as documented on my SDR-IQ v1.05. 344 | def getversion(self, id): 345 | x = self.getitem(0x0004, [ id ]) 346 | assert x[0] == id 347 | x = x[1:] 348 | return x[0] + 256*x[1] 349 | 350 | # RF gain, in dB. 351 | # this controls a combination of pre-amp and attenuator. 352 | def getgain(self): 353 | x = self.getitem(0x0038, [ 0 ] ) 354 | db = x[1] 355 | if db == -10 & 0xff: 356 | return -10 357 | if db == -20 & 0xff: 358 | return -20 359 | if db == -30 & 0xff: 360 | return -30 361 | return db 362 | 363 | # 0, -10, -20, -30 364 | # -10 is probably good. 365 | def setgain(self, db): 366 | self.setitem(0x0038, [ 0, db & 0xff ]) 367 | 368 | # the IF gain controls which of the 20 A/D bits are 369 | # returned in the 16-bit I/Q stream. 370 | # only 0, 6, 12, 18, and 24 dB. 371 | def getifgain(self): 372 | x = self.getitem(0x0040, [ 0 ] ) 373 | return x[1] 374 | 375 | # 12 is probably good. 376 | def setifgain(self, db): 377 | self.setitem(0x0040, [ 0, db ]) 378 | 379 | # I/Q data output sample rate 380 | def getrate(self): 381 | x = self.getitem(0x00B8, [ 0 ] ) 382 | x = x[1:] 383 | z = x[0] 384 | z += x[1]*256 385 | z += x[2]*256*256 386 | z += x[3]*256*256*256 387 | for r in rates: 388 | if z == r[0]: 389 | return r[1] 390 | return 0 391 | 392 | def setrate(self, rate): 393 | code = None 394 | for r in rates: 395 | if rate == r[1]: 396 | code = r[0] 397 | if code == None: 398 | sys.stderr.write("sdriq: unknown I/Q rate %d\n" % (rate)) 399 | sys.exit(1) 400 | data = [ 0 ] 401 | data += x32(code) 402 | self.setitem(0x00B8, data) 403 | 404 | # always more or less 66666667 Hz. 405 | def getinputrate(self): 406 | x = self.getitem(0x00B0, [ 0 ] ) 407 | x = x[1:] 408 | z = x[0] 409 | z += x[1]*256 410 | z += x[2]*256*256 411 | z += x[3]*256*256*256 412 | return z 413 | 414 | # set to something very near 66666667 Hz to correct small 415 | # inaccuracies in the SDR-IQ's clock. 416 | def setinputrate(self, hz): 417 | data = [ 0 ] 418 | data += x32(hz) 419 | self.setitem(0x00B0, data) 420 | 421 | # ask the SDR-IQ to start generating samples. 422 | def setrun(self, run): 423 | self.running = True 424 | data = [ ] 425 | data += [ 0x81 ] 426 | if run: 427 | data += [ 0x02 ] # 1=idle, 2=run 428 | else: 429 | data += [ 0x01 ] # 1=idle, 2=run 430 | data += [ 0 ] # 0=contiguous, 2=one shot, 3=trigger 431 | data += [ 1 ] # number of one-shot blocks 432 | self.setitem(0x0018, data) 433 | 434 | # return a numpy array of complex floats, real=I, imag=Q. 435 | def readiq(self): 436 | buf = None 437 | while buf == None: 438 | self.data_mu.acquire() 439 | if len(self.data) > 0: 440 | buf = self.data[0] 441 | self.data = self.data[1:] 442 | self.data_mu.release() 443 | if buf == None: 444 | time.sleep(0.1) 445 | 446 | # buf[] is 8192 long. 447 | # I1 lsb, I1 msb, Q1 lsb, Q1 msb, I2 lsb, I2 msb, &c. 448 | 449 | #print buf[1024:1024+10] 450 | 451 | buf = numpy.array(buf) 452 | 453 | ilsb = buf[0::4] 454 | imsb = buf[1::4] 455 | ii = numpy.add(ilsb, imsb * 256.0) 456 | 457 | qlsb = buf[2::4] 458 | qmsb = buf[3::4] 459 | qq = numpy.add(qlsb, qmsb * 256.0) 460 | 461 | ret = ii + 1j*qq 462 | 463 | return ret 464 | -------------------------------------------------------------------------------- /sdrplay.py: -------------------------------------------------------------------------------- 1 | # 2 | # control an SDRPlay RSP, and read I/Q samples. 3 | # 4 | # uses the SDRPlay API library (libmirsdrapi-rsp.so). 5 | # 6 | # Robert Morris, AB1HL 7 | # 8 | 9 | import ctypes 10 | import time 11 | import sys 12 | import numpy 13 | import threading 14 | 15 | # 16 | # if already connected, return existing SDRplay, 17 | # otherwise a new one. 18 | # 19 | the_sdrplay = None 20 | mu = threading.Lock() 21 | def open(dev): 22 | global the_sdrplay, mu 23 | mu.acquire() 24 | if the_sdrplay == None: 25 | the_sdrplay = SDRplay() 26 | sdr = the_sdrplay 27 | mu.release() 28 | return sdr 29 | 30 | class SDRplay: 31 | def __init__(self): 32 | # SDRplay config. 33 | self.samplerate = 2048000 # what to tell the SDRplay. 34 | self.decimate = 8 # tell SDRplay to give us every 8th sample. 35 | self.use_callback = True # use new API callback rather than ReadPacket 36 | 37 | # callback appends incoming buffers here. 38 | # each element is [ i[], q[] ]. 39 | self.cb_bufs = [ ] 40 | self.cb_seq = None # next sample num expected by callback 41 | self.cb_time = time.time() # UNIX time of end of cb_bufs 42 | self.cb_bufs_mu = threading.Lock() 43 | 44 | # on mac, this must exist: /usr/local/lib/libusb-1.0.0.dylib 45 | # on linux, setenv LD_LIBRARY_PATH /usr/local/lib 46 | # on linux, may need to be run as root to get at USB device. 47 | 48 | # try a few different names for the library 49 | names = [ 50 | "/usr/local/lib/libmirsdrapi-rsp.so.1.95", 51 | "libmirsdrapi-rsp.so.1.95", 52 | "libmirsdrapi-rsp.so", 53 | "./libmirsdrapi-rsp.so.1.95", 54 | ] 55 | self.lib = None 56 | for name in names: 57 | try: 58 | self.lib = ctypes.cdll.LoadLibrary(name) 59 | print("sdrplay: loaded API from %s" % (name)) 60 | break 61 | except: 62 | pass 63 | if self.lib == None: 64 | sys.stderr.write("sdrplay: could not load API library libmisdrapi-rsp.so\n") 65 | sys.exit(1) 66 | 67 | vers = ctypes.c_float(0.0) 68 | self.lib.mir_sdr_ApiVersion(ctypes.byref(vers)) 69 | if vers.value < 1.95-0.00001 or vers.value > 1.95+0.00001: 70 | sys.stderr.write("sdrplay.py: warning: needs API version 1.95, got %f" % (vers.value)) 71 | 72 | #self.lib.mir_sdr_DebugEnable(1) 73 | 74 | sps = ctypes.c_int(0) 75 | 76 | # type of the callback function 77 | t1 = ctypes.CFUNCTYPE(ctypes.c_int, 78 | ctypes.POINTER(ctypes.c_int16), # xi 79 | ctypes.POINTER(ctypes.c_int16), # xq 80 | ctypes.c_int, # firstSampleNum 81 | ctypes.c_int, # grChanged 82 | ctypes.c_int, # rfChanged 83 | ctypes.c_int, # fsChanged 84 | ctypes.c_uint, # numSamples 85 | ctypes.c_uint, # reset 86 | ctypes.c_void_p) # cbContext 87 | self.cb1 = t1(self.callback) 88 | 89 | t2 = ctypes.CFUNCTYPE(ctypes.c_int) 90 | self.cb2 = t2(self.callbackGC) 91 | 92 | if self.use_callback: 93 | # new streaming/callback API 94 | self.lib.mir_sdr_DecimateControl(1, self.decimate, 0) 95 | self.lib.mir_sdr_AgcControl(1, -30, 0, 0, 0, 0, 0); 96 | newGr = ctypes.c_int(40) 97 | sysGr = ctypes.c_int(40) 98 | err = self.lib.mir_sdr_StreamInit(ctypes.byref(newGr), 99 | ctypes.c_double(self.samplerate / 1000000.0), # sample rate, millions 100 | ctypes.c_double(1000.0), # center frequency, MHz 101 | 200, # mir_sdr_BW_0_200 102 | 0, # mir_sdr_IF_Zero 103 | 0, # LNAEnable 104 | ctypes.byref(sysGr), 105 | 1, # useGrAltMode 106 | ctypes.byref(sps), 107 | self.cb1, 108 | self.cb2, 109 | 0) 110 | if err != 0: 111 | sys.stderr.write("sdrplay: mir_sdr_StreamInit failed: %d\n" % (err)) 112 | sys.exit(1) 113 | else: 114 | # older ReadPacket API 115 | err = self.lib.mir_sdr_Init(40, # gRdB 116 | ctypes.c_double(self.samplerate / 1000000.0), # sample rate, millions 117 | ctypes.c_double(14.076), # center frequency, MHz 118 | 200, # mir_sdr_BW_0_200 119 | 0, # mir_sdr_IF_Zero 120 | ctypes.byref(sps)) # samplesPerPacket 121 | if err != 0: 122 | sys.stderr.write("sdrplay: mir_sdr_Init failed: %d\n" % (err)) 123 | sys.exit(1) 124 | 125 | 126 | self.samplesPerPacket = sps.value 127 | self.expect = None 128 | 129 | # what does this do? 130 | self.lib.mir_sdr_SetDcMode(4, 0) 131 | self.lib.mir_sdr_SetDcTrackTime(63) 132 | 133 | def setfreq(self, hz): 134 | # this doesn't work if you're changing bands. 135 | # err = self.lib.mir_sdr_SetRf(ctypes.c_double(float(hz)), 1, 0) 136 | # if err != 0: 137 | # sys.stderr.write("sdrplay: SetRf(%d) failed: %d\n" % (hz, err)) 138 | 139 | newGr = ctypes.c_int(40) 140 | sysGr = ctypes.c_int(40) 141 | sps = ctypes.c_int(0) 142 | err = self.lib.mir_sdr_Reinit(ctypes.byref(newGr), # gRdB 143 | ctypes.c_double(0.0), # fsMHz 144 | ctypes.c_double(hz / 1000000.0), # rfMHz 145 | 0, # bwType, 146 | 0, # ifType 147 | 0, # LoMode 148 | 0, # LNAEnable 149 | ctypes.byref(sysGr), # gRdBsystem, 150 | 1, # useGrAltMode 151 | ctypes.byref(sps), 152 | 0x04) # mir_sdr_CHANGE_RF_FREQ 153 | if err != 0: 154 | sys.stderr.write("sdrplay: mir_sdr_Reinig(rfMHz=%f) failed: %d\n" % (hz/1000000.0, err)) 155 | 156 | 157 | # for weakaudio. probably 256000. 158 | def getrate(self): 159 | if self.use_callback: 160 | return self.samplerate / float(self.decimate) 161 | else: 162 | return self.samplerate 163 | 164 | def callback(self, xi, xq, firstSampleNum, grChanged, 165 | rfChanged, fsChanged, numSamples, reset, 166 | cbContext): 167 | 168 | # theory: firstSampleNum is 16 bits, and upper bits are junk. 169 | firstSampleNum &= 0xffff 170 | 171 | # ii = [ xi[i] for i in range(0, numSamples) ] 172 | # qq = [ xq[i] for i in range(0, numSamples) ] 173 | 174 | istring = ctypes.string_at(ctypes.addressof(xi.contents), numSamples * 2) 175 | ii = numpy.fromstring(istring, dtype=numpy.int16) 176 | 177 | qstring = ctypes.string_at(ctypes.addressof(xq.contents), numSamples * 2) 178 | qq = numpy.fromstring(qstring, dtype=numpy.int16) 179 | 180 | self.cb_bufs_mu.acquire() 181 | 182 | if reset != 0: 183 | self.cb_seq = None 184 | self.cb_time = time.time() 185 | 186 | if self.cb_seq != None and self.cb_seq != firstSampleNum: 187 | print("SDRplay callback missed %d samples; %d %d %d" % (firstSampleNum - self.cb_seq, 188 | self.cb_seq, firstSampleNum, numSamples)) 189 | 190 | self.cb_bufs.append([ ii, qq ]) 191 | self.cb_seq = (firstSampleNum + numSamples) & 0xffff 192 | self.cb_time += numSamples / float(self.getrate()) 193 | 194 | self.cb_bufs_mu.release() 195 | 196 | return 0 197 | 198 | def callbackGC(self): 199 | return 0 200 | 201 | # internal, for old non-callback API. 202 | # returns I/Q as a numpy complex array. 203 | def rawread(self): 204 | aty = ctypes.c_int16 * self.samplesPerPacket 205 | xi = aty() 206 | xq = aty() 207 | firstSampleNum = ctypes.c_uint(0) 208 | grChanged = ctypes.c_uint(0) 209 | rfChanged = ctypes.c_uint(0) 210 | fsChanged = ctypes.c_uint(0) 211 | err = self.lib.mir_sdr_ReadPacket(ctypes.byref(xi), 212 | ctypes.byref(xq), 213 | ctypes.byref(firstSampleNum), 214 | ctypes.byref(grChanged), 215 | ctypes.byref(rfChanged), 216 | ctypes.byref(fsChanged)) 217 | if err != 0: 218 | sys.stderr.write("sdrplay: mir_sdrReadPacket failed: %d\n" % (err)) 219 | sys.exit(1) 220 | 221 | # I don't know if these are needed. 222 | if grChanged.value != 0: 223 | self.lib.mir_sdr_ResetUpdateFlags(1, 0, 0) 224 | if rfChanged.value != 0: 225 | self.lib.mir_sdr_ResetUpdateFlags(0, 1, 0) 226 | if fsChanged.value != 0: 227 | self.lib.mir_sdr_ResetUpdateFlags(0, 0, 1) 228 | 229 | # ii = numpy.fromstring(xi, dtype=numpy.int16) 230 | # qq = numpy.fromstring(xq, dtype=numpy.int16) 231 | 232 | ii = [ xi[i] for i in range(0, self.samplesPerPacket) ] 233 | ii = numpy.array(ii).astype(numpy.float64) 234 | 235 | qq = [ xq[i] for i in range(0, self.samplesPerPacket) ] 236 | qq = numpy.array(qq).astype(numpy.float64) 237 | 238 | iq = ii + 1j*qq 239 | 240 | # theory: firstSampleNum is only 16 bits, upper bits are junk. 241 | num = firstSampleNum.value 242 | num = num & 0xffff 243 | if self.expect != None and num != self.expect: 244 | print("%d vs %d -- gap %d" % (self.expect, num, num - self.expect)) 245 | 246 | self.expect = (num + self.samplesPerPacket) & 0xffff 247 | 248 | return iq 249 | 250 | # blocking read. 251 | # returns [ samples, end_time ] 252 | def readiq(self): 253 | if self.use_callback: 254 | while True: 255 | self.cb_bufs_mu.acquire() 256 | bufbuf = self.cb_bufs 257 | end_time = self.cb_time 258 | self.cb_bufs = [ ] 259 | self.cb_bufs_mu.release() 260 | 261 | if len(bufbuf) > 0: 262 | break 263 | time.sleep(0.1) 264 | 265 | 266 | bufs = [ ] 267 | for e in bufbuf: 268 | ii = numpy.array(e[0]).astype(numpy.float64) 269 | qq = numpy.array(e[1]).astype(numpy.float64) 270 | iq = ii + 1j*qq 271 | bufs.append(iq) 272 | 273 | buf = numpy.concatenate(bufs) 274 | else: 275 | buf = self.rawread() 276 | 277 | buf = buf / 10.0 # get rid of spurious clip warnings 278 | 279 | return [ buf, end_time ] 280 | -------------------------------------------------------------------------------- /weak.cfg.example: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # defaults for all sections, if not overridden. 3 | #mycall: X1XYZ 4 | #mygrid: FF99 5 | 6 | [wsprmon] 7 | # you can say "mycall: XXX" here to override the DEFAULT section. 8 | 9 | [jt65mon] 10 | 11 | [jt65i] 12 | 13 | [ft8i] 14 | -------------------------------------------------------------------------------- /weakargs.py: -------------------------------------------------------------------------------- 1 | # 2 | # standard argument parsing 3 | # 4 | 5 | import argparse 6 | import sys 7 | import weakaudio 8 | import weakcat 9 | import time 10 | 11 | class Once(argparse.Action): 12 | def __call__(self, parser, namespace, values, option_string = None): 13 | # print '{n} {v} {o}'.format(n = namespace, v = values, o = option_string) 14 | if getattr(namespace, self.dest) is not None: 15 | msg = '{o} can only be specified once'.format(o = option_string) 16 | raise argparse.ArgumentError(None, msg) 17 | setattr(namespace, self.dest, values) 18 | # 19 | # set up for argument parsing, with standard arguments. 20 | # caller can then optionally call parser.add_argument() 21 | # for non-standard arguments, then weakargs.parse_args(). 22 | # 23 | def stdparse(description): 24 | parser = argparse.ArgumentParser(description=description) 25 | parser.add_argument("-card", nargs=2, metavar=('CARD', 'CHAN'), action=Once) 26 | parser.add_argument("-cat", nargs=2, metavar=('TYPE', 'DEV'), action=Once) 27 | parser.add_argument("-card2", nargs=2, metavar=('CARD', 'CHAN')) 28 | parser.add_argument("-card3", nargs=2, metavar=('CARD', 'CHAN')) 29 | parser.add_argument("-card4", nargs=2, metavar=('CARD', 'CHAN')) 30 | parser.add_argument("-cat2", nargs=2, metavar=('TYPE', 'DEV')) 31 | parser.add_argument("-cat3", nargs=2, metavar=('TYPE', 'DEV')) 32 | parser.add_argument("-cat4", nargs=2, metavar=('TYPE', 'DEV')) 33 | parser.add_argument("-levels", action='store_true') 34 | parser.add_argument("-v", action='store_true') 35 | 36 | def myerror(message): 37 | parser.print_usage(sys.stderr) 38 | weakaudio.usage() 39 | weakcat.usage() 40 | parser.exit(2, ('%s: error: %s\n') % (parser.prog, message)) 41 | 42 | parser.error = myerror 43 | 44 | return parser 45 | 46 | # 47 | # parse, and standard post-parsing actions. 48 | # 49 | def parse_args(parser): 50 | args = parser.parse_args() 51 | 52 | # don't require -cat if the "card" is really a controllable 53 | # radio itself. 54 | if args.card != None and args.card[0] in [ "sdrip", "sdriq", "eb200", "sdrplay" ] and args.cat == None: 55 | args.cat = args.card 56 | if args.card2 != None and args.card2[0] in [ "sdrip", "sdriq", "eb200", "sdrplay" ] and args.cat2 == None: 57 | args.cat2 = args.card2 58 | if args.card3 != None and args.card3[0] in [ "sdrip", "sdriq", "eb200", "sdrplay" ] and args.cat3 == None: 59 | args.cat3 = args.card3 60 | if args.card4 != None and args.card4[0] in [ "sdrip", "sdriq", "eb200", "sdrplay" ] and args.cat4 == None: 61 | args.cat4 = args.card4 62 | 63 | if args.levels == True: 64 | weakaudio.levels(args.card) 65 | 66 | return args 67 | -------------------------------------------------------------------------------- /weakaudio.py: -------------------------------------------------------------------------------- 1 | # 2 | # get at sound cards on both Mac and FreeBSD, 3 | # using pyaudio / portaudio. 4 | # 5 | 6 | import sys 7 | import numpy 8 | import time 9 | import threading 10 | import multiprocessing 11 | import os 12 | 13 | import weakutil 14 | 15 | import sdrip 16 | import sdriq 17 | import eb200 18 | import sdrplay 19 | import fmdemod 20 | 21 | # desc is [ "6", "0" ] for a sound card -- sixth card, channel 0 (left). 22 | # desc is [ "sdrip", "192.168.1.2" ] for RFSpace SDR-IP. 23 | def new(desc, rate): 24 | # sound card? 25 | if desc[0].isdigit(): 26 | return Stream(int(desc[0]), int(desc[1]), rate) 27 | 28 | if desc[0] == "sdrip": 29 | return SDRIP(desc[1], rate) 30 | 31 | if desc[0] == "sdriq": 32 | return SDRIQ(desc[1], rate) 33 | 34 | if desc[0] == "eb200": 35 | return EB200(desc[1], rate) 36 | 37 | if desc[0] == "sdrplay": 38 | return SDRplay(desc[1], rate) 39 | 40 | sys.stderr.write("weakaudio: cannot understand card %s\n" % (desc[0])) 41 | usage() 42 | sys.exit(1) 43 | 44 | # need a single one of these even if multiple streams. 45 | global_pya = None 46 | 47 | def pya(): 48 | global global_pya 49 | import pyaudio 50 | if global_pya == None: 51 | # suppress Jack and ALSA error messages on Linux. 52 | #nullfd = os.open("/dev/null", 1) 53 | #oerr = os.dup(2) 54 | #os.dup2(nullfd, 2) 55 | 56 | global_pya = pyaudio.PyAudio() 57 | 58 | #os.dup2(oerr, 2) 59 | #os.close(oerr) 60 | #os.close(nullfd) 61 | return global_pya 62 | 63 | # find the lowest supported input rate >= rate. 64 | # needed on Linux but not the Mac (which converts as needed). 65 | def x_pya_input_rate(card, rate): 66 | import pyaudio 67 | rates = [ rate, 8000, 11025, 12000, 16000, 22050, 44100, 48000 ] 68 | for r in rates: 69 | if r >= rate: 70 | ok = False 71 | try: 72 | ok = pya().is_format_supported(r, 73 | input_device=card, 74 | input_format=pyaudio.paInt16, 75 | input_channels=1) 76 | except: 77 | pass 78 | if ok: 79 | return r 80 | sys.stderr.write("weakaudio: no input rate >= %d\n" % (rate)) 81 | sys.exit(1) 82 | 83 | # sub-process to avoid initializing pyaudio in main 84 | # process, since that makes subsequent forks and 85 | # multiprocessing not work. 86 | def pya_input_rate(card, rate): 87 | rpipe, wpipe = multiprocessing.Pipe(False) 88 | pid = os.fork() 89 | if pid == 0: 90 | rpipe.close() 91 | x = x_pya_input_rate(card, rate) 92 | wpipe.send(x) 93 | os._exit(0) 94 | wpipe.close() 95 | x = rpipe.recv() 96 | os.waitpid(pid, 0) 97 | rpipe.close() 98 | return x 99 | 100 | def x_pya_output_rate(card, rate): 101 | import pyaudio 102 | rates = [ rate, 8000, 11025, 12000, 16000, 22050, 44100, 48000 ] 103 | for r in rates: 104 | if r >= rate: 105 | ok = False 106 | try: 107 | ok = pya().is_format_supported(r, 108 | output_device=card, 109 | output_format=pyaudio.paInt16, 110 | output_channels=1) 111 | except: 112 | pass 113 | if ok: 114 | return r 115 | sys.stderr.write("weakaudio: no output rate >= %d\n" % (rate)) 116 | sys.exit(1) 117 | 118 | def pya_output_rate(card, rate): 119 | rpipe, wpipe = multiprocessing.Pipe(False) 120 | pid = os.fork() 121 | if pid == 0: 122 | rpipe.close() 123 | x = x_pya_output_rate(card, rate) 124 | wpipe.send(x) 125 | os._exit(0) 126 | wpipe.close() 127 | x = rpipe.recv() 128 | os.waitpid(pid, 0) 129 | rpipe.close() 130 | return x 131 | 132 | class Stream: 133 | def __init__(self, card, chan, rate): 134 | self.use_oss = False 135 | #self.use_oss = ("freebsd" in sys.platform) 136 | self.card = card 137 | self.chan = chan 138 | 139 | # UNIX time of audio stream time zero. 140 | self.t0 = None 141 | 142 | if rate == None: 143 | rate = pya_input_rate(card, 8000) 144 | 145 | self.rate = rate # the sample rate the app wants. 146 | self.cardrate = rate # the rate at which the card is running. 147 | 148 | self.cardbufs = [ ] 149 | self.cardlock = threading.Lock() 150 | 151 | self.last_adc_end = None 152 | self.last_end_time = None 153 | 154 | if self.use_oss: 155 | self.oss_open() 156 | else: 157 | self.pya_open() 158 | 159 | self.resampler = weakutil.Resampler(self.cardrate, self.rate) 160 | 161 | # rate at which len(self.raw_read()) increases. 162 | self.rawrate = self.cardrate 163 | 164 | # returns [ buf, tm ] 165 | # where tm is UNIX seconds of the last sample. 166 | # non-blocking. 167 | # reads from a pipe from pya_dev2pipe in the pya sub-process. 168 | # XXX won't work for oss. 169 | def read(self): 170 | [ buf1, tm ] = self.raw_read() 171 | buf2 = self.postprocess(buf1) 172 | return [ buf2, tm ] 173 | 174 | def raw_read(self): 175 | bufs = [ ] 176 | end_time = self.last_end_time 177 | while self.rpipe.poll(): 178 | e = self.rpipe.recv() 179 | # e is [ pcm, unix_end_time ] 180 | bufs.append(e[0]) 181 | end_time = e[1] 182 | 183 | if len(bufs) > 0: 184 | buf = numpy.concatenate(bufs) 185 | else: 186 | buf = numpy.array([]) 187 | 188 | self.last_end_time = end_time 189 | 190 | return [ buf, end_time ] 191 | 192 | def postprocess(self, buf): 193 | if len(buf) > 0: 194 | buf = self.resampler.resample(buf) 195 | return buf 196 | 197 | def junklog(self, msg): 198 | msg1 = "[%d, %d] %s\n" % (self.card, self.chan, msg) 199 | sys.stderr.write(msg1) 200 | f = open("ft8-junk.txt", "a") 201 | f.write(msg1) 202 | f.close() 203 | 204 | # PyAudio calls this in a separate thread. 205 | def pya_callback(self, in_data, frame_count, time_info, status): 206 | import pyaudio 207 | 208 | if status != 0: 209 | self.junklog("pya_callback status %d\n" % (status)) 210 | 211 | pcm = numpy.fromstring(in_data, dtype=numpy.int16) 212 | pcm = pcm[self.chan::self.chans] 213 | 214 | assert frame_count == len(pcm) 215 | 216 | # time of first sample in pcm[], in seconds since start. 217 | adc_time = time_info['input_buffer_adc_time'] 218 | # time of last sample 219 | adc_end = adc_time + (len(pcm) / float(self.cardrate)) 220 | 221 | if self.last_adc_end != None: 222 | if adc_end < self.last_adc_end or adc_end > self.last_adc_end + 5: 223 | self.junklog("pya last_adc_end %s adc_end %s" % (self.last_adc_end, adc_end)) 224 | expected = (adc_end - self.last_adc_end) * float(self.cardrate) 225 | expected = int(round(expected)) 226 | shortfall = expected - len(pcm) 227 | if abs(shortfall) > 20: 228 | self.junklog("pya expected %d got %d" % (expected, len(pcm))) 229 | #if shortfall > 100: 230 | # pcm = numpy.append(numpy.zeros(shortfall, dtype=pcm.dtype), pcm) 231 | 232 | self.last_adc_end = adc_end 233 | 234 | # set up to convert from stream time to UNIX time. 235 | # pya_strm.get_time() returns the UNIX time corresponding 236 | # to the current audio stream time. it's PortAudio's Pa_GetStreamTime(). 237 | if self.t0 == None: 238 | if self.pya_strm == None: 239 | return ( None, pyaudio.paContinue ) 240 | ut = time.time() 241 | st = self.pya_strm.get_time() 242 | self.t0 = ut - st 243 | 244 | # translate time of last sample to UNIX time. 245 | unix_end = adc_end + self.t0 246 | 247 | self.cardlock.acquire() 248 | self.cardbufs.append([ pcm, unix_end ]) 249 | self.cardlock.release() 250 | 251 | return ( None, pyaudio.paContinue ) 252 | 253 | def pya_open(self): 254 | self.cardrate = pya_input_rate(self.card, self.rate) 255 | 256 | # read from sound card in a separate process, since Python 257 | # scheduler seems sometimes not to run the py audio thread 258 | # often enough. 259 | sys.stdout.flush() 260 | rpipe, wpipe = multiprocessing.Pipe(False) 261 | proc = multiprocessing.Process(target=self.pya_dev2pipe, args=[rpipe,wpipe]) 262 | proc.start() 263 | wpipe.close() 264 | self.rpipe = rpipe 265 | 266 | # executes in a sub-process. 267 | def pya_dev2pipe(self, rpipe, wpipe): 268 | import pyaudio 269 | 270 | rpipe.close() 271 | 272 | if "freebsd" in sys.platform: 273 | # always ask for 2 channels, since on FreeBSD if you 274 | # open left with chans=1 and right with chans=2 you 275 | # get mixing. 276 | self.chans = 2 277 | else: 278 | # but needs to be 1 for RigBlaster on Linux. 279 | self.chans = 1 280 | assert self.chan < self.chans 281 | 282 | # perhaps this controls how often the callback is called. 283 | # too big and ft8.py's read() is delayed long enough to 284 | # cut into FT8 decoding time. too small and apparently the 285 | # callback thread can't keep up. 286 | bufsize = int(self.cardrate / 8) # was 4 287 | 288 | # pya.open in this sub-process so that pya starts the callback thread 289 | # here too. 290 | xpya = pya() 291 | self.pya_strm = None 292 | self.pya_strm = xpya.open(format=pyaudio.paInt16, 293 | input_device_index=self.card, 294 | channels=self.chans, 295 | rate=self.cardrate, 296 | frames_per_buffer=bufsize, 297 | stream_callback=self.pya_callback, 298 | output=False, 299 | input=True) 300 | 301 | # copy buffers from self.cardbufs, where pya_callback left them, 302 | # to the pipe to the parent process. can't do this in the callback 303 | # because the pipe write might block. 304 | # each object on the pipe is [ pcm, unix_end ]. 305 | while True: 306 | self.cardlock.acquire() 307 | bufs = self.cardbufs 308 | self.cardbufs = [ ] 309 | self.cardlock.release() 310 | if len(bufs) > 0: 311 | for e in bufs: 312 | try: 313 | wpipe.send(e) 314 | except: 315 | os._exit(1) 316 | else: 317 | time.sleep(0.05) 318 | 319 | 320 | def oss_open(self): 321 | import ossaudiodev 322 | self.oss = ossaudiodev.open("/dev/dsp" + str(self.card) + ".0", "r") 323 | self.oss.setfmt(ossaudiodev.AFMT_S16_LE) 324 | self.oss.channels(2) 325 | assert self.oss.speed(self.rate) == self.rate 326 | self.th = threading.Thread(target=lambda : self.oss_thread()) 327 | self.th.daemon = True 328 | self.th.start() 329 | 330 | # dedicating reading thread because oss's buffering seems 331 | # to be pretty limited, and wspr.py spends 50 seconds in 332 | # process() while not calling read(). 333 | def oss_thread(self): 334 | # XXX the card probably doesn't read the first sample at this 335 | # exact point, and probably doesn't read at exactly self.rate 336 | # samples per second. 337 | self.cardtime = time.time() 338 | 339 | while True: 340 | # the read() blocks. 341 | buf = self.oss.read(8192) 342 | assert len(buf) > 0 343 | both = numpy.fromstring(buf, dtype=numpy.int16) 344 | got = both[self.chan::self.chans] 345 | 346 | self.cardlock.acquire() 347 | self.cardbufs.append(got) 348 | self.cardtime += len(got) / float(self.rate) 349 | self.cardlock.release() 350 | 351 | # print levels, to help me adjust volume control. 352 | def levels(self): 353 | while True: 354 | time.sleep(1) 355 | [ buf, junk ] = self.read() 356 | if len(buf) > 0: 357 | print("avg=%.0f max=%.0f" % (numpy.mean(abs(buf)), numpy.max(buf))) 358 | 359 | class SDRIP: 360 | def __init__(self, ip, rate): 361 | if rate == None: 362 | rate = 11025 363 | 364 | self.ip = ip 365 | self.rate = rate 366 | self.sdrrate = 32000 367 | self.fm = fmdemod.FMDemod(self.sdrrate) 368 | 369 | self.resampler = weakutil.Resampler(self.sdrrate, self.rate) 370 | 371 | self.sdr = sdrip.open(ip) 372 | self.sdr.setrate(self.sdrrate) 373 | #self.sdr.setgain(-10) 374 | 375 | # now weakcat.SDRIP.read() calls setrun(). 376 | #self.sdr.setrun() 377 | 378 | self.starttime = None # for faking a sample clock 379 | self.cardcount = 0 # for faking a sample clock 380 | 381 | self.bufbuf = [ ] 382 | self.cardlock = threading.Lock() 383 | self.th = threading.Thread(target=lambda : self.sdr_thread()) 384 | self.th.daemon = True 385 | self.th.start() 386 | 387 | # rate at which len(self.raw_read()) increases. 388 | self.rawrate = self.sdrrate 389 | 390 | def junklog(self, msg): 391 | msg1 = "[%s] %s\n" % (self.ip, msg) 392 | #sys.stderr.write(msg1) 393 | f = open("ft8-junk.txt", "a") 394 | f.write(msg1) 395 | f.close() 396 | 397 | # returns [ buf, tm ] 398 | # where tm is UNIX seconds of the last sample. 399 | def read(self): 400 | [ buf1, tm ] = self.raw_read() 401 | buf2 = self.postprocess(buf1) 402 | return [ buf2, tm ] 403 | 404 | def raw_read(self): 405 | # delay setrun() until the last moment, so that 406 | # all other parameters have likely been set. 407 | if self.sdr.running == False: 408 | self.sdr.setrun() 409 | 410 | self.cardlock.acquire() 411 | bufbuf = self.bufbuf 412 | cardcount = self.cardcount 413 | self.bufbuf = [ ] 414 | self.cardlock.release() 415 | 416 | if self.starttime != None: 417 | buf_time = self.starttime + cardcount / float(self.sdrrate) 418 | else: 419 | buf_time = time.time() # XXX 420 | 421 | if len(bufbuf) == 0: 422 | return [ numpy.array([]), buf_time ] 423 | 424 | buf1 = numpy.concatenate(bufbuf) 425 | 426 | return [ buf1, buf_time ] 427 | 428 | def postprocess(self, buf1): 429 | if len(buf1) == 0: 430 | return numpy.array([]) 431 | 432 | if self.sdr.mode == "usb": 433 | buf2 = weakutil.iq2usb(buf1) # I/Q -> USB 434 | elif self.sdr.mode == "fm": 435 | [ buf2, junk ] = self.fm.demod(buf1) # I/Q -> FM 436 | else: 437 | sys.stderr.write("weakaudio: SDRIP unknown mode %s\n" % (self.sdr.mode)) 438 | sys.exit(1) 439 | 440 | buf3 = self.resampler.resample(buf2) 441 | 442 | return buf3 443 | 444 | def sdr_thread(self): 445 | 446 | while True: 447 | # read pipe from sub-process. 448 | got = self.sdr.readiq() 449 | 450 | self.cardlock.acquire() 451 | self.bufbuf.append(got) 452 | self.cardcount += len(got) 453 | if self.starttime == None: 454 | self.starttime = time.time() 455 | self.cardlock.release() 456 | 457 | # print levels, to help me adjust volume control. 458 | def levels(self): 459 | while True: 460 | time.sleep(1) 461 | [ buf, junk ] = self.read() 462 | if len(buf) > 0: 463 | print("avg=%.0f max=%.0f" % (numpy.mean(abs(buf)), numpy.max(buf))) 464 | 465 | class SDRIQ: 466 | def __init__(self, ip, rate): 467 | if rate == None: 468 | rate = 11025 469 | 470 | self.rate = rate 471 | self.sdrrate = 8138 472 | 473 | self.bufbuf = [ ] 474 | self.starttime = time.time() # for faking a sample clock 475 | self.cardcount = 0 # for faking a sample clock 476 | self.cardlock = threading.Lock() 477 | 478 | self.resampler = weakutil.Resampler(self.sdrrate, self.rate) 479 | 480 | self.sdr = sdriq.open(ip) 481 | self.sdr.setrate(self.sdrrate) 482 | self.sdr.setgain(0) 483 | self.sdr.setifgain(18) # I don't know how to set this! 484 | 485 | self.th = threading.Thread(target=lambda : self.sdr_thread()) 486 | self.th.daemon = True 487 | self.th.start() 488 | 489 | self.rawrate = self.sdrrate 490 | 491 | # returns [ buf, tm ] 492 | # where tm is UNIX seconds of the last sample. 493 | def read(self): 494 | [ buf1, tm ] = self.raw_read() 495 | buf2 = self.postprocess(buf1) 496 | return [ buf2, tm ] 497 | 498 | def raw_read(self): 499 | if self.sdr.running == False: 500 | self.sdr.setrun(True) 501 | 502 | self.cardlock.acquire() 503 | bufbuf = self.bufbuf 504 | cardcount = self.cardcount 505 | self.bufbuf = [ ] 506 | self.cardlock.release() 507 | 508 | buf_time = self.starttime + cardcount / float(self.sdrrate) 509 | 510 | if len(bufbuf) == 0: 511 | return [ numpy.array([]), buf_time ] 512 | 513 | buf = numpy.concatenate(bufbuf) 514 | 515 | return [ buf, buf_time ] 516 | 517 | def postprocess(self, buf1): 518 | if len(buf1) == 0: 519 | return numpy.array([]) 520 | 521 | buf = weakutil.iq2usb(buf1) # I/Q -> USB 522 | 523 | buf = self.resampler.resample(buf) 524 | 525 | # no matter how I set its RF or IF gain, 526 | # the SDR-IQ generates peaks around 145000, 527 | # or I and Q values of 65535. cut this down 528 | # so application doesn't think the SDR-IQ is clipping. 529 | buf = buf / 10.0 530 | 531 | return buf 532 | 533 | def sdr_thread(self): 534 | self.starttime = time.time() 535 | 536 | while True: 537 | # read i/q blocks, float64, to reduce CPU time in 538 | # this thread, which drains the UDP socket. 539 | got = self.sdr.readiq() 540 | 541 | self.cardlock.acquire() 542 | self.bufbuf.append(got) 543 | self.cardcount += len(got) 544 | self.cardlock.release() 545 | 546 | # print levels, to help me adjust volume control. 547 | def levels(self): 548 | while True: 549 | time.sleep(1) 550 | [ buf, junk ] = self.read() 551 | if len(buf) > 0: 552 | print("avg=%.0f max=%.0f" % (numpy.mean(abs(buf)), numpy.max(buf))) 553 | 554 | class EB200: 555 | def __init__(self, ip, rate): 556 | if rate == None: 557 | rate = 8000 558 | 559 | self.rate = rate 560 | 561 | self.time_mu = threading.Lock() 562 | self.cardtime = time.time() # UNIX time just after last sample in bufbuf 563 | 564 | self.sdr = eb200.open(ip) 565 | self.sdrrate = self.sdr.getrate() 566 | 567 | self.resampler = weakutil.Resampler(self.sdrrate, self.rate) 568 | 569 | # returns [ buf, tm ] 570 | # where tm is UNIX seconds of the last sample. 571 | # blocks until input is available. 572 | def read(self): 573 | buf = self.sdr.readaudio() 574 | 575 | self.time_mu.acquire() 576 | self.cardtime += len(buf) / float(self.sdrrate) 577 | buf_time = self.cardtime 578 | self.time_mu.release() 579 | 580 | buf = self.resampler.resample(buf) 581 | 582 | return [ buf, buf_time ] 583 | 584 | # print levels, to help me adjust volume control. 585 | def levels(self): 586 | while True: 587 | time.sleep(1) 588 | [ buf, junk ] = self.read() 589 | if len(buf) > 0: 590 | print("avg=%.0f max=%.0f" % (numpy.mean(abs(buf)), numpy.max(buf))) 591 | 592 | class SDRplay: 593 | def __init__(self, dev, rate): 594 | if rate == None: 595 | rate = 11025 596 | 597 | self.rate = rate 598 | 599 | self.sdr = sdrplay.open(dev) 600 | self.sdrrate = self.sdr.getrate() 601 | 602 | self.resampler = weakutil.Resampler(self.sdrrate, self.rate) 603 | 604 | # returns [ buf, tm ] 605 | # where tm is UNIX seconds of the last sample. 606 | def read(self): 607 | [ buf, buf_time ] = self.sdr.readiq() 608 | 609 | buf = weakutil.iq2usb(buf) # I/Q -> USB 610 | 611 | buf = self.resampler.resample(buf) 612 | 613 | return [ buf, buf_time ] 614 | 615 | # print levels, to help me adjust volume control. 616 | def levels(self): 617 | while True: 618 | time.sleep(1) 619 | [ buf, junk ] = self.read() 620 | if len(buf) > 0: 621 | print("avg=%.0f max=%.0f" % (numpy.mean(abs(buf)), numpy.max(buf))) 622 | 623 | # 624 | # for Usage(), print out a list of audio cards 625 | # and associated number (for the "card" argument). 626 | # 627 | def usage(): 628 | import pyaudio 629 | ndev = pya().get_device_count() 630 | sys.stderr.write("sound card numbers for -card and -out:\n") 631 | for i in range(0, ndev): 632 | info = pya().get_device_info_by_index(i) 633 | sys.stderr.write(" %d: %s, channels=%d" % (i, 634 | info['name'], 635 | info['maxInputChannels'])) 636 | if True and info['maxInputChannels'] > 0: 637 | rates = [ 11025, 12000, 16000, 22050, 44100, 48000 ] 638 | for rate in rates: 639 | try: 640 | ok = pya().is_format_supported(rate, 641 | input_device=i, 642 | input_format=pyaudio.paInt16, 643 | input_channels=1) 644 | except: 645 | ok = False 646 | if ok: 647 | sys.stderr.write(" %d" % (rate)) 648 | sys.stderr.write("\n") 649 | sys.stderr.write(" or -card sdrip IPADDR\n") 650 | sys.stderr.write(" or -card sdriq /dev/SERIALPORT\n") 651 | sys.stderr.write(" or -card eb200 IPADDR\n") 652 | sys.stderr.write(" or -card sdrplay sdrplay\n") 653 | 654 | # implement -levels. 655 | # print sound card avg/peak once per second, to adjust level. 656 | # never returns. 657 | def levels(card): 658 | if card == None: 659 | sys.stderr.write("-levels requires -card\n") 660 | sys.exit(1) 661 | c = new(card, 11025) 662 | c.levels() 663 | sys.exit(0) 664 | -------------------------------------------------------------------------------- /wsprmon.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # WSPR receiver. 5 | # 6 | # switches among bands if weakcat.py understands the radio. 7 | # reports to wsprnet if mycall/mygrid defined in weak.ini. 8 | # 9 | # Robert Morris, AB1HL 10 | # 11 | 12 | import wspr 13 | import sys 14 | import os 15 | import time 16 | import weakaudio 17 | import numpy 18 | import threading 19 | import re 20 | import random 21 | import copy 22 | import weakcat 23 | from six.moves import urllib 24 | #import urllib.request, urllib.parse, urllib.error 25 | import weakutil 26 | import weakargs 27 | 28 | # look only at these bands. 29 | plausible = [ "80", "40", "30", "20", "17" ] 30 | 31 | b2f = { "80" : 3.568600, "40" : 7.038600, "30" : 10.138700, "20" : 14.095600, 32 | "17" : 18.104600, "15" : 21.094600, "12" : 24.924600, 33 | "10" : 28.124600, "6" : 50.293000, "2" : 144.489 } 34 | 35 | def load_prefixes(): 36 | d = { } 37 | f = open("jt65prefixes.dat") 38 | for ln in f: 39 | ln = re.sub(r'\t', ' ', ln) 40 | ln = re.sub(r' *', ' ', ln) 41 | ln.strip() 42 | ln = re.sub(r' *\(.*\) *', '', ln) 43 | ln.strip() 44 | m = re.search(r'^([A-Z0-9]+) +(.*)', ln) 45 | if m != None: 46 | d[m.group(1)] = m.group(2) 47 | f.close() 48 | return d 49 | 50 | def look_prefix(call, d): 51 | if len(call) == 5 and call[0:3] == "KG4": 52 | # KG4xx is Guantanamo, KG4x and KG4xxx are not. 53 | return "Guantanamo Bay" 54 | 55 | while len(call) > 0: 56 | if call in d: 57 | return d[call] 58 | call = call[0:-1] 59 | return None 60 | 61 | # weighted choice (to pick bands). 62 | # a[i] = [ value, weight ] 63 | def wchoice(a, n): 64 | total = 0.0 65 | for e in a: 66 | total += e[1] 67 | 68 | ret = [ ] 69 | while len(ret) < n: 70 | x = random.random() * total 71 | for ai in range(0, len(a)): 72 | e = a[ai] 73 | if x <= e[1]: 74 | ret.append(e[0]) 75 | total -= e[1] 76 | a = a[0:ai] + a[ai+1:] 77 | break 78 | x -= e[1] 79 | 80 | return ret 81 | 82 | def wchoice_test(): 83 | a = [ [ "a", .1 ], [ "b", .1 ], [ "c", .4 ], [ "d", .3 ], [ "e", .1 ] ] 84 | counts = { } 85 | for iter in range(0, 500): 86 | x = wchoice(a, 2) 87 | for e in x: 88 | counts[e] = counts.get(e, 0) + 1 89 | print(counts) 90 | 91 | class WSPRMon: 92 | def __init__(self, incard, cat, oneband): 93 | self.mycall = weakutil.cfg("wsprmon", "mycall") 94 | self.mygrid = weakutil.cfg("wsprmon", "mygrid") 95 | 96 | self.running = True 97 | self.rate = 12000 98 | self.logname = "wspr-log.txt" 99 | self.bandname = "wspr-band.txt" 100 | self.jtname = "wspr" 101 | self.verbose = False 102 | 103 | self.incard = incard 104 | self.oneband = oneband 105 | 106 | if cat != None: 107 | self.cat = weakcat.open(cat) 108 | self.cat.sync() 109 | self.cat.set_usb_data() 110 | else: 111 | self.cat = None 112 | 113 | # for each band, count of received signals last time we 114 | # looked at it, to guess most profitable band. 115 | self.bandinfo = { } 116 | 117 | # for each two-minute interval, the band we were listening on. 118 | self.minband = { } 119 | 120 | # has readall() processed each interval? 121 | self.mindone = { } 122 | 123 | self.prefixes = load_prefixes() 124 | 125 | def start(self): 126 | self.r = wspr.WSPR() 127 | self.r.cardrate = self.rate 128 | self.r.opencard(self.incard) 129 | self.rth = threading.Thread(target=lambda : self.r.gocard()) 130 | self.rth.daemon = True 131 | self.rth.start() 132 | 133 | if self.mycall == None or self.mygrid == None: 134 | print("not reporting to wsprnet because no mycall/mygrid in weak.cfg") 135 | elif True: 136 | self.nth = threading.Thread(target=lambda : self.gonet()) 137 | self.nth.daemon = True 138 | self.nth.start() 139 | print("reporting to wsprnet as %s at %s." % (self.mycall, self.mygrid)) 140 | else: 141 | print("not reporting to wsprnet.") 142 | 143 | def close(self): 144 | self.running = False 145 | self.r.close() 146 | self.rth.join() 147 | self.nth.join() 148 | self.pya.terminate() 149 | 150 | # thread to send to wsprnet. 151 | # hints from wsjtx wsprnet.cpp and 152 | # http://blog.marxy.org/2015/12/wsprnet-down-up-down.html 153 | def gonet(self): 154 | mi = 0 155 | while self.running: 156 | time.sleep(30) 157 | msgs = self.r.get_msgs() 158 | while mi < len(msgs): 159 | msg = msgs[mi] 160 | mi += 1 161 | # msg is a wspr.Decode. 162 | if not (msg.minute in self.minband): 163 | continue 164 | band = self.minband[msg.minute] 165 | pp = self.parse(msg.msg) 166 | if pp == None: 167 | continue 168 | [ call, grid, dbm ] = pp 169 | 170 | when = self.r.start_time + 60*msg.minute 171 | gm = time.gmtime(when) 172 | 173 | url = "http://wsprnet.org/post?" 174 | url += "function=wspr&" 175 | url += "rcall=%s&" % (self.mycall) 176 | url += "rgrid=%s&" % (self.mygrid) 177 | url += "rqrg=%.6f&" % (b2f[band]) # my frequency, mHz 178 | url += "date=%02d%02d%02d&" % (gm.tm_year-2000, gm.tm_mon, gm.tm_mday) 179 | url += "time=%02d%02d&" % (gm.tm_hour, gm.tm_min) 180 | url += "sig=%.0f&" % (msg.snr) 181 | url += "dt=%.1f&" % (msg.dt) 182 | url += "drift=%.1f&" % (msg.drift) 183 | url += "tqrg=%.6f&" % (b2f[band] + msg.hz()/1000000.0) 184 | url += "tcall=%s&" % (call) 185 | url += "tgrid=%s&" % (grid) 186 | url += "dbm=%s&" % (dbm) 187 | url += "version=weakmon-0.3&" 188 | url += "mode=2" 189 | 190 | try: 191 | req = urllib.request.urlopen(url) 192 | for junk in req: 193 | pass 194 | req.close() 195 | except: 196 | print("wsprnet GET failed for %s" % (msg.msg)) 197 | pass 198 | 199 | # process messages from one cycle ago, i.e. the latest 200 | # cycle for which both reception and 201 | # decoding have completed. 202 | def readall(self): 203 | now = time.time() 204 | nowmin = self.r.minute(now) 205 | for min in range(max(0, nowmin-6), nowmin, 2): 206 | if min in self.mindone: 207 | continue 208 | self.mindone[min] = True 209 | 210 | if not (min in self.minband): 211 | continue 212 | band = self.minband[min] 213 | 214 | bandcount = 0 215 | msgs = self.r.get_msgs() 216 | # each msg is a wspr.Decode. 217 | for m in msgs[len(msgs)-50:]: 218 | if m.minute == min: 219 | bandcount += 1 220 | self.log(self.r.start_time + 60*min, band, m.hz(), m.msg, m.snr, m.dt, m.drift) 221 | x = self.bandinfo.get(band, 0) 222 | self.bandinfo[band] = 0.5 * x + 0.5 * bandcount 223 | 224 | # turn "WB4HIR EM95 33" into ["WB4HIR", "EM95", "33"], or None. 225 | def parse(self, msg): 226 | msg = msg.strip() 227 | msg = re.sub(r' *', ' ', msg) 228 | m = re.search(r'^([A-Z0-9\/]+) ([A-Z0-9]+) ([0-9]+)', msg) 229 | if m == None: 230 | print("wsprmon log could not parse %s" % (msg)) 231 | return None 232 | 233 | call = m.group(1) 234 | grid = m.group(2) 235 | dbm = m.group(3) 236 | 237 | return [ call, grid, dbm ] 238 | 239 | def log(self, when, band, hz, msg, snr, dt, drift): 240 | pp = self.parse(msg) 241 | if pp == None: 242 | return 243 | [ call, grid, dbm ] = pp 244 | 245 | entity = look_prefix(call, self.prefixes) 246 | 247 | # b2f is mHz 248 | freq = b2f[band] + hz / 1000000.0 249 | 250 | ts = self.r.ts(when) 251 | ts = re.sub(r':[0-9][0-9]$', '', ts) # delete seconds 252 | 253 | info = "%s %9.6f %s %s %s %.0f %.1f %.1f %s" % (ts, 254 | freq, 255 | call, 256 | grid, 257 | dbm, 258 | snr, 259 | dt, 260 | drift, 261 | entity) 262 | print("%s" % (info)) 263 | 264 | f = open(self.logname, "a") 265 | f.write("%s\n" % (info)) 266 | f.close() 267 | 268 | # return a good band on which to listen. 269 | def rankbands(self): 270 | global plausible 271 | 272 | # are we missing bandinfo for any bands? 273 | missing = [ ] 274 | for b in plausible: 275 | if self.bandinfo.get(b) == None: 276 | missing.append(b) 277 | 278 | # always explore missing bands first. 279 | if len(missing) > 0: 280 | band = missing[0] 281 | # so we no longer count it as "missing". 282 | self.bandinfo[band] = 0 283 | return band 284 | 285 | # most profitable bands, best first. 286 | best = sorted(plausible, key = lambda b : -self.bandinfo.get(b, -1)) 287 | 288 | if random.random() < 0.3 or self.bandinfo[best[0]] <= 0.1: 289 | band = random.choice(plausible) 290 | else: 291 | wa = [ [ b, self.bandinfo[b] ] for b in best ] 292 | band = wchoice(wa, 1)[0] 293 | 294 | return band 295 | 296 | def go(self): 297 | while self.running: 298 | # wait until we'are at the start of a two-minute interval. 299 | # that is, don't tell the radio to change bands in the 300 | # middle of an interval. 301 | while True: 302 | if self.running == False: 303 | return 304 | second = self.r.second(time.time()) 305 | if second >= 119 or second < 1: 306 | break 307 | time.sleep(0.2) 308 | 309 | # choose a band. 310 | if self.oneband != None: 311 | band = self.oneband 312 | else: 313 | band = self.rankbands() 314 | if self.cat != None: 315 | self.cat.setf(0, int(b2f[band] * 1000000.0)) 316 | 317 | now = time.time() 318 | if self.r.second(now) < 5: 319 | min = self.r.minute(now) 320 | else: 321 | min = self.r.minute(now + 5) 322 | 323 | # remember the band for this minute, for readall(). 324 | self.minband[min] = band 325 | 326 | if self.verbose: 327 | sys.stdout.write("band %s ; " % (band)) 328 | for b in self.bandinfo: 329 | sys.stdout.write("%s %.1f, " % (b, self.bandinfo[b])) 330 | sys.stdout.write("\n") 331 | sys.stdout.flush() 332 | 333 | # make sure we get into the next minute 334 | time.sleep(5) 335 | 336 | # collect incoming message reports. 337 | while self.running: 338 | now = time.time() 339 | second = self.r.second(now) 340 | if second >= 118: 341 | break 342 | self.readall() 343 | time.sleep(1) 344 | 345 | def oldmain(): 346 | incard = None 347 | cattype = None 348 | catdev = None 349 | oneband = None 350 | levels = False 351 | vflag = False 352 | 353 | i = 1 354 | while i < len(sys.argv): 355 | if sys.argv[i] == "-in": 356 | incard = sys.argv[i+1] 357 | i += 2 358 | elif sys.argv[i] == "-cat": 359 | cattype = sys.argv[i+1] 360 | catdev = sys.argv[i+2] 361 | i += 3 362 | elif sys.argv[i] == "-band": 363 | oneband = sys.argv[i+1] 364 | i += 2 365 | elif sys.argv[i] == "-levels": 366 | levels = True 367 | i += 1 368 | elif sys.argv[i] == "-v": 369 | vflag = True 370 | i += 1 371 | else: 372 | usage() 373 | 374 | if levels: 375 | # print sound card avg/peak once per second, to 376 | # adjust level. 377 | if incard == None: 378 | usage() 379 | c = weakaudio.new(incard, 12000) 380 | c.levels() 381 | sys.exit(0) 382 | 383 | if catdev == None and oneband == None: 384 | sys.stderr.write("wsprmon needs either -cat or -band\n") 385 | usage() 386 | 387 | if incard != None: 388 | w = WSPRMon(incard, cattype, catdev, oneband) 389 | w.verbose = vflag 390 | w.start() 391 | w.go() 392 | w.close() 393 | else: 394 | usage() 395 | 396 | def main(): 397 | parser = weakargs.stdparse('Decode WSPR.') 398 | parser.add_argument("-band") 399 | args = weakargs.parse_args(parser) 400 | 401 | if args.card == None: 402 | parser.error("wsprmon requires -card") 403 | 404 | if args.cat == None and args.band == None: 405 | parser.error("wsprmon needs either -cat or -band") 406 | 407 | w = WSPRMon(args.card, args.cat, args.band) 408 | w.verbose = args.v 409 | w.start() 410 | w.go() 411 | w.close() 412 | 413 | sys.exit(0) 414 | 415 | main() 416 | -------------------------------------------------------------------------------- /wwvbmon.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | # 4 | # WWVB phase-shift keying sound-card demodulator 5 | # 6 | # set radio to 59 khz, upper side band. 7 | # radio must be within 10 hz of correct frequency. 8 | # 9 | # my setup uses a Z10024A low-pass filter to keep out AM broadcast. 10 | # an outdoor dipole or indoor W1VLF antenna work well. 11 | # 12 | # Robert Morris, AB1HL 13 | # 14 | 15 | import numpy 16 | import wave 17 | import weakaudio 18 | import weakcat 19 | import weakutil 20 | import weakargs 21 | import weakaudio 22 | import scipy 23 | import scipy.signal 24 | import sys 25 | import os 26 | import math 27 | import time 28 | import calendar 29 | import subprocess 30 | import argparse 31 | 32 | # a[] and b[] are -1/0/1 bit sequences. 33 | # in how many bits are they identical? 34 | def bitmatch(a, b): 35 | n = 0 36 | i = 0 37 | while i < len(a) and i < len(b): 38 | if a[i] != 0 and b[i] != 0 and a[i] == b[i]: 39 | n += 1 40 | i += 1 41 | return n 42 | 43 | # invert a -1/1 bit sequence. 44 | def invert(a): 45 | b = a[:] 46 | for i in range(0, len(b)): 47 | b[i] *= -1 48 | return b 49 | 50 | # part of wwvb checksum work. 51 | # tm[] is 0/1 array of the 26 time bits. 52 | # a[] is the array of indices of bits to xor. 53 | def xsum(tm, a): 54 | z = 0 55 | for i in range(0, len(a)): 56 | b = tm[a[i]] 57 | z ^= b 58 | if z == 0: 59 | return -1 60 | else: 61 | return 1 62 | 63 | # http://gordoncluster.wordpress.com/2014/02/13/python-numpy-how-to-generate-moving-averages-efficiently-part-2/ 64 | def smooth(values, window): 65 | weights = numpy.repeat(1.0, window)/window 66 | sma = numpy.convolve(values, weights, 'valid') 67 | return sma 68 | 69 | class WWVB: 70 | center = 1000 # 60 khz shifted to here in audio 71 | filterwidth = 20 # bandpass filter width in hertz 72 | searchhz = 10 # only look for WWVB at +/- searchhz 73 | 74 | # set these to True in order to search for the signal in 75 | # time and frequency. set them to False if the PC clock is 76 | # correct, the radio frequency is accurate, and the goal 77 | # is to measure reception quality rather than to learn the time. 78 | searchtime = True 79 | searchfreq = True 80 | 81 | debug = False 82 | 83 | filter = None 84 | c2filter = None 85 | c3filter = None 86 | samples = numpy.array([0]) 87 | offset = 0 88 | flywheel = 0 89 | flyfreq = None # carrier frequency of last good ecc 90 | 91 | # remember all the good CRC offset/minute pairs, 92 | # to try to guess the most likely correct time for 93 | # any given minute with a bad CRC. 94 | timepairs = numpy.zeros([0,2]) # each is [ offset, minute ] 95 | 96 | def __init__(self): 97 | pass 98 | 99 | def openwav(self, filename): 100 | self.wav = wave.open(filename) 101 | self.wav_channels = self.wav.getnchannels() 102 | self.wav_width = self.wav.getsampwidth() 103 | self.rate = self.wav.getframerate() 104 | # for guess1() / weakutil.freq_from_fft(). 105 | weakutil.init_freq_from_fft(59 * self.rate) 106 | weakutil.fft_sizes([ 59 * self.rate ]) 107 | 108 | def readwav(self, chan): 109 | z = self.wav.readframes(1024) 110 | if self.wav_width == 1: 111 | zz = numpy.fromstring(z, numpy.int8) 112 | elif self.wav_width == 2: 113 | if (len(z) % 2) == 1: 114 | return numpy.array([]) 115 | zz = numpy.fromstring(z, numpy.int16) 116 | else: 117 | sys.stderr.write("oops wave_width %d" % (self.wav_width)) 118 | sys.exit(1) 119 | if self.wav_channels == 1: 120 | return zz 121 | elif self.wav_channels == 2: 122 | return zz[chan::2] # chan 0/1 => left/right 123 | else: 124 | sys.stderr.write("oops wav_channels %d" % (self.wav_channels)) 125 | sys.exit(1) 126 | 127 | def gowav(self, filename, chan): 128 | self.openwav(filename) 129 | while True: 130 | buf = self.readwav(chan) 131 | if buf.size < 1: 132 | break 133 | self.gotsamples(buf, 0) 134 | while self.process(False): 135 | pass 136 | self.process(True) 137 | 138 | def opencard(self, desc): 139 | self.rate = 8000 140 | self.audio = weakaudio.new(desc, self.rate) 141 | # for guess1() / weakutil.freq_from_fft(). 142 | weakutil.init_freq_from_fft(59 * self.rate) 143 | weakutil.fft_sizes([ 59 * self.rate ]) 144 | 145 | def gocard(self): 146 | while True: 147 | [ buf, buf_time ] = self.audio.read() 148 | 149 | if len(buf) > 0: 150 | mx = numpy.max(numpy.abs(buf)) 151 | if mx > 30000: 152 | sys.stderr.write("!") 153 | self.gotsamples(buf, buf_time) 154 | while self.process(False): 155 | pass 156 | else: 157 | time.sleep(0.2) 158 | 159 | def gotsamples(self, buf, time_of_last): 160 | # the band-pass filter. 161 | if self.filter == None: 162 | self.filter = weakutil.butter_bandpass(self.center - self.filterwidth/2, 163 | self.center + self.filterwidth/2, 164 | self.rate, 3) 165 | self.zi = scipy.signal.lfiltic(self.filter[0], 166 | self.filter[1], 167 | [0]) 168 | 169 | zi = scipy.signal.lfilter(self.filter[0], self.filter[1], buf, zi=self.zi) 170 | self.samples = numpy.concatenate((self.samples, zi[0])) 171 | self.zi = zi[1] 172 | 173 | # remember time of self.sample[0] 174 | # XXX off by filter delay 175 | self.samples_time = time_of_last - len(self.samples) / float(self.rate) 176 | 177 | def guess1(self, a, center, width): 178 | fx = weakutil.freq_from_fft(a, self.rate, center - width/2, center + width/2) 179 | return fx 180 | 181 | # guess the frequency of the WWVB carrier. 182 | # only looks +/- 10 hz. 183 | def guess(self): 184 | # apply FFT to abs(samples) then divide by two, 185 | # since psk has no energy at "carrier". 186 | sa = numpy.abs(self.samples) 187 | 188 | n = 0 189 | fx = 0 190 | sz = 59*self.rate 191 | while (n+1)*sz <= len(sa) and (n+1)*sz <= 60*self.rate: 192 | xx = self.guess1(sa[n*sz:(n+1)*sz], 2*self.center, self.searchhz * 2.0) 193 | fx += xx 194 | n += 1 195 | fx /= n 196 | 197 | return fx / 2.0 198 | 199 | # guess what the minute must be for a given sample offset, 200 | # based on past decoded minutes and their sample offsets. 201 | def guessminute(self, offset): 202 | if len(self.timepairs) < 1: 203 | return -1 204 | 205 | offsets = self.timepairs[:,0] 206 | minutes = self.timepairs[:,1] 207 | xx = numpy.subtract(offset, offsets) 208 | xx = numpy.divide(xx, self.rate * 60.0) 209 | guesses = numpy.add(minutes, xx) 210 | m2 = numpy.median(guesses) 211 | 212 | return m2 213 | 214 | # minute m at sample offset passed CRC. 215 | # add it to timepairs. 216 | def addpair(self, offset, m): 217 | self.timepairs = numpy.concatenate((self.timepairs, [[ offset, m ]])) 218 | 219 | # what bits do we expect for this minute? 220 | # return -1, 0, 1 for bit=0, bit=unknown, bit=1. 221 | def guessbits(self, m): 222 | # sync 223 | bits = [ -1, -1, 1, 1, 1, -1, 1, 1, -1, 1, -1, -1, -1 ] 224 | if m >= 0: 225 | # time crc 226 | bits += self.crc(m)[::-1] 227 | # the time 228 | for i in range(25, -1, -1): 229 | if (m & (1 << i)) != 0: 230 | bits += [ 1 ] 231 | else: 232 | bits += [ -1 ] 233 | if i == 25 or i == 16 or i == 7: 234 | bits += [ 0 ] 235 | else: 236 | # don't know the time or CRC. 237 | for i in range(13, 47): 238 | bits += [ 0 ] 239 | # dst/ls and notice XXX DST off 240 | bits += [ -1, 1, 0, -1, -1, -1 ] 241 | # dst XXX 2nd sun of march, 1st sun of nov 242 | bits += [ -1, 1, 1, -1, 1, 1 ] 243 | # final sync bit 244 | bits += [ -1 ] 245 | return bits 246 | 247 | def process(self, eof): 248 | bitsamples = float(self.rate) 249 | if eof: 250 | if len(self.samples) < 65 * bitsamples: 251 | return False 252 | elif self.flywheel > 0 and len(self.timepairs) > 1: 253 | if len(self.samples) < 65 * bitsamples: 254 | return False 255 | else: 256 | if len(self.samples) < 130 * bitsamples: 257 | return False 258 | 259 | if False: 260 | print("saving to aaa.wav, max %.0f" % (numpy.max(self.samples))) 261 | weakutil.writewav(self.samples, "aaa.wav", self.rate) 262 | 263 | # pad at start and end in case samples started late / ended early. 264 | #sm = numpy.mean(self.samples) 265 | #sd = numpy.std(self.samples) 266 | #self.samples = numpy.append(numpy.random.normal(sm, sd, self.rate), self.samples) 267 | #self.samples = numpy.append(self.samples, numpy.random.normal(sm, sd, self.rate)) 268 | 269 | # the main mode of failure seems to be that the 270 | # measured frequency is wrong, perhaps because of 271 | # heterodynes very close to 60 khz. so try 272 | # a few different carrier frequencies. 273 | # a frequency error of 0.008 Hz (in 1000 Hz) is enough to 274 | # shift the phase by 180 degrees by the end of the minute. 275 | # sadly the ECC is weak enough that you can't try too 276 | # many things before getting a falsely correct ECC. 277 | 278 | guessfx = self.guess() 279 | if self.searchfreq: 280 | # we're not sure what the correct frequency is 281 | fx = guessfx 282 | else: 283 | # self.center is the exact frequency to look for 284 | fx = self.center 285 | 286 | # try to decode. 287 | # match is sample number. 288 | # matchscore is -1 or (more or less) the number of matching bits. 289 | # bits is the 60 demodulated bits. 290 | # eccok is True iff the error-correcting code was correct. 291 | # m is 0 or the decoded minute. 292 | (match, matchscore, bits, eccok, m) = self.tryfreq(fx) 293 | 294 | if self.searchfreq and self.flyfreq != None: 295 | (match2, matchscore2, bits2, eccok2, m2) = self.tryfreq(self.flyfreq) 296 | if matchscore2 > matchscore: 297 | # the match at the flywheel frequency was stronger than at 298 | # the guessed frequency. 299 | matchscore = matchscore2 300 | fx = self.flyfreq 301 | match = match2 302 | bits = bits2 303 | eccok = eccok2 304 | m = m2 305 | 306 | if self.searchtime: 307 | guessm = self.guessminute(self.offset + match) 308 | if guessm > 0 and match >= 0 and len(bits) >= 60: 309 | guessbits = self.guessbits(int(round(guessm))) 310 | ngood = bitmatch(guessbits, bits) 311 | else: 312 | ngood = 0 313 | else: 314 | guessm = self.unix2minute(time.time()) 315 | guessbits = self.guessbits(guessm) 316 | if m > 0 and len(bits) >= 60: 317 | ngood = bitmatch(guessbits, bits) 318 | else: 319 | ngood = 0 320 | 321 | matchsec = 0 322 | if match >= 0: 323 | tt = self.samples_time + match / float(self.rate) 324 | ttt = time.gmtime(int(tt)) 325 | matchsec = ttt.tm_sec + (tt - int(tt)) 326 | 327 | # print this minute's results: 328 | # time from UNIX clock. 329 | # time decoded from WWVB. 330 | # audio frequency (nominally 1000 hz). 331 | # time offset (in seconds) between UNIX clock and WWVB signal. 332 | 333 | if match >= 0: 334 | unix = self.samples_time + match / float(self.rate) 335 | unixts = self.ts(unix + 30) 336 | else: 337 | unixts = self.ts(time.time()) 338 | if eccok: 339 | wwvbts = self.ts(self.minute2unix(m)) 340 | else: 341 | wwvbts = "-" 342 | print("%s %s %.3f %.2f" % (unixts, 343 | wwvbts, 344 | guessfx, 345 | matchsec)) 346 | 347 | sys.stdout.flush() 348 | 349 | if match >= 0: 350 | if eccok: 351 | self.flywheel = 5 352 | self.flyfreq = fx 353 | self.addpair(self.offset + match, m) 354 | consume = match + 59*bitsamples 355 | else: 356 | if self.flywheel > 0: 357 | consume = 60*bitsamples 358 | else: 359 | consume = len(self.samples) - 60*bitsamples 360 | if consume > 50*bitsamples: 361 | consume = 50*bitsamples 362 | 363 | consume = int(round(consume)) 364 | self.samples = self.samples[consume:] 365 | self.offset += consume 366 | self.samples_time += consume / float(self.rate) 367 | self.flywheel -= 1 368 | 369 | if False: 370 | sys.stderr.write("quitting after first minute\n") 371 | sys.exit(0) 372 | 373 | return True 374 | 375 | # current UNIX time.time() to real minute of century. 376 | def unix2minute(self, now): 377 | century = time.strptime("1 jan 2000 UTC", "%d %b %Y %Z") 378 | csec = calendar.timegm(century) 379 | mins = (now - csec) / 60 380 | mins -= len(self.samples) / (self.rate * 60.0) 381 | mins = int(round(mins)) 382 | return mins 383 | 384 | # convert WWVB minute of century to UNIX seconds-since-1970. 385 | def minute2unix(self, min): 386 | century = time.strptime("1 jan 2000 UTC", "%d %b %Y %Z") 387 | csec = calendar.timegm(century) 388 | csec += min * 60 389 | return csec 390 | 391 | # turn UNIX seconds into UTC hh:mm for printing. 392 | def ts(self, now): 393 | tm = time.gmtime(int(now)) 394 | s = time.strftime("%H:%M", tm) 395 | return s 396 | 397 | # try to decode, given a guess at the carrier audio frequency. 398 | def tryfreq(self, fx): 399 | bitsamples = float(self.rate) 400 | secondspersample = 1.0 / float(self.rate) 401 | cyclespersample = secondspersample * fx 402 | phasepersample = cyclespersample * 2.0 * numpy.pi 403 | 404 | # synthetic carrier at frequency fx. 405 | i0 = 0 406 | i1 = len(self.samples) 407 | n = i1 - i0 408 | ph0 = i0 * phasepersample 409 | ph1 = ph0 + n * phasepersample 410 | qq = numpy.pi / 2.0 411 | ref = numpy.sin(numpy.linspace(ph0, ph1 - phasepersample, n)) 412 | refq = numpy.sin(numpy.linspace(ph0 + qq, ph1 - phasepersample + qq, n)) 413 | 414 | # product of carrier and signal. 415 | # this yields a steady-ish > 0 when in phase, 416 | # and < 0 when out of phase. 417 | prod = numpy.multiply(self.samples, ref) 418 | prodq = numpy.multiply(self.samples, refq) 419 | smoothwindow = int(round(bitsamples)) 420 | prod = smooth(prod, smoothwindow) 421 | prodq = smooth(prodq, smoothwindow) 422 | if numpy.mean(numpy.abs(prodq)) > numpy.mean(numpy.abs(prod)): 423 | # 90 degree shift of carrier gave stronger product 424 | prod = prodq 425 | 426 | # indices of samples that are just before each 427 | # zero crossing in either direction. 428 | # this finds bit boundaries. 429 | zc = numpy.where(numpy.diff(numpy.sign(prod)))[0] 430 | za = [ ] 431 | for zz in zc: 432 | # reference to start 433 | bn = zz / bitsamples 434 | ri = (bn - int(bn)) * bitsamples 435 | za.append(ri) 436 | if len(za) < 1: 437 | starti = 0 438 | else: 439 | starti = numpy.median(za) # good start of bit 440 | samples_starti = starti + int(smoothwindow/2) 441 | midi = starti + (bitsamples / 2) # good start of bit 442 | 443 | # decode bits. may have zero and one reversed; will 444 | # fix after we find the sync sequence. 445 | bits = [ ] 446 | i = midi 447 | while round(i) < len(prod): 448 | if prod[int(round(i))] > 0: 449 | bb = -1 450 | else: 451 | bb = 1 452 | bits += [ bb ] 453 | i += bitsamples 454 | 455 | # do we trust guess at where minute starts and 456 | # what the current time is? 457 | flying = (self.flywheel > 0) and (len(self.timepairs) > 1) 458 | 459 | # generate a set of known bits to look for, 460 | # including current time+CRC if known. 461 | if self.searchtime == False: 462 | # look for current real time 463 | pat = self.guessbits(self.unix2minute(time.time())) 464 | elif flying: 465 | # we probably know the current time bits to expect. 466 | guessm = int(round(self.guessminute(self.offset))) 467 | pat = self.guessbits(guessm) 468 | else: 469 | # we may not know the current time. 470 | pat = self.guessbits(-1) 471 | 472 | # also an inverted copy of the search pattern. 473 | patr = invert(pat) 474 | 475 | # look for offset of sync sequence &c in bits[]. 476 | if self.searchtime == False or flying or len(bits) >= 120: 477 | # there must be a match in these samples, either because 478 | # flywheel says a minute starts at bits[1], or becuse 479 | # we have two entire minutes of bits. 480 | if self.searchtime == False: 481 | # real-time samples, so we know where minute started. 482 | tt = self.samples_time + samples_starti / float(self.rate) 483 | tti = int(round(tt)) 484 | ttt = time.gmtime(tti) 485 | # samples[0] arived at ttt.tm_sec -- second within minute 486 | i0 = 60 - ttt.tm_sec 487 | if i0 >= 60: 488 | i0 -= 60 489 | if i0 < 0: 490 | print("oops i0 %d" % (i0)) 491 | i0 = 0 492 | i1 = i0 + 1 493 | # print "starti %d tt %.3f tm_sec %d i0 %d" % (starti, tt, ttt.tm_sec, i0) 494 | elif flying: 495 | # we expect minute to start at bits[1], but 496 | # might have gradually slipped over time. 497 | i0 = 0 498 | i1 = 3 499 | else: 500 | # we don't know where second should start. 501 | i0 = 0 502 | i1 = len(bits) - 60 503 | match = -1 504 | matchscore = -1 505 | matchrev = False 506 | matchecc = False 507 | for i in range(i0, i1): 508 | score1 = bitmatch(pat, bits[i:]) 509 | if score1 > matchscore: 510 | matchscore = score1 511 | match = i 512 | matchrev = False 513 | (matchecc, xm) = self.decode(bits[match:match+60]) 514 | if matchecc: 515 | matchscore += 10 516 | score2 = bitmatch(patr, bits[i:]) 517 | if score2 > matchscore: 518 | matchscore = score2 519 | match = i 520 | matchrev = True 521 | xbits = bits[:] 522 | for j in range(0, len(xbits)): 523 | xbits[j] *= -1 524 | (matchecc, xm) = self.decode(xbits[match:match+60]) 525 | if matchecc: 526 | matchscore += 10 527 | if matchrev: 528 | for j in range(0, len(bits)): 529 | bits[j] *= -1 530 | else: 531 | # we don't know where second should start. 532 | match = -1 533 | i = 0 534 | while match < 0 and i <= len(bits) - 60: 535 | if bits[i:i+13] == pat: 536 | match = i 537 | if bits[i:i+13] == patr: 538 | for j in range(0, len(bits)): 539 | bits[j] *= -1 540 | match = i 541 | i += 1 542 | 543 | if match >= 0: 544 | (eccok, m) = self.decode(bits[match:match+60]) 545 | return (samples_starti + match*self.rate, matchscore, bits[match:match+60], eccok, m) 546 | else: 547 | return (samples_starti + match*self.rate, -1, bits, False, 0) 548 | 549 | # return CRC for a given minute. 550 | # returns the five CRC bits in an array, as -1 or 1. 551 | def crc(self, m): 552 | tm = [] 553 | for i in range(0, 26): 554 | if m & (1 << i): 555 | tm.append(1) 556 | else: 557 | tm.append(0) 558 | 559 | # calculate expected hamming ECC code on time 560 | p = [ 0, 0, 0, 0, 0 ] 561 | p[0] = xsum(tm, [ 23, 21, 20, 17, 16, 15, 14, 13, 9, 8, 6, 5, 4, 2, 0 ]) 562 | p[1] = xsum(tm, [ 24, 22, 21, 18, 17, 16, 15, 14, 10, 9, 7, 6, 5, 3, 1 ]) 563 | p[2] = xsum(tm, [ 25, 23, 22, 19, 18, 17, 16, 15, 11, 10, 8, 7, 6, 4, 2 ]) 564 | p[3] = xsum(tm, [ 24, 21, 19, 18, 15, 14, 13, 12, 11, 7, 6, 4, 3, 2, 0 ]) 565 | p[4] = xsum(tm, [ 25, 22, 20, 19, 16, 15, 14, 13, 12, 8, 7, 5, 4, 3, 1 ]) 566 | 567 | return p 568 | 569 | def decode(self, bits): 570 | if len(bits) < 60: 571 | return ( False, 0 ) 572 | if self.debug: 573 | sys.stdout.write("sync: ") 574 | for i in range(0, 13): 575 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 576 | sys.stdout.write("\n") 577 | 578 | sys.stdout.write("ecc: ") 579 | for i in range(13, 18): 580 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 581 | sys.stdout.write("\n") 582 | 583 | sys.stdout.write("time: ") 584 | for i in range(18, 47): 585 | if i == 19 or i == 29 or i == 39: 586 | sys.stdout.write("(%d) " % ((bits[i]+1)/2)) 587 | else: 588 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 589 | sys.stdout.write("\n") 590 | 591 | sys.stdout.write("dst/ls: ") 592 | for i in range(47, 53): 593 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 594 | sys.stdout.write("\n") 595 | 596 | sys.stdout.write("dst: ") 597 | for i in range(53, 59): 598 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 599 | sys.stdout.write("\n") 600 | 601 | sys.stdout.write("end: ") 602 | for i in range(59, 60): 603 | sys.stdout.write("%d " % ((bits[i]+1)/2)) 604 | sys.stdout.write("\n") 605 | 606 | # binary minute of century 607 | m = 0 608 | e = 25 609 | i = 18 610 | while i < 47: 611 | if bits[i] > 0: 612 | m += 2**e 613 | i += 1 614 | e -= 1 615 | if i == 19 or i == 29 or i == 39: 616 | i += 1 617 | 618 | p = self.crc(m) 619 | 620 | if m != 0 and p[4] == bits[13] and p[3] == bits[14] and p[2] == bits[15] and p[1] == bits[16] and p[0] == bits[17]: 621 | eccok = True 622 | else: 623 | eccok = False 624 | 625 | return (eccok, m) 626 | 627 | def main(): 628 | parser = weakargs.stdparse('Decode phase-shift WWVB.') 629 | parser.add_argument("-center", metavar='Hz', default=1000.0, type=float) 630 | parser.add_argument("-file") 631 | args = weakargs.parse_args(parser) 632 | 633 | if args.cat != None: 634 | cat = weakcat.open(args.cat) 635 | cat.set_usb_data() 636 | cat.setf(0, 59000) 637 | 638 | if (args.card == None) == (args.file == None): 639 | parser.error("one of -card and -file are required") 640 | 641 | if args.file != None: 642 | r = WWVB() 643 | r.center = args.center 644 | r.gowav(args.file, 0) 645 | sys.exit(0) 646 | 647 | if args.card != None: 648 | r = WWVB() 649 | r.center = args.center 650 | r.opencard(args.card) 651 | r.gocard() 652 | sys.exit(0) 653 | 654 | parser.error("one of -card, -file, or -levels is required") 655 | 656 | sys.exit(1) 657 | 658 | main() 659 | --------------------------------------------------------------------------------