├── m
└── README
├── samplerate
├── .gitattributes
├── _samplerate_data
│ ├── libsamplerate.dylib
│ ├── libsamplerate-32bit.dll
│ ├── libsamplerate-64bit.dll
│ ├── build_samplerate_mxe.sh
│ └── README.md
├── .gitignore
├── exceptions.py
├── __init__.py
├── LICENSE.rst
├── samplerate_build.py
├── _src.py
├── tests
│ └── test_samplerate.py
├── lowlevel.py
├── converters.py
└── _version.py
├── chunk
├── COPIED_FROM.txt
├── README.rst
├── LICENSE
└── __init__.py
├── .octaverc
├── .gitignore
├── tools
└── README
├── fast_agc.yaml
├── default_agc.yaml
├── scan_continuous.yaml
├── scan_squelch.yaml
├── kiwi
├── __init__.py
├── wavreader.py
├── worker.py
├── rigctld.py
└── wsclient.py
├── kiwi_iq_wav_to_c2.py
├── CHANGE_LOG
├── mod_pywebsocket
├── stream.py
├── _stream_base.py
├── http_header_util.py
├── __init__.py
├── _stream_hixie75.py
├── common.py
└── util.py
├── test
└── kiwi_server.py
├── proc_kiwi_iq_wav.m
├── waterfall_data_analysis.ipynb
├── src
└── read_kiwi_iq_wav.cc
├── microkiwi_waterfall.py
├── SCANNING
├── README.md
└── kiwiwfrecorder.py
/m/README:
--------------------------------------------------------------------------------
1 | A directory for Octave *.m files
2 |
--------------------------------------------------------------------------------
/samplerate/.gitattributes:
--------------------------------------------------------------------------------
1 | samplerate/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/chunk/COPIED_FROM.txt:
--------------------------------------------------------------------------------
1 | Copied from: github.com/youknowone/python-deadlib/tree/main/chunk
2 |
--------------------------------------------------------------------------------
/.octaverc:
--------------------------------------------------------------------------------
1 | addpath (['m' pathsep() 'oct'])
2 | pkg load signal
3 | ##graphics_toolkit('gnuplot')
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.png
3 | *.wav
4 | log*
5 | *~
6 | gnss_pos/
7 | *.o
8 | *.oct
9 | *.npy
10 |
--------------------------------------------------------------------------------
/tools/README:
--------------------------------------------------------------------------------
1 | The latest version of wsprdaemon can be found at: https://github.com/rrobinett/wsprdaemon
2 |
--------------------------------------------------------------------------------
/samplerate/_samplerate_data/libsamplerate.dylib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jks-prv/kiwiclient/HEAD/samplerate/_samplerate_data/libsamplerate.dylib
--------------------------------------------------------------------------------
/fast_agc.yaml:
--------------------------------------------------------------------------------
1 | AGC :
2 | 'on': True ## beware 'on' vs on
3 | hang: False
4 | thresh: -130
5 | slope: 1
6 | decay: 1000
7 | gain: 50
8 |
--------------------------------------------------------------------------------
/samplerate/_samplerate_data/libsamplerate-32bit.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jks-prv/kiwiclient/HEAD/samplerate/_samplerate_data/libsamplerate-32bit.dll
--------------------------------------------------------------------------------
/samplerate/_samplerate_data/libsamplerate-64bit.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jks-prv/kiwiclient/HEAD/samplerate/_samplerate_data/libsamplerate-64bit.dll
--------------------------------------------------------------------------------
/default_agc.yaml:
--------------------------------------------------------------------------------
1 | AGC :
2 | 'on': False ## beware 'on' vs on
3 | hang: False
4 | thresh: -100
5 | slope: 6
6 | decay: 1000
7 | gain: 50
8 |
--------------------------------------------------------------------------------
/scan_continuous.yaml:
--------------------------------------------------------------------------------
1 | Scan:
2 | wait: 1 # seconds
3 | dwell: 5 # seconds
4 | frequencies: [2500, 3330, 5000, 7850, 10000, 14670, 15000, 20000] # kHz, time stations
5 |
--------------------------------------------------------------------------------
/samplerate/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.py[cod]
3 |
4 | .coverage
5 | .cache/
6 | .eggs/
7 | *.egg-info
8 | dist/
9 | build/
10 |
11 | docs/_build
12 |
13 | tags
14 | .vscode/
15 |
16 | samplerate/_src.py
--------------------------------------------------------------------------------
/scan_squelch.yaml:
--------------------------------------------------------------------------------
1 | Scan:
2 | threshold: 20 # dB
3 | wait: 1 # seconds
4 | dwell: 6 # seconds
5 | # frequencies: [6661, 6559, 6311, 8948, 8912] # kHz, HFDL
6 | frequencies: [4024, 4030, 4039] # kHz, STANAG-4539 bursts
7 | pbc: true
8 |
--------------------------------------------------------------------------------
/kiwi/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | ## -*- python -*-
3 |
4 | #from .client import KiwiSDRStream
5 | #from .client import KiwiError,KiwiTooBusyError,KiwiDownErrorKiwiCampError,KiwiBadPasswordError,KiwiTimeLimitError,KiwiServerTerminatedConnection,KiwiUnknownModulation
6 | from .client import *
7 | from .worker import KiwiWorker
8 | from .wavreader import *
9 |
--------------------------------------------------------------------------------
/chunk/README.rst:
--------------------------------------------------------------------------------
1 | Dead battery redistribution
2 | ===========================
3 |
4 | Python is moving forward! Python finally started to remove dead batteries.
5 | For more information, see `PEP 594 `_.
6 |
7 | If your project depends on a module that has been removed from the standard,
8 | here is the redistribution of the dead batteries.
9 |
--------------------------------------------------------------------------------
/samplerate/exceptions.py:
--------------------------------------------------------------------------------
1 | """Exceptions
2 |
3 | """
4 |
5 |
6 | class ResamplingError(RuntimeError):
7 | """Runtime exception of libsamplerate"""
8 |
9 | def __init__(self, error):
10 | from samplerate.lowlevel import src_strerror
11 | message = 'libsamplerate error #{}: {}'.format(error,
12 | src_strerror(error))
13 | super(ResamplingError, self).__init__(message)
14 | self.error = error
15 |
--------------------------------------------------------------------------------
/samplerate/__init__.py:
--------------------------------------------------------------------------------
1 | """Python bindings for libsamplerate based on CFFI and NumPy.
2 |
3 | """
4 | # Versioning
5 | from samplerate._version import get_versions
6 | __version__ = get_versions()['version']
7 | del get_versions
8 |
9 | from samplerate.lowlevel import __libsamplerate_version__
10 |
11 | # Convenience imports
12 | from samplerate.exceptions import ResamplingError
13 | from samplerate.converters import resample
14 | from samplerate.converters import Resampler
15 | from samplerate.converters import CallbackResampler
16 |
--------------------------------------------------------------------------------
/samplerate/_samplerate_data/build_samplerate_mxe.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | JOBS=8
3 | export PATH="$(pwd)/mxe/usr/bin:$PATH"
4 | for TARGET in x86_64-w64-mingw32.static i686-w64-mingw32.static
5 | do
6 | make -C mxe libsamplerate -j$JOBS JOBS=$JOBS MXE_TARGETS=$TARGET
7 | $TARGET-gcc -O2 -shared -o libsamplerate-$TARGET.dll -Wl,--whole-archive -lsamplerate -Wl,--no-whole-archive
8 | $TARGET-strip libsamplerate-$TARGET.dll
9 | chmod -x libsamplerate-$TARGET.dll
10 | done
11 | mv libsamplerate-x86_64-w64-mingw32.static.dll libsamplerate-64bit.dll
12 | mv libsamplerate-i686-w64-mingw32.static.dll libsamplerate-32bit.dll
13 |
--------------------------------------------------------------------------------
/samplerate/_samplerate_data/README.md:
--------------------------------------------------------------------------------
1 | # libsamplerate binaries
2 |
3 | These are statically compiled dynamic libraries of
4 | [libsamplerate](http://www.mega-nerd.com/libsamplerate/).
5 |
6 | ## DLLs for Windows (32-bit and 64-bit)
7 |
8 | The instructions follow the README of
9 | [libsndfile-binaries](https://github.com/bastibe/libsndfile-binaries). The DLLs
10 | were created on macOS using [MXE](http://mxe.cc) with the
11 | `build_samplerate_mxe.sh` script:
12 |
13 | git clone https://github.com/mxe/mxe.git
14 | ./build-samplerate.sh
15 |
16 | ## Dylib for macOS (64-bit)
17 |
18 | Build using [Homebrew](http://brew.sh/):
19 |
20 | brew install libsamplerate
21 | cp /usr/local/lib/libsamplerate.dylib .
22 | # There should be no further dependencies:
23 | otool -L libsamplerate.dylib
24 |
--------------------------------------------------------------------------------
/samplerate/LICENSE.rst:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright © 2017 Tino Wagner
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the "Software"), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/kiwi_iq_wav_to_c2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import soundfile,struct,sys
4 | import numpy as np
5 |
6 | ## convert kiwirecorder IQ wav files to .c2 file for wsprd
7 |
8 | def decode_kiwi_wav_filename(fn):
9 | """ input: kiwirecorder wav file name
10 | output: .c2 filename (length 14) and frequency in kHz"""
11 | fields = fn.split('/')[-1].split('_')
12 | date_and_time = fields[0]
13 | c2_filename = date_and_time[2:8] + '_' + date_and_time[9:13] + '.c2'
14 | freq_kHz = int(fields[1])/1e3
15 | return c2_filename,freq_kHz
16 |
17 | def convert_kiwi_wav_to_c2(fn):
18 | """ input: kiwirecorder IQ wav file name with sample rate 375 Hz
19 | output: .c2 file for wsprd"""
20 | c2_filename,freq_kHz = decode_kiwi_wav_filename(fn)
21 | iq,fs = soundfile.read(fn)
22 | assert(fs == 375) ## sample rate has to be 375 Hz
23 | assert(iq.shape[1] == 2) ## works only for IQ mode recordings
24 | iq[:,1] *= -1
25 | iq.resize((45000,2), refcheck=False)
26 | f = open(c2_filename, 'wb')
27 | f.write(struct.pack('<14sid', c2_filename.encode(), 2, freq_kHz))
28 | f.write(iq.astype(np.float32).flatten().tobytes())
29 | f.close()
30 | print('in=' + fn + ' out='+c2_filename)
31 |
32 | if __name__ == '__main__':
33 | if len(sys.argv) == 1:
34 | print('convert kiwirecorder IQ wav files to .c2 file for wsprd')
35 | print('USAGE: ' + sys.argv[0]+ ' .wav filenames')
36 | for fn in sys.argv[1:]:
37 | convert_kiwi_wav_to_c2(fn)
38 |
39 |
--------------------------------------------------------------------------------
/samplerate/samplerate_build.py:
--------------------------------------------------------------------------------
1 | from cffi import FFI
2 |
3 | ffibuilder = FFI()
4 | ffibuilder.set_source('samplerate._src', None)
5 |
6 | ffibuilder.cdef("""
7 | typedef struct SRC_STATE_tag SRC_STATE ;
8 |
9 | typedef struct
10 | { const float *data_in ;
11 | float *data_out ;
12 |
13 | long input_frames, output_frames ;
14 | long input_frames_used, output_frames_gen ;
15 |
16 | int end_of_input ;
17 |
18 | double src_ratio ;
19 | } SRC_DATA ;
20 |
21 | // Simple API
22 | int src_simple (SRC_DATA *data, int converter_type, int channels) ;
23 |
24 | // Full API
25 | SRC_STATE* src_new (int converter_type, int channels, int *error) ;
26 | SRC_STATE* src_delete (SRC_STATE *state) ;
27 | int src_process (SRC_STATE *state, SRC_DATA *data) ;
28 | int src_reset (SRC_STATE *state) ;
29 | int src_set_ratio (SRC_STATE *state, double new_ratio) ;
30 | int src_is_valid_ratio (double ratio) ;
31 |
32 | // Callback API
33 | typedef long (*src_callback_t) (void *cb_data, float **data) ;
34 | SRC_STATE* src_callback_new (src_callback_t func,
35 | int converter_type, int channels,
36 | int *error, void* cb_data) ;
37 | long src_callback_read (SRC_STATE *state, double src_ratio,
38 | long frames, float *data) ;
39 |
40 | // Extern "Python"-style callback dropped in favor of compiler-less
41 | // ABI mode ...
42 | //extern "Python" long src_input_callback(void*, float**);
43 |
44 | // Misc
45 | int src_error (SRC_STATE *state) ;
46 | const char* src_strerror (int error) ;
47 | const char *src_get_name (int converter_type) ;
48 | const char *src_get_description (int converter_type) ;
49 | const char *src_get_version (void) ;
50 | void src_short_to_float_array (const short *in, float *out, int len) ;
51 | void src_float_to_short_array (const float *in, short *out, int len) ;
52 | void src_int_to_float_array (const int *in, float *out, int len) ;
53 | void src_float_to_int_array (const float *in, int *out, int len) ;
54 | """)
55 |
56 | if __name__ == '__main__':
57 | ffibuilder.compile(verbose=True)
58 |
--------------------------------------------------------------------------------
/samplerate/_src.py:
--------------------------------------------------------------------------------
1 | # auto-generated file
2 | import _cffi_backend
3 |
4 | ffi = _cffi_backend.FFI('samplerate._src',
5 | _version = 0x2601,
6 | _types = b'\x00\x00\x01\x0D\x00\x00\x46\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x04\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x2D\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x06\x11\x00\x00\x4B\x03\x00\x00\x00\x0F\x00\x00\x47\x0D\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x47\x0D\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x45\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x01\x11\x00\x00\x15\x11\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x01\x11\x00\x00\x0E\x01\x00\x00\x00\x0F\x00\x00\x04\x0D\x00\x00\x0E\x01\x00\x00\x00\x0F\x00\x00\x2A\x0D\x00\x00\x01\x11\x00\x00\x0E\x01\x00\x00\x09\x01\x00\x00\x49\x03\x00\x00\x00\x0F\x00\x00\x2A\x0D\x00\x00\x0D\x11\x00\x00\x2B\x03\x00\x00\x00\x0F\x00\x00\x4B\x0D\x00\x00\x49\x03\x00\x00\x06\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x4B\x0D\x00\x00\x32\x11\x00\x00\x4A\x03\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x4B\x0D\x00\x00\x04\x03\x00\x00\x2B\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x4B\x0D\x00\x00\x4A\x03\x00\x00\x2B\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x00\x48\x03\x00\x00\x02\x01\x00\x00\x0D\x01\x00\x00\x05\x01\x00\x00\x00\x01',
7 | _globals = (b'\x00\x00\x08\x23src_callback_new',0,b'\x00\x00\x27\x23src_callback_read',0,b'\x00\x00\x00\x23src_delete',0,b'\x00\x00\x19\x23src_error',0,b'\x00\x00\x31\x23src_float_to_int_array',0,b'\x00\x00\x36\x23src_float_to_short_array',0,b'\x00\x00\x0F\x23src_get_description',0,b'\x00\x00\x0F\x23src_get_name',0,b'\x00\x00\x12\x23src_get_version',0,b'\x00\x00\x3B\x23src_int_to_float_array',0,b'\x00\x00\x24\x23src_is_valid_ratio',0,b'\x00\x00\x03\x23src_new',0,b'\x00\x00\x1C\x23src_process',0,b'\x00\x00\x19\x23src_reset',0,b'\x00\x00\x20\x23src_set_ratio',0,b'\x00\x00\x40\x23src_short_to_float_array',0,b'\x00\x00\x14\x23src_simple',0,b'\x00\x00\x0F\x23src_strerror',0),
8 | _struct_unions = ((b'\x00\x00\x00\x45\x00\x00\x00\x02$SRC_DATA',b'\x00\x00\x32\x11data_in',b'\x00\x00\x2B\x11data_out',b'\x00\x00\x2A\x11input_frames',b'\x00\x00\x2A\x11output_frames',b'\x00\x00\x2A\x11input_frames_used',b'\x00\x00\x2A\x11output_frames_gen',b'\x00\x00\x04\x11end_of_input',b'\x00\x00\x22\x11src_ratio'),(b'\x00\x00\x00\x46\x00\x00\x00\x10SRC_STATE_tag',)),
9 | _typenames = (b'\x00\x00\x00\x45SRC_DATA',b'\x00\x00\x00\x46SRC_STATE',b'\x00\x00\x00\x09src_callback_t'),
10 | )
11 |
--------------------------------------------------------------------------------
/CHANGE_LOG:
--------------------------------------------------------------------------------
1 | CHANGE_LOG
2 |
3 | For the Github commit log see here: github.com/jks-prv/kiwiclient/commits/master
4 |
5 | v1.7 August 20, 2025
6 | Incorporated standard-chunk source code to eliminate all the problems with Python 13 and
7 | Debian 13 not being able to find and include the correct package.
8 |
9 | Most .py files: fix/add "#!/usr/bin/env python3" (thanks Rob)
10 |
11 | v1.6 July 11, 2025
12 | By request: The resampling option now works with netcat and camping modes.
13 |
14 | v1.5 June 15, 2025
15 | Added continuous frequency scanning capability.
16 | Kiwirecorder previously had a squelch-based scanning function using parameters specified
17 | with a YAML file and the --scan-yaml option. A set of frequencies would be scanned and the
18 | scanning stopped when the squelch opened. Continuous scanning will now occur if the
19 | "threshold" parameter is not given in the YAML file. See the file SCANNING for complete info.
20 |
21 | Note that the former example file scan.yaml has been renamed to scan_squelch.yaml to
22 | distinguish it from the new example file scan_continuous.yaml used in frequency scanning.
23 |
24 | Added Kiwi channel camping feature.
25 | Instead of creating a new connection to the Kiwi when kiwirecorder is run it can now "camp"
26 | onto an existing connection and record whatever audio the camped user is listening to.
27 | Very similar to the camp capability that is presented via the camp/queue panel that appears
28 | when you try and connect to a Kiwi when all its channels are full. See the option
29 | --camp=CAMP_CHAN where CAMP_CHAN is the channel number to camp on (e.g. 0..4 for rx0..rx4).
30 |
31 | Note that a 2-channel (stereo) file or netcat stream is always produced because you can't
32 | predict what the camped user will do. They may switch from a mono mode (e.g. USB) to a
33 | stereo mode (e.g. IQ) at any time. For mono modes the single channel audio is duplicated in
34 | both channels. Things also work when the camped user switches compression mode on and off.
35 |
36 | Camp mode also works with the --netcat option. For an example see the Makefile "camp" target.
37 | Note that camping does not currently support squelch or GPS timestamps.
38 |
39 | Netcat function:
40 | Made all the audio modes work correctly: mono/stereo, compression/no-compression.
41 | Note that netcat does not currently support squelch or GPS timestamps.
42 |
43 | Squelch function:
44 | Made tail timing correct for the various audio modes: mono/stereo, compression/no-compression.
45 |
--------------------------------------------------------------------------------
/chunk/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2001-2023 Python Software Foundation; All Rights Reserved
2 |
3 | This code originally taken from the Python 3.11.3 distribution
4 | and it is therefore now released under the following Python-style
5 | license:
6 |
7 | 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and
8 | the Individual or Organization ("Licensee") accessing and
9 | otherwise using nntplib software in source or binary form and
10 | its associated documentation.
11 |
12 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby
13 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
14 | analyze, test, perform and/or display publicly, prepare derivative works,
15 | distribute, and otherwise use nntplib alone or in any derivative
16 | version, provided, however, that PSF's License Agreement and PSF's notice of
17 | copyright, i.e., "Copyright © 2001-2023 Python Software Foundation; All Rights
18 | Reserved" are retained in nntplib alone or in any derivative version
19 | prepared by Licensee.
20 |
21 | 3. In the event Licensee prepares a derivative work that is based on or
22 | incorporates nntplib or any part thereof, and wants to make the
23 | derivative work available to others as provided herein, then Licensee hereby
24 | agrees to include in any such work a brief summary of the
25 | changes made to nntplib.
26 |
27 | 4. PSF is making nntplib available to Licensee on an "AS IS" basis.
28 | PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF
29 | EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR
30 | WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE
31 | USE OF NNTPLIB WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
32 |
33 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF NNTPLIB
34 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
35 | MODIFYING, DISTRIBUTING, OR OTHERWISE USING NNTPLIB, OR ANY DERIVATIVE
36 | THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
37 |
38 | 6. This License Agreement will automatically terminate upon a material breach of
39 | its terms and conditions.
40 |
41 | 7. Nothing in this License Agreement shall be deemed to create any relationship
42 | of agency, partnership, or joint venture between PSF and Licensee. This License
43 | Agreement does not grant permission to use PSF trademarks or trade name in a
44 | trademark sense to endorse or promote products or services of Licensee, or any
45 | third party.
46 |
47 | 8. By copying, installing or otherwise using nntplib, Licensee agrees
48 | to be bound by the terms and conditions of this License Agreement.
49 |
--------------------------------------------------------------------------------
/samplerate/tests/test_samplerate.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 |
4 | # so samplerate module is found when doing "pytest samplerate/tests/test_samplerate.py"
5 | # in kiwiclient top-level directory
6 | import sys, os
7 | sys.path.insert(0, os.path.abspath('.'))
8 | import samplerate
9 |
10 |
11 | @pytest.fixture(scope="module", params=[1, 2])
12 | def data(request):
13 | num_channels = request.param
14 | periods = np.linspace(0, 10, 1000)
15 | input_data = [
16 | np.sin(2 * np.pi * periods + i * np.pi / 2)
17 | for i in range(num_channels)
18 | ]
19 | return ((num_channels, input_data[0])
20 | if num_channels == 1 else (num_channels, np.transpose(input_data)))
21 |
22 |
23 | @pytest.fixture(params=list(samplerate.converters.ConverterType))
24 | def converter_type(request):
25 | return request.param
26 |
27 |
28 | def test_simple(data, converter_type, ratio=2.0):
29 | _, input_data = data
30 | samplerate.resample(input_data, ratio, converter_type)
31 |
32 |
33 | def test_process(data, converter_type, ratio=2.0):
34 | num_channels, input_data = data
35 | src = samplerate.Resampler(converter_type, num_channels)
36 | src.process(input_data, ratio)
37 |
38 |
39 | def test_match(data, converter_type, ratio=2.123):
40 | num_channels, input_data = data
41 | output_simple = samplerate.resample(input_data, ratio, converter_type)
42 | resampler = samplerate.Resampler(converter_type, channels=num_channels)
43 | if num_channels == 2:
44 | output_full = np.empty((0,num_channels), dtype=np.float32)
45 | for i in range(10):
46 | output_full = np.append(output_full, resampler.process(input_data[100*i:100*(i+1),:], ratio), axis=0)
47 | m = output_simple.shape[0]
48 | output_simple = np.resize(output_simple, output_full.shape)
49 | assert np.allclose(output_simple, output_full) and output_full.shape[0] > 0.8*m
50 | else:
51 | output_full = np.empty(0, dtype=np.float32)
52 | for i in range(10):
53 | output_full = np.append(output_full, resampler.process(input_data[100*i:100*(i+1)], ratio))
54 | m = len(output_simple)
55 | output_simple = np.resize(output_simple, output_full.shape)
56 | assert np.allclose(output_simple, output_full) and len(output_full) > 0.8*m
57 |
58 |
59 | def test_callback(data, converter_type, ratio=2.0):
60 | from samplerate import CallbackResampler
61 | num_channels, input_data = data
62 |
63 | def producer():
64 | yield input_data
65 | while True:
66 | yield None
67 |
68 | callback = lambda p=producer(): next(p)
69 |
70 | with CallbackResampler(callback, ratio, converter_type, channels=num_channels) as resampler:
71 | resampler.read(int(ratio) * input_data.shape[0])
72 |
--------------------------------------------------------------------------------
/mod_pywebsocket/stream.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """This file exports public symbols."""
32 |
33 |
34 | from mod_pywebsocket._stream_base import BadOperationException
35 | from mod_pywebsocket._stream_base import ConnectionTerminatedException
36 | from mod_pywebsocket._stream_base import InvalidFrameException
37 | from mod_pywebsocket._stream_base import InvalidUTF8Exception
38 | from mod_pywebsocket._stream_base import UnsupportedFrameException
39 | from mod_pywebsocket._stream_hixie75 import StreamHixie75
40 | from mod_pywebsocket._stream_hybi import Frame
41 | from mod_pywebsocket._stream_hybi import Stream
42 | from mod_pywebsocket._stream_hybi import StreamOptions
43 |
44 | # These methods are intended to be used by WebSocket client developers to have
45 | # their implementations receive broken data in tests.
46 | from mod_pywebsocket._stream_hybi import create_close_frame
47 | from mod_pywebsocket._stream_hybi import create_header
48 | from mod_pywebsocket._stream_hybi import create_length_header
49 | from mod_pywebsocket._stream_hybi import create_ping_frame
50 | from mod_pywebsocket._stream_hybi import create_pong_frame
51 | from mod_pywebsocket._stream_hybi import create_binary_frame
52 | from mod_pywebsocket._stream_hybi import create_text_frame
53 | from mod_pywebsocket._stream_hybi import create_closing_handshake_body
54 |
55 |
56 | # vi:sts=4 sw=4 et
57 |
--------------------------------------------------------------------------------
/test/kiwi_server.py:
--------------------------------------------------------------------------------
1 | ## -*- python -*-
2 |
3 | ## kiwisdr websocket simulator
4 | ## * needs python>=3.7
5 | ## * only one client is supported
6 | ## * IQ mode and GNSS timestamps are not supported
7 |
8 | import asyncio
9 | import websockets
10 | import struct
11 | import random
12 | import sys
13 |
14 | @asyncio.coroutine
15 | def consumer_handler(websocket, path):
16 | try:
17 | while True:
18 | message = yield from websocket.recv()
19 | if message.find('SET keepalive') < 0:
20 | print('got', path, message)
21 | sys.stdout.flush()
22 | except Exception as e:
23 | print(e)
24 |
25 | @asyncio.coroutine
26 | def producer_handler(websocket, path):
27 | try:
28 | i=0
29 | while True:
30 | if (i%10000) == 0:
31 | print('i=%12d %10.2f h' % (i, i/12000./3600*512))
32 | sys.stdout.flush()
33 | ##data = b''.join([b'SND', struct.pack('=2
35 | if n<2
36 | xx = {};
37 | last_gpsfix = 255;
38 | return
39 | endif
40 |
41 | ## indices of fresh GNSS timestamps
42 | idx_ts = find(diff(cat(1, x.gpslast)) < 0);
43 | idx_ts = make_complete(idx_ts, n);
44 |
45 | ## remove outliers
46 | ts = cat(1,x.gpssec) + 1e-9*cat(1,x.gpsnsec);
47 | idx_ts = fix_outliers(ts, idx_ts, fs);
48 | idx_ts = make_complete(idx_ts, n);
49 | ts = ts(idx_ts);
50 |
51 | ## helper functions
52 | gpssec = @(s) s.gpssec + 1e-9*s.gpsnsec;
53 | get_fs = @(i) 512*(idx_ts(i) - idx_ts(i-1)) / (gpssec(x(idx_ts(i))) - gpssec(x(idx_ts(i-1))));
54 |
55 | ## keep history
56 | fs = [];
57 |
58 | ## (1) interval before 1st fresh timestamp: extrapolate backwards
59 | if idx_ts(1) > 1
60 | fs(end+1) = get_fs(2);
61 | k = 0;
62 | for j=1:idx_ts(1)-1
63 | xx(j).t = ts(1) - 512*(idx_ts(1)-1)/fs(end) + (512*k+[0:numel(x(j).z)-1])'/fs(end);
64 | xx(j).z = x(j).z;
65 | k += 1;
66 | end
67 | end
68 |
69 | ## (2) in between fresh timestamps: extrapolate between fresh timestamps
70 | for i=2:numel(idx_ts)
71 | ##__fs = [ idx_ts(i) - idx_ts(i-1) gpssec(x(idx_ts(i))) - gpssec(x(idx_ts(i-1))) ]
72 | fs(end+1) = get_fs(i);
73 | k = 0;
74 | for j=idx_ts(i-1):idx_ts(i)-1
75 | xx(j).t = ts(i-1) + (512*k+[0:numel(x(j).z)-1])'/fs(end);
76 | xx(j).z = x(j).z;
77 | k += 1;
78 | end
79 | end
80 |
81 | ## (3) after the last fresh timestamp: extrapolate after the last fresh timestamp
82 | k = 0;
83 | for j=idx_ts(end):n
84 | xx(j).t = ts(end) + (512*k+[0:numel(x(j).z)-1])'/fs(end);
85 | xx(j).z = x(j).z;
86 | k += 1;
87 | end
88 |
89 | if numel(fs) < 3 ## something wrong happened above and the sample rate value is not ok
90 | last_gpsfix = 253;
91 | end
92 |
93 | ## reduce the influence of outliers
94 | fs = median(fs);
95 | endfunction
96 |
97 | function idx_ts=fix_outliers(t, idx_ts, fs)
98 | ## more than one sample difference -> outlier
99 | outliers = find(abs(diff(t(idx_ts))*fs - 512*diff(idx_ts)) > 1);
100 |
101 | b = ones(size(idx_ts))==1;
102 | m = numel(outliers);
103 | for i=1:2:m
104 | if i+1 <= m
105 | b(outliers(i)+1:outliers(i+1)+1) = false;
106 | else
107 | b(outliers(i)+1:end) = false;
108 | end
109 | end
110 | ## filter idx_ts
111 | idx_ts(~b) = [];
112 | endfunction
113 |
114 | function idx_ts=make_complete(idx_ts, n)
115 | ## add fake fresh GNSS timestamps indices if numel(idx_ts)<2
116 | if isempty(idx_ts)
117 | idx_ts = [1 n];
118 | elseif numel(idx_ts) == 1
119 | if n-idx_ts > idx_ts
120 | idx_ts = [idx_ts n];
121 | else
122 | idx_ts = [1 idx_ts];
123 | end
124 | end
125 | endfunction
126 |
--------------------------------------------------------------------------------
/kiwi/wavreader.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- python -*-
3 |
4 | try:
5 | import collections.abc as collectionsAbc # python 3.6+
6 | except ImportError:
7 | import collections as collectionsAbc
8 | import struct
9 | import numpy as np
10 |
11 | # so chunk module is found when doing "python3 kiwi/wavreader.py"
12 | # in kiwiclient top-level directory
13 | import sys, os
14 | sys.path.insert(0, os.path.abspath('.'))
15 | from chunk import Chunk # local copy from standard-chunk
16 |
17 | class KiwiIQWavError(Exception):
18 | pass
19 |
20 | class KiwiIQWavReader(collectionsAbc.Iterator):
21 | def __init__(self, f):
22 | super(KiwiIQWavReader, self).__init__()
23 | self._frame_counter = 0
24 | self._last_gpssec = -1
25 | try:
26 | self._f = open(f, 'rb')
27 | self._initfp(self._f)
28 | except:
29 | if self._f:
30 | self._f.close()
31 | raise
32 |
33 | def __del__(self):
34 | if self._f:
35 | self._f.close()
36 |
37 | def _initfp(self, file):
38 | self._file = Chunk(file, bigendian = 0)
39 | if self._file.getname() != b'RIFF':
40 | raise KiwiIQWavError('file does not start with RIFF id')
41 | if self._file.read(4) != b'WAVE':
42 | raise KiwiIQWavError('not a WAVE file')
43 |
44 | chunk = Chunk(self._file, bigendian = 0)
45 | if chunk.getname() != b'fmt ':
46 | raise KiwiIQWavError('fmt chunk is missing')
47 |
48 | self._proc_chunk_fmt(chunk)
49 | chunk.skip()
50 |
51 | ## for python3
52 | def __next__(self):
53 | return self.next()
54 |
55 | ## for python2
56 | def next(self):
57 | try:
58 | chunk = Chunk(self._file, bigendian = 0)
59 | if chunk.getname() != b'kiwi':
60 | raise KiwiIQWavError('missing KiwiSDR GNSS time stamp')
61 |
62 | self._proc_chunk_kiwi(chunk)
63 | chunk.skip()
64 |
65 | chunk = Chunk(self._file, bigendian = 0)
66 | if chunk.getname() != b'data':
67 | raise KiwiIQWavError('missing WAVE data chunk')
68 |
69 | tz = self._proc_chunk_data(chunk)
70 | chunk.skip()
71 | return tz
72 | except EOFError:
73 | raise StopIteration
74 |
75 | def process_iq_samples(self, t,z):
76 | ## print(len(t), len(z))
77 | pass
78 |
79 | def get_samplerate(self):
80 | return self._samplerate
81 |
82 | def _proc_chunk_fmt(self, chunk):
83 | wFormatTag, nchannels, self._samplerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack('= 0:
95 | if self._frame_counter < 3:
96 | self._samplerate = n/(self.gpssec - self._last_gpssec)
97 | else:
98 | self._samplerate = 0.9*self._samplerate + 0.1*n/(self.gpssec - self._last_gpssec)
99 |
100 | if self._frame_counter >= 2:
101 | t = np.arange(start = self.gpssec,
102 | stop = self.gpssec + (n-0.5)/self._samplerate,
103 | step = 1/self._samplerate,
104 | dtype = np.float64)
105 | ##t = self.gpssec + np.array(range(n))/self._samplerate
106 | self.process_iq_samples(t,z)
107 |
108 | self._last_gpssec = self.gpssec
109 | self._frame_counter += (self._frame_counter < 3)
110 | return t,z
111 |
112 | def read_kiwi_iq_wav(filename):
113 | t = []
114 | z = []
115 | for _t,_z in KiwiIQWavReader(filename):
116 | if _t is None:
117 | continue
118 | t.append(_t)
119 | z.append(_z)
120 | return np.concatenate(t), np.concatenate(z)
121 |
122 | if __name__ == '__main__':
123 | import sys
124 | if len(sys.argv) != 2:
125 | print('Usage: ...')
126 |
127 | [t,z]=read_kiwi_iq_wav(sys.argv[1])
128 | print (len(t),len(z), t[-1], z[-1], (t[-1]-t[-2])*1e6)
129 |
--------------------------------------------------------------------------------
/waterfall_data_analysis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "%pylab inline\n",
10 | "import struct"
11 | ]
12 | },
13 | {
14 | "cell_type": "markdown",
15 | "metadata": {},
16 | "source": [
17 | "# Read in data from binary waterfall file"
18 | ]
19 | },
20 | {
21 | "cell_type": "code",
22 | "execution_count": null,
23 | "metadata": {},
24 | "outputs": [],
25 | "source": [
26 | "header_len = 8+26 # 2 unsigned int for center_freq and span: 8 bytes PLUS 26 bytes for datetime\n",
27 | "nbin = 1024\n",
28 | "\n",
29 | "with open(\"wf.bin\", \"rb\") as fd:\n",
30 | " buff = fd.read()\n",
31 | "\n",
32 | "length = len(buff[header_len:])\n",
33 | "n_t = length / nbin\n",
34 | "\n",
35 | "header = struct.unpack('2I26s', buff[:header_len])\n",
36 | "data = struct.unpack('%dB'%length, buff[header_len:])\n",
37 | "\n",
38 | "waterfall_array = np.reshape(np.array(data[:]), (n_t, nbin))\n",
39 | "waterfall_array -= 255\n",
40 | "\n",
41 | "avg_wf = np.mean(waterfall_array[:,:], axis=0)\n",
42 | "\n",
43 | "center_frequency, span, rec_time = header\n",
44 | "start_freq = center_frequency - span/2\n",
45 | "stop_freq = center_frequency + span/2\n",
46 | "print \"Recording date:\", rec_time"
47 | ]
48 | },
49 | {
50 | "cell_type": "markdown",
51 | "metadata": {},
52 | "source": [
53 | "# Plot waterfall data"
54 | ]
55 | },
56 | {
57 | "cell_type": "code",
58 | "execution_count": null,
59 | "metadata": {},
60 | "outputs": [],
61 | "source": [
62 | "figure(figsize=(40,8))\n",
63 | "nticks = 11\n",
64 | "xticks(np.linspace(0,nbin,nticks), np.linspace(start_freq, stop_freq, nticks))\n",
65 | "pcolormesh(waterfall_array[:,1:], cmap=cm.jet , vmin=-100, vmax=-46)\n",
66 | "title(\"Recording time: %s\"%rec_time)\n",
67 | "colorbar()"
68 | ]
69 | },
70 | {
71 | "cell_type": "markdown",
72 | "metadata": {},
73 | "source": [
74 | "# Plot a few spectra"
75 | ]
76 | },
77 | {
78 | "cell_type": "code",
79 | "execution_count": null,
80 | "metadata": {},
81 | "outputs": [],
82 | "source": [
83 | "figure(figsize=(20,8))\n",
84 | "nticks = 11\n",
85 | "xticks(np.linspace(0,nbin,nticks), np.linspace(start_freq, stop_freq, nticks))\n",
86 | "for spectrum in waterfall_array[1:10]:\n",
87 | " plot(spectrum[1:])"
88 | ]
89 | },
90 | {
91 | "cell_type": "markdown",
92 | "metadata": {},
93 | "source": [
94 | "# Plot signal levels distribution (instantaneous and average)"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": null,
100 | "metadata": {},
101 | "outputs": [],
102 | "source": [
103 | "figure(figsize=(20,8))\n",
104 | "_=hist(waterfall_array[0,:], bins=40, normed=True)\n",
105 | "xlim(-110,0)\n",
106 | "xlabel(\"Signal level (dB)\")\n",
107 | "ylabel(\"Occurrences\")\n",
108 | "title(\"Instantaneous power level distribution\")\n",
109 | "\n",
110 | "figure(figsize=(20,8))\n",
111 | "_=hist(avg_wf, bins=40, normed=True)\n",
112 | "xlim(-110,0)\n",
113 | "xlabel(\"Signal level (dB)\")\n",
114 | "ylabel(\"Occurrences\")\n",
115 | "title(\"Average power level distribution\")\n"
116 | ]
117 | },
118 | {
119 | "cell_type": "markdown",
120 | "metadata": {},
121 | "source": [
122 | "# Compute SNR for the whole band (average data)"
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": null,
128 | "metadata": {},
129 | "outputs": [],
130 | "source": [
131 | "median = np.median(avg_wf)\n",
132 | "perc95 = np.percentile(avg_wf, 95)\n",
133 | "\n",
134 | "print \"SNR estimation: median: %f dB, 95th perc.: %f dB, SNR: %f dB\" % (median, perc95, perc95-median)"
135 | ]
136 | },
137 | {
138 | "cell_type": "code",
139 | "execution_count": null,
140 | "metadata": {},
141 | "outputs": [],
142 | "source": []
143 | }
144 | ],
145 | "metadata": {
146 | "kernelspec": {
147 | "display_name": "Python 2",
148 | "language": "python",
149 | "name": "python2"
150 | },
151 | "language_info": {
152 | "codemirror_mode": {
153 | "name": "ipython",
154 | "version": 2
155 | },
156 | "file_extension": ".py",
157 | "mimetype": "text/x-python",
158 | "name": "python",
159 | "nbconvert_exporter": "python",
160 | "pygments_lexer": "ipython2",
161 | "version": "2.7.15"
162 | }
163 | },
164 | "nbformat": 4,
165 | "nbformat_minor": 2
166 | }
167 |
--------------------------------------------------------------------------------
/kiwi/worker.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | ## -*- python -*-
3 |
4 | import logging, time
5 | import threading
6 | from traceback import print_exc
7 |
8 | from .client import KiwiTooBusyError, KiwiRedirectError, KiwiTimeLimitError, KiwiServerTerminatedConnection
9 | from .rigctld import Rigctld
10 |
11 | class KiwiWorker(threading.Thread):
12 | def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
13 | super(KiwiWorker, self).__init__(group=group, target=target, name=name)
14 | self._recorder, self._options, self._delay_run, self._run_event = args
15 | self._recorder._reader = True
16 | self._event = threading.Event()
17 | self._rigctld = None
18 | if self._options.rigctl_enabled:
19 | self._rigctld = Rigctld(self._recorder, self._options.rigctl_port, self._options.rigctl_address)
20 |
21 | def _do_run(self):
22 | return self._run_event.is_set()
23 |
24 | def run(self):
25 | self.connect_count = self._options.connect_retries
26 | self.busy_count = self._options.busy_retries
27 | if self._delay_run: # let snd/wf get established first
28 | time.sleep(3)
29 |
30 | while self._do_run():
31 | try:
32 | self._recorder.connect(self._options.server_host, self._options.server_port)
33 |
34 | except Exception as e:
35 | logging.warn("Failed to connect, sleeping and reconnecting error='%s'" %e)
36 | if self._options.is_kiwi_tdoa:
37 | self._options.status = 1
38 | break
39 | self.connect_count -= 1
40 | if self._options.connect_retries > 0 and self.connect_count == 0:
41 | break
42 | if self._options.connect_timeout > 0:
43 | self._event.wait(timeout = self._options.connect_timeout)
44 | continue
45 |
46 | try:
47 | self._recorder.open()
48 | while self._do_run():
49 | self._recorder.run()
50 | # do things like freq changes while not receiving sound
51 | if self._rigctld:
52 | self._rigctld.run()
53 |
54 | except KiwiServerTerminatedConnection as e:
55 | if self._options.no_api:
56 | msg = ''
57 | else:
58 | msg = ' Reconnecting after 5 seconds'
59 | logging.info("%s:%s %s.%s" % (self._options.server_host, self._options.server_port, e, msg))
60 | self._recorder.close()
61 | if self._options.no_api: ## don't retry
62 | break
63 | self._recorder._start_ts = None ## this makes the recorder open a new file on restart
64 | self._event.wait(timeout=5)
65 | continue
66 |
67 | except KiwiTooBusyError:
68 | if self._options.is_kiwi_tdoa:
69 | self._options.status = 2
70 | break
71 | self.busy_count -= 1
72 | if self._options.busy_retries > 0 and self.busy_count == 0:
73 | break
74 | logging.warn("%s:%d Too busy now. Reconnecting after %d seconds"
75 | % (self._options.server_host, self._options.server_port, self._options.busy_timeout))
76 | if self._options.busy_timeout > 0:
77 | self._event.wait(timeout = self._options.busy_timeout)
78 | continue
79 |
80 | except KiwiRedirectError as e:
81 | prev = self._options.server_host +':'+ str(self._options.server_port)
82 | # http://host:port
83 | # ^^^^ ^^^^
84 | uri = str(e).split(':')
85 | self._options.server_host = uri[1][2:]
86 | self._options.server_port = uri[2]
87 | logging.warn("%s Too busy now. Redirecting to %s:%s" % (prev, self._options.server_host, self._options.server_port))
88 | if self._options.is_kiwi_tdoa:
89 | self._options.status = 2
90 | break
91 | self._event.wait(timeout=2)
92 | continue
93 |
94 | except KiwiTimeLimitError:
95 | break
96 |
97 | except Exception as e:
98 | if self._options.is_kiwi_tdoa:
99 | self._options.status = 1
100 | print_exc()
101 | break
102 |
103 | self._run_event.clear() # tell all other threads to stop
104 | self._recorder.close()
105 | self._recorder._close_func()
106 | if self._rigctld:
107 | self._rigctld.close()
108 |
--------------------------------------------------------------------------------
/src/read_kiwi_iq_wav.cc:
--------------------------------------------------------------------------------
1 | // -*- C++ -*-
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #if defined(__GNUC__) && not defined(__MINGW32__)
8 | # define _PACKED __attribute__((__packed__))
9 | #else
10 | # define _PACKED
11 | # define _USE_PRAGMA_PACK
12 | #endif
13 |
14 | #ifdef _USE_PRAGMA_PACK
15 | # pragma pack(push, 1)
16 | #endif
17 |
18 | #if __cplusplus != 201103L
19 | # include
20 | # define static_assert(A,B) assert(A)
21 | #endif
22 |
23 | class chunk_base {
24 | public:
25 | chunk_base()
26 | : _id()
27 | , _size() {
28 | static_assert(sizeof(chunk_base) == 8, "chunk_base has wrong packed size");
29 | }
30 | std::string id() const { return std::string((char*)(_id), 4); }
31 | std::streampos size() const { return _size; }
32 | private:
33 | int8_t _id[4];
34 | uint32_t _size;
35 | } _PACKED;
36 |
37 | class chunk_riff : public chunk_base {
38 | public:
39 | chunk_riff()
40 | : _format() {
41 | static_assert(sizeof(chunk_riff) == 8+4, "chunk_riff has wrong packed size");
42 | }
43 | std::string format() const { return std::string((char*)(_format), 4); }
44 |
45 | private:
46 | int8_t _format[4];
47 | } _PACKED;
48 |
49 | class chunk_fmt : public chunk_base {
50 | public:
51 | chunk_fmt()
52 | : _format()
53 | , _num_channels()
54 | , _sample_rate()
55 | , _byte_rate()
56 | , _block_align()
57 | , _dummy() {
58 | static_assert(sizeof(chunk_fmt) == 8+16, "chunk_fmt has wrong packed size");
59 | }
60 | uint16_t format() const { return _format; }
61 | uint16_t num_channels() const { return _num_channels; }
62 | uint32_t sample_rate() const { return _sample_rate; }
63 | uint32_t byte_rate() const { return _byte_rate; }
64 | uint16_t block_align() const { return _block_align; }
65 |
66 | protected:
67 | uint16_t _format;
68 | uint16_t _num_channels;
69 | uint32_t _sample_rate;
70 | uint32_t _byte_rate;
71 | uint16_t _block_align;
72 | uint16_t _dummy;
73 | } _PACKED;
74 |
75 | class chunk_kiwi : public chunk_base {
76 | public:
77 | chunk_kiwi()
78 | : _last()
79 | , _dummy()
80 | , _gpssec()
81 | , _gpsnsec() {
82 | static_assert(sizeof(chunk_kiwi) == 8+10, "chunk_kiwi has wrong packed size");
83 | }
84 | uint8_t last() const { return _last; }
85 | uint32_t gpssec() const { return _gpssec; }
86 | uint32_t gpsnsec() const { return _gpsnsec; }
87 | private:
88 | uint8_t _last, _dummy;
89 | uint32_t _gpssec, _gpsnsec;
90 | } _PACKED;
91 |
92 | #ifdef _USE_PRAGMA_PACK
93 | # pragma pack(pop)
94 | # undef _USE_PRAGMA_PACK
95 | #endif
96 |
97 | #undef _PACKED
98 |
99 | DEFUN_DLD (read_kiwi_iq_wav, args, nargout, "[d,sample_rate]=read_kiwi_iq_wav(\"(i/32768.0f, q/32768.0f);
153 | }
154 | if (j != n)
155 | error("incomplete 'data' chunk");
156 | cell_z(data_counter++) = a;
157 | } else if (c.id() == "kiwi") {
158 | file.seekg(pos);
159 | chunk_kiwi kiwi;
160 | file.read((char*)(&kiwi), sizeof(kiwi));
161 | if (!file)
162 | error("incomplete 'kiwi' chunk");
163 | cell_last(data_counter) = kiwi.last();
164 | cell_gpssec(data_counter) = kiwi.gpssec();
165 | cell_gpsnsec(data_counter) = kiwi.gpsnsec();
166 | } else {
167 | octave_stdout << "skipping unknown chunk '" << c.id() << "'" << std::endl;
168 | file.seekg(file.tellg() + c.size());
169 | }
170 | }
171 | octave_map map;
172 | map.setfield("z", cell_z);
173 | if (cell_last.length() == cell_z.length()) {
174 | map.setfield("gpslast", cell_last);
175 | map.setfield("gpssec", cell_gpssec);
176 | map.setfield("gpsnsec", cell_gpsnsec);
177 | } else {
178 | error("number of GNSS timestamps does not match number of IQ samples");
179 | }
180 | retval(0) = map;
181 |
182 | return retval;
183 | }
184 |
--------------------------------------------------------------------------------
/microkiwi_waterfall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import numpy as np
4 | import struct
5 |
6 | import array
7 | import logging
8 | import socket
9 | import struct
10 | import time
11 | from datetime import datetime
12 |
13 | from kiwi import wsclient
14 |
15 | import mod_pywebsocket.common
16 | from mod_pywebsocket.stream import Stream
17 | from mod_pywebsocket.stream import StreamOptions
18 |
19 | import sys
20 | if sys.version_info > (3,):
21 | buffer = memoryview
22 | def bytearray2str(b):
23 | return b.decode('ascii')
24 | else:
25 | def bytearray2str(b):
26 | return str(b)
27 |
28 | from optparse import OptionParser
29 |
30 | parser = OptionParser()
31 | parser.add_option("-f", "--file", dest="filename", type=str,
32 | help="write waterfall data to binary FILE", metavar="FILE")
33 | parser.add_option("-s", "--server", type=str,
34 | help="server name", dest="server", default='192.168.1.82')
35 | parser.add_option("-p", "--port", type=int,
36 | help="port number", dest="port", default=8073)
37 | parser.add_option("-l", "--length", type=int,
38 | help="how many samples to draw from the server", dest="length", default=100)
39 | parser.add_option("-z", "--zoom", type=int,
40 | help="zoom factor", dest="zoom", default=0)
41 | parser.add_option("-o", "--offset", type=int,
42 | help="start frequency in kHz", dest="offset_khz", default=0)
43 | parser.add_option("-v", "--verbose", type=int,
44 | help="whether to print progress and debug info", dest="verbosity", default=0)
45 |
46 |
47 | options = vars(parser.parse_args()[0])
48 |
49 | if 'filename' in options:
50 | filename = options['filename']
51 | else:
52 | filename = None
53 |
54 | host = options['server']
55 | port = options['port']
56 | print ("KiwiSDR Server: %s:%d" % (host,port))
57 | # the default number of bins is 1024
58 | bins = 1024
59 | print ("Number of waterfall bins: %d" % bins)
60 |
61 | zoom = options['zoom']
62 | print ("Zoom factor:", zoom)
63 |
64 | full_span = 30000.0 # for a 30MHz kiwiSDR
65 | if zoom>0:
66 | span = full_span / 2.**zoom
67 | else:
68 | span = full_span
69 |
70 | start = options['offset_khz']
71 | stop = start + span
72 | rbw = span/bins
73 | center_freq = span/2+start
74 | print ("Start %.3f, Stop %.3f, Center %.3f, Span %.3f (MHz)" % (start/1000, stop/1000, center_freq/1000, span/1000))
75 |
76 | if start < 0 or stop > full_span:
77 | s = "Frequency and zoom values result in span outside 0 - %d kHz range" % full_span
78 | raise Exception(s)
79 |
80 | now = str(datetime.now())
81 | header_bin = struct.pack("II26s", int(center_freq), int(span), bytes(now, 'utf-8'))
82 |
83 | print ("Trying to contact server...")
84 | try:
85 | mysocket = socket.socket()
86 | mysocket.connect((host, port))
87 | except:
88 | print ("Failed to connect")
89 | exit()
90 | print ("Socket open...")
91 |
92 | uri = '/%d/%s' % (int(time.time()), 'W/F')
93 | handshake = wsclient.ClientHandshakeProcessor(mysocket, host, port)
94 | handshake.handshake(uri)
95 |
96 | request = wsclient.ClientRequest(mysocket)
97 | request.ws_version = mod_pywebsocket.common.VERSION_HYBI13
98 |
99 | stream_option = StreamOptions()
100 | stream_option.mask_send = True
101 | stream_option.unmask_receive = False
102 |
103 | mystream = Stream(request, stream_option)
104 | print ("Data stream active...")
105 |
106 |
107 | # send a sequence of messages to the server, hardcoded for now
108 | # max wf speed, no compression
109 | msg_list = ['SET auth t=kiwi p=', 'SET zoom=%d cf=%d'%(zoom,center_freq),\
110 | 'SET maxdb=0 mindb=-100', 'SET wf_speed=4', 'SET wf_comp=0']
111 | for msg in msg_list:
112 | mystream.send_message(msg)
113 | print ("Starting to retrieve waterfall data...")
114 | # number of samples to draw from server
115 | length = options['length']
116 | # create a numpy array to contain the waterfall data
117 | wf_data = np.zeros((length, bins))
118 | binary_wf_list = []
119 | time = 0
120 | while time self.chunksize:
114 | raise RuntimeError
115 | self.file.seek(self.offset + pos, 0)
116 | self.size_read = pos
117 |
118 | def tell(self):
119 | if self.closed:
120 | raise ValueError("I/O operation on closed file")
121 | return self.size_read
122 |
123 | def read(self, size=-1):
124 | """Read at most size bytes from the chunk.
125 | If size is omitted or negative, read until the end
126 | of the chunk.
127 | """
128 |
129 | if self.closed:
130 | raise ValueError("I/O operation on closed file")
131 | if self.size_read >= self.chunksize:
132 | return b''
133 | if size < 0:
134 | size = self.chunksize - self.size_read
135 | if size > self.chunksize - self.size_read:
136 | size = self.chunksize - self.size_read
137 | data = self.file.read(size)
138 | self.size_read = self.size_read + len(data)
139 | if self.size_read == self.chunksize and \
140 | self.align and \
141 | (self.chunksize & 1):
142 | dummy = self.file.read(1)
143 | self.size_read = self.size_read + len(dummy)
144 | return data
145 |
146 | def skip(self):
147 | """Skip the rest of the chunk.
148 | If you are not interested in the contents of the chunk,
149 | this method should be called so that the file points to
150 | the start of the next chunk.
151 | """
152 |
153 | if self.closed:
154 | raise ValueError("I/O operation on closed file")
155 | if self.seekable:
156 | try:
157 | n = self.chunksize - self.size_read
158 | # maybe fix alignment
159 | if self.align and (self.chunksize & 1):
160 | n = n + 1
161 | self.file.seek(n, 1)
162 | self.size_read = self.size_read + n
163 | return
164 | except OSError:
165 | pass
166 | while self.size_read < self.chunksize:
167 | n = min(8192, self.chunksize - self.size_read)
168 | dummy = self.read(n)
169 | if not dummy:
170 | raise EOFError
171 |
--------------------------------------------------------------------------------
/SCANNING:
--------------------------------------------------------------------------------
1 | [updated 17 June 2025]
2 |
3 | A discussion and examples of the kiwirecorder scanning and squelch capabilities.
4 |
5 | Please refer to example invocations in the Makefile.
6 | Search for the "# squelch" and "# scanning" sections of the Makefile.
7 | See also the README.md file for general information about kiwiclient / kiwirecorder.
8 |
9 | Because a list of frequencies associated with scanning might be extensive, and would otherwise
10 | cause the command line invocation of kiwirecorder to become unwieldy, a separate YAML-formatted file
11 | is used to store scanning parameters. In the Makefile this can be seen as:
12 |
13 | --scan-yaml scan_squelch.yaml
14 | --scan-yaml scan_continuous.yaml
15 |
16 | The example scan_squelch.yaml file contains:
17 | Scan:
18 | threshold: 20 # dB
19 | wait: 1 # seconds
20 | dwell: 6 # seconds
21 | # frequencies: [6661, 6559, 6311, 8948, 8912] # kHz, HFDL
22 | frequencies: [4024, 4030, 4039] # kHz, STANAG-4539 bursts
23 | pbc: true
24 |
25 | Whereas the example scan_continuous.yaml contains:
26 | Scan:
27 | wait: 1 # seconds
28 | dwell: 5 # seconds
29 | frequencies: [2500, 3330, 5000, 7850, 10000, 14670, 15000, 20000] # kHz, time stations
30 |
31 | The presence of the "threshold" parameter distinguishes a squelching scan from a continuous scan.
32 | The "#" character precedes a comment.
33 |
34 | A squelching scan works as follows. Kiwirecorder maintains a continuous measurement of the signal
35 | level over time by averaging readings from the S-meter transmitted along with the received audio.
36 | When the signal level on a frequency channel exceeds the threshold the scan stops and a few file
37 | begins recording. When the level drops below the threshold the file is closed and scanning resumes.
38 |
39 | The "wait" time is applied after tuning to the next entry in the frequency list and delays detection
40 | of the signal and possible start of the file recording. "dwell" is the amount of time the scan will
41 | remain on the frequency before moving on to the next one if no signal is detected. The parameter
42 | "--squelch-tail=(secs, default=1)" on the command line (not in the YAML file) determines how long
43 | the recording continues after the signal falls below the threshold.
44 |
45 | Without a threshold parameter the scan becomes continuous. Recording a file for the dwell time on
46 | each frequency in turn, independent of any signal level.
47 |
48 | When discussing receive frequencies there is always the question of carrier/dial frequency versus
49 | passband center frequency. This is only a consideration for passbands that are not symmetrical
50 | around 0 Hz. That is, sideband/CW modes compared to the AM/SAM modes. Although note that you can
51 | always adjust the passband in any mode to be anything you want. So it could change from symmetrical
52 | to asymmetrical. IQ mode is sometimes setup both ways.
53 |
54 | Sometimes a signal is centered on a round number, and if you want to record it in USB (an
55 | assymetrical passband) then it would be nice to specify these round numbered values in the frequency
56 | lists. Instead of mentally subtracting half the passband offset to get the carrier/dial frequency to
57 | enter into the list. That's when you can use the "pbc: true" entry in the YAML file (or the "--pbc"
58 | parameter on the command line). This means the frequency specified is to be interpreted as a
59 | passband center frequency. And the actual carrier/dial frequency is equal to freq - pbc.
60 |
61 | In the scan_squelch.yaml file above the HFDL and STANAG-4539 frequencies are the center of the
62 | signal as seen on the waterfall display (e.g. 4030 kHz). So the "pbc" parameter is used to allow
63 | 4030 to be specified in the frequency list instead of 4028.5 = 4030 - 1.5 kHz, since 1.5 kHz is the
64 | center of the default USB passband of 300 - 2700 Hz (300 + (2700-300)/2 = 1500).
65 |
66 | In the scan_continuous.yaml file all the time station freqnecies are carrier/dial frequencies
67 | since AM mode is used. So the "pbc: true" and "--pbc" parameters are not used.
68 |
69 | Hint: If you hold down the keyboard "shift" key while mousing around the waterfall display the
70 | frequency at the cursor center will be displayed. This is helpful to understand the exact
71 | frequencies of the passband edges.
72 |
73 | Sample output of kiwirecorder using the example scan_continuous.yaml file via running "make scan":
74 |
75 | >>> make clean; make scan
76 | rm -f *.log *.wav *.png *.txt *.npy *.nc
77 | python3 -u kiwirecorder.py -s wessex.zapto.org -p 8074 --log_level=info -m amn --scan-yaml=scan_continuous.yaml
78 | 2025-06-17 09:26:20,576 pid 21197 started sound recorder 0
79 | 2025-06-17 09:26:22,159 pid 21197 Kiwi server version: 1.813
80 | 2025-06-17 09:26:22,519 pid 21197 GNSS position: lat,lon=[+50.74, -2.64]
81 | 2025-06-17 09:26:23,702 pid 21197 GNSS position: lat,lon=[+50.74, -2.64]
82 | Median: -103.2 Thr: -109.2 S Scan: 21:26:34 S DWELL 2500 kHz 2025-06-17 09:26:34,954 pid 21197 Started a new file: 20250616T212634Z_2500000_amn.wav
83 | Block: 00000065, RSSI: -76.2 Scan: 21:26:41 S DWELL 3330 kHz 2025-06-17 09:26:41,125 pid 21197 Started a new file: 20250616T212641Z_3330000_amn.wav
84 | Block: 00000089, RSSI: -88.7 Scan: 21:26:47 S DWELL 5000 kHz 2025-06-17 09:26:47,239 pid 21197 Started a new file: 20250616T212647Z_5000000_amn.wav
85 | Block: 000000ae, RSSI: -98.5 Scan: 21:26:53 S DWELL 7850 kHz 2025-06-17 09:26:53,583 pid 21197 Started a new file: 20250616T212653Z_7850000_amn.wav
86 | Block: 000000d2, RSSI: -84.8 Scan: 21:26:59 S DWELL 10000 kHz 2025-06-17 09:26:59,707 pid 21197 Started a new file: 20250616T212659Z_10000000_amn.wav
87 | Block: 000000f6, RSSI: -107.7 Scan: 21:27:05 S DWELL 14670 kHz 2025-06-17 09:27:05,816 pid 21197 Started a new file: 20250616T212705Z_14670000_amn.wav
88 | Block: 0000011a, RSSI: -106.0 Scan: 21:27:11 S DWELL 15000 kHz 2025-06-17 09:27:11,882 pid 21197 Started a new file: 20250616T212711Z_15000000_amn.wav
89 | Block: 0000013d, RSSI: -114.0 Scan: 21:27:17 S DWELL 20000 kHz 2025-06-17 09:27:17,935 pid 21197 Started a new file: 20250616T212717Z_20000000_amn.wav
90 | Block: 00000162, RSSI: -103.3 Scan: 21:27:24 S DWELL 2500 kHz 2025-06-17 09:27:24,256 pid 21197 Started a new file: 20250616T212724Z_2500000_amn.wav
91 | ^C
92 | Block: 00000177, RSSI: -103.0 Scan: 21:27:27 S DWELL 2500 kHz KeyboardInterrupt: threads successfully closed
93 | >>>
94 |
95 | Try "make scan-sq" to scan with squelching via --scan-yaml=scan_squelch.yaml
96 | To see how squelch alone works without frequency scanning try "make sq".
97 |
98 | Less logging info will be shown if the "--log_level=info" parameter is removed.
99 |
100 | [end-of-document]
101 |
--------------------------------------------------------------------------------
/mod_pywebsocket/_stream_base.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """Base stream class.
32 | """
33 |
34 |
35 | # Note: request.connection.write/read are used in this module, even though
36 | # mod_python document says that they should be used only in connection
37 | # handlers. Unfortunately, we have no other options. For example,
38 | # request.write/read are not suitable because they don't allow direct raw bytes
39 | # writing/reading.
40 |
41 |
42 | import socket
43 |
44 | from mod_pywebsocket import util
45 |
46 |
47 | # Exceptions
48 |
49 |
50 | class ConnectionTerminatedException(Exception):
51 | """This exception will be raised when a connection is terminated
52 | unexpectedly.
53 | """
54 |
55 | pass
56 |
57 |
58 | class InvalidFrameException(ConnectionTerminatedException):
59 | """This exception will be raised when we received an invalid frame we
60 | cannot parse.
61 | """
62 |
63 | pass
64 |
65 |
66 | class BadOperationException(Exception):
67 | """This exception will be raised when send_message() is called on
68 | server-terminated connection or receive_message() is called on
69 | client-terminated connection.
70 | """
71 |
72 | pass
73 |
74 |
75 | class UnsupportedFrameException(Exception):
76 | """This exception will be raised when we receive a frame with flag, opcode
77 | we cannot handle. Handlers can just catch and ignore this exception and
78 | call receive_message() again to continue processing the next frame.
79 | """
80 |
81 | pass
82 |
83 |
84 | class InvalidUTF8Exception(Exception):
85 | """This exception will be raised when we receive a text frame which
86 | contains invalid UTF-8 strings.
87 | """
88 |
89 | pass
90 |
91 |
92 | class StreamBase(object):
93 | """Base stream class."""
94 |
95 | def __init__(self, request):
96 | """Construct an instance.
97 |
98 | Args:
99 | request: mod_python request.
100 | """
101 |
102 | self._logger = util.get_class_logger(self)
103 |
104 | self._request = request
105 |
106 | def _read(self, length):
107 | """Reads length bytes from connection. In case we catch any exception,
108 | prepends remote address to the exception message and raise again.
109 |
110 | Raises:
111 | ConnectionTerminatedException: when read returns empty string.
112 | """
113 |
114 | try:
115 | read_bytes = self._request.connection.read(length)
116 | if not read_bytes:
117 | raise ConnectionTerminatedException(
118 | 'Receiving %d byte failed. Peer (%r) closed connection' %
119 | (length, (self._request.connection.remote_addr,)))
120 | return read_bytes
121 | except socket.error as e:
122 | # Catch a socket.error. Because it's not a child class of the
123 | # IOError prior to Python 2.6, we cannot omit this except clause.
124 | # Use %s rather than %r for the exception to use human friendly
125 | # format.
126 | raise ConnectionTerminatedException(
127 | 'Receiving %d byte failed. socket.error (%s) occurred' %
128 | (length, e))
129 | except IOError as e:
130 | # Also catch an IOError because mod_python throws it.
131 | raise ConnectionTerminatedException(
132 | 'Receiving %d byte failed. IOError (%s) occurred' %
133 | (length, e))
134 |
135 | def _write(self, bytes_to_write):
136 | """Writes given bytes to connection. In case we catch any exception,
137 | prepends remote address to the exception message and raise again.
138 | """
139 |
140 | try:
141 | self._request.connection.write(bytes_to_write)
142 | except Exception as e:
143 | util.prepend_message_to_exception(
144 | 'Failed to send message to %r: ' %
145 | (self._request.connection.remote_addr,),
146 | e)
147 | raise
148 |
149 | def receive_bytes(self, length):
150 | """Receives multiple bytes. Retries read when we couldn't receive the
151 | specified amount.
152 |
153 | Raises:
154 | ConnectionTerminatedException: when read returns empty string.
155 | """
156 |
157 | read_bytes = []
158 | while length > 0:
159 | new_read_bytes = self._read(length)
160 | read_bytes.append(new_read_bytes)
161 | length -= len(new_read_bytes)
162 | if length!=0 and type(read_bytes[0]) == str:
163 | return ''.join(read_bytes)
164 | else:
165 | return bytearray().join(read_bytes)
166 |
167 | def _read_until(self, delim_char):
168 | """Reads bytes until we encounter delim_char. The result will not
169 | contain delim_char.
170 |
171 | Raises:
172 | ConnectionTerminatedException: when read returns empty string.
173 | """
174 |
175 | read_bytes = []
176 | while True:
177 | ch = self._read(1)
178 | if ch == delim_char:
179 | break
180 | read_bytes.append(ch)
181 | return ''.join(read_bytes)
182 |
183 |
184 | # vi:sts=4 sw=4 et
185 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [updated 20 August 2025]
2 |
3 | # KiwiClient
4 |
5 | This is the version v1.7 Python client for KiwiSDR. It allows you to:
6 |
7 | * Receive data streams with audio samples, IQ samples, S-meter and waterfall data
8 | * Issue commands to the KiwiSDR
9 |
10 | ## Install
11 |
12 | Visit [github.com/jks-prv/kiwiclient](https://github.com/jks-prv/kiwiclient)
13 | Click the green `'Code'` button and select `'Download ZIP'`
14 | You should find the file `'kiwiclient-master.zip'` in your download directory.
15 | Copy it to the appropriate destination and unzip it if necessary.
16 | Perhaps rename the resulting directory `'kiwiclient-master'` to just `'kiwiclient'`.
17 | Change to that directory.
18 | Here you will find a `'Makefile'` containing various usage examples.
19 | Assuming your system has `'Make'` and `'Python3'` installed type `'make help'` to get started.
20 | Or without `'Make'` type `'python3 kiwirecorder.py --help'`
21 | It is also possible to use the `'git'` tools to checkout a kiwiclient clone that is easier to keep updated.
22 | For example `'git clone https://github.com/jks-prv/kiwiclient.git'`
23 |
24 | ## Dependencies
25 |
26 | Python3 is required.
27 |
28 | Make sure the Python package `'numpy'` is installed.
29 | On many Linux distributions the command would be similar to `'apt install python3-numpy'`
30 | On macOS try `'pip3 install numpy'` or perhaps `'python3 -m pip install numpy'`
31 |
32 | ## Resampling
33 |
34 | If you want high-quality resampling based on libsamplerate (SRC) you should build the version
35 | included with KiwiClient that has fixes rather than using the standard python-samplerate package.
36 | Follow these steps. Ask on the Kiwi forum if you have problems: [forum.kiwisdr.com](https://forum.kiwisdr.com)
37 | * Install the Python package `'cffi'`
38 | * Install the `'libsamplerate'` library using your system's package manager.
39 | Note: this is not the Python package `'samplerate'` but the native code library `'libsamplerate'`
40 | (e.g. x86\_64 or arm64).
41 | * Windows: download from `'github.com/libsndfile/libsamplerate/releases'`
42 | * Linux: use a package manager, e.g. `'apt install libsamplerate'` or `'libsamplerate0'`
43 | * macOS: use a package manager like brew: `'brew install libsamplerate'`
44 | * Run the samplerate module builder `'make samplerate_build'`.
45 | This generates a Python wrapper around `'libsamplerate'` in the file `'samplerate/_src.py'`
46 | * Install `'pytest'` using the Python package manager or perhaps `'pip3 install pytest'`
47 | * Test by running `'make samplerate_test'`
48 | * If your system says `'pytest'` isn't found try `'make samplerate_test2'`
49 |
50 | If you can't build the Kiwi version then install the regular Python package: `'pip3 install samplerate'`
51 | If either samplerate module is not found then low-quality resampling based on linear interpolation is used.
52 | A message indicating which resampler is being used will be shown.
53 | Set the environment variable `'USE_LIBSAMPLERATE'` to '`False'` to force the linear interpolator to be used.
54 |
55 | ## Demo code
56 |
57 | The following demo programs are provided. Use the `--help` argument to see all program options.
58 |
59 | * `kiwirecorder`: Record audio to WAV files, with squelch. Option `--wf` prints various waterfall statistics.
Adding option `--wf-png` records the waterfall as a PNG file. `--help` for more info.
60 | * `kiwiwfrecorder`: Specialty program. Saves waterfall data and GPS timestamps to .npy format file.
61 | * `kiwifax`: Decode radiofax and save as PNGs, with auto start, stop, and phasing.
62 | * `kiwiclientd`: Plays Kiwi audio on sound cards (real & virtual) for use by programs like fldigi and wsjtx.
63 | Implements hamlib rigctl network interface so the Kiwi freq & mode can be controlled by these programs.
64 | * `kiwi_nc`: Deprecated. Use the `--nc` option with `kiwirecorder`. Command line pipeline tool in the style of `netcat`.
65 | Example: streaming IQ samples to `dumphfdl` (see the `Makefile` target `dumphfdl`).
66 |
67 | The `Makefile` contains numerous examples of how to use these programs.
68 |
69 | ## IS0KYB micro tools
70 |
71 | Two utilities have been added to simplify the waterfall data acquisition/storage and data analysis.
72 | The SNR ratio (a la Pierre Ynard) is computed each time.
73 | There is now the possibility to change zoom level and offset frequency.
74 |
75 | * `microkiwi_waterfall.py`: launch this program with no filename and just the SNR will be computed, with a filename, the raw waterfall data is saved. Launch with `--help` to list all options.
76 | * `waterfall_data_analysis.ipynb`: this is a demo jupyter notebook to interactively analyze waterfall data. Easily transformable into a standalone python program.
77 |
78 | The data is, at the moment, transferred in uncompressed format.
79 |
80 | ## Guide to the code
81 |
82 | ### kiwirecorder.py
83 | * Can record audio data, IQ samples, and waterfall data.
84 | * The complete list of options can be obtained by `python3 kiwirecorder.py --help` or `make help`.
85 | * It is possible to record from more than one KiwiSDR simultaneously, see again `--help`.
86 | * For recording IQ samples there is the `-w` or `--kiwi-wav` option: this writes a .wav file
87 | which includes GNSS timestamps (see below).
88 | * The `--netcat` option can stream raw or .wav-formatted samples to standard output.
89 | * Kiwirecorder can "camp" onto an existing KiwiSDR audio channel. See the `--camp-chan` option.
90 | * AGC options can be specified in a YAML-formatted file, `--agc-yaml` option, see `default_agc.yaml`.
91 | * Scanning options (with optional squelch) can be specified in a YAML-formatted file, `--scan-yaml` option.
92 | See the file `SCANNING` for detailed info.
93 | * Note the above YAML options need PyYAML to be installed.
94 | * See the `Makefile` for many usage examples.
95 | * Ask on the KiwiSDR Forum for help: [forum.kiwisdr.com](https://forum.kiwisdr.com)
96 |
97 | ### kiwiclient.py
98 |
99 | Base class for receiving websocket data from a KiwiSDR.
100 | It provides the following methods which can be used in derived classes:
101 |
102 | * `_process_audio_samples(self, seq, samples, rssi)`: audio samples
103 | * `_process_iq_samples(self, seq, samples, rssi, gps)`: IQ samples
104 | * `_process_waterfall_samples(self, seq, samples)`: waterfall data
105 |
106 | ## IQ .wav files with GNSS timestamps
107 | ### kiwirecorder.py configuration
108 | * Use the option `-m iq --kiwi-wav --station=[name]` for recording IQ samples with GNSS time stamps.
109 | * The resulting .wav files contains non-standard WAV chunks with GNSS timestamps.
110 | * If a directory with name `gnss_pos/` exists, a text file `gnss_pos/[name].txt` will be created which contains latitude and longitude as provided by the KiwiSDR; existing files are overwritten.
111 |
112 | ### Working with the recorded .wav files
113 | * There is an Octave extension for reading such WAV files, see `read_kiwi_iq_wav.cc` where the details of the non-standard WAV chunk can be found; it needs to be compiled in this way: `make install`.
114 | * For using read_kiwi_iq_wav an Octave function `proc_kiwi_iq_wav.m` is provided; type `help proc_kiwi_iq_wav` in Octave for documentation.
115 | * There is a Makefile rule to invoke the above: `make proc f=filename.wav`
116 | * For checking the integrity of a .wav file using `client/wavreader.py` run: `make wav f=filename.wav`
117 |
118 | [end-of-document]
119 |
--------------------------------------------------------------------------------
/mod_pywebsocket/http_header_util.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """Utilities for parsing and formatting headers that follow the grammar defined
32 | in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
33 | """
34 |
35 | try:
36 | from urllib.parse import urlparse
37 | except ImportError:
38 | import urlparse
39 |
40 | _SEPARATORS = '()<>@,;:\\"/[]?={} \t'
41 |
42 |
43 | def _is_char(c):
44 | """Returns true iff c is in CHAR as specified in HTTP RFC."""
45 |
46 | return ord(c) <= 127
47 |
48 |
49 | def _is_ctl(c):
50 | """Returns true iff c is in CTL as specified in HTTP RFC."""
51 |
52 | return ord(c) <= 31 or ord(c) == 127
53 |
54 |
55 | class ParsingState(object):
56 |
57 | def __init__(self, data):
58 | self.data = data
59 | self.head = 0
60 |
61 |
62 | def peek(state, pos=0):
63 | """Peeks the character at pos from the head of data."""
64 |
65 | if state.head + pos >= len(state.data):
66 | return None
67 |
68 | return state.data[state.head + pos]
69 |
70 |
71 | def consume(state, amount=1):
72 | """Consumes specified amount of bytes from the head and returns the
73 | consumed bytes. If there's not enough bytes to consume, returns None.
74 | """
75 |
76 | if state.head + amount > len(state.data):
77 | return None
78 |
79 | result = state.data[state.head:state.head + amount]
80 | state.head = state.head + amount
81 | return result
82 |
83 |
84 | def consume_string(state, expected):
85 | """Given a parsing state and a expected string, consumes the string from
86 | the head. Returns True if consumed successfully. Otherwise, returns
87 | False.
88 | """
89 |
90 | pos = 0
91 |
92 | for c in expected:
93 | if c != peek(state, pos):
94 | return False
95 | pos += 1
96 |
97 | consume(state, pos)
98 | return True
99 |
100 |
101 | def consume_lws(state):
102 | """Consumes a LWS from the head. Returns True if any LWS is consumed.
103 | Otherwise, returns False.
104 |
105 | LWS = [CRLF] 1*( SP | HT )
106 | """
107 |
108 | original_head = state.head
109 |
110 | consume_string(state, '\r\n')
111 |
112 | pos = 0
113 |
114 | while True:
115 | c = peek(state, pos)
116 | if c == ' ' or c == '\t':
117 | pos += 1
118 | else:
119 | if pos == 0:
120 | state.head = original_head
121 | return False
122 | else:
123 | consume(state, pos)
124 | return True
125 |
126 |
127 | def consume_lwses(state):
128 | """Consumes *LWS from the head."""
129 |
130 | while consume_lws(state):
131 | pass
132 |
133 |
134 | def consume_token(state):
135 | """Consumes a token from the head. Returns the token or None if no token
136 | was found.
137 | """
138 |
139 | pos = 0
140 |
141 | while True:
142 | c = peek(state, pos)
143 | if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
144 | if pos == 0:
145 | return None
146 |
147 | return consume(state, pos)
148 | else:
149 | pos += 1
150 |
151 |
152 | def consume_token_or_quoted_string(state):
153 | """Consumes a token or a quoted-string, and returns the token or unquoted
154 | string. If no token or quoted-string was found, returns None.
155 | """
156 |
157 | original_head = state.head
158 |
159 | if not consume_string(state, '"'):
160 | return consume_token(state)
161 |
162 | result = []
163 |
164 | expect_quoted_pair = False
165 |
166 | while True:
167 | if not expect_quoted_pair and consume_lws(state):
168 | result.append(' ')
169 | continue
170 |
171 | c = consume(state)
172 | if c is None:
173 | # quoted-string is not enclosed with double quotation
174 | state.head = original_head
175 | return None
176 | elif expect_quoted_pair:
177 | expect_quoted_pair = False
178 | if _is_char(c):
179 | result.append(c)
180 | else:
181 | # Non CHAR character found in quoted-pair
182 | state.head = original_head
183 | return None
184 | elif c == '\\':
185 | expect_quoted_pair = True
186 | elif c == '"':
187 | return ''.join(result)
188 | elif _is_ctl(c):
189 | # Invalid character %r found in qdtext
190 | state.head = original_head
191 | return None
192 | else:
193 | result.append(c)
194 |
195 |
196 | def quote_if_necessary(s):
197 | """Quotes arbitrary string into quoted-string."""
198 |
199 | quote = False
200 | if s == '':
201 | return '""'
202 |
203 | result = []
204 | for c in s:
205 | if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
206 | quote = True
207 |
208 | if c == '"' or _is_ctl(c):
209 | result.append('\\' + c)
210 | else:
211 | result.append(c)
212 |
213 | if quote:
214 | return '"' + ''.join(result) + '"'
215 | else:
216 | return ''.join(result)
217 |
218 |
219 | def parse_uri(uri):
220 | """Parse absolute URI then return host, port and resource."""
221 |
222 | parsed = urlparse.urlsplit(uri)
223 | if parsed.scheme != 'wss' and parsed.scheme != 'ws':
224 | # |uri| must be a relative URI.
225 | # TODO(toyoshim): Should validate |uri|.
226 | return None, None, uri
227 |
228 | if parsed.hostname is None:
229 | return None, None, None
230 |
231 | port = None
232 | try:
233 | port = parsed.port
234 | except ValueError as e:
235 | # port property cause ValueError on invalid null port description like
236 | # 'ws://host:/path'.
237 | return None, None, None
238 |
239 | if port is None:
240 | if parsed.scheme == 'ws':
241 | port = 80
242 | else:
243 | port = 443
244 |
245 | path = parsed.path
246 | if not path:
247 | path += '/'
248 | if parsed.query:
249 | path += '?' + parsed.query
250 | if parsed.fragment:
251 | path += '#' + parsed.fragment
252 |
253 | return parsed.hostname, port, path
254 |
255 |
256 | #try:
257 | # urlparse.uses_netloc.index('ws')
258 | # urlparse.uses_netloc.index('ws')
259 | #except ValueError as e:
260 | # # urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries.
261 | # urlparse.uses_netloc.append('ws')
262 | # urlparse.uses_netloc.append('wss')
263 |
264 |
265 | # vi:sts=4 sw=4 et
266 |
--------------------------------------------------------------------------------
/mod_pywebsocket/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """WebSocket extension for Apache HTTP Server.
32 |
33 | mod_pywebsocket is a WebSocket extension for Apache HTTP Server
34 | intended for testing or experimental purposes. mod_python is required.
35 |
36 |
37 | Installation
38 | ============
39 |
40 | 0. Prepare an Apache HTTP Server for which mod_python is enabled.
41 |
42 | 1. Specify the following Apache HTTP Server directives to suit your
43 | configuration.
44 |
45 | If mod_pywebsocket is not in the Python path, specify the following.
46 | is the directory where mod_pywebsocket is installed.
47 |
48 | PythonPath "sys.path+['']"
49 |
50 | Always specify the following. is the directory where
51 | user-written WebSocket handlers are placed.
52 |
53 | PythonOption mod_pywebsocket.handler_root
54 | PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
55 |
56 | To limit the search for WebSocket handlers to a directory
57 | under , configure as follows:
58 |
59 | PythonOption mod_pywebsocket.handler_scan
60 |
61 | is useful in saving scan time when
62 | contains many non-WebSocket handler files.
63 |
64 | If you want to allow handlers whose canonical path is not under the root
65 | directory (i.e. symbolic link is in root directory but its target is not),
66 | configure as follows:
67 |
68 | PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On
69 |
70 | Example snippet of httpd.conf:
71 | (mod_pywebsocket is in /websock_lib, WebSocket handlers are in
72 | /websock_handlers, port is 80 for ws, 443 for wss.)
73 |
74 |
75 | PythonPath "sys.path+['/websock_lib']"
76 | PythonOption mod_pywebsocket.handler_root /websock_handlers
77 | PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
78 |
79 |
80 | 2. Tune Apache parameters for serving WebSocket. We'd like to note that at
81 | least TimeOut directive from core features and RequestReadTimeout
82 | directive from mod_reqtimeout should be modified not to kill connections
83 | in only a few seconds of idle time.
84 |
85 | 3. Verify installation. You can use example/console.html to poke the server.
86 |
87 |
88 | Writing WebSocket handlers
89 | ==========================
90 |
91 | When a WebSocket request comes in, the resource name
92 | specified in the handshake is considered as if it is a file path under
93 | and the handler defined in
94 | /_wsh.py is invoked.
95 |
96 | For example, if the resource name is /example/chat, the handler defined in
97 | /example/chat_wsh.py is invoked.
98 |
99 | A WebSocket handler is composed of the following three functions:
100 |
101 | web_socket_do_extra_handshake(request)
102 | web_socket_transfer_data(request)
103 | web_socket_passive_closing_handshake(request)
104 |
105 | where:
106 | request: mod_python request.
107 |
108 | web_socket_do_extra_handshake is called during the handshake after the
109 | headers are successfully parsed and WebSocket properties (ws_location,
110 | ws_origin, and ws_resource) are added to request. A handler
111 | can reject the request by raising an exception.
112 |
113 | A request object has the following properties that you can use during the
114 | extra handshake (web_socket_do_extra_handshake):
115 | - ws_resource
116 | - ws_origin
117 | - ws_version
118 | - ws_location (HyBi 00 only)
119 | - ws_extensions (HyBi 06 and later)
120 | - ws_deflate (HyBi 06 and later)
121 | - ws_protocol
122 | - ws_requested_protocols (HyBi 06 and later)
123 |
124 | The last two are a bit tricky. See the next subsection.
125 |
126 |
127 | Subprotocol Negotiation
128 | -----------------------
129 |
130 | For HyBi 06 and later, ws_protocol is always set to None when
131 | web_socket_do_extra_handshake is called. If ws_requested_protocols is not
132 | None, you must choose one subprotocol from this list and set it to
133 | ws_protocol.
134 |
135 | For HyBi 00, when web_socket_do_extra_handshake is called,
136 | ws_protocol is set to the value given by the client in
137 | Sec-WebSocket-Protocol header or None if
138 | such header was not found in the opening handshake request. Finish extra
139 | handshake with ws_protocol untouched to accept the request subprotocol.
140 | Then, Sec-WebSocket-Protocol header will be sent to
141 | the client in response with the same value as requested. Raise an exception
142 | in web_socket_do_extra_handshake to reject the requested subprotocol.
143 |
144 |
145 | Data Transfer
146 | -------------
147 |
148 | web_socket_transfer_data is called after the handshake completed
149 | successfully. A handler can receive/send messages from/to the client
150 | using request. mod_pywebsocket.msgutil module provides utilities
151 | for data transfer.
152 |
153 | You can receive a message by the following statement.
154 |
155 | message = request.ws_stream.receive_message()
156 |
157 | This call blocks until any complete text frame arrives, and the payload data
158 | of the incoming frame will be stored into message. When you're using IETF
159 | HyBi 00 or later protocol, receive_message() will return None on receiving
160 | client-initiated closing handshake. When any error occurs, receive_message()
161 | will raise some exception.
162 |
163 | You can send a message by the following statement.
164 |
165 | request.ws_stream.send_message(message)
166 |
167 |
168 | Closing Connection
169 | ------------------
170 |
171 | Executing the following statement or just return-ing from
172 | web_socket_transfer_data cause connection close.
173 |
174 | request.ws_stream.close_connection()
175 |
176 | close_connection will wait
177 | for closing handshake acknowledgement coming from the client. When it
178 | couldn't receive a valid acknowledgement, raises an exception.
179 |
180 | web_socket_passive_closing_handshake is called after the server receives
181 | incoming closing frame from the client peer immediately. You can specify
182 | code and reason by return values. They are sent as a outgoing closing frame
183 | from the server. A request object has the following properties that you can
184 | use in web_socket_passive_closing_handshake.
185 | - ws_close_code
186 | - ws_close_reason
187 |
188 |
189 | Threading
190 | ---------
191 |
192 | A WebSocket handler must be thread-safe if the server (Apache or
193 | standalone.py) is configured to use threads.
194 |
195 |
196 | Configuring WebSocket Extension Processors
197 | ------------------------------------------
198 |
199 | See extensions.py for supported WebSocket extensions. Note that they are
200 | unstable and their APIs are subject to change substantially.
201 |
202 | A request object has these extension processing related attributes.
203 |
204 | - ws_requested_extensions:
205 |
206 | A list of common.ExtensionParameter instances representing extension
207 | parameters received from the client in the client's opening handshake.
208 | You shouldn't modify it manually.
209 |
210 | - ws_extensions:
211 |
212 | A list of common.ExtensionParameter instances representing extension
213 | parameters to send back to the client in the server's opening handshake.
214 | You shouldn't touch it directly. Instead, call methods on extension
215 | processors.
216 |
217 | - ws_extension_processors:
218 |
219 | A list of loaded extension processors. Find the processor for the
220 | extension you want to configure from it, and call its methods.
221 | """
222 |
223 |
224 | # vi:sts=4 sw=4 et tw=72
225 |
--------------------------------------------------------------------------------
/samplerate/lowlevel.py:
--------------------------------------------------------------------------------
1 | """Lowlevel wrappers around libsamplerate.
2 |
3 | The docstrings of the `src_*` functions are adapted from the libsamplerate
4 | header file.
5 | """
6 | import os as _os
7 | import sys as _sys
8 | from ctypes.util import find_library as _find_library
9 |
10 | import numpy as _np
11 |
12 | # Locate and load libsamplerate
13 | from samplerate._src import ffi
14 | lib_basename = 'libsamplerate'
15 | lib_filename = _find_library('samplerate')
16 |
17 | if _os.environ.get('READTHEDOCS') == 'True':
18 | # Mock minimum C API for Read the Docs
19 | class MockLib(object):
20 | @classmethod
21 | def src_get_version(cls):
22 | return ffi.new('char[]', 'libsamplerate-0.1.9 (c) ...')
23 | lib_filename = 'mock'
24 | _lib = MockLib()
25 | elif lib_filename is None:
26 | if _sys.platform == 'darwin':
27 | lib_filename = '{}.dylib'.format(lib_basename)
28 | elif _sys.platform == 'win32':
29 | from platform import architecture
30 | lib_filename = '{}-{}.dll'.format(lib_basename, architecture()[0])
31 | else:
32 | raise OSError('{} not found'.format(lib_basename))
33 | lib_filename = _os.path.join(
34 | _os.path.dirname(_os.path.abspath(__file__)), '_samplerate_data',
35 | lib_filename)
36 | _lib = ffi.dlopen(lib_filename)
37 | else:
38 | _lib = ffi.dlopen(lib_filename)
39 |
40 |
41 | def _check_data(data):
42 | """Check whether `data` is a valid input/output for libsamplerate.
43 |
44 | Returns
45 | -------
46 | num_frames
47 | Number of frames in `data`.
48 | channels
49 | Number of channels in `data`.
50 |
51 | Raises
52 | ------
53 | ValueError: If invalid data is supplied.
54 | """
55 | if not (data.dtype == _np.float32 and data.flags.c_contiguous):
56 | raise ValueError('supplied data must be float32 and C contiguous')
57 | if data.ndim == 2:
58 | num_frames, channels = data.shape
59 | elif data.ndim == 1:
60 | num_frames, channels = data.size, 1
61 | else:
62 | raise ValueError('rank > 2 not supported')
63 | return num_frames, channels
64 |
65 |
66 | def src_strerror(error):
67 | """Convert the error number into a string."""
68 | return ffi.string(_lib.src_strerror(error)).decode()
69 |
70 |
71 | def src_get_name(converter_type):
72 | """Return the name of the converter given by `converter_type`."""
73 | return ffi.string(_lib.src_get_name(converter_type)).decode()
74 |
75 |
76 | def src_get_description(converter_type):
77 | """Return the description of the converter given by `converter_type`."""
78 | return ffi.string(_lib.src_get_description(converter_type)).decode()
79 |
80 |
81 | def src_get_version():
82 | """Return the version string of libsamplerate."""
83 | return ffi.string(_lib.src_get_version()).decode()
84 |
85 |
86 | def src_simple(input_data, output_data, ratio, converter_type, channels):
87 | """Perform a single conversion from an input buffer to an output buffer.
88 |
89 | Simple interface for performing a single conversion from input buffer to
90 | output buffer at a fixed conversion ratio. Simple interface does not require
91 | initialisation as it can only operate on a single buffer worth of audio.
92 | """
93 | input_frames, _ = _check_data(input_data)
94 | output_frames, _ = _check_data(output_data)
95 | data = ffi.new('SRC_DATA*')
96 | data.input_frames = input_frames
97 | data.output_frames = output_frames
98 | data.src_ratio = ratio
99 | data.data_in = ffi.cast('float*', ffi.from_buffer(input_data))
100 | data.data_out = ffi.cast('float*', ffi.from_buffer(output_data))
101 | error = _lib.src_simple(data, converter_type, channels)
102 | return error, data.input_frames_used, data.output_frames_gen
103 |
104 |
105 | def src_new(converter_type, channels):
106 | """Initialise a new sample rate converter.
107 |
108 | Parameters
109 | ----------
110 | converter_type : int
111 | Converter to be used.
112 | channels : int
113 | Number of channels.
114 |
115 | Returns
116 | -------
117 | state
118 | An anonymous pointer to the internal state of the converter.
119 | error : int
120 | Error code.
121 | """
122 | error = ffi.new('int*')
123 | state = _lib.src_new(converter_type, channels, error)
124 | return state, error[0]
125 |
126 |
127 | def src_delete(state):
128 | """Release `state`.
129 |
130 | Cleanup all internal allocations.
131 | """
132 | _lib.src_delete(state)
133 |
134 |
135 | def src_process(state, input_data, output_data, ratio, end_of_input=0):
136 | """Standard processing function.
137 |
138 | Returns non zero on error.
139 | """
140 | input_frames, _ = _check_data(input_data)
141 | output_frames, _ = _check_data(output_data)
142 | data = ffi.new('SRC_DATA*')
143 | data.input_frames = input_frames
144 | data.output_frames = output_frames
145 | data.src_ratio = ratio
146 | data.data_in = ffi.cast('float*', ffi.from_buffer(input_data))
147 | data.data_out = ffi.cast('float*', ffi.from_buffer(output_data))
148 | data.end_of_input = end_of_input
149 | error = _lib.src_process(state, data)
150 | return error, data.input_frames_used, data.output_frames_gen
151 |
152 |
153 | def src_error(state):
154 | """Return an error number."""
155 | return _lib.src_error(state) if state else None
156 |
157 |
158 | def src_reset(state):
159 | """Reset the internal SRC state.
160 |
161 | Does not modify the quality settings.
162 | Does not free any memory allocations.
163 | Returns non zero on error.
164 | """
165 | return _lib.src_reset(state) if state else None
166 |
167 |
168 | def src_set_ratio(state, new_ratio):
169 | """Set a new SRC ratio.
170 |
171 | This allows step responses in the conversion ratio.
172 | Returns non zero on error.
173 | """
174 | return _lib.src_set_ratio(state, new_ratio) if state else None
175 |
176 |
177 | def src_is_valid_ratio(ratio):
178 | """Return `True` if ratio is a valid conversion ratio, `False` otherwise.
179 | """
180 | return bool(_lib.src_is_valid_ratio(ratio))
181 |
182 |
183 | @ffi.callback('src_callback_t')
184 | def _src_input_callback(cb_data, data):
185 | """Internal callback function to be used with the callback API.
186 |
187 | Pulls the Python callback function from the handle contained in `cb_data`
188 | and calls it to fetch frames. Frames are converted to the format required by
189 | the API (float, interleaved channels). A reference to these data is kept
190 | internally.
191 |
192 | Returns
193 | -------
194 | frames : int
195 | The number of frames supplied.
196 | """
197 | cb_data = ffi.from_handle(cb_data)
198 | ret = cb_data['callback']()
199 | if ret is None:
200 | cb_data['last_input'] = None
201 | return 0 # No frames supplied
202 | input_data = _np.require(ret, requirements='C', dtype=_np.float32)
203 | input_frames, channels = _check_data(input_data)
204 |
205 | # Check whether the correct number of channels is supplied by user.
206 | if cb_data['channels'] != channels:
207 | raise ValueError('Invalid number of channels in callback.')
208 |
209 | # Store a reference of the input data to ensure it is still alive when
210 | # accessed by libsamplerate.
211 | cb_data['last_input'] = input_data
212 |
213 | data[0] = ffi.cast('float*', ffi.from_buffer(input_data))
214 | return input_frames
215 |
216 |
217 | def src_callback_new(callback, converter_type, channels):
218 | """Initialisation for the callback based API.
219 |
220 | Parameters
221 | ----------
222 | callback : function
223 | Called whenever new frames are to be read. Must return a NumPy array
224 | of shape (num_frames, channels).
225 | converter_type : int
226 | Converter to be used.
227 | channels : int
228 | Number of channels.
229 |
230 | Returns
231 | -------
232 | state
233 | An anonymous pointer to the internal state of the converter.
234 | handle
235 | A CFFI handle to the callback data.
236 | error : int
237 | Error code.
238 |
239 | """
240 | cb_data = {'callback': callback, 'channels': channels}
241 | handle = ffi.new_handle(cb_data)
242 | error = ffi.new('int*')
243 | state = _lib.src_callback_new(_src_input_callback, converter_type,
244 | channels, error, handle)
245 | if state == ffi.NULL:
246 | return None, handle, error[0]
247 | return state, handle, error[0]
248 |
249 |
250 | def src_callback_read(state, ratio, frames, data):
251 | """Read up to `frames` worth of data using the callback API.
252 |
253 | Returns
254 | -------
255 | frames : int
256 | Number of frames read or -1 on error.
257 | """
258 | data_ptr = ffi.cast('float*f', ffi.from_buffer(data))
259 | return _lib.src_callback_read(state, ratio, frames, data_ptr)
260 |
261 |
262 | __libsamplerate_version__ = src_get_version()
263 | if __libsamplerate_version__.startswith(lib_basename):
264 | __libsamplerate_version__ = __libsamplerate_version__[len(
265 | lib_basename) + 1:__libsamplerate_version__.find(' ')]
266 |
--------------------------------------------------------------------------------
/mod_pywebsocket/_stream_hixie75.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """This file provides a class for parsing/building frames of the WebSocket
32 | protocol version HyBi 00 and Hixie 75.
33 |
34 | Specification:
35 | - HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
36 | - Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
37 | """
38 |
39 |
40 | from mod_pywebsocket import common
41 | from mod_pywebsocket._stream_base import BadOperationException
42 | from mod_pywebsocket._stream_base import ConnectionTerminatedException
43 | from mod_pywebsocket._stream_base import InvalidFrameException
44 | from mod_pywebsocket._stream_base import StreamBase
45 | from mod_pywebsocket._stream_base import UnsupportedFrameException
46 | from mod_pywebsocket import util
47 |
48 |
49 | class StreamHixie75(StreamBase):
50 | """A class for parsing/building frames of the WebSocket protocol version
51 | HyBi 00 and Hixie 75.
52 | """
53 |
54 | def __init__(self, request, enable_closing_handshake=False):
55 | """Construct an instance.
56 |
57 | Args:
58 | request: mod_python request.
59 | enable_closing_handshake: to let StreamHixie75 perform closing
60 | handshake as specified in HyBi 00, set
61 | this option to True.
62 | """
63 |
64 | StreamBase.__init__(self, request)
65 |
66 | self._logger = util.get_class_logger(self)
67 |
68 | self._enable_closing_handshake = enable_closing_handshake
69 |
70 | self._request.client_terminated = False
71 | self._request.server_terminated = False
72 |
73 | def send_message(self, message, end=True, binary=False):
74 | """Send message.
75 |
76 | Args:
77 | message: unicode string to send.
78 | binary: not used in hixie75.
79 |
80 | Raises:
81 | BadOperationException: when called on a server-terminated
82 | connection.
83 | """
84 |
85 | if not end:
86 | raise BadOperationException(
87 | 'StreamHixie75 doesn\'t support send_message with end=False')
88 |
89 | if binary:
90 | raise BadOperationException(
91 | 'StreamHixie75 doesn\'t support send_message with binary=True')
92 |
93 | if self._request.server_terminated:
94 | raise BadOperationException(
95 | 'Requested send_message after sending out a closing handshake')
96 |
97 | self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
98 |
99 | def _read_payload_length_hixie75(self):
100 | """Reads a length header in a Hixie75 version frame with length.
101 |
102 | Raises:
103 | ConnectionTerminatedException: when read returns empty string.
104 | """
105 |
106 | length = 0
107 | while True:
108 | b_str = self._read(1)
109 | b = ord(b_str)
110 | length = length * 128 + (b & 0x7f)
111 | if (b & 0x80) == 0:
112 | break
113 | return length
114 |
115 | def receive_message(self):
116 | """Receive a WebSocket frame and return its payload an unicode string.
117 |
118 | Returns:
119 | payload unicode string in a WebSocket frame.
120 |
121 | Raises:
122 | ConnectionTerminatedException: when read returns empty
123 | string.
124 | BadOperationException: when called on a client-terminated
125 | connection.
126 | """
127 |
128 | if self._request.client_terminated:
129 | raise BadOperationException(
130 | 'Requested receive_message after receiving a closing '
131 | 'handshake')
132 |
133 | while True:
134 | # Read 1 byte.
135 | # mp_conn.read will block if no bytes are available.
136 | # Timeout is controlled by TimeOut directive of Apache.
137 | frame_type_str = self.receive_bytes(1)
138 | frame_type = ord(frame_type_str)
139 | if (frame_type & 0x80) == 0x80:
140 | # The payload length is specified in the frame.
141 | # Read and discard.
142 | length = self._read_payload_length_hixie75()
143 | if length > 0:
144 | _ = self.receive_bytes(length)
145 | # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
146 | # /client terminated/ flag and abort these steps.
147 | if not self._enable_closing_handshake:
148 | continue
149 |
150 | if frame_type == 0xFF and length == 0:
151 | self._request.client_terminated = True
152 |
153 | if self._request.server_terminated:
154 | self._logger.debug(
155 | 'Received ack for server-initiated closing '
156 | 'handshake')
157 | return None
158 |
159 | self._logger.debug(
160 | 'Received client-initiated closing handshake')
161 |
162 | self._send_closing_handshake()
163 | self._logger.debug(
164 | 'Sent ack for client-initiated closing handshake')
165 | return None
166 | else:
167 | # The payload is delimited with \xff.
168 | bytes = self._read_until('\xff')
169 | # The WebSocket protocol section 4.4 specifies that invalid
170 | # characters must be replaced with U+fffd REPLACEMENT
171 | # CHARACTER.
172 | message = bytes.decode('utf-8', 'replace')
173 | if frame_type == 0x00:
174 | return message
175 | # Discard data of other types.
176 |
177 | def _send_closing_handshake(self):
178 | if not self._enable_closing_handshake:
179 | raise BadOperationException(
180 | 'Closing handshake is not supported in Hixie 75 protocol')
181 |
182 | self._request.server_terminated = True
183 |
184 | # 5.3 the server may decide to terminate the WebSocket connection by
185 | # running through the following steps:
186 | # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
187 | # start of the closing handshake.
188 | self._write('\xff\x00')
189 |
190 | def close_connection(self, unused_code='', unused_reason=''):
191 | """Closes a WebSocket connection.
192 |
193 | Raises:
194 | ConnectionTerminatedException: when closing handshake was
195 | not successfull.
196 | """
197 |
198 | if self._request.server_terminated:
199 | self._logger.debug(
200 | 'Requested close_connection but server is already terminated')
201 | return
202 |
203 | if not self._enable_closing_handshake:
204 | self._request.server_terminated = True
205 | self._logger.debug('Connection closed')
206 | return
207 |
208 | self._send_closing_handshake()
209 | self._logger.debug('Sent server-initiated closing handshake')
210 |
211 | # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
212 | # or until a server-defined timeout expires.
213 | #
214 | # For now, we expect receiving closing handshake right after sending
215 | # out closing handshake, and if we couldn't receive non-handshake
216 | # frame, we take it as ConnectionTerminatedException.
217 | message = self.receive_message()
218 | if message is not None:
219 | raise ConnectionTerminatedException(
220 | 'Didn\'t receive valid ack for closing handshake')
221 | # TODO: 3. close the WebSocket connection.
222 | # note: mod_python Connection (mp_conn) doesn't have close method.
223 |
224 | def send_ping(self, body):
225 | raise BadOperationException(
226 | 'StreamHixie75 doesn\'t support send_ping')
227 |
228 |
229 | # vi:sts=4 sw=4 et
230 |
--------------------------------------------------------------------------------
/kiwi/rigctld.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # Emulates a subset of the hamlib rigctld interface, allowing programs
4 | # like fldigi and wsjtx to query and change the frequency and mode of
5 | # a kiwisdr channel.
6 | #
7 | # The subset of commands corresponds to the commands actually used by
8 | # fldigi and wsjtx.
9 | #
10 | # (C) 2020 Rik van Riel
11 | # Released under the GNU General Public License (GPL) version 2 or newer
12 |
13 | import array
14 | import logging
15 | import socket
16 | import struct
17 | import time
18 | import select
19 |
20 | class rigsocket(socket.socket):
21 | def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
22 | super().__init__(family, type, proto, fileno)
23 | self.buffer=""
24 |
25 | def recv_command(self):
26 | buf = self.recv(4096)
27 | try:
28 | self.buffer += buf.decode('ASCII')
29 | except socket.error:
30 | # just ignore non-ASCII
31 | self.buffer = ""
32 | return ""
33 |
34 | if len(self.buffer) == 0:
35 | return ""
36 |
37 | # the buffer contains one or more complete commands
38 | if self.buffer[-1] == "\n":
39 | result = self.buffer
40 | self.buffer = ""
41 | return result
42 |
43 | # nabbed from socket.accept, but returns a rigsock instead
44 | def accept(self):
45 | fd, addr = self._accept()
46 | rigsock = rigsocket(self.family, self.type, self.proto, fileno=fd)
47 | if socket.getdefaulttimeout() is None and self.gettimeout():
48 | sock.setblocking(True)
49 | return rigsock, addr
50 |
51 |
52 | class Rigctld(object):
53 | def __init__(self, kiwisdrstream=None, port=None, ipaddr=None):
54 | self._kiwisdrstream = kiwisdrstream
55 | self._listenport = port
56 | self._clientsockets = []
57 | # default localhost on port 6400
58 | if port == None:
59 | port = 6400
60 | if ipaddr == None:
61 | ipaddr = "127.0.0.1"
62 |
63 | try:
64 | socket.inet_aton(ipaddr)
65 | addr = (ipaddr, port)
66 | s = rigsocket(socket.AF_INET, socket.SOCK_STREAM)
67 | except socket.error:
68 | addr = (ipaddr, port, 0, 0)
69 | s = rigsocket(socket.AF_INET6, socket.SOCK_STREAM)
70 |
71 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
72 | s.setblocking(0)
73 |
74 | try:
75 | s.bind(addr)
76 | except socket.error:
77 | logging.error("could not bind to port ", port)
78 | s.close()
79 | raise
80 |
81 | s.listen()
82 | self._serversocket = s
83 |
84 | def close(self):
85 | for s in self._clientsockets:
86 | s.close()
87 | self._clientsockets.remove(s)
88 | self._serversocket.close()
89 |
90 | def _set_modulation(self, command):
91 | # The M (set modulation) command has two parameters:
92 | # M
93 | # mode is mandatory, passband is optional
94 | try:
95 | splitcmd = command.split()
96 | mod = splitcmd[1]
97 | try:
98 | hc = int(splitcmd[2])
99 | except:
100 | hc = None
101 | freq = self._kiwisdrstream.get_frequency()
102 | # print("calling set_mod", mod, lc, hc, freq)
103 | self._kiwisdrstream.set_mod(mod, None, hc, freq)
104 | return "RPRT 0\n"
105 | except:
106 | return "RPRT -1\n"
107 |
108 | def _set_frequency(self, command):
109 | try:
110 | # hamlib freq is in Hz, kiwisdr in kHz
111 | # format: F 1234.00000
112 | newfreq = command[2:]
113 | freq = float(newfreq) / 1000
114 | except:
115 | try:
116 | # format: F VFOA 1234.00000
117 | newfreq = command[7:]
118 | freq = float(newfreq) / 1000
119 | except:
120 | print("could not decode frequency from %s" % command)
121 | return "RPRT -1\n"
122 |
123 | try:
124 | mod = self._kiwisdrstream.get_mod()
125 | lc = self._kiwisdrstream.get_lowcut()
126 | hc = self._kiwisdrstream.get_highcut()
127 | # print("calling set_mod ", mod, lc, hc, freq)
128 | self._kiwisdrstream.set_mod(mod, lc, hc, freq)
129 | return "RPRT 0\n"
130 | except:
131 | return "RPRT -1\n"
132 |
133 | def _dump_state(self):
134 | # hamlib expects this large table of rig info when connecting
135 | rigctlver = "0\n"
136 | rig_model = "2\n"
137 | itu_region = "0\n"
138 | freqs = "0.000000 30000000.000000"
139 | modes = "0x2f" # AM LSB USB CW (NB)FM see hamlib/rig.h
140 | # no tx power, one VFO per channel, one antenna
141 | rx_range = "{} {} -1 -1 0x1 0x1\n".format(freqs, modes)
142 | rx_end = "0 0 0 0 0 0 0\n"
143 | tx_range = ""
144 | tx_end = "0 0 0 0 0 0 0\n"
145 | tuningsteps = ""
146 | for step in ["1", "100", "1000", "5000", "9000", "10000"]:
147 | tuningsteps += "{} {}\n".format(modes, step)
148 | steps_end = "0 0\n"
149 | ssbfilt = "0xc 2200\n"
150 | cwfilt = "0x2 500\n"
151 | amfilt = "0x1 6000\n"
152 | fmfilt = "0x20 12000\n"
153 | filt_end = "0 0\n"
154 | max_rit = "0\n"
155 | max_xit = "0\n"
156 | max_ifshift = "0\n"
157 | announces = "0\n"
158 | preamp = "\n"
159 | attenuator = "\n"
160 | get_func = "0x0\n"
161 | set_func = "0x0\n"
162 | get_level = "0x0\n"
163 | set_level = "0x0\n"
164 | get_parm = "0x0\n"
165 | set_parm = "0x0\n"
166 | vfo_ops = "vfo_ops=0x0\n"
167 | ptt_type = "ptt_type=0x0\n"
168 | done = "done\n"
169 |
170 | message = rigctlver + rig_model + itu_region
171 | message += rx_range + rx_end + tx_range + tx_end
172 | message += tuningsteps + steps_end
173 | message += ssbfilt + cwfilt + amfilt + fmfilt + filt_end
174 | message += max_rit + max_xit + max_ifshift + announces
175 | message += preamp + attenuator
176 | message += get_func + set_func + get_level + set_level
177 | message += get_parm + set_parm + vfo_ops + ptt_type + done
178 |
179 | return message
180 |
181 | def _handle_command(self, sock, command):
182 | # Remove leading '\' from command
183 | if command.startswith("\\"):
184 | command = command[1:]
185 |
186 | if command.startswith('q'):
187 | # quit
188 | try:
189 | sock.send("RPRT 0\n".encode('ASCII'))
190 | sock.close()
191 | self._clientsockets.remove(sock)
192 | except:
193 | pass
194 | return "RPRT 0\n"
195 | elif command.startswith('chk_vfo'):
196 | return "CHKVFO 0\n"
197 | elif command.startswith('get_lock_mode'):
198 | # unlocked
199 | return "2\n"
200 | elif command.startswith('get_powerstat'):
201 | # always report that power is on
202 | return "1\n"
203 | elif command.startswith('dump_state'):
204 | return self._dump_state()
205 | elif command.startswith('f'):
206 | # get frequency
207 | freqinhz = int(self._kiwisdrstream.get_frequency() * 1000)
208 | return "{}\n".format(freqinhz)
209 | elif command.startswith('F'):
210 | return self._set_frequency(command)
211 | elif command.startswith('m'):
212 | # get modulation
213 | highcut = int(self._kiwisdrstream._highcut)
214 | mod = self._kiwisdrstream.get_mod()
215 | return "{}\n{}\n".format(mod.upper(), highcut)
216 | elif command.startswith('M'):
217 | return self._set_modulation(command)
218 | elif command.startswith('s'):
219 | # get split mode
220 | return "0\nVFOA\n"
221 | elif command.startswith('v'):
222 | return "VFOA\n"
223 | elif command.startswith('V'):
224 | # We cannot switch the VFO on a single kiwisdr channel
225 | return "RPRT 0\n"
226 |
227 | print("Received unknown command: ", command)
228 | return "RPRT 0\n"
229 |
230 | def run(self):
231 | # first accept a new connection, if there is any
232 | try:
233 | sock, addr = self._serversocket.accept()
234 | self._clientsockets.append(sock)
235 | except socket.error:
236 | # no new connections this time
237 | pass
238 |
239 | # check for incoming traffic on existing connections
240 | read_list = self._clientsockets
241 | readable, writable, errored = select.select(read_list, [], [], 0) if len(read_list) > 0 else ([],[],[])
242 |
243 | for s in errored:
244 | s.close()
245 | self._clientsockets.remove(s)
246 |
247 | for s in readable:
248 | try:
249 | command = s.recv_command()
250 | except socket.error:
251 | continue
252 |
253 | if command != None and len(command) > 0:
254 | reply = ""
255 | # sometimes hamlib programs send multiple commands at once
256 | for line in command.splitlines():
257 | reply += self._handle_command(s, command)
258 | else:
259 | continue
260 |
261 | try:
262 | reply = reply.encode('ASCII')
263 | s.send(reply)
264 | except socket.error:
265 | continue
266 | # EOF
267 |
--------------------------------------------------------------------------------
/mod_pywebsocket/common.py:
--------------------------------------------------------------------------------
1 | # Copyright 2012, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """This file must not depend on any module specific to the WebSocket protocol.
32 | """
33 |
34 |
35 | from mod_pywebsocket import http_header_util
36 |
37 |
38 | # Additional log level definitions.
39 | LOGLEVEL_FINE = 9
40 |
41 | # Constants indicating WebSocket protocol version.
42 | VERSION_HIXIE75 = -1
43 | VERSION_HYBI00 = 0
44 | VERSION_HYBI01 = 1
45 | VERSION_HYBI02 = 2
46 | VERSION_HYBI03 = 2
47 | VERSION_HYBI04 = 4
48 | VERSION_HYBI05 = 5
49 | VERSION_HYBI06 = 6
50 | VERSION_HYBI07 = 7
51 | VERSION_HYBI08 = 8
52 | VERSION_HYBI09 = 8
53 | VERSION_HYBI10 = 8
54 | VERSION_HYBI11 = 8
55 | VERSION_HYBI12 = 8
56 | VERSION_HYBI13 = 13
57 | VERSION_HYBI14 = 13
58 | VERSION_HYBI15 = 13
59 | VERSION_HYBI16 = 13
60 | VERSION_HYBI17 = 13
61 |
62 | # Constants indicating WebSocket protocol latest version.
63 | VERSION_HYBI_LATEST = VERSION_HYBI13
64 |
65 | # Port numbers
66 | DEFAULT_WEB_SOCKET_PORT = 80
67 | DEFAULT_WEB_SOCKET_SECURE_PORT = 443
68 |
69 | # Schemes
70 | WEB_SOCKET_SCHEME = 'ws'
71 | WEB_SOCKET_SECURE_SCHEME = 'wss'
72 |
73 | # Frame opcodes defined in the spec.
74 | OPCODE_CONTINUATION = 0x0
75 | OPCODE_TEXT = 0x1
76 | OPCODE_BINARY = 0x2
77 | OPCODE_CLOSE = 0x8
78 | OPCODE_PING = 0x9
79 | OPCODE_PONG = 0xa
80 |
81 | # UUIDs used by HyBi 04 and later opening handshake and frame masking.
82 | WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
83 |
84 | # Opening handshake header names and expected values.
85 | UPGRADE_HEADER = 'Upgrade'
86 | WEBSOCKET_UPGRADE_TYPE = 'websocket'
87 | WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
88 | CONNECTION_HEADER = 'Connection'
89 | UPGRADE_CONNECTION_TYPE = 'Upgrade'
90 | HOST_HEADER = 'Host'
91 | ORIGIN_HEADER = 'Origin'
92 | SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin'
93 | SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
94 | SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
95 | SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
96 | SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
97 | SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
98 | SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft'
99 | SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1'
100 | SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
101 | SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
102 |
103 | # Extensions
104 | DEFLATE_FRAME_EXTENSION = 'deflate-frame'
105 | PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
106 | X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
107 | MUX_EXTENSION = 'mux_DO_NOT_USE'
108 |
109 | # Status codes
110 | # Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
111 | # STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
112 | # Could not be used for codes in actual closing frames.
113 | # Application level errors must use codes in the range
114 | # STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
115 | # range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
116 | # by IANA. Usually application must define user protocol level errors in the
117 | # range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
118 | STATUS_NORMAL_CLOSURE = 1000
119 | STATUS_GOING_AWAY = 1001
120 | STATUS_PROTOCOL_ERROR = 1002
121 | STATUS_UNSUPPORTED_DATA = 1003
122 | STATUS_NO_STATUS_RECEIVED = 1005
123 | STATUS_ABNORMAL_CLOSURE = 1006
124 | STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
125 | STATUS_POLICY_VIOLATION = 1008
126 | STATUS_MESSAGE_TOO_BIG = 1009
127 | STATUS_MANDATORY_EXTENSION = 1010
128 | STATUS_INTERNAL_ENDPOINT_ERROR = 1011
129 | STATUS_TLS_HANDSHAKE = 1015
130 | STATUS_USER_REGISTERED_BASE = 3000
131 | STATUS_USER_REGISTERED_MAX = 3999
132 | STATUS_USER_PRIVATE_BASE = 4000
133 | STATUS_USER_PRIVATE_MAX = 4999
134 | # Following definitions are aliases to keep compatibility. Applications must
135 | # not use these obsoleted definitions anymore.
136 | STATUS_NORMAL = STATUS_NORMAL_CLOSURE
137 | STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
138 | STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
139 | STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
140 | STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
141 | STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
142 |
143 | # HTTP status codes
144 | HTTP_STATUS_BAD_REQUEST = 400
145 | HTTP_STATUS_FORBIDDEN = 403
146 | HTTP_STATUS_NOT_FOUND = 404
147 |
148 |
149 | def is_control_opcode(opcode):
150 | return (opcode >> 3) == 1
151 |
152 |
153 | class ExtensionParameter(object):
154 |
155 | """This is exchanged on extension negotiation in opening handshake."""
156 |
157 | def __init__(self, name):
158 | self._name = name
159 | # TODO(tyoshino): Change the data structure to more efficient one such
160 | # as dict when the spec changes to say like
161 | # - Parameter names must be unique
162 | # - The order of parameters is not significant
163 | self._parameters = []
164 |
165 | def name(self):
166 | """Return the extension name."""
167 | return self._name
168 |
169 | def add_parameter(self, name, value):
170 | """Add a parameter."""
171 | self._parameters.append((name, value))
172 |
173 | def get_parameters(self):
174 | """Return the parameters."""
175 | return self._parameters
176 |
177 | def get_parameter_names(self):
178 | """Return the names of the parameters."""
179 | return [name for name, unused_value in self._parameters]
180 |
181 | def has_parameter(self, name):
182 | """Test if a parameter exists."""
183 | for param_name, param_value in self._parameters:
184 | if param_name == name:
185 | return True
186 | return False
187 |
188 | def get_parameter_value(self, name):
189 | """Get the value of a specific parameter."""
190 | for param_name, param_value in self._parameters:
191 | if param_name == name:
192 | return param_value
193 |
194 |
195 | class ExtensionParsingException(Exception):
196 |
197 | """Exception to handle errors in extension parsing."""
198 |
199 | def __init__(self, name):
200 | super(ExtensionParsingException, self).__init__(name)
201 |
202 |
203 | def _parse_extension_param(state, definition):
204 | param_name = http_header_util.consume_token(state)
205 |
206 | if param_name is None:
207 | raise ExtensionParsingException('No valid parameter name found')
208 |
209 | http_header_util.consume_lwses(state)
210 |
211 | if not http_header_util.consume_string(state, '='):
212 | definition.add_parameter(param_name, None)
213 | return
214 |
215 | http_header_util.consume_lwses(state)
216 |
217 | # TODO(tyoshino): Add code to validate that parsed param_value is token
218 | param_value = http_header_util.consume_token_or_quoted_string(state)
219 | if param_value is None:
220 | raise ExtensionParsingException(
221 | 'No valid parameter value found on the right-hand side of '
222 | 'parameter %r' % param_name)
223 |
224 | definition.add_parameter(param_name, param_value)
225 |
226 |
227 | def _parse_extension(state):
228 | extension_token = http_header_util.consume_token(state)
229 | if extension_token is None:
230 | return None
231 |
232 | extension = ExtensionParameter(extension_token)
233 |
234 | while True:
235 | http_header_util.consume_lwses(state)
236 |
237 | if not http_header_util.consume_string(state, ';'):
238 | break
239 |
240 | http_header_util.consume_lwses(state)
241 |
242 | try:
243 | _parse_extension_param(state, extension)
244 | except ExtensionParsingException as e:
245 | raise ExtensionParsingException(
246 | 'Failed to parse parameter for %r (%r)' %
247 | (extension_token, e))
248 |
249 | return extension
250 |
251 |
252 | def parse_extensions(data):
253 | """Parse Sec-WebSocket-Extensions header value.
254 |
255 | Returns a list of ExtensionParameter objects.
256 | Leading LWSes must be trimmed.
257 | """
258 | state = http_header_util.ParsingState(data)
259 |
260 | extension_list = []
261 | while True:
262 | extension = _parse_extension(state)
263 | if extension is not None:
264 | extension_list.append(extension)
265 |
266 | http_header_util.consume_lwses(state)
267 |
268 | if http_header_util.peek(state) is None:
269 | break
270 |
271 | if not http_header_util.consume_string(state, ','):
272 | raise ExtensionParsingException(
273 | 'Failed to parse Sec-WebSocket-Extensions header: '
274 | 'Expected a comma but found %r' %
275 | http_header_util.peek(state))
276 |
277 | http_header_util.consume_lwses(state)
278 |
279 | if len(extension_list) == 0:
280 | raise ExtensionParsingException(
281 | 'No valid extension entry found')
282 |
283 | return extension_list
284 |
285 |
286 | def format_extension(extension):
287 | """Format an ExtensionParameter object."""
288 | formatted_params = [extension.name()]
289 | for param_name, param_value in extension.get_parameters():
290 | if param_value is None:
291 | formatted_params.append(param_name)
292 | else:
293 | quoted_value = http_header_util.quote_if_necessary(param_value)
294 | formatted_params.append('%s=%s' % (param_name, quoted_value))
295 | return '; '.join(formatted_params)
296 |
297 |
298 | def format_extensions(extension_list):
299 | """Format a list of ExtensionParameter objects."""
300 | formatted_extension_list = []
301 | for extension in extension_list:
302 | formatted_extension_list.append(format_extension(extension))
303 | return ', '.join(formatted_extension_list)
304 |
305 |
306 | # vi:sts=4 sw=4 et
307 |
--------------------------------------------------------------------------------
/kiwiwfrecorder.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | ## -*- python -*-
3 |
4 | ## to be merged into kiwirecorder.py
5 |
6 | import gc, logging, os, time, threading, os
7 | import numpy as np
8 | from traceback import print_exc
9 | from kiwi import KiwiSDRStream, KiwiWorker
10 | from optparse import OptionParser
11 | try:
12 | from Queue import Queue,Empty ## python2
13 | except ImportError:
14 | from queue import Queue,Empty ## python3
15 |
16 |
17 | class KiwiSoundRecorder(KiwiSDRStream):
18 | def __init__(self, options, q):
19 | super(KiwiSoundRecorder, self).__init__()
20 | self._options = options
21 | self._queue = q
22 | self._type = 'SND'
23 | self._freq = options.frequency
24 | self._num_skip = 2 ## skip data at the start of the WS stream with seq < 2
25 |
26 | def _setup_rx_params(self):
27 | self.set_name(self._options.user)
28 | mod = 'iq'
29 | lp_cut = -1000
30 | hp_cut = +1000
31 | self.set_mod(mod, lp_cut, hp_cut, self._freq)
32 | self.set_agc(on=True)
33 |
34 | def _process_iq_samples(self, seq, samples, rssi, gps, fmt):
35 | if self._num_skip != 0:
36 | if seq < 2:
37 | print('IQ: skipping seq=', seq)
38 | self._num_skip -= 1
39 | return
40 | else:
41 | self._num_skip = 0
42 | gps_time = gps['gpssec'] + 1e-9*gps['gpsnsec']
43 | self._queue.put([seq, gps_time])
44 |
45 | class KiwiWaterfallRecorder(KiwiSDRStream):
46 | def __init__(self, options, q):
47 | super(KiwiWaterfallRecorder, self).__init__()
48 | self._options = options
49 | self._queue = q
50 | self._type = 'W/F'
51 | self._freq = options.frequency
52 | self._zoom = options.zoom
53 | self._freq_bins = None
54 | self._num_channels = 2
55 | self._num_skip = 2 ## skip data at the start of the WS stream with seq < 2
56 |
57 | def _setup_rx_params(self):
58 | self._set_zoom_cf(self._zoom, self._freq)
59 | self._set_maxdb_mindb(-10, -110) # needed, but values don't matter
60 | self._freq_bins = self._freq + (0.5+np.arange(self.WF_BINS))/self.WF_BINS * self.zoom_to_span(self._options.zoom)
61 | #self._set_wf_comp(True)
62 | self._set_wf_comp(False)
63 | self._set_wf_speed(1) # 1 Hz update
64 | self.set_name(self._options.user)
65 |
66 | def _process_waterfall_samples(self, seq, samples):
67 | if self._num_skip != 0:
68 | if seq < 2:
69 | self._num_skip -= 1
70 | return
71 | else:
72 | self._num_skip = 0
73 | logging.info('process_wf_samples: seq= %5d %s' % (seq, samples))
74 | self._queue.put({'seq': seq,
75 | 'freq_bins': self._freq_bins,
76 | 'wf_samples': samples})
77 |
78 | class Consumer(threading.Thread):
79 | """ Combines WF data with precise GNSS timestamps from the SND stream """
80 | def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
81 | super(Consumer, self).__init__(group=group, target=target, name=name)
82 | self._options, self._snd_queue, self._wf_queue, self._run_event = args
83 | self._event = threading.Event()
84 | self._store = dict()
85 | self._wf_data = None
86 | self._start_ts = None
87 |
88 | def run(self):
89 | while self._run_event.is_set():
90 | try:
91 | self.proc()
92 | except Exception:
93 | print_exc()
94 | break
95 | self._run_event.clear() # tell all other threads to stop
96 |
97 | def proc(self):
98 | if self._wf_data is None:
99 | try:
100 | self._wf_data = self._wf_queue.get(timeout=1)
101 | except Empty:
102 | return
103 |
104 | self.update_store()
105 | if self._wf_data['seq'] in self._store:
106 | now = time.gmtime()
107 | if self._start_ts is None:
108 | self._start_ts = now
109 | with open(self._get_output_filename(), 'wb') as f:
110 | np.save(f, self._wf_data['freq_bins'])
111 |
112 | ## GNSS timestamp for seq obtained from the SND WS stream
113 | ts = self._store.pop(self._wf_data['seq'])
114 | logging.info('found seq %d %f %d (%d|%d,%d)'
115 | % (self._wf_data['seq'], ts, len(self._wf_data['wf_samples']),
116 | len(self._store), self._wf_queue.qsize(), self._snd_queue.qsize()))
117 | with open(self._get_output_filename(), 'ab') as f:
118 | np.save(f, np.array((ts, self._wf_data['wf_samples']),
119 | dtype=[('ts', np.float64), ('wf', ('B', 1024))]))
120 |
121 | self.prune_store(ts)
122 | self._wf_data = None
123 | else:
124 | time.sleep(0.1)
125 |
126 | def _get_output_filename(self):
127 | station = '' if self._options.station is None else '_'+ self._options.station
128 | return '%s_%d-%d%s.npy' % (time.strftime('%Y%m%dT%H%M%SZ', self._start_ts),
129 | round(self._wf_data['freq_bins'][ 0] * 1000),
130 | round(self._wf_data['freq_bins'][-1] * 1000),
131 | station)
132 |
133 | def update_store(self):
134 | """ put all available timestamps from the SND stream into the store """
135 | while True:
136 | try:
137 | seq,ts = self._snd_queue.get(timeout=0.01)
138 | self._store[seq] = ts
139 | except Empty:
140 | break
141 | except Exception:
142 | print_exc()
143 | break
144 |
145 | def prune_store(self, ts):
146 | """ remove all timestamps before 'ts' """
147 | for x in list(self._store.items()):
148 | k,v = x
149 | if v < ts:
150 | self._store.pop(k)
151 |
152 | def join_threads(threads):
153 | [t._event.set() for t in threads]
154 | [t.join() for t in threading.enumerate() if t is not threading.current_thread()]
155 |
156 | def main():
157 | parser = OptionParser()
158 | parser.add_option('-s', '--server-host',
159 | dest='server_host', type='string',
160 | default='localhost', help='Server host')
161 | parser.add_option('-p', '--server-port',
162 | dest='server_port', type='string',
163 | default="8073", help='Server port, default 8073')
164 | parser.add_option('--pw', '--password',
165 | dest='password', type='string', default='',
166 | help='Kiwi login password')
167 | parser.add_option('--connect-timeout', '--connect_timeout',
168 | dest='connect_timeout',
169 | type='int', default=15,
170 | help='Retry timeout(sec) connecting to host')
171 | parser.add_option('--connect-retries', '--connect_retries',
172 | dest='connect_retries',
173 | type='int', default=0,
174 | help='Number of retries when connecting to host (retries forever by default)')
175 | parser.add_option('--busy-timeout', '--busy_timeout',
176 | dest='busy_timeout',
177 | type='int', default=15,
178 | help='Retry timeout(sec) when host is busy')
179 | parser.add_option('--busy-retries', '--busy_retries',
180 | dest='busy_retries',
181 | type='int', default=0,
182 | help='Number of retries when host is busy (retries forever by default)')
183 | parser.add_option('-k', '--socket-timeout', '--socket_timeout',
184 | dest='socket_timeout', type='int', default=10,
185 | help='Timeout(sec) for sockets')
186 | parser.add_option('--tlimit-pw', '--tlimit-password',
187 | dest='tlimit_password', type='string', default='',
188 | help='Connect time limit exemption password (if required)')
189 | parser.add_option('-u', '--user',
190 | dest='user', type='string', default='kiwirecorder.py',
191 | help='Kiwi connection user name')
192 | parser.add_option('-f', '--freq',
193 | dest='frequency',
194 | type='float', default=1000,
195 | help='Frequency to tune to, in kHz')
196 | parser.add_option('-z', '--zoom',
197 | dest='zoom', type='int', default=0,
198 | help='Zoom level 0-14')
199 | parser.add_option('--station',
200 | dest='station',
201 | type='string', default=None,
202 | help='Station ID to be appended to filename',)
203 | parser.add_option('--log', '--log-level', '--log_level', type='choice',
204 | dest='log_level', default='warn',
205 | choices=['debug', 'info', 'warn', 'error', 'critical'],
206 | help='Log level: debug|info|warn(default)|error|critical')
207 | (opt, unused_args) = parser.parse_args()
208 |
209 | ## clean up OptionParser which has cyclic references
210 | parser.destroy()
211 |
212 | opt.rigctl_enabled = False
213 | opt.is_kiwi_tdoa = False
214 | opt.tlimit = None
215 | opt.no_api = True
216 | opt.nolocal = False
217 | opt.S_meter = -1
218 | opt.ADC_OV = None
219 | opt.freq_pbc = None
220 | opt.wf_cal = None
221 | opt.netcat = False
222 | opt.wideband = False
223 |
224 | FORMAT = '%(asctime)-15s pid %(process)5d %(message)s'
225 | logging.basicConfig(level=logging.getLevelName(opt.log_level.upper()), format=FORMAT)
226 | #if opt.log_level.upper() == 'DEBUG':
227 | # gc.set_debug(gc.DEBUG_SAVEALL | gc.DEBUG_LEAK | gc.DEBUG_UNCOLLECTABLE)
228 |
229 | run_event = threading.Event()
230 | run_event.set()
231 |
232 | snd_queue,wf_queue = [Queue(),Queue()]
233 | snd_recorder = KiwiWorker(args=(KiwiSoundRecorder (opt, snd_queue), opt, False, run_event))
234 | wf_recorder = KiwiWorker(args=(KiwiWaterfallRecorder(opt, wf_queue), opt, False, run_event))
235 | consumer = Consumer(args=(opt,snd_queue,wf_queue,run_event))
236 |
237 | threads = [snd_recorder, wf_recorder, consumer]
238 |
239 | try:
240 | opt.start_time = time.time()
241 | opt.ws_timestamp = int(time.time() + os.getpid()) & 0xffffffff
242 | opt.idx = 0
243 | snd_recorder.start()
244 |
245 | opt.start_time = time.time()
246 | opt.ws_timestamp = int(time.time() + os.getpid()+1) & 0xffffffff
247 | opt.idx = 0
248 | wf_recorder.start()
249 |
250 | consumer.start()
251 |
252 | while run_event.is_set():
253 | time.sleep(.5)
254 |
255 | except KeyboardInterrupt:
256 | run_event.clear()
257 | join_threads(threads)
258 | print("KeyboardInterrupt: threads successfully closed")
259 | except Exception:
260 | print_exc()
261 | run_event.clear()
262 | join_threads(threads)
263 | print("Exception: threads successfully closed")
264 |
265 | #logging.debug('gc %s' % gc.garbage)
266 |
267 | if __name__ == '__main__':
268 | #import faulthandler
269 | #faulthandler.enable()
270 | main()
271 | # EOF
272 |
--------------------------------------------------------------------------------
/samplerate/converters.py:
--------------------------------------------------------------------------------
1 | """Converters
2 |
3 | """
4 | from __future__ import print_function, division
5 | from enum import Enum
6 | import numpy as np
7 |
8 |
9 | class ConverterType(Enum):
10 | """Enum of samplerate converter types.
11 |
12 | Pass any of the members, or their string or value representation, as
13 | ``converter_type`` in the resamplers.
14 | """
15 | sinc_best = 0
16 | sinc_medium = 1
17 | sinc_fastest = 2
18 | zero_order_hold = 3
19 | linear = 4
20 |
21 |
22 | def _get_converter_type(identifier):
23 | """Return the converter type for `identifier`."""
24 | if isinstance(identifier, str):
25 | return ConverterType[identifier]
26 | if isinstance(identifier, ConverterType):
27 | return identifier
28 | return ConverterType(identifier)
29 |
30 |
31 | def resample(input_data, ratio, converter_type='sinc_best', verbose=False):
32 | """Resample the signal in `input_data` at once.
33 |
34 | Parameters
35 | ----------
36 | input_data : ndarray
37 | Input data. A single channel is provided as a 1D array of `num_frames` length.
38 | Input data with several channels is represented as a 2D array of shape
39 | (`num_frames`, `num_channels`). For use with `libsamplerate`, `input_data`
40 | is converted to 32-bit float and C (row-major) memory order.
41 | ratio : float
42 | Conversion ratio = output sample rate / input sample rate.
43 | converter_type : ConverterType, str, or int
44 | Sample rate converter.
45 | verbose : bool
46 | If `True`, print additional information about the conversion.
47 |
48 | Returns
49 | -------
50 | output_data : ndarray
51 | Resampled input data.
52 |
53 | Note
54 | ----
55 | If samples are to be processed in chunks, `Resampler` and
56 | `CallbackResampler` will provide better results and allow for variable
57 | conversion ratios.
58 | """
59 | from samplerate.lowlevel import src_simple
60 | from samplerate.exceptions import ResamplingError
61 |
62 | input_data = np.require(input_data, requirements='C', dtype=np.float32)
63 | if input_data.ndim == 2:
64 | num_frames, channels = input_data.shape
65 | output_shape = (int(num_frames * ratio), channels)
66 | elif input_data.ndim == 1:
67 | num_frames, channels = input_data.size, 1
68 | output_shape = (int(num_frames * ratio), )
69 | else:
70 | raise ValueError('rank > 2 not supported')
71 |
72 | output_data = np.empty(output_shape, dtype=np.float32)
73 | converter_type = _get_converter_type(converter_type)
74 |
75 | (error, input_frames_used, output_frames_gen) \
76 | = src_simple(input_data, output_data, ratio,
77 | converter_type.value, channels)
78 |
79 | if error != 0:
80 | raise ResamplingError(error)
81 |
82 | if verbose:
83 | info = ('samplerate info:\n'
84 | '{} input frames used\n'
85 | '{} output frames generated\n'
86 | .format(input_frames_used, output_frames_gen))
87 | print(info)
88 |
89 | return (output_data[:output_frames_gen, :]
90 | if channels > 1 else output_data[:output_frames_gen])
91 |
92 |
93 | class Resampler(object):
94 | """Resampler.
95 |
96 | Parameters
97 | ----------
98 | converter_type : ConverterType, str, or int
99 | Sample rate converter.
100 | num_channels : int
101 | Number of channels.
102 | """
103 | def __init__(self, converter_type='sinc_fastest', channels=1):
104 | from samplerate.lowlevel import ffi, src_new, src_delete
105 | from samplerate.exceptions import ResamplingError
106 |
107 | converter_type = _get_converter_type(converter_type)
108 | state, error = src_new(converter_type.value, channels)
109 | self._state = ffi.gc(state, src_delete)
110 | self._converter_type = converter_type
111 | self._channels = channels
112 | if channels == 1:
113 | self._frame_buffer = np.zeros(0, dtype=np.float32)
114 | else:
115 | self._frame_buffer = np.zeros((0,channels), dtype=np.float32)
116 | if error != 0:
117 | raise ResamplingError(error)
118 |
119 | @property
120 | def kiwi_samplerate(self):
121 | return True
122 |
123 | @property
124 | def converter_type(self):
125 | """Converter type."""
126 | return self._converter_type
127 |
128 | @property
129 | def channels(self):
130 | """Number of channels."""
131 | return self._channels
132 |
133 | def reset(self):
134 | """Reset internal state."""
135 | from samplerate.lowlevel import src_reset
136 | if self._channels == 1:
137 | self._frame_buffer = np.empty(0, dtype=np.float32)
138 | else:
139 | self._frame_buffer = np.empty((0,channels), dtype=np.float32)
140 | return src_reset(self._state)
141 |
142 | def set_ratio(self, new_ratio):
143 | """Set a new conversion ratio immediately."""
144 | from samplerate.lowlevel import src_set_ratio
145 | return src_set_ratio(self._state, new_ratio)
146 |
147 | def process(self, input_data, ratio, end_of_input=False, verbose=False):
148 | """Resample the signal in `input_data`.
149 |
150 | Parameters
151 | ----------
152 | input_data : ndarray
153 | Input data. A single channel is provided as a 1D array of `num_frames` length.
154 | Input data with several channels is represented as a 2D array of shape
155 | (`num_frames`, `num_channels`). For use with `libsamplerate`, `input_data`
156 | is converted to 32-bit float and C (row-major) memory order.
157 | ratio : float
158 | Conversion ratio = output sample rate / input sample rate.
159 | end_of_input : int
160 | Set to `True` if no more data is available, or to `False` otherwise.
161 | verbose : bool
162 | If `True`, print additional information about the conversion.
163 |
164 | Returns
165 | -------
166 | output_data : ndarray
167 | Resampled input data.
168 | """
169 | from samplerate.lowlevel import src_process
170 | from samplerate.exceptions import ResamplingError
171 |
172 | input_data = np.require(input_data, requirements='C', dtype=np.float32)
173 | self._frame_buffer = np.append(self._frame_buffer, input_data, axis=0)
174 | if input_data.ndim == 2:
175 | num_frames, channels = self._frame_buffer.shape
176 | output_shape = (int(num_frames * ratio), channels)
177 | elif input_data.ndim == 1:
178 | num_frames, channels = self._frame_buffer.shape[0], 1
179 | output_shape = (int(num_frames * ratio), )
180 | else:
181 | raise ValueError('rank > 2 not supported')
182 |
183 | if channels != self._channels:
184 | raise ValueError('Invalid number of channels in input data.')
185 |
186 | output_data = np.empty(output_shape, dtype=np.float32)
187 |
188 | (error, input_frames_used, output_frames_gen) = src_process(
189 | self._state, self._frame_buffer, output_data, ratio, end_of_input)
190 |
191 | if error != 0:
192 | raise ResamplingError(error)
193 |
194 | if channels > 1:
195 | self._frame_buffer = self._frame_buffer[input_frames_used:, :]
196 | else:
197 | self._frame_buffer = self._frame_buffer[input_frames_used:]
198 |
199 | if verbose:
200 | info = ('samplerate info:\n'
201 | '{} input frames used\n'
202 | '{} output frames generated\n'
203 | '{} buffer size\n'
204 | .format(input_frames_used, output_frames_gen, self._frame_buffer.shape[0]))
205 | print(info)
206 |
207 | return (output_data[:output_frames_gen, :]
208 | if channels > 1 else output_data[:output_frames_gen])
209 |
210 |
211 | class CallbackResampler(object):
212 | """CallbackResampler.
213 |
214 | Parameters
215 | ----------
216 | callback : function
217 | Function that returns new frames on each call, or `None` otherwise.
218 | A single channel is provided as a 1D array of `num_frames` length.
219 | Input data with several channels is represented as a 2D array of shape
220 | (`num_frames`, `num_channels`). For use with `libsamplerate`, `input_data`
221 | is converted to 32-bit float and C (row-major) memory order.
222 | ratio : float
223 | Conversion ratio = output sample rate / input sample rate.
224 | converter_type : ConverterType, str, or int
225 | Sample rate converter.
226 | channels : int
227 | Number of channels.
228 | """
229 | def __init__(self, callback, ratio, converter_type='sinc_fastest',
230 | channels=1):
231 | if channels < 1:
232 | raise ValueError('Invalid number of channels.')
233 | self._callback = callback
234 | self._ratio = ratio
235 | self._converter_type = _get_converter_type(converter_type)
236 | self._channels = channels
237 | self._state = None
238 | self._handle = None
239 | self._create()
240 |
241 | def _create(self):
242 | """Create new callback resampler."""
243 | from samplerate.lowlevel import ffi, src_callback_new, src_delete
244 | from samplerate.exceptions import ResamplingError
245 |
246 | state, handle, error = src_callback_new(
247 | self._callback, self._converter_type.value, self._channels)
248 | if error != 0:
249 | raise ResamplingError(error)
250 | self._state = ffi.gc(state, src_delete)
251 | self._handle = handle
252 |
253 | def _destroy(self):
254 | """Destroy resampler state."""
255 | if self._state:
256 | self._state = None
257 | self._handle = None
258 |
259 | def __enter__(self):
260 | return self
261 |
262 | def __exit__(self, *args):
263 | self._destroy()
264 |
265 | def set_starting_ratio(self, ratio):
266 | """ Set the starting conversion ratio for the next `read` call. """
267 | from samplerate.lowlevel import src_set_ratio
268 | if self._state is None:
269 | self._create()
270 | src_set_ratio(self._state, ratio)
271 | self.ratio = ratio
272 |
273 | def reset(self):
274 | """Reset state."""
275 | from samplerate.lowlevel import src_reset
276 | if self._state is None:
277 | self._create()
278 | src_reset(self._state)
279 |
280 | @property
281 | def ratio(self):
282 | """Conversion ratio = output sample rate / input sample rate."""
283 | return self._ratio
284 |
285 | @ratio.setter
286 | def ratio(self, ratio):
287 | self._ratio = ratio
288 |
289 | def read(self, num_frames):
290 | """Read a number of frames from the resampler.
291 |
292 | Parameters
293 | ----------
294 | num_frames : int
295 | Number of frames to read.
296 |
297 | Returns
298 | -------
299 | output_data : ndarray
300 | Resampled frames as a (`num_output_frames`, `num_channels`) or
301 | (`num_output_frames`,) array. Note that this may return fewer frames
302 | than requested, for example when no more input is available.
303 | """
304 | from samplerate.lowlevel import src_callback_read, src_error
305 | from samplerate.exceptions import ResamplingError
306 |
307 | if self._state is None:
308 | self._create()
309 | if self._channels > 1:
310 | output_shape = (num_frames, self._channels)
311 | elif self._channels == 1:
312 | output_shape = (num_frames, )
313 | output_data = np.empty(output_shape, dtype=np.float32)
314 |
315 | ret = src_callback_read(self._state, self._ratio, num_frames,
316 | output_data)
317 | if ret == 0:
318 | error = src_error(self._state)
319 | if error:
320 | raise ResamplingError(error)
321 |
322 | return (output_data[:ret, :]
323 | if self._channels > 1 else output_data[:ret])
324 |
--------------------------------------------------------------------------------
/mod_pywebsocket/util.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011, Google Inc.
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above
11 | # copyright notice, this list of conditions and the following disclaimer
12 | # in the documentation and/or other materials provided with the
13 | # distribution.
14 | # * Neither the name of Google Inc. nor the names of its
15 | # contributors may be used to endorse or promote products derived from
16 | # this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 |
31 | """WebSocket utilities."""
32 |
33 |
34 | import array
35 | import errno
36 |
37 | # Import hash classes from a module available and recommended for each Python
38 | # version and re-export those symbol. Use sha and md5 module in Python 2.4, and
39 | # hashlib module in Python 2.6.
40 | try:
41 | import hashlib
42 | md5_hash = hashlib.md5
43 | sha1_hash = hashlib.sha1
44 | except ImportError:
45 | import md5
46 | import sha
47 | md5_hash = md5.md5
48 | sha1_hash = sha.sha
49 |
50 | try:
51 | from StringIO import StringIO
52 | except ImportError:
53 | from io import StringIO
54 |
55 | import logging
56 | import os
57 | import sys
58 | import re
59 | import socket
60 | import traceback
61 | import zlib
62 |
63 | try:
64 | from mod_pywebsocket import fast_masking
65 | except ImportError:
66 | pass
67 |
68 |
69 | def get_stack_trace():
70 | """Get the current stack trace as string.
71 |
72 | This is needed to support Python 2.3.
73 | TODO: Remove this when we only support Python 2.4 and above.
74 | Use traceback.format_exc instead.
75 | """
76 | out = StringIO.StringIO()
77 | traceback.print_exc(file=out)
78 | return out.getvalue()
79 |
80 |
81 | def prepend_message_to_exception(message, exc):
82 | """Prepend message to the exception."""
83 | exc.args = (message + str(exc),)
84 | return
85 |
86 |
87 | def __translate_interp(interp, cygwin_path):
88 | """Translate interp program path for Win32 python to run cygwin program
89 | (e.g. perl). Note that it doesn't support path that contains space,
90 | which is typically true for Unix, where #!-script is written.
91 | For Win32 python, cygwin_path is a directory of cygwin binaries.
92 |
93 | Args:
94 | interp: interp command line
95 | cygwin_path: directory name of cygwin binary, or None
96 | Returns:
97 | translated interp command line.
98 | """
99 | if not cygwin_path:
100 | return interp
101 | m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
102 | if m:
103 | cmd = os.path.join(cygwin_path, m.group(1))
104 | return cmd + m.group(2)
105 | return interp
106 |
107 |
108 | def get_script_interp(script_path, cygwin_path=None):
109 | r"""Get #!-interpreter command line from the script.
110 |
111 | It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
112 | it could run "/usr/bin/perl -wT hello.pl".
113 | When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
114 | "/usr/bin/perl" to "\perl.exe".
115 |
116 | Args:
117 | script_path: pathname of the script
118 | cygwin_path: directory name of cygwin binary, or None
119 | Returns:
120 | #!-interpreter command line, or None if it is not #!-script.
121 | """
122 | fp = open(script_path)
123 | line = fp.readline()
124 | fp.close()
125 | m = re.match('^#!(.*)', line)
126 | if m:
127 | return __translate_interp(m.group(1), cygwin_path)
128 | return None
129 |
130 |
131 | def wrap_popen3_for_win(cygwin_path):
132 | """Wrap popen3 to support #!-script on Windows.
133 |
134 | Args:
135 | cygwin_path: path for cygwin binary if command path is needed to be
136 | translated. None if no translation required.
137 | """
138 | __orig_popen3 = os.popen3
139 |
140 | def __wrap_popen3(cmd, mode='t', bufsize=-1):
141 | cmdline = cmd.split(' ')
142 | interp = get_script_interp(cmdline[0], cygwin_path)
143 | if interp:
144 | cmd = interp + ' ' + cmd
145 | return __orig_popen3(cmd, mode, bufsize)
146 |
147 | os.popen3 = __wrap_popen3
148 |
149 |
150 | def hexify(s):
151 | r= ' '.join(map(lambda x: '%02x' % x, bytearray(s)))
152 | return r
153 |
154 |
155 | def get_class_logger(o):
156 | """Return the logging class information."""
157 | return logging.getLogger(
158 | '%s.%s' % (o.__class__.__module__, o.__class__.__name__))
159 |
160 |
161 | class NoopMasker(object):
162 | """A NoOp masking object.
163 |
164 | This has the same interface as RepeatedXorMasker but just returns
165 | the string passed in without making any change.
166 | """
167 |
168 | def __init__(self):
169 | """NoOp."""
170 | pass
171 |
172 | def mask(self, s):
173 | """NoOp."""
174 | return s
175 |
176 |
177 | class RepeatedXorMasker(object):
178 |
179 | """A masking object that applies XOR on the string.
180 |
181 | Applies XOR on the string given to mask method with the masking bytes
182 | given to the constructor repeatedly. This object remembers the position
183 | in the masking bytes the last mask method call ended and resumes from
184 | that point on the next mask method call.
185 | """
186 |
187 | def __init__(self, masking_key):
188 | self._masking_key = masking_key
189 | self._masking_key_index = 0
190 |
191 | def _mask_using_swig(self, s):
192 | """Perform the mask via SWIG."""
193 | masked_data = fast_masking.mask(
194 | s, self._masking_key, self._masking_key_index)
195 | self._masking_key_index = (
196 | (self._masking_key_index + len(s)) % len(self._masking_key))
197 | return masked_data
198 |
199 | def _mask_using_array(self, s):
200 | """Perform the mask via python."""
201 | result = array.array('B')
202 | if sys.version_info > (3,):
203 | result.frombytes(bytes(s))
204 | else:
205 | result.fromstring(bytes(s))
206 |
207 |
208 | # Use temporary local variables to eliminate the cost to access
209 | # attributes
210 | if type(self._masking_key[0]) is int:
211 | masking_key = [x for x in self._masking_key]
212 | else:
213 | masking_key = map(ord, self._masking_key)
214 | masking_key_size = len(masking_key)
215 | masking_key_index = self._masking_key_index
216 |
217 | for i in range(len(result)):
218 | result[i] ^= masking_key[masking_key_index]
219 | masking_key_index = (masking_key_index + 1) % masking_key_size
220 |
221 | self._masking_key_index = masking_key_index
222 |
223 | if sys.version_info > (3,):
224 | return result.tobytes()
225 | else:
226 | return result.tostring()
227 |
228 | if 'fast_masking' in globals():
229 | mask = _mask_using_swig
230 | else:
231 | mask = _mask_using_array
232 |
233 |
234 | # By making wbits option negative, we can suppress CMF/FLG (2 octet) and
235 | # ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
236 | # deflate library. DICTID won't be added as far as we don't set dictionary.
237 | # LZ77 window of 32K will be used for both compression and decompression.
238 | # For decompression, we can just use 32K to cover any windows size. For
239 | # compression, we use 32K so receivers must use 32K.
240 | #
241 | # Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
242 | # to decode.
243 | #
244 | # See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
245 | # Python. See also RFC1950 (ZLIB 3.3).
246 |
247 |
248 | class _Deflater(object):
249 |
250 | def __init__(self, window_bits):
251 | self._logger = get_class_logger(self)
252 |
253 | self._compress = zlib.compressobj(
254 | zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
255 |
256 | def compress(self, bytes):
257 | compressed_bytes = self._compress.compress(bytes)
258 | self._logger.debug('Compress input %r', bytes)
259 | self._logger.debug('Compress result %r', compressed_bytes)
260 | return compressed_bytes
261 |
262 | def compress_and_flush(self, bytes):
263 | compressed_bytes = self._compress.compress(bytes)
264 | compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
265 | self._logger.debug('Compress input %r', bytes)
266 | self._logger.debug('Compress result %r', compressed_bytes)
267 | return compressed_bytes
268 |
269 | def compress_and_finish(self, bytes):
270 | compressed_bytes = self._compress.compress(bytes)
271 | compressed_bytes += self._compress.flush(zlib.Z_FINISH)
272 | self._logger.debug('Compress input %r', bytes)
273 | self._logger.debug('Compress result %r', compressed_bytes)
274 | return compressed_bytes
275 |
276 |
277 | class _Inflater(object):
278 |
279 | def __init__(self, window_bits):
280 | self._logger = get_class_logger(self)
281 | self._window_bits = window_bits
282 |
283 | self._unconsumed = ''
284 |
285 | self.reset()
286 |
287 | def decompress(self, size):
288 | if not (size == -1 or size > 0):
289 | raise Exception('size must be -1 or positive')
290 |
291 | data = ''
292 |
293 | while True:
294 | if size == -1:
295 | data += self._decompress.decompress(self._unconsumed)
296 | # See Python bug http://bugs.python.org/issue12050 to
297 | # understand why the same code cannot be used for updating
298 | # self._unconsumed for here and else block.
299 | self._unconsumed = ''
300 | else:
301 | data += self._decompress.decompress(
302 | self._unconsumed, size - len(data))
303 | self._unconsumed = self._decompress.unconsumed_tail
304 | if self._decompress.unused_data:
305 | # Encountered a last block (i.e. a block with BFINAL = 1) and
306 | # found a new stream (unused_data). We cannot use the same
307 | # zlib.Decompress object for the new stream. Create a new
308 | # Decompress object to decompress the new one.
309 | #
310 | # It's fine to ignore unconsumed_tail if unused_data is not
311 | # empty.
312 | self._unconsumed = self._decompress.unused_data
313 | self.reset()
314 | if size >= 0 and len(data) == size:
315 | # data is filled. Don't call decompress again.
316 | break
317 | else:
318 | # Re-invoke Decompress.decompress to try to decompress all
319 | # available bytes before invoking read which blocks until
320 | # any new byte is available.
321 | continue
322 | else:
323 | # Here, since unused_data is empty, even if unconsumed_tail is
324 | # not empty, bytes of requested length are already in data. We
325 | # don't have to "continue" here.
326 | break
327 |
328 | if data:
329 | self._logger.debug('Decompressed %r', data)
330 | return data
331 |
332 | def append(self, data):
333 | self._logger.debug('Appended %r', data)
334 | self._unconsumed += data
335 |
336 | def reset(self):
337 | self._logger.debug('Reset')
338 | self._decompress = zlib.decompressobj(-self._window_bits)
339 |
340 |
341 | # Compresses/decompresses given octets using the method introduced in RFC1979.
342 |
343 |
344 | class _RFC1979Deflater(object):
345 | """A compressor class that applies DEFLATE to given byte sequence and
346 | flushes using the algorithm described in the RFC1979 section 2.1.
347 | """
348 |
349 | def __init__(self, window_bits, no_context_takeover):
350 | self._deflater = None
351 | if window_bits is None:
352 | window_bits = zlib.MAX_WBITS
353 | self._window_bits = window_bits
354 | self._no_context_takeover = no_context_takeover
355 |
356 | def filter(self, bytes, end=True, bfinal=False):
357 | if self._deflater is None:
358 | self._deflater = _Deflater(self._window_bits)
359 |
360 | if bfinal:
361 | result = self._deflater.compress_and_finish(bytes)
362 | # Add a padding block with BFINAL = 0 and BTYPE = 0.
363 | result = result + chr(0)
364 | self._deflater = None
365 | return result
366 |
367 | result = self._deflater.compress_and_flush(bytes)
368 | if end:
369 | # Strip last 4 octets which is LEN and NLEN field of a
370 | # non-compressed block added for Z_SYNC_FLUSH.
371 | result = result[:-4]
372 |
373 | if self._no_context_takeover and end:
374 | self._deflater = None
375 |
376 | return result
377 |
378 |
379 | class _RFC1979Inflater(object):
380 | """A decompressor class a la RFC1979.
381 |
382 | A decompressor class for byte sequence compressed and flushed following
383 | the algorithm described in the RFC1979 section 2.1.
384 | """
385 |
386 | def __init__(self, window_bits=zlib.MAX_WBITS):
387 | self._inflater = _Inflater(window_bits)
388 |
389 | def filter(self, bytes):
390 | # Restore stripped LEN and NLEN field of a non-compressed block added
391 | # for Z_SYNC_FLUSH.
392 | self._inflater.append(bytes + '\x00\x00\xff\xff')
393 | return self._inflater.decompress(-1)
394 |
395 |
396 | class DeflateSocket(object):
397 | """A wrapper class for socket object to intercept send and recv to perform
398 | deflate compression and decompression transparently.
399 | """
400 |
401 | # Size of the buffer passed to recv to receive compressed data.
402 | _RECV_SIZE = 4096
403 |
404 | def __init__(self, socket):
405 | self._socket = socket
406 |
407 | self._logger = get_class_logger(self)
408 |
409 | self._deflater = _Deflater(zlib.MAX_WBITS)
410 | self._inflater = _Inflater(zlib.MAX_WBITS)
411 |
412 | def recv(self, size):
413 | """Receives data from the socket specified on the construction up
414 | to the specified size. Once any data is available, returns it even
415 | if it's smaller than the specified size.
416 | """
417 |
418 | # TODO(tyoshino): Allow call with size=0. It should block until any
419 | # decompressed data is available.
420 | if size <= 0:
421 | raise Exception('Non-positive size passed')
422 | while True:
423 | data = self._inflater.decompress(size)
424 | if len(data) != 0:
425 | return data
426 |
427 | read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
428 | if not read_data:
429 | return ''
430 | self._inflater.append(read_data)
431 |
432 | def sendall(self, bytes):
433 | self.send(bytes)
434 |
435 | def send(self, bytes):
436 | self._socket.sendall(self._deflater.compress_and_flush(bytes))
437 | return len(bytes)
438 |
439 |
440 | # vi:sts=4 sw=4 et
441 |
--------------------------------------------------------------------------------
/kiwi/wsclient.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Modified echo client from the pywebsocket examples
4 | """
5 |
6 | import base64
7 | import logging
8 | import os
9 | import re
10 | import socket
11 |
12 | from mod_pywebsocket import common
13 | from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor
14 | from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
15 | from mod_pywebsocket.extensions import _PerMessageDeflateFramer
16 | from mod_pywebsocket.extensions import _parse_window_bits
17 | from mod_pywebsocket import util
18 |
19 |
20 | _TIMEOUT_SEC = 10
21 | _UNDEFINED_PORT = -1
22 |
23 | _UPGRADE_HEADER = 'Upgrade: websocket\r\n'
24 | _UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n'
25 | _CONNECTION_HEADER = 'Connection: Upgrade\r\n'
26 |
27 |
28 | class ClientHandshakeError(Exception):
29 | pass
30 |
31 |
32 | def _build_method_line(resource):
33 | return ('GET %s HTTP/1.1\r\n' % resource).encode()
34 |
35 |
36 | def _origin_header(header, origin):
37 | # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
38 | # and the /origin/ value, converted to ASCII lowercase, to /fields/.
39 | return '%s: %s\r\n' % (header, origin.lower())
40 |
41 |
42 | def _format_host_header(host, port, secure):
43 | # 4.1 9. Let /hostport/ be an empty string.
44 | # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
45 | # /hostport/
46 | hostport = host.lower()
47 | # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
48 | # is true, and /port/ is not 443, then append a U+003A COLON character
49 | # (:) followed by the value of /port/, expressed as a base-ten integer,
50 | # to /hostport/
51 | if ((not secure and port != common.DEFAULT_WEB_SOCKET_PORT) or
52 | (secure and port != common.DEFAULT_WEB_SOCKET_SECURE_PORT)):
53 | hostport += ':' + str(port)
54 | # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
55 | # character, and /hostport/, to /fields/.
56 | return '%s: %s\r\n' % (common.HOST_HEADER, hostport)
57 |
58 |
59 | def _receive_bytes(socket, length):
60 | bytes = []
61 | remaining = length
62 | while remaining > 0:
63 | received_bytes = socket.recv(remaining)
64 | if not received_bytes:
65 | raise IOError(
66 | 'Connection closed before receiving requested length '
67 | '(requested %d bytes but received only %d bytes)' %
68 | (length, length - remaining))
69 | bytes.append(received_bytes)
70 | remaining -= len(received_bytes)
71 | return bytearray().join(bytes).decode('utf-8')
72 |
73 |
74 | def _get_mandatory_header(fields, name):
75 | """Gets the value of the header specified by name from fields.
76 |
77 | This function expects that there's only one header with the specified name
78 | in fields. Otherwise, raises an ClientHandshakeError.
79 | """
80 |
81 | values = fields.get(name.lower())
82 | if values is None or len(values) == 0:
83 | raise ClientHandshakeError(
84 | '%s header not found: %r' % (name, values))
85 | if len(values) > 1:
86 | raise ClientHandshakeError(
87 | 'Multiple %s headers found: %r' % (name, values))
88 | return values[0]
89 |
90 |
91 | def _validate_mandatory_header(fields, name,
92 | expected_value, case_sensitive=False):
93 | """Gets and validates the value of the header specified by name from
94 | fields.
95 |
96 | If expected_value is specified, compares expected value and actual value
97 | and raises an ClientHandshakeError on failure. You can specify case
98 | sensitiveness in this comparison by case_sensitive parameter. This function
99 | expects that there's only one header with the specified name in fields.
100 | Otherwise, raises an ClientHandshakeError.
101 | """
102 |
103 | value = _get_mandatory_header(fields, name)
104 |
105 | if ((case_sensitive and value != expected_value) or
106 | (not case_sensitive and value.lower() != expected_value.lower())):
107 | raise ClientHandshakeError(
108 | 'Illegal value for header %s: %r (expected) vs %r (actual)' %
109 | (name, expected_value, value))
110 |
111 |
112 | class ClientHandshakeBase(object):
113 | """A base class for WebSocket opening handshake processors for each
114 | protocol version.
115 | """
116 |
117 | def __init__(self):
118 | self._logger = util.get_class_logger(self)
119 |
120 | def _read_fields(self):
121 | # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
122 | fields = {}
123 | while True: # "Field"
124 | # 4.1 33. let /name/ and /value/ be empty byte arrays
125 | name = ''
126 | value = ''
127 | # 4.1 34. read /name/
128 | name = self._read_name()
129 | if name is None:
130 | break
131 | # 4.1 35. read spaces
132 | # TODO(tyoshino): Skip only one space as described in the spec.
133 | ch = self._skip_spaces()
134 | # 4.1 36. read /value/
135 | value = self._read_value(ch)
136 | # 4.1 37. read a byte from the server
137 | ch = _receive_bytes(self._socket, 1)
138 | if ch != '\n': # 0x0A
139 | raise ClientHandshakeError(
140 | 'Expected LF but found %r while reading value %r for '
141 | 'header %r' % (ch, value, name))
142 | self._logger.debug('Received %r header', name)
143 | # 4.1 38. append an entry to the /fields/ list that has the name
144 | # given by the string obtained by interpreting the /name/ byte
145 | # array as a UTF-8 stream and the value given by the string
146 | # obtained by interpreting the /value/ byte array as a UTF-8 byte
147 | # stream.
148 | fields.setdefault(name, []).append(value)
149 | # 4.1 39. return to the "Field" step above
150 | return fields
151 |
152 | def _read_name(self):
153 | # 4.1 33. let /name/ be empty byte arrays
154 | name = ''
155 | while True:
156 | # 4.1 34. read a byte from the server
157 | ch = _receive_bytes(self._socket, 1)
158 | if ch == '\r': # 0x0D
159 | return None
160 | elif ch == '\n': # 0x0A
161 | raise ClientHandshakeError(
162 | 'Unexpected LF when reading header name %r' % name)
163 | elif ch == ':': # 0x3A
164 | return name
165 | elif ch >= 'A' and ch <= 'Z': # Range 0x31 to 0x5A
166 | ch = chr(ord(ch) + 0x20)
167 | name += ch
168 | else:
169 | name += ch
170 |
171 | def _skip_spaces(self):
172 | # 4.1 35. read a byte from the server
173 | while True:
174 | ch = _receive_bytes(self._socket, 1)
175 | if ch == ' ': # 0x20
176 | continue
177 | return ch
178 |
179 | def _read_value(self, ch):
180 | # 4.1 33. let /value/ be empty byte arrays
181 | value = ''
182 | # 4.1 36. read a byte from server.
183 | while True:
184 | if ch == '\r': # 0x0D
185 | return value
186 | elif ch == '\n': # 0x0A
187 | raise ClientHandshakeError(
188 | 'Unexpected LF when reading header value %r' % value)
189 | else:
190 | value += ch
191 | ch = _receive_bytes(self._socket, 1)
192 |
193 |
194 | def _get_permessage_deflate_framer(extension_response):
195 | """Validate the response and return a framer object using the parameters in
196 | the response. This method doesn't accept the server_.* parameters.
197 | """
198 |
199 | client_max_window_bits = None
200 | client_no_context_takeover = None
201 |
202 | client_max_window_bits_name = (
203 | PerMessageDeflateExtensionProcessor.
204 | _CLIENT_MAX_WINDOW_BITS_PARAM)
205 | client_no_context_takeover_name = (
206 | PerMessageDeflateExtensionProcessor.
207 | _CLIENT_NO_CONTEXT_TAKEOVER_PARAM)
208 |
209 | # We didn't send any server_.* parameter.
210 | # Handle those parameters as invalid if found in the response.
211 |
212 | for param_name, param_value in extension_response.get_parameters():
213 | if param_name == client_max_window_bits_name:
214 | if client_max_window_bits is not None:
215 | raise ClientHandshakeError(
216 | 'Multiple %s found' % client_max_window_bits_name)
217 |
218 | parsed_value = _parse_window_bits(param_value)
219 | if parsed_value is None:
220 | raise ClientHandshakeError(
221 | 'Bad %s: %r' %
222 | (client_max_window_bits_name, param_value))
223 | client_max_window_bits = parsed_value
224 | elif param_name == client_no_context_takeover_name:
225 | if client_no_context_takeover is not None:
226 | raise ClientHandshakeError(
227 | 'Multiple %s found' % client_no_context_takeover_name)
228 |
229 | if param_value is not None:
230 | raise ClientHandshakeError(
231 | 'Bad %s: Has value %r' %
232 | (client_no_context_takeover_name, param_value))
233 | client_no_context_takeover = True
234 |
235 | if client_no_context_takeover is None:
236 | client_no_context_takeover = False
237 |
238 | return _PerMessageDeflateFramer(client_max_window_bits,
239 | client_no_context_takeover)
240 |
241 |
242 | class ClientHandshakeProcessor(ClientHandshakeBase):
243 | """WebSocket opening handshake processor for
244 | draft-ietf-hybi-thewebsocketprotocol-06 and later.
245 | """
246 |
247 | def __init__(self, socket, host, port, origin=None, deflate_frame=False, use_permessage_deflate=False):
248 | super(ClientHandshakeProcessor, self).__init__()
249 |
250 | self._socket = socket
251 | self._host = host
252 | self._port = port
253 | self._origin = origin
254 | self._deflate_frame = deflate_frame
255 | self._use_permessage_deflate = use_permessage_deflate
256 |
257 | self._logger = util.get_class_logger(self)
258 |
259 | def handshake(self, resource):
260 | """Performs opening handshake on the specified socket.
261 |
262 | Raises:
263 | ClientHandshakeError: handshake failed.
264 | """
265 |
266 | request_line = _build_method_line(resource)
267 | self._logger.debug('Client\'s opening handshake Request-Line: %r', request_line)
268 |
269 | fields = []
270 | fields.append(_format_host_header(self._host, self._port, False))
271 | fields.append(_UPGRADE_HEADER)
272 | fields.append(_CONNECTION_HEADER)
273 | if self._origin is not None:
274 | fields.append(_origin_header(common.ORIGIN_HEADER, self._origin))
275 |
276 | original_key = os.urandom(16)
277 | self._key = base64.b64encode(original_key)
278 | self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, self._key, util.hexify(original_key))
279 | fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY_HEADER, self._key.decode()))
280 | fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER, common.VERSION_HYBI_LATEST))
281 | extensions_to_request = []
282 |
283 | if self._deflate_frame:
284 | extensions_to_request.append(common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION))
285 |
286 | if self._use_permessage_deflate:
287 | extension = common.ExtensionParameter(common.PERMESSAGE_DEFLATE_EXTENSION)
288 | # Accept the client_max_window_bits extension parameter by default.
289 | extension.add_parameter(PerMessageDeflateExtensionProcessor._CLIENT_MAX_WINDOW_BITS_PARAM, None)
290 | extensions_to_request.append(extension)
291 |
292 | if len(extensions_to_request) != 0:
293 | fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_EXTENSIONS_HEADER, common.format_extensions(extensions_to_request)))
294 |
295 | self._socket.sendall(request_line)
296 | for field in fields:
297 | self._socket.sendall(field.encode())
298 | self._socket.sendall(b'\r\n')
299 |
300 | self._logger.debug('Sent client\'s opening handshake headers: %r', fields)
301 | self._logger.debug('Start reading Status-Line')
302 |
303 | status_line = ''
304 | while True:
305 | ch = _receive_bytes(self._socket, 1)
306 | status_line += ch
307 | if ch == '\n':
308 | break
309 |
310 | m = re.match('HTTP/\\d+.\\d+ (\\d\\d\\d) .*\r\n', status_line)
311 | if m is None:
312 | raise ClientHandshakeError('Wrong status line format: %r' % status_line)
313 | status_code = m.group(1)
314 | if status_code != '101':
315 | self._logger.debug('Unexpected status code %s with following headers: %r', status_code, self._read_fields())
316 | raise ClientHandshakeError('Expected HTTP status code 101 but found %r' % status_code)
317 |
318 | self._logger.debug('Received valid Status-Line')
319 | self._logger.debug('Start reading headers until we see an empty line')
320 |
321 | fields = self._read_fields()
322 |
323 | ch = _receive_bytes(self._socket, 1)
324 | if ch != '\n': # 0x0A
325 | raise ClientHandshakeError(
326 | 'Expected LF but found %r while reading value %r for header '
327 | 'name %r' % (ch, value, name))
328 |
329 | self._logger.debug('Received an empty line')
330 | self._logger.debug('Server\'s opening handshake headers: %r', fields)
331 |
332 | _validate_mandatory_header(fields, common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE, False)
333 | _validate_mandatory_header(fields, common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE, False)
334 |
335 | accept = _get_mandatory_header(fields, common.SEC_WEBSOCKET_ACCEPT_HEADER)
336 | # Validate
337 | try:
338 | binary_accept = base64.b64decode(accept)
339 | except TypeError as e:
340 | raise HandshakeError(
341 | 'Illegal value for header %s: %r' %
342 | (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
343 |
344 | if len(binary_accept) != 20:
345 | raise ClientHandshakeError(
346 | 'Decoded value of %s is not 20-byte long' %
347 | common.SEC_WEBSOCKET_ACCEPT_HEADER)
348 |
349 | self._logger.debug('Response for challenge : %r (%s)', accept, util.hexify(binary_accept))
350 |
351 | binary_expected_accept = util.sha1_hash(self._key + common.WEBSOCKET_ACCEPT_UUID.encode()).digest()
352 | expected_accept = base64.b64encode(binary_expected_accept)
353 | self._logger.debug(
354 | 'Expected response for challenge: %r (%s)',
355 | expected_accept, util.hexify(binary_expected_accept))
356 |
357 | if accept.encode() != expected_accept:
358 | raise ClientHandshakeError(
359 | 'Invalid %s header: %r (expected: %s)' %
360 | (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, expected_accept))
361 |
362 | deflate_frame_accepted = False
363 | permessage_deflate_accepted = False
364 |
365 | extensions_header = fields.get(common.SEC_WEBSOCKET_EXTENSIONS_HEADER.lower())
366 | accepted_extensions = []
367 | if extensions_header is not None and len(extensions_header) != 0:
368 | accepted_extensions = common.parse_extensions(extensions_header[0])
369 |
370 | # TODO(bashi): Support the new style perframe compression extension.
371 | for extension in accepted_extensions:
372 | extension_name = extension.name()
373 | if (extension_name == common.DEFLATE_FRAME_EXTENSION and self._deflate_frame):
374 | deflate_frame_accepted = True
375 | processor = DeflateFrameExtensionProcessor(extension)
376 | unused_extension_response = processor.get_extension_response()
377 | self._deflate_frame = processor
378 | continue
379 | elif (extension_name == common.PERMESSAGE_DEFLATE_EXTENSION and self._use_permessage_deflate):
380 | permessage_deflate_accepted = True
381 | framer = _get_permessage_deflate_framer(extension)
382 | framer.set_compress_outgoing_enabled(True)
383 | self._use_permessage_deflate = framer
384 | continue
385 |
386 | raise ClientHandshakeError('Unexpected extension %r' % extension_name)
387 |
388 | if (self._deflate_frame and not deflate_frame_accepted):
389 | raise ClientHandshakeError('Requested %s, but the server rejected it' % common.DEFLATE_FRAME_EXTENSION)
390 | if (self._use_permessage_deflate and not permessage_deflate_accepted):
391 | raise ClientHandshakeError('Requested %s, but the server rejected it' % common.PERMESSAGE_DEFLATE_EXTENSION)
392 |
393 | # TODO(tyoshino): Handle Sec-WebSocket-Protocol
394 | # TODO(tyoshino): Handle Cookie, etc.
395 |
396 |
397 | class ClientConnection(object):
398 | """A wrapper for socket object to provide the mp_conn interface.
399 | mod_pywebsocket library is designed to be working on Apache mod_python's
400 | mp_conn object.
401 | """
402 |
403 | def __init__(self, socket):
404 | self._socket = socket
405 |
406 | def write(self, data):
407 | try:
408 | self._socket.sendall(data)
409 | except Exception as e:
410 | logging.debug('ClientConnection write error: "%s"' % e)
411 |
412 | def read(self, n):
413 | return self._socket.recv(n)
414 |
415 | def get_remote_addr(self):
416 | return self._socket.getpeername()
417 | remote_addr = property(get_remote_addr)
418 |
419 |
420 | class ClientRequest(object):
421 | """A wrapper class just to make it able to pass a socket object to
422 | functions that expect a mp_request object.
423 | """
424 |
425 | def __init__(self, socket):
426 | self._logger = util.get_class_logger(self)
427 |
428 | self._socket = socket
429 | self.connection = ClientConnection(socket)
430 |
431 |
432 |
--------------------------------------------------------------------------------
/samplerate/_version.py:
--------------------------------------------------------------------------------
1 |
2 | # This file helps to compute a version number in source trees obtained from
3 | # git-archive tarball (such as those provided by githubs download-from-tag
4 | # feature). Distribution tarballs (built by setup.py sdist) and build
5 | # directories (produced by setup.py build) will contain a much shorter file
6 | # that just contains the computed version number.
7 |
8 | # This file is released into the public domain. Generated by
9 | # versioneer-0.18 (https://github.com/warner/python-versioneer)
10 |
11 | """Git implementation of _version.py."""
12 |
13 | import errno
14 | import os
15 | import re
16 | import subprocess
17 | import sys
18 |
19 |
20 | def get_keywords():
21 | """Get the keywords needed to look up the version information."""
22 | # these strings will be replaced by git during git-archive.
23 | # setup.py/versioneer.py will grep for the variable names, so they must
24 | # each be defined on a line of their own. _version.py will just call
25 | # get_keywords().
26 | git_refnames = " (HEAD -> master)"
27 | git_full = "ed73d7a39e61bfb34b03dade14ffab59aa27922a"
28 | git_date = "2019-04-10 08:46:22 +0200"
29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
30 | return keywords
31 |
32 |
33 | class VersioneerConfig:
34 | """Container for Versioneer configuration parameters."""
35 |
36 |
37 | def get_config():
38 | """Create, populate and return the VersioneerConfig() object."""
39 | # these strings are filled in when 'setup.py versioneer' creates
40 | # _version.py
41 | cfg = VersioneerConfig()
42 | cfg.VCS = "git"
43 | cfg.style = "pep440"
44 | cfg.tag_prefix = ""
45 | cfg.parentdir_prefix = "python-samplerate-"
46 | cfg.versionfile_source = "samplerate/_version.py"
47 | cfg.verbose = False
48 | return cfg
49 |
50 |
51 | class NotThisMethod(Exception):
52 | """Exception raised if a method is not valid for the current scenario."""
53 |
54 |
55 | LONG_VERSION_PY = {}
56 | HANDLERS = {}
57 |
58 |
59 | def register_vcs_handler(vcs, method): # decorator
60 | """Decorator to mark a method as the handler for a particular VCS."""
61 | def decorate(f):
62 | """Store f in HANDLERS[vcs][method]."""
63 | if vcs not in HANDLERS:
64 | HANDLERS[vcs] = {}
65 | HANDLERS[vcs][method] = f
66 | return f
67 | return decorate
68 |
69 |
70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
71 | env=None):
72 | """Call the given command(s)."""
73 | assert isinstance(commands, list)
74 | p = None
75 | for c in commands:
76 | try:
77 | dispcmd = str([c] + args)
78 | # remember shell=False, so use git.cmd on windows, not just git
79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env,
80 | stdout=subprocess.PIPE,
81 | stderr=(subprocess.PIPE if hide_stderr
82 | else None))
83 | break
84 | except EnvironmentError:
85 | e = sys.exc_info()[1]
86 | if e.errno == errno.ENOENT:
87 | continue
88 | if verbose:
89 | print("unable to run %s" % dispcmd)
90 | print(e)
91 | return None, None
92 | else:
93 | if verbose:
94 | print("unable to find command, tried %s" % (commands,))
95 | return None, None
96 | stdout = p.communicate()[0].strip()
97 | if sys.version_info[0] >= 3:
98 | stdout = stdout.decode()
99 | if p.returncode != 0:
100 | if verbose:
101 | print("unable to run %s (error)" % dispcmd)
102 | print("stdout was %s" % stdout)
103 | return None, p.returncode
104 | return stdout, p.returncode
105 |
106 |
107 | def versions_from_parentdir(parentdir_prefix, root, verbose):
108 | """Try to determine the version from the parent directory name.
109 |
110 | Source tarballs conventionally unpack into a directory that includes both
111 | the project name and a version string. We will also support searching up
112 | two directory levels for an appropriately named parent directory
113 | """
114 | rootdirs = []
115 |
116 | for i in range(3):
117 | dirname = os.path.basename(root)
118 | if dirname.startswith(parentdir_prefix):
119 | return {"version": dirname[len(parentdir_prefix):],
120 | "full-revisionid": None,
121 | "dirty": False, "error": None, "date": None}
122 | else:
123 | rootdirs.append(root)
124 | root = os.path.dirname(root) # up a level
125 |
126 | if verbose:
127 | print("Tried directories %s but none started with prefix %s" %
128 | (str(rootdirs), parentdir_prefix))
129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
130 |
131 |
132 | @register_vcs_handler("git", "get_keywords")
133 | def git_get_keywords(versionfile_abs):
134 | """Extract version information from the given file."""
135 | # the code embedded in _version.py can just fetch the value of these
136 | # keywords. When used from setup.py, we don't want to import _version.py,
137 | # so we do it with a regexp instead. This function is not used from
138 | # _version.py.
139 | keywords = {}
140 | try:
141 | f = open(versionfile_abs, "r")
142 | for line in f.readlines():
143 | if line.strip().startswith("git_refnames ="):
144 | mo = re.search(r'=\s*"(.*)"', line)
145 | if mo:
146 | keywords["refnames"] = mo.group(1)
147 | if line.strip().startswith("git_full ="):
148 | mo = re.search(r'=\s*"(.*)"', line)
149 | if mo:
150 | keywords["full"] = mo.group(1)
151 | if line.strip().startswith("git_date ="):
152 | mo = re.search(r'=\s*"(.*)"', line)
153 | if mo:
154 | keywords["date"] = mo.group(1)
155 | f.close()
156 | except EnvironmentError:
157 | pass
158 | return keywords
159 |
160 |
161 | @register_vcs_handler("git", "keywords")
162 | def git_versions_from_keywords(keywords, tag_prefix, verbose):
163 | """Get version information from git keywords."""
164 | if not keywords:
165 | raise NotThisMethod("no keywords at all, weird")
166 | date = keywords.get("date")
167 | if date is not None:
168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
170 | # -like" string, which we must then edit to make compliant), because
171 | # it's been around since git-1.5.3, and it's too difficult to
172 | # discover which version we're using, or to work around using an
173 | # older one.
174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
175 | refnames = keywords["refnames"].strip()
176 | if refnames.startswith("$Format"):
177 | if verbose:
178 | print("keywords are unexpanded, not using")
179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
180 | refs = set([r.strip() for r in refnames.strip("()").split(",")])
181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those.
183 | TAG = "tag: "
184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
185 | if not tags:
186 | # Either we're using git < 1.8.3, or there really are no tags. We use
187 | # a heuristic: assume all version tags have a digit. The old git %d
188 | # expansion behaves like git log --decorate=short and strips out the
189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish
190 | # between branches and tags. By ignoring refnames without digits, we
191 | # filter out many common branch names like "release" and
192 | # "stabilization", as well as "HEAD" and "master".
193 | tags = set([r for r in refs if re.search(r'\d', r)])
194 | if verbose:
195 | print("discarding '%s', no digits" % ",".join(refs - tags))
196 | if verbose:
197 | print("likely tags: %s" % ",".join(sorted(tags)))
198 | for ref in sorted(tags):
199 | # sorting will prefer e.g. "2.0" over "2.0rc1"
200 | if ref.startswith(tag_prefix):
201 | r = ref[len(tag_prefix):]
202 | if verbose:
203 | print("picking %s" % r)
204 | return {"version": r,
205 | "full-revisionid": keywords["full"].strip(),
206 | "dirty": False, "error": None,
207 | "date": date}
208 | # no suitable tags, so version is "0+unknown", but full hex is still there
209 | if verbose:
210 | print("no suitable tags, using unknown + full revision id")
211 | return {"version": "0+unknown",
212 | "full-revisionid": keywords["full"].strip(),
213 | "dirty": False, "error": "no suitable tags", "date": None}
214 |
215 |
216 | @register_vcs_handler("git", "pieces_from_vcs")
217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
218 | """Get version from 'git describe' in the root of the source tree.
219 |
220 | This only gets called if the git-archive 'subst' keywords were *not*
221 | expanded, and _version.py hasn't already been rewritten with a short
222 | version string, meaning we're inside a checked out source tree.
223 | """
224 | GITS = ["git"]
225 | if sys.platform == "win32":
226 | GITS = ["git.cmd", "git.exe"]
227 |
228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
229 | hide_stderr=True)
230 | if rc != 0:
231 | if verbose:
232 | print("Directory %s not under git control" % root)
233 | raise NotThisMethod("'git rev-parse --git-dir' returned error")
234 |
235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
236 | # if there isn't one, this yields HEX[-dirty] (no NUM)
237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
238 | "--always", "--long",
239 | "--match", "%s*" % tag_prefix],
240 | cwd=root)
241 | # --long was added in git-1.5.5
242 | if describe_out is None:
243 | raise NotThisMethod("'git describe' failed")
244 | describe_out = describe_out.strip()
245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
246 | if full_out is None:
247 | raise NotThisMethod("'git rev-parse' failed")
248 | full_out = full_out.strip()
249 |
250 | pieces = {}
251 | pieces["long"] = full_out
252 | pieces["short"] = full_out[:7] # maybe improved later
253 | pieces["error"] = None
254 |
255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
256 | # TAG might have hyphens.
257 | git_describe = describe_out
258 |
259 | # look for -dirty suffix
260 | dirty = git_describe.endswith("-dirty")
261 | pieces["dirty"] = dirty
262 | if dirty:
263 | git_describe = git_describe[:git_describe.rindex("-dirty")]
264 |
265 | # now we have TAG-NUM-gHEX or HEX
266 |
267 | if "-" in git_describe:
268 | # TAG-NUM-gHEX
269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
270 | if not mo:
271 | # unparseable. Maybe git-describe is misbehaving?
272 | pieces["error"] = ("unable to parse git-describe output: '%s'"
273 | % describe_out)
274 | return pieces
275 |
276 | # tag
277 | full_tag = mo.group(1)
278 | if not full_tag.startswith(tag_prefix):
279 | if verbose:
280 | fmt = "tag '%s' doesn't start with prefix '%s'"
281 | print(fmt % (full_tag, tag_prefix))
282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
283 | % (full_tag, tag_prefix))
284 | return pieces
285 | pieces["closest-tag"] = full_tag[len(tag_prefix):]
286 |
287 | # distance: number of commits since tag
288 | pieces["distance"] = int(mo.group(2))
289 |
290 | # commit: short hex revision ID
291 | pieces["short"] = mo.group(3)
292 |
293 | else:
294 | # HEX: no tags
295 | pieces["closest-tag"] = None
296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
297 | cwd=root)
298 | pieces["distance"] = int(count_out) # total number of commits
299 |
300 | # commit date: see ISO-8601 comment in git_versions_from_keywords()
301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
302 | cwd=root)[0].strip()
303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
304 |
305 | return pieces
306 |
307 |
308 | def plus_or_dot(pieces):
309 | """Return a + if we don't already have one, else return a ."""
310 | if "+" in pieces.get("closest-tag", ""):
311 | return "."
312 | return "+"
313 |
314 |
315 | def render_pep440(pieces):
316 | """Build up version string, with post-release "local version identifier".
317 |
318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
320 |
321 | Exceptions:
322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
323 | """
324 | if pieces["closest-tag"]:
325 | rendered = pieces["closest-tag"]
326 | if pieces["distance"] or pieces["dirty"]:
327 | rendered += plus_or_dot(pieces)
328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
329 | if pieces["dirty"]:
330 | rendered += ".dirty"
331 | else:
332 | # exception #1
333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"],
334 | pieces["short"])
335 | if pieces["dirty"]:
336 | rendered += ".dirty"
337 | return rendered
338 |
339 |
340 | def render_pep440_pre(pieces):
341 | """TAG[.post.devDISTANCE] -- No -dirty.
342 |
343 | Exceptions:
344 | 1: no tags. 0.post.devDISTANCE
345 | """
346 | if pieces["closest-tag"]:
347 | rendered = pieces["closest-tag"]
348 | if pieces["distance"]:
349 | rendered += ".post.dev%d" % pieces["distance"]
350 | else:
351 | # exception #1
352 | rendered = "0.post.dev%d" % pieces["distance"]
353 | return rendered
354 |
355 |
356 | def render_pep440_post(pieces):
357 | """TAG[.postDISTANCE[.dev0]+gHEX] .
358 |
359 | The ".dev0" means dirty. Note that .dev0 sorts backwards
360 | (a dirty tree will appear "older" than the corresponding clean one),
361 | but you shouldn't be releasing software with -dirty anyways.
362 |
363 | Exceptions:
364 | 1: no tags. 0.postDISTANCE[.dev0]
365 | """
366 | if pieces["closest-tag"]:
367 | rendered = pieces["closest-tag"]
368 | if pieces["distance"] or pieces["dirty"]:
369 | rendered += ".post%d" % pieces["distance"]
370 | if pieces["dirty"]:
371 | rendered += ".dev0"
372 | rendered += plus_or_dot(pieces)
373 | rendered += "g%s" % pieces["short"]
374 | else:
375 | # exception #1
376 | rendered = "0.post%d" % pieces["distance"]
377 | if pieces["dirty"]:
378 | rendered += ".dev0"
379 | rendered += "+g%s" % pieces["short"]
380 | return rendered
381 |
382 |
383 | def render_pep440_old(pieces):
384 | """TAG[.postDISTANCE[.dev0]] .
385 |
386 | The ".dev0" means dirty.
387 |
388 | Eexceptions:
389 | 1: no tags. 0.postDISTANCE[.dev0]
390 | """
391 | if pieces["closest-tag"]:
392 | rendered = pieces["closest-tag"]
393 | if pieces["distance"] or pieces["dirty"]:
394 | rendered += ".post%d" % pieces["distance"]
395 | if pieces["dirty"]:
396 | rendered += ".dev0"
397 | else:
398 | # exception #1
399 | rendered = "0.post%d" % pieces["distance"]
400 | if pieces["dirty"]:
401 | rendered += ".dev0"
402 | return rendered
403 |
404 |
405 | def render_git_describe(pieces):
406 | """TAG[-DISTANCE-gHEX][-dirty].
407 |
408 | Like 'git describe --tags --dirty --always'.
409 |
410 | Exceptions:
411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
412 | """
413 | if pieces["closest-tag"]:
414 | rendered = pieces["closest-tag"]
415 | if pieces["distance"]:
416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
417 | else:
418 | # exception #1
419 | rendered = pieces["short"]
420 | if pieces["dirty"]:
421 | rendered += "-dirty"
422 | return rendered
423 |
424 |
425 | def render_git_describe_long(pieces):
426 | """TAG-DISTANCE-gHEX[-dirty].
427 |
428 | Like 'git describe --tags --dirty --always -long'.
429 | The distance/hash is unconditional.
430 |
431 | Exceptions:
432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
433 | """
434 | if pieces["closest-tag"]:
435 | rendered = pieces["closest-tag"]
436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
437 | else:
438 | # exception #1
439 | rendered = pieces["short"]
440 | if pieces["dirty"]:
441 | rendered += "-dirty"
442 | return rendered
443 |
444 |
445 | def render(pieces, style):
446 | """Render the given version pieces into the requested style."""
447 | if pieces["error"]:
448 | return {"version": "unknown",
449 | "full-revisionid": pieces.get("long"),
450 | "dirty": None,
451 | "error": pieces["error"],
452 | "date": None}
453 |
454 | if not style or style == "default":
455 | style = "pep440" # the default
456 |
457 | if style == "pep440":
458 | rendered = render_pep440(pieces)
459 | elif style == "pep440-pre":
460 | rendered = render_pep440_pre(pieces)
461 | elif style == "pep440-post":
462 | rendered = render_pep440_post(pieces)
463 | elif style == "pep440-old":
464 | rendered = render_pep440_old(pieces)
465 | elif style == "git-describe":
466 | rendered = render_git_describe(pieces)
467 | elif style == "git-describe-long":
468 | rendered = render_git_describe_long(pieces)
469 | else:
470 | raise ValueError("unknown style '%s'" % style)
471 |
472 | return {"version": rendered, "full-revisionid": pieces["long"],
473 | "dirty": pieces["dirty"], "error": None,
474 | "date": pieces.get("date")}
475 |
476 |
477 | def get_versions():
478 | """Get version information or return default if unable to do so."""
479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
480 | # __file__, we can work backwards from there to the root. Some
481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
482 | # case we can only use expanded keywords.
483 |
484 | cfg = get_config()
485 | verbose = cfg.verbose
486 |
487 | try:
488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
489 | verbose)
490 | except NotThisMethod:
491 | pass
492 |
493 | try:
494 | root = os.path.realpath(__file__)
495 | # versionfile_source is the relative path from the top of the source
496 | # tree (where the .git directory might live) to this file. Invert
497 | # this to find the root from __file__.
498 | for i in cfg.versionfile_source.split('/'):
499 | root = os.path.dirname(root)
500 | except NameError:
501 | return {"version": "0+unknown", "full-revisionid": None,
502 | "dirty": None,
503 | "error": "unable to find root of source tree",
504 | "date": None}
505 |
506 | try:
507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
508 | return render(pieces, cfg.style)
509 | except NotThisMethod:
510 | pass
511 |
512 | try:
513 | if cfg.parentdir_prefix:
514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
515 | except NotThisMethod:
516 | pass
517 |
518 | return {"version": "0+unknown", "full-revisionid": None,
519 | "dirty": None,
520 | "error": "unable to compute version", "date": None}
521 |
--------------------------------------------------------------------------------