Exemple #1
0
def new_kiwiworker(o, band_hops_str, idx):
    options = copy(o)

    def _extract_band(band_hops_str):
        local = str(band_hops_str).split('|')
        # ['FT8', 'FT4', 'FT8', 'FT8']
        mode_hops = [
            config.MODES[b[-1]] if b[-1] in config.MODES else "FT8"
            for b in local
        ]
        # ['20', '30', '40', '60']
        band_hops = [
            b[:-1] if b[-1] not in [str(n) for n in range(0, 9)] else b
            for b in local
        ]
        # [14074, 10140, 7074, 5357] in KHz
        freq_hops = [
            config.BANDS[mode_hops[i]][b] * 1000
            for i, b in enumerate(band_hops)
        ]
        return mode_hops, band_hops, freq_hops

    options.band_hops_str = band_hops_str
    options.mode_hops, options.band_hops, options.freq_hops = _extract_band(
        band_hops_str)
    options.idx = idx
    options.timestamp = int(time.time() + os.getpid() + idx) & 0xffffffff
    # tmp dirs preparation
    for i, mode in enumerate(options.mode_hops):
        dir = os.path.join(Config.tmpdir(), options.station, mode,
                           options.band_hops[i])
        if not os.path.isdir(dir):
            os.makedirs(dir, exist_ok=True)
        else:
            os.popen("rm -f %s/*.wav" % dir)

    worker = KiwiWorker(target=WsjtSoundRecorder(options),
                        name="%s-%s" %
                        (options.station, options.band_hops_str))

    return worker
Exemple #2
0
def main():
    # extend the OptionParser so that we can print multiple paragraphs in
    # the help text
    class MyParser(OptionParser):
        def format_description(self, formatter):
            result = []
            for paragraph in self.description:
                result.append(formatter.format_description(paragraph))
            return "\n".join(result[:-1])  # drop last \n

        def format_epilog(self, formatter):
            result = []
            for paragraph in self.epilog:
                result.append(formatter.format_epilog(paragraph))
            return "".join(result)

    usage = "%prog -s SERVER -p PORT -f FREQ -m MODE [other options]"
    description = [
        "kiwirecorder.py records data from one or more KiwiSDRs to your disk."
        " It takes a number of options as inputs, the most basic of which"
        " are shown above.",
        "To record data from multiple Kiwis at once, use the same syntax,"
        " but pass a list of values (where applicable) instead of a single value."
        " Each list of values should be comma-separated and without spaces."
        " For instance, to record one Kiwi at localhost on port 80, and another Kiwi"
        " at example.com port 8073, run the following:",
        "    kiwirecorder.py -s localhost,example.com -p 80,8073 -f 10000,10000 -m am",
        "In this example, both Kiwis will record on 10,000 kHz (10 MHz) in AM mode."
        " Any option that states \"can be a comma-separated list\" also means a single"
        " value will be duplicated across multiple connection. In the above example"
        " the simpler \"-f 10000\" can been used.", ""
    ]
    epilog = []  # text here would go after the options list
    parser = MyParser(usage=usage, description=description, epilog=epilog)
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host (can be a comma-separated list)',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '-p',
        '--server-port',
        dest='server_port',
        type='string',
        default=8073,
        help='Server port, default 8073 (can be a comma-separated list)',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pw',
        '--password',
        dest='password',
        type='string',
        default='',
        help='Kiwi login password (if required, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit-pw',
        '--tlimit-password',
        dest='tlimit_password',
        type='string',
        default='',
        help=
        'Connect time limit exemption password (if required, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '-u',
        '--user',
        dest='user',
        type='string',
        default='kiwirecorder.py',
        help='Kiwi connection user name (can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--station',
        dest='station',
        type='string',
        default=None,
        help=
        'Station ID to be appended to filename (can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    parser.add_option('-q',
                      '--quiet',
                      dest='quiet',
                      default=False,
                      action='store_true',
                      help='Don\'t print progress messages')
    parser.add_option('-d',
                      '--dir',
                      dest='dir',
                      type='string',
                      default=None,
                      help='Optional destination directory for files')
    parser.add_option(
        '--fn',
        '--filename',
        dest='filename',
        type='string',
        default='',
        help=
        'Use fixed filename instead of generated filenames (optional station ID(s) will apply, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit',
        '--time-limit',
        dest='tlimit',
        type='float',
        default=None,
        help='Record time limit in seconds. Ignored when --dt-sec used.')
    parser.add_option('--dt-sec',
                      dest='dt',
                      type='int',
                      default=0,
                      help='Start a new file when mod(sec_of_day,dt) == 0')
    parser.add_option('--launch-delay',
                      '--launch_delay',
                      dest='launch_delay',
                      type='int',
                      default=0,
                      help='Delay (secs) in launching multiple connections')
    parser.add_option(
        '--connect-retries',
        '--connect_retries',
        dest='connect_retries',
        type='int',
        default=0,
        help=
        'Number of retries when connecting to host (retries forever by default)'
    )
    parser.add_option('--connect-timeout',
                      '--connect_timeout',
                      dest='connect_timeout',
                      type='int',
                      default=15,
                      help='Retry timeout(sec) connecting to host')
    parser.add_option('-k',
                      '--socket-timeout',
                      '--socket_timeout',
                      dest='socket_timeout',
                      type='int',
                      default=10,
                      help='Socket timeout(sec) during data transfers')
    parser.add_option(
        '--OV',
        dest='ADC_OV',
        default=False,
        action='store_true',
        help='Print "ADC OV" message when Kiwi ADC is overloaded')
    parser.add_option(
        '--ts',
        '--tstamp',
        '--timestamp',
        dest='tstamp',
        default=False,
        action='store_true',
        help='Add timestamps to output. Applies only to S-meter mode currently.'
    )
    parser.add_option(
        '--stats',
        dest='stats',
        default=False,
        action='store_true',
        help=
        'Print additional statistics. Applies only to S-meter mode currently.')
    parser.add_option(
        '--no-api',
        dest='no_api',
        default=False,
        action='store_true',
        help='Simulate connection to Kiwi using improper/incomplete API')
    parser.add_option('-v',
                      '-V',
                      '--version',
                      dest='krec_version',
                      default=False,
                      action='store_true',
                      help='Print version number and exit')

    group = OptionGroup(parser, "Audio connection options", "")
    group.add_option(
        '-f',
        '--freq',
        dest='frequency',
        type='string',
        default=1000,
        help='Frequency to tune to, in kHz (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option(
        '-m',
        '--modulation',
        dest='modulation',
        type='string',
        default='am',
        help=
        'Modulation; one of am, lsb, usb, cw, nbfm, iq (default passband if -L/-H not specified)'
    )
    group.add_option('--ncomp',
                     '--no_compression',
                     dest='compression',
                     default=True,
                     action='store_false',
                     help='Don\'t use audio compression')
    group.add_option('-L',
                     '--lp-cutoff',
                     dest='lp_cut',
                     type='float',
                     default=None,
                     help='Low-pass cutoff frequency, in Hz')
    group.add_option('-H',
                     '--hp-cutoff',
                     dest='hp_cut',
                     type='float',
                     default=None,
                     help='High-pass cutoff frequency, in Hz')
    group.add_option(
        '-r',
        '--resample',
        dest='resample',
        type='int',
        default=0,
        help=
        'Resample output file to new sample rate in Hz. The resampling ratio has to be in the range [1/256,256]'
    )
    group.add_option('-T',
                     '--squelch-threshold',
                     dest='thresh',
                     type='float',
                     default=None,
                     help='Squelch threshold, in dB.')
    group.add_option(
        '--squelch-tail',
        dest='squelch_tail',
        type='float',
        default=1,
        help=
        'Time for which the squelch remains open after the signal is below threshold.'
    )
    group.add_option(
        '-g',
        '--agc-gain',
        dest='agc_gain',
        type='string',
        default=None,
        help=
        'AGC gain; if set, AGC is turned off (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option('--agc-yaml',
                     dest='agc_yaml_file',
                     type='string',
                     default=None,
                     help='AGC options provided in a YAML-formatted file')
    group.add_option('--nb',
                     dest='nb',
                     action='store_true',
                     default=False,
                     help='Enable noise blanker with default parameters.')
    group.add_option(
        '--nb-gate',
        dest='nb_gate',
        type='int',
        default=100,
        help='Noise blanker gate time in usec (100 to 5000, default 100)')
    group.add_option(
        '--nb-th',
        '--nb-thresh',
        dest='nb_thresh',
        type='int',
        default=50,
        help='Noise blanker threshold in percent (0 to 100, default 50)')
    group.add_option(
        '-w',
        '--kiwi-wav',
        dest='is_kiwi_wav',
        default=False,
        action='store_true',
        help=
        'In the wav file include KIWI header containing GPS time-stamps (only for IQ mode)'
    )
    group.add_option('--kiwi-tdoa',
                     dest='is_kiwi_tdoa',
                     default=False,
                     action='store_true',
                     help='Used when called by Kiwi TDoA extension')
    group.add_option(
        '--test-mode',
        dest='test_mode',
        default=False,
        action='store_true',
        help='Write wav data to /dev/null (Linux) or NUL (Windows)')
    group.add_option(
        '--snd',
        '--sound',
        dest='sound',
        default=False,
        action='store_true',
        help=
        'Also process sound data when in waterfall or S-meter mode (sound connection options above apply)'
    )
    parser.add_option_group(group)

    group = OptionGroup(parser, "S-meter mode options", "")
    group.add_option(
        '--S-meter',
        '--s-meter',
        dest='S_meter',
        type='int',
        default=-1,
        help=
        'Report S-meter (RSSI) value after S_METER number of averages. S_METER=0 does no averaging and reports each RSSI value received. Options --ts and --stats apply.'
    )
    group.add_option('--sdt-sec',
                     dest='sdt',
                     type='int',
                     default=0,
                     help='S-meter measurement interval')
    parser.add_option_group(group)

    group = OptionGroup(parser, "Waterfall connection options", "")
    group.add_option('--wf',
                     dest='waterfall',
                     default=False,
                     action='store_true',
                     help='Process waterfall data instead of audio')
    group.add_option('-z',
                     '--zoom',
                     dest='zoom',
                     type='int',
                     default=0,
                     help='Zoom level 0-14')
    parser.add_option_group(group)

    (options, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    if options.krec_version:
        print('kiwirecorder v1.0')
        sys.exit()

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
                        format=FORMAT)
    if options.log_level.upper() == 'DEBUG':
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    if options.S_meter >= 0:
        if options.S_meter > 0 and options.sdt != 0:
            raise Exception(
                'Options --S-meter > 0 and --sdt-sec != 0 are incompatible. Did you mean to use --S-meter=0 ?'
            )
        options.quiet = True

    if options.tlimit is not None and options.dt != 0:
        print('Warning: --tlimit ignored when --dt-sec option used')

    ### decode AGC YAML file options
    options.agc_yaml = None
    if options.agc_yaml_file:
        try:
            if not HAS_PyYAML:
                raise Exception(
                    'PyYAML not installed: sudo apt install python-yaml / sudo apt install python3-yaml / pip install pyyaml / pip3 install pyyaml'
                )
            with open(options.agc_yaml_file) as yaml_file:
                documents = yaml.full_load(yaml_file)
                logging.debug('AGC file %s: %s' %
                              (options.agc_yaml_file, documents))
                logging.debug('Got AGC paramteres from file %s: %s' %
                              (options.agc_yaml_file, documents['AGC']))
                options.agc_yaml = documents['AGC']
        except KeyError:
            logging.fatal('The YAML file does not contain AGC options')
            return
        except Exception as e:
            logging.fatal(e)
            return

    options.raw = False
    options.rigctl_enabled = False
    gopt = options
    multiple_connections, options = options_cross_product(options)

    snd_recorders = []
    if not gopt.waterfall or (gopt.waterfall and gopt.sound):
        for i, opt in enumerate(options):
            opt.multiple_connections = multiple_connections
            opt.idx = i
            snd_recorders.append(
                KiwiWorker(args=(KiwiSoundRecorder(opt), opt, run_event)))

    wf_recorders = []
    if gopt.waterfall:
        for i, opt in enumerate(options):
            opt.multiple_connections = multiple_connections
            opt.idx = i
            wf_recorders.append(
                KiwiWorker(args=(KiwiWaterfallRecorder(opt), opt, run_event)))

    try:
        for i, r in enumerate(snd_recorders):
            if opt.launch_delay != 0 and i != 0 and options[
                    i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            #logging.info("started sound recorder %d, timestamp=%d" % (i, options[i].timestamp))
            logging.info("started sound recorder %d" % i)

        for i, r in enumerate(wf_recorders):
            if i != 0 and options[i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            logging.info("started waterfall recorder %d" % i)

        while run_event.is_set():
            time.sleep(.1)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(snd_recorders, wf_recorders)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception as e:
        print_exc()
        run_event.clear()
        join_threads(snd_recorders, wf_recorders)
        print("Exception: threads successfully closed")

    if gopt.is_kiwi_tdoa:
        for i, opt in enumerate(options):
            # NB: MUST be a print (i.e. not a logging.info)
            print("status=%d,%d" % (i, opt.status))

    logging.debug('gc %s' % gc.garbage)
Exemple #3
0
def main():
    # extend the OptionParser so that we can print multiple paragraphs in
    # the help text
    class MyParser(OptionParser):
        def format_description(self, formatter):
            result = []
            for paragraph in self.description:
                result.append(formatter.format_description(paragraph))
            return "\n".join(result[:-1])  # drop last \n

        def format_epilog(self, formatter):
            result = []
            for paragraph in self.epilog:
                result.append(formatter.format_epilog(paragraph))
            return "".join(result)

    usage = "%prog -s SERVER -p PORT -f FREQ -m MODE [other options]"
    description = [
        "kiwiclientd.py receives audio from a KiwiSDR and plays"
        " it to a (virtual) sound device. This can be used to"
        " send KiwiSDR audio to various programs to decode the"
        " received signals."
        " This program also accepts hamlib rigctl commands over"
        " a network socket to change the kiwisdr frequency"
        " To stream multiple KiwiSDR channels at once, use the"
        " same syntax, but pass a list of values (where applicable)"
        " instead of single values. For example, to stream"
        " two KiwiSDR channels in USB to the virtual sound cards"
        " kiwisdr0 & kiwisdr1, with the rigctl ports 6400 &"
        " 6401 respectively, run the following:",
        "$ kiwiclientd.py -s kiwisdr.example.com -p 8073 -f 10000 -m usb --snddev kiwisnd0,kiwisnd1 --rigctl-port 6400,6401",
        ""
    ]
    epilog = []  # text here would go after the options list
    parser = MyParser(usage=usage, description=description, epilog=epilog)
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host (can be a comma-separated list)',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '-p',
        '--server-port',
        dest='server_port',
        type='string',
        default=8073,
        help='Server port, default 8073 (can be a comma-separated list)',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pw',
        '--password',
        dest='password',
        type='string',
        default='',
        help='Kiwi login password (if required, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit-pw',
        '--tlimit-password',
        dest='tlimit_password',
        type='string',
        default='',
        help=
        'Connect time limit exemption password (if required, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '-u',
        '--user',
        dest='user',
        type='string',
        default='kiwiclientd',
        help='Kiwi connection user name (can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    parser.add_option('-q',
                      '--quiet',
                      dest='quiet',
                      default=False,
                      action='store_true',
                      help='Don\'t print progress messages')
    parser.add_option(
        '--tlimit',
        '--time-limit',
        dest='tlimit',
        type='float',
        default=None,
        help='Record time limit in seconds. Ignored when --dt-sec used.')
    parser.add_option('--launch-delay',
                      '--launch_delay',
                      dest='launch_delay',
                      type='int',
                      default=0,
                      help='Delay (secs) in launching multiple connections')
    parser.add_option(
        '--connect-retries',
        '--connect_retries',
        dest='connect_retries',
        type='int',
        default=0,
        help=
        'Number of retries when connecting to host (retries forever by default)'
    )
    parser.add_option('--connect-timeout',
                      '--connect_timeout',
                      dest='connect_timeout',
                      type='int',
                      default=15,
                      help='Retry timeout(sec) connecting to host')
    parser.add_option('-k',
                      '--socket-timeout',
                      '--socket_timeout',
                      dest='socket_timeout',
                      type='int',
                      default=10,
                      help='Socket timeout(sec) during data transfers')
    parser.add_option(
        '--OV',
        dest='ADC_OV',
        default=False,
        action='store_true',
        help='Print "ADC OV" message when Kiwi ADC is overloaded')
    parser.add_option('-v',
                      '-V',
                      '--version',
                      dest='krec_version',
                      default=False,
                      action='store_true',
                      help='Print version number and exit')

    group = OptionGroup(parser, "Audio connection options", "")
    group.add_option(
        '-f',
        '--freq',
        dest='frequency',
        type='string',
        default=1000,
        help='Frequency to tune to, in kHz (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option(
        '-m',
        '--modulation',
        dest='modulation',
        type='string',
        default='am',
        help=
        'Modulation; one of am, lsb, usb, cw, nbfm, iq (default passband if -L/-H not specified)'
    )
    group.add_option('--ncomp',
                     '--no_compression',
                     dest='compression',
                     default=True,
                     action='store_false',
                     help='Don\'t use audio compression')
    group.add_option('-L',
                     '--lp-cutoff',
                     dest='lp_cut',
                     type='float',
                     default=None,
                     help='Low-pass cutoff frequency, in Hz')
    group.add_option('-H',
                     '--hp-cutoff',
                     dest='hp_cut',
                     type='float',
                     default=None,
                     help='High-pass cutoff frequency, in Hz')
    group.add_option(
        '-r',
        '--resample',
        dest='resample',
        type='int',
        default=0,
        help=
        'Resample output file to new sample rate in Hz. The resampling ratio has to be in the range [1/256,256]'
    )
    group.add_option('-T',
                     '--squelch-threshold',
                     dest='thresh',
                     type='float',
                     default=None,
                     help='Squelch threshold, in dB.')
    group.add_option(
        '--squelch-tail',
        dest='squelch_tail',
        type='float',
        default=1,
        help=
        'Time for which the squelch remains open after the signal is below threshold.'
    )
    group.add_option(
        '-g',
        '--agc-gain',
        dest='agc_gain',
        type='string',
        default=None,
        help=
        'AGC gain; if set, AGC is turned off (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option('--nb',
                     dest='nb',
                     action='store_true',
                     default=False,
                     help='Enable noise blanker with default parameters.')
    group.add_option(
        '--nb-gate',
        dest='nb_gate',
        type='int',
        default=100,
        help='Noise blanker gate time in usec (100 to 5000, default 100)')
    group.add_option(
        '--nb-th',
        '--nb-thresh',
        dest='nb_thresh',
        type='int',
        default=50,
        help='Noise blanker threshold in percent (0 to 100, default 50)')
    parser.add_option_group(group)

    group = OptionGroup(parser, "Sound device options", "")
    group.add_option(
        '--snddev',
        '--sound-device',
        dest='sounddevice',
        type='string',
        default='',
        action='callback',
        help='Sound device to play kiwi audio on (can be comma separated list)',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    group.add_option('--ls-snd',
                     '--list-sound-devices',
                     dest='list_sound_devices',
                     default=False,
                     action='store_true',
                     help='List available sound devices and exit')
    parser.add_option_group(group)

    group = OptionGroup(parser, "Rig control options", "")
    group.add_option('--rigctl',
                     '--enable-rigctl',
                     dest='rigctl_enabled',
                     default=True,
                     action='store_true',
                     help='Enable rigctld backend for frequency changes.')
    group.add_option(
        '--rigctl-port',
        '--rigctl-port',
        dest='rigctl_port',
        type='string',
        default='6400',
        help=
        'Port listening for rigctl commands (default 6400, can be comma separated list',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    group.add_option('--rigctl-addr',
                     '--rigctl-address',
                     dest='rigctl_address',
                     type='string',
                     default=None,
                     help='Address to listen on (default 127.0.0.1)')
    parser.add_option_group(group)

    (options, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    if options.krec_version:
        print('kiwiclientd v1.0')
        sys.exit()

    if options.list_sound_devices:
        print(sc.all_speakers())
        sys.exit()

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
                        format=FORMAT)
    if options.log_level.upper() == 'DEBUG':
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    if options.tlimit is not None and options.dt != 0:
        print('Warning: --tlimit ignored when --dt-sec option used')

    options.sdt = 0
    options.dir = None
    options.raw = False
    options.sound = True
    options.no_api = False
    options.tstamp = False
    options.station = None
    options.filename = None
    options.test_mode = False
    options.is_kiwi_wav = False
    options.is_kiwi_tdoa = False
    gopt = options
    multiple_connections, options = options_cross_product(options)

    snd_recorders = []
    for i, opt in enumerate(options):
        opt.multiple_connections = multiple_connections
        opt.idx = i
        snd_recorders.append(
            KiwiWorker(args=(KiwiSoundRecorder(opt), opt, run_event)))

    try:
        for i, r in enumerate(snd_recorders):
            if opt.launch_delay != 0 and i != 0 and options[
                    i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            #logging.info("started kiwi client %d, timestamp=%d" % (i, options[i].timestamp))
            logging.info("started kiwi client %d" % i)

        while run_event.is_set():
            time.sleep(.1)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(snd_recorders)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception as e:
        print_exc()
        run_event.clear()
        join_threads(snd_recorders)
        print("Exception: threads successfully closed")

    logging.debug('gc %s' % gc.garbage)
def main():
    parser = OptionParser()
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host')
    parser.add_option('-p',
                      '--server-port',
                      dest='server_port',
                      type='string',
                      default="8073",
                      help='Server port, default 8073')
    parser.add_option('--pw',
                      '--password',
                      dest='password',
                      type='string',
                      default='',
                      help='Kiwi login password')
    parser.add_option('-u',
                      '--user',
                      dest='user',
                      type='string',
                      default='kiwirecorder.py',
                      help='Kiwi connection user name')
    parser.add_option('-f',
                      '--freq',
                      dest='frequency',
                      type='float',
                      default=1000,
                      help='Frequency to tune to, in kHz')
    parser.add_option('-z',
                      '--zoom',
                      dest='zoom',
                      type='int',
                      default=0,
                      help='Zoom level 0-14')
    parser.add_option(
        '--station',
        dest='station',
        type='string',
        default=None,
        help='Station ID to be appended to filename',
    )
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    (opt, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    opt.is_kiwi_tdoa = False
    opt.socket_timeout = 10
    opt.tlimit = None
    opt.no_api = True
    opt.raw = False
    opt.S_meter = -1
    opt.ADC_OV = None

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(opt.log_level.upper()),
                        format=FORMAT)
    if opt.log_level.upper() == 'DEBUG':
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    snd_queue, wf_queue = [Queue(), Queue()]
    snd_recorder = KiwiWorker(args=(KiwiSoundRecorder(opt, snd_queue), opt,
                                    run_event))
    wf_recorder = KiwiWorker(args=(KiwiWaterfallRecorder(opt, wf_queue), opt,
                                   run_event))
    consumer = Consumer(args=(opt, snd_queue, wf_queue, run_event))

    threads = [snd_recorder, wf_recorder, consumer]

    try:
        opt.start_time = time.time()
        opt.tstamp = int(time.time() + os.getpid()) & 0xffffffff
        opt.idx = 0
        snd_recorder.start()

        opt.start_time = time.time()
        opt.tstamp = int(time.time() + os.getpid() + 1) & 0xffffffff
        opt.idx = 1
        wf_recorder.start()

        consumer.start()

        while run_event.is_set():
            time.sleep(.5)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(threads)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception:
        print_exc()
        run_event.clear()
        join_threads(threads)
        print("Exception: threads successfully closed")

    logging.debug('gc %s' % gc.garbage)
Exemple #5
0
def main():
    parser = OptionParser()
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host (can be a comma-delimited list)',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '-p',
        '--server-port',
        dest='server_port',
        type='string',
        default=8073,
        help='Server port, default 8073 (can be a comma delimited list)',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pw',
        '--password',
        dest='password',
        type='string',
        default='',
        help='Kiwi login password (if required, can be a comma delimited list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit-pw',
        '--tlimit-password',
        dest='tlimit_password',
        type='string',
        default='',
        help=
        'Connect time limit exemption password (if required, can be a comma delimited list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option('-u',
                      '--user',
                      dest='user',
                      type='string',
                      default='kiwirecorder.py',
                      help='Kiwi connection user name',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '--station',
        dest='station',
        type='string',
        default=None,
        help=
        'Station ID to be appended to filename (can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    parser.add_option('-q',
                      '--quiet',
                      dest='quiet',
                      default=False,
                      action='store_true',
                      help='Don\'t print progress messages')
    parser.add_option('-d',
                      '--dir',
                      dest='dir',
                      type='string',
                      default=None,
                      help='Optional destination directory for files')
    parser.add_option(
        '--fn',
        '--filename',
        dest='filename',
        type='string',
        default='',
        help=
        'Use fixed filename instead of generated filenames (optional station ID(s) will apply)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit',
        '--time-limit',
        dest='tlimit',
        type='float',
        default=None,
        help='Record time limit in seconds. Ignored when --dt-sec used.')
    parser.add_option('--dt-sec',
                      dest='dt',
                      type='int',
                      default=0,
                      help='Start a new file when mod(sec_of_day,dt) == 0')
    parser.add_option('--launch-delay',
                      '--launch_delay',
                      dest='launch_delay',
                      type='int',
                      default=0,
                      help='Delay (secs) in launching multiple connections')
    parser.add_option(
        '--connect-retries',
        '--connect_retries',
        dest='connect_retries',
        type='int',
        default=0,
        help=
        'Number of retries when connecting to host (retries forever by default)'
    )
    parser.add_option('--connect-timeout',
                      '--connect_timeout',
                      dest='connect_timeout',
                      type='int',
                      default=15,
                      help='Retry timeout(sec) connecting to host')
    parser.add_option('-k',
                      '--socket-timeout',
                      '--socket_timeout',
                      dest='socket_timeout',
                      type='int',
                      default=10,
                      help='Socket timeout(sec) during data transfers')
    parser.add_option(
        '--OV',
        dest='ADC_OV',
        default=False,
        action='store_true',
        help='Print "ADC OV" message when Kiwi ADC is overloaded')
    parser.add_option(
        '--ts',
        '--tstamp',
        '--timestamp',
        dest='tstamp',
        default=False,
        action='store_true',
        help='Add timestamps to output. Applies only to S-meter mode currently.'
    )
    parser.add_option(
        '--stats',
        dest='stats',
        default=False,
        action='store_true',
        help=
        'Print additional statistics. Applies only to S-meter mode currently.')
    parser.add_option(
        '--no-api',
        dest='no_api',
        default=False,
        action='store_true',
        help='Simulate connection to Kiwi using improper/incomplete API')

    group = OptionGroup(parser, "Audio connection options", "")
    group.add_option(
        '-f',
        '--freq',
        dest='frequency',
        type='string',
        default=1000,
        help='Frequency to tune to, in kHz (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option(
        '-m',
        '--modulation',
        dest='modulation',
        type='string',
        default='am',
        help=
        'Modulation; one of am, lsb, usb, cw, nbfm, iq (default passband if -L/-H not specified)'
    )
    group.add_option('--ncomp',
                     '--no_compression',
                     dest='compression',
                     default=True,
                     action='store_false',
                     help='Don\'t use audio compression')
    group.add_option('-L',
                     '--lp-cutoff',
                     dest='lp_cut',
                     type='float',
                     default=None,
                     help='Low-pass cutoff frequency, in Hz')
    group.add_option('-H',
                     '--hp-cutoff',
                     dest='hp_cut',
                     type='float',
                     default=None,
                     help='High-pass cutoff frequency, in Hz')
    group.add_option(
        '-r',
        '--resample',
        dest='resample',
        type='int',
        default=0,
        help=
        'Resample output file to new sample rate in Hz. The resampling ratio has to be in the range [1/256,256]'
    )
    group.add_option('-T',
                     '--squelch-threshold',
                     dest='thresh',
                     type='float',
                     default=None,
                     help='Squelch threshold, in dB.')
    group.add_option(
        '--squelch-tail',
        dest='squelch_tail',
        type='float',
        default=1,
        help=
        'Time for which the squelch remains open after the signal is below threshold.'
    )
    group.add_option(
        '-g',
        '--agc-gain',
        dest='agc_gain',
        type='string',
        default=None,
        help=
        'AGC gain; if set, AGC is turned off (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    group.add_option('--nb',
                     dest='nb',
                     action='store_true',
                     default=False,
                     help='Enable noise blanker with default parameters.')
    group.add_option(
        '--nb-gate',
        dest='nb_gate',
        type='int',
        default=100,
        help='Noise blanker gate time in usec (100 to 5000, default 100)')
    group.add_option(
        '--nb-th',
        '--nb-thresh',
        dest='nb_thresh',
        type='int',
        default=50,
        help='Noise blanker threshold in percent (0 to 100, default 50)')
    group.add_option(
        '-w',
        '--kiwi-wav',
        dest='is_kiwi_wav',
        default=False,
        action='store_true',
        help=
        'In the wav file include KIWI header containing GPS time-stamps (only for IQ mode)'
    )
    group.add_option('--kiwi-tdoa',
                     dest='is_kiwi_tdoa',
                     default=False,
                     action='store_true',
                     help='Used when called by Kiwi TDoA extension')
    group.add_option(
        '--test-mode',
        dest='test_mode',
        default=False,
        action='store_true',
        help='Write wav data to /dev/null (Linux) or NUL (Windows)')
    group.add_option(
        '--snd',
        '--sound',
        dest='sound',
        default=False,
        action='store_true',
        help=
        'Also process sound data when in waterfall or S-meter mode (sound connection options above apply)'
    )
    group.add_option(
        '-a',
        '--audio',
        dest='audio',
        default=False,
        action='store_true',
        help='Get audio output instead of writing to disk (mod by linkz)')
    parser.add_option_group(group)

    group = OptionGroup(parser, "S-meter mode options", "")
    group.add_option(
        '--S-meter',
        '--s-meter',
        dest='S_meter',
        type='int',
        default=-1,
        help=
        'Report S-meter (RSSI) value after S_METER number of averages. S_METER=0 does no averaging and reports each RSSI value received. Options --ts and --stats apply.'
    )
    parser.add_option('--sdt-sec',
                      dest='sdt',
                      type='int',
                      default=0,
                      help='S-meter measurement interval')
    parser.add_option_group(group)

    group = OptionGroup(parser, "Waterfall connection options", "")
    group.add_option('--wf',
                     dest='waterfall',
                     default=False,
                     action='store_true',
                     help='Process waterfall data instead of audio')
    group.add_option('-z',
                     '--zoom',
                     dest='zoom',
                     type='int',
                     default=0,
                     help='Zoom level 0-14')
    parser.add_option_group(group)

    (options, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
                        format=FORMAT)
    if options.log_level.upper() == 'DEBUG':
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    if options.S_meter >= 0:
        if options.S_meter > 0 and options.sdt != 0:
            raise Exception(
                'Options --S-meter > 0 and --sdt-sec != 0 are incompatible. Did you mean to use --S-meter=0 ?'
            )
        options.quiet = True

    if options.tlimit is not None and options.dt != 0:
        print('Warning: --tlimit ignored when --dt-sec option used')

    options.raw = False
    gopt = options
    multiple_connections, options = options_cross_product(options)

    snd_recorders = []
    if not gopt.waterfall or (gopt.waterfall and gopt.sound):
        for i, opt in enumerate(options):
            opt.multiple_connections = multiple_connections
            opt.idx = i
            snd_recorders.append(
                KiwiWorker(args=(KiwiSoundRecorder(opt), opt, run_event)))

    wf_recorders = []
    if gopt.waterfall:
        for i, opt in enumerate(options):
            opt.multiple_connections = multiple_connections
            opt.idx = i
            wf_recorders.append(
                KiwiWorker(args=(KiwiWaterfallRecorder(opt), opt, run_event)))

    try:
        for i, r in enumerate(snd_recorders):
            if opt.launch_delay != 0 and i != 0 and options[
                    i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            #logging.info("started sound recorder %d, timestamp=%d" % (i, options[i].timestamp))
            logging.info("started sound recorder %d" % i)

        for i, r in enumerate(wf_recorders):
            if i != 0 and options[i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            logging.info("started waterfall recorder %d" % i)

        while run_event.is_set():
            time.sleep(.1)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(snd_recorders, wf_recorders)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception as e:
        print_exc()
        run_event.clear()
        join_threads(snd_recorders, wf_recorders)
        print("Exception: threads successfully closed")

    if gopt.is_kiwi_tdoa:
        for i, opt in enumerate(options):
            # NB: MUST be a print (i.e. not a logging.info)
            print("status=%d,%d" % (i, opt.status))

    logging.debug('gc %s' % gc.garbage)
def main():
    parser = OptionParser()
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    parser.add_option(
        '--progress',
        dest='progress',
        default=False,
        action='store_true',
        help='Print progress messages instead of output of binary data')
    parser.add_option('-k',
                      '--socket-timeout',
                      '--socket_timeout',
                      dest='socket_timeout',
                      type='int',
                      default=10,
                      help='Timeout(sec) for sockets')
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host (can be a comma-delimited list)',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '-p',
        '--server-port',
        dest='server_port',
        type='string',
        default=8073,
        help='Server port, default 8073 (can be a comma delimited list)',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pw',
        '--password',
        dest='password',
        type='string',
        default='',
        help='Kiwi login password (if required, can be a comma delimited list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option('-u',
                      '--user',
                      dest='user',
                      type='string',
                      default='kiwirecorder.py',
                      help='Kiwi connection user name',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option('--launch-delay',
                      '--launch_delay',
                      dest='launch_delay',
                      type='int',
                      default=0,
                      help='Delay (secs) in launching multiple connections')
    parser.add_option(
        '-f',
        '--freq',
        dest='frequency',
        type='string',
        default=1000,
        help='Frequency to tune to, in kHz (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    parser.add_option('-m',
                      '--modulation',
                      dest='modulation',
                      type='string',
                      default='am',
                      help='Modulation; one of am, lsb, usb, cw, nbfm, iq')
    parser.add_option('--ncomp',
                      '--no_compression',
                      dest='compression',
                      default=True,
                      action='store_false',
                      help='Don\'t use audio compression')
    parser.add_option('-L',
                      '--lp-cutoff',
                      dest='lp_cut',
                      type='float',
                      default=100,
                      help='Low-pass cutoff frequency, in Hz')
    parser.add_option('-H',
                      '--hp-cutoff',
                      dest='hp_cut',
                      type='float',
                      default=2600,
                      help='Low-pass cutoff frequency, in Hz')
    parser.add_option('--tlimit',
                      '--time-limit',
                      dest='tlimit',
                      type='float',
                      default=None,
                      help='Record time limit in seconds')
    parser.add_option('-T',
                      '--squelch-threshold',
                      dest='thresh',
                      type='float',
                      default=None,
                      help='Squelch threshold, in dB.')
    parser.add_option(
        '--squelch-tail',
        dest='squelch_tail',
        type='float',
        default=1,
        help=
        'Time for which the squelch remains open after the signal is below threshold.'
    )
    parser.add_option(
        '-g',
        '--agc-gain',
        dest='agc_gain',
        type='string',
        default=None,
        help=
        'AGC gain; if set, AGC is turned off (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    parser.add_option('--wf',
                      '--waterfall',
                      dest='waterfall',
                      default=False,
                      action='store_true',
                      help='Process waterfall data instead of audio')
    parser.add_option(
        '--admin',
        dest='admin',
        default=False,
        action='store_true',
        help='Kiwi connection: admin instead of default audio stream.')

    (options, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
                        format=FORMAT)
    if options.log_level.upper() == 'DEBUG':
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    options.raw = True
    options.S_meter = -1
    options.ADC_OV = None
    options.is_kiwi_tdoa = False
    options.no_api = False
    options.connect_retries = 0
    options.connect_timeout = 3
    gopt = options
    multiple_connections, options = options_cross_product(options)

    nc_inst = []
    for i, opt in enumerate(options):
        opt.multiple_connections = multiple_connections
        opt.idx = 0
        nc_inst.append(KiwiWorker(args=(KiwiNetcat(opt, True), opt,
                                        run_event)))
        opt.writer_init = False
        opt.idx = 1
        nc_inst.append(
            KiwiWorker(args=(KiwiNetcat(opt, False), opt, run_event)))

    try:
        for i, r in enumerate(nc_inst):
            if opt.launch_delay != 0 and i != 0 and options[
                    i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            #logging.info("started sound recorder %d, timestamp=%d" % (i, options[i].timestamp))
            logging.info("started sound recorder %d" % i)

        while run_event.is_set():
            time.sleep(.1)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(nc_inst)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception as e:
        print_exc()
        run_event.clear()
        join_threads(nc_inst)
        print("Exception: threads successfully closed")

    logging.debug('gc %s' % gc.garbage)
Exemple #7
0
def main():
    parser = OptionParser()
    parser.add_option(
        '--log',
        '--log-level',
        '--log_level',
        type='choice',
        dest='log_level',
        default='warn',
        choices=['debug', 'info', 'warn', 'error', 'critical'],
        help='Log level: debug|info|warn(default)|error|critical')
    parser.add_option(
        '--progress',
        dest='progress',
        default=False,
        action='store_true',
        help='Print progress messages instead of output of binary data')
    parser.add_option('-k',
                      '--socket-timeout',
                      '--socket_timeout',
                      dest='socket_timeout',
                      type='int',
                      default=10,
                      help='Timeout(sec) for sockets')
    parser.add_option('-s',
                      '--server-host',
                      dest='server_host',
                      type='string',
                      default='localhost',
                      help='Server host (can be a comma-delimited list)',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option(
        '-p',
        '--server-port',
        dest='server_port',
        type='string',
        default=8073,
        help='Server port, default 8073 (can be a comma delimited list)',
        action='callback',
        callback_args=(int, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pw',
        '--password',
        dest='password',
        type='string',
        default='',
        help='Kiwi login password (if required, can be a comma delimited list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--tlimit-pw',
        '--tlimit-password',
        dest='tlimit_password',
        type='string',
        default='',
        help=
        'Connect time limit exemption password (if required, can be a comma-separated list)',
        action='callback',
        callback_args=(str, ),
        callback=get_comma_separated_args)
    parser.add_option('-u',
                      '--user',
                      dest='user',
                      type='string',
                      default='kiwi_nc.py',
                      help='Kiwi connection user name',
                      action='callback',
                      callback_args=(str, ),
                      callback=get_comma_separated_args)
    parser.add_option('--launch-delay',
                      '--launch_delay',
                      dest='launch_delay',
                      type='int',
                      default=0,
                      help='Delay (secs) in launching multiple connections')
    parser.add_option(
        '-f',
        '--freq',
        dest='frequency',
        type='string',
        default=1000,
        help='Frequency to tune to, in kHz (can be a comma-separated list). '
        'For sideband modes (lsb/lsn/usb/usn/cw/cwn) this is the carrier frequency. See --pbc option below.',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    parser.add_option(
        '--pbc',
        '--freq-pbc',
        dest='freq_pbc',
        action='store_true',
        default=False,
        help=
        'For sideband modes (lsb/lsn/usb/usn/cw/cwn) interpret -f/--freq frequency as the passband center frequency.'
    )
    parser.add_option(
        '-m',
        '--modulation',
        dest='modulation',
        type='string',
        default='am',
        help=
        'Modulation; one of am/amn, sam/sau/sal/sas/qam, lsb/lsn, usb/usn, cw/cwn, nbfm, iq (default passband if -L/-H not specified)'
    )
    parser.add_option(
        '--ncomp',
        '--no_compression',
        dest='compression',
        default=True,
        action='store_false',
        help='Don\'t use audio compression (IQ mode never uses compression)')
    parser.add_option('-L',
                      '--lp-cutoff',
                      dest='lp_cut',
                      type='float',
                      default=100,
                      help='Low-pass cutoff frequency, in Hz')
    parser.add_option('-H',
                      '--hp-cutoff',
                      dest='hp_cut',
                      type='float',
                      default=2600,
                      help='Low-pass cutoff frequency, in Hz')
    parser.add_option('--tlimit',
                      '--time-limit',
                      dest='tlimit',
                      type='float',
                      default=None,
                      help='Record time limit in seconds')
    parser.add_option('-T',
                      '--squelch-threshold',
                      dest='thresh',
                      type='float',
                      default=None,
                      help='Squelch threshold, in dB.')
    parser.add_option(
        '--squelch-tail',
        dest='squelch_tail',
        type='float',
        default=1,
        help=
        'Time for which the squelch remains open after the signal is below threshold.'
    )
    parser.add_option(
        '-g',
        '--agc-gain',
        dest='agc_gain',
        type='string',
        default=None,
        help=
        'AGC gain; if set, AGC is turned off (can be a comma-separated list)',
        action='callback',
        callback_args=(float, ),
        callback=get_comma_separated_args)
    parser.add_option('--agc-decay',
                      dest='agc_decay',
                      type='int',
                      default=1000,
                      help='AGC decay (msec); if set, AGC is turned on')
    parser.add_option('--agc-yaml',
                      dest='agc_yaml_file',
                      type='string',
                      default=None,
                      help='AGC options provided in a YAML-formatted file')
    parser.add_option('--de-emp',
                      dest='de_emp',
                      action='store_true',
                      default=False,
                      help='Enable de-emphasis.')
    parser.add_option('--wf',
                      '--waterfall',
                      dest='waterfall',
                      default=False,
                      action='store_true',
                      help='Process waterfall data instead of audio')

    group = OptionGroup(parser, "KiwiSDR development options", "")
    group.add_option('--gc-stats',
                     dest='gc_stats',
                     default=False,
                     action='store_true',
                     help='Print garbage collection stats')
    group.add_option(
        '--admin',
        dest='admin',
        default=False,
        action='store_true',
        help='Kiwi connection: admin instead of default audio stream.')
    parser.add_option_group(group)

    (options, unused_args) = parser.parse_args()

    ## clean up OptionParser which has cyclic references
    parser.destroy()

    FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
    logging.basicConfig(level=logging.getLevelName(options.log_level.upper()),
                        format=FORMAT)
    if options.gc_stats:
        gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)

    run_event = threading.Event()
    run_event.set()

    ### decode AGC YAML file options
    options.agc_yaml = None
    if options.agc_yaml_file:
        try:
            if not HAS_PyYAML:
                raise Exception(
                    'PyYAML not installed: sudo apt install python-yaml / sudo apt install python3-yaml / pip install pyyaml / pip3 install pyyaml'
                )
            with open(options.agc_yaml_file) as yaml_file:
                documents = yaml.full_load(yaml_file)
                logging.debug('AGC file %s: %s' %
                              (options.agc_yaml_file, documents))
                logging.debug('Got AGC parameters from file %s: %s' %
                              (options.agc_yaml_file, documents['AGC']))
                options.agc_yaml = documents['AGC']
        except KeyError:
            logging.fatal('The YAML file does not contain AGC options')
            return
        except Exception as e:
            logging.fatal(e)
            return

    options.raw = True
    options.S_meter = -1
    options.ADC_OV = None
    options.is_kiwi_tdoa = False
    options.no_api = False
    options.connect_retries = 0
    options.connect_timeout = 3
    options.rigctl_enabled = False
    gopt = options
    multiple_connections, options = options_cross_product(options)

    nc_inst = []
    for i, opt in enumerate(options):
        opt.multiple_connections = multiple_connections
        opt.idx = 0
        nc_inst.append(KiwiWorker(args=(KiwiNetcat(opt, True), opt,
                                        run_event)))
        if gopt.admin:
            opt.writer_init = False
            opt.idx = 1
            nc_inst.append(
                KiwiWorker(args=(KiwiNetcat(opt, False), opt, run_event)))

    try:
        for i, r in enumerate(nc_inst):
            if opt.launch_delay != 0 and i != 0 and options[
                    i - 1].server_host == options[i].server_host:
                time.sleep(opt.launch_delay)
            r.start()
            #logging.info("started sound recorder %d, timestamp=%d" % (i, options[i].timestamp))
            logging.info("started sound recorder %d" % i)

        while run_event.is_set():
            time.sleep(.1)

    except KeyboardInterrupt:
        run_event.clear()
        join_threads(nc_inst)
        print("KeyboardInterrupt: threads successfully closed")
    except Exception as e:
        print_exc()
        run_event.clear()
        join_threads(nc_inst)
        print("Exception: threads successfully closed")

    if gopt.gc_stats:
        logging.debug('gc %s' % gc.garbage)