Compare commits

...

16 Commits
v1.12 ... v1.13

Author SHA1 Message Date
Roman Zeyde
e637e701df bump version 2015-09-19 12:13:19 +03:00
Roman Zeyde
5be6684fa6 travis: upgrade pytest version for Python 3.5 2015-09-19 12:05:58 +03:00
Roman Zeyde
0876be18e4 Merge branch 'py35' 2015-09-19 12:02:24 +03:00
Roman Zeyde
3b8f913fcb setup: Python 3.5 is supported 2015-09-19 12:01:52 +03:00
Roman Zeyde
45d4ccae76 use --assert=plain on Travis 2015-09-19 11:58:30 +03:00
Roman Zeyde
7cb05aaaf7 travis: test under Python 3.5 2015-09-19 11:45:50 +03:00
Roman Zeyde
3911f16bd7 tox: fix indent to tabs 2015-09-18 21:07:43 +03:00
Roman Zeyde
b5f8e07ae2 recv: define integration gain as member variable 2015-08-16 18:30:30 +03:00
Roman Zeyde
835841bf2e test_calib: test frequency change case 2015-08-16 18:21:40 +03:00
Roman Zeyde
c70b3c9dc7 calib: remove AttributeHolder 2015-08-16 17:55:40 +03:00
Roman Zeyde
544dd28ddd common: add docstrings 2015-08-16 17:55:40 +03:00
Roman Zeyde
c887dbf4e6 README: use screencasts instead of videos 2015-08-15 08:35:22 +03:00
Roman Zeyde
bf6282127c docs: link to readthedocs.org 2015-08-14 13:07:04 +03:00
Roman Zeyde
65f2559a19 README: fix whitespace 2015-08-13 10:07:07 +03:00
Roman Zeyde
1c6f8894a5 setup.py: fix pytest invocation 2015-08-11 10:43:28 +03:00
Roman Zeyde
c19d11744f remove requirements.txt 2015-08-11 10:39:00 +03:00
11 changed files with 71 additions and 53 deletions

View File

@@ -6,9 +6,11 @@ python:
- "3.2"
- "3.3"
- "3.4"
- "3.5"
install:
- pip install .
- pip install pytest>=2.7.3 --upgrade
- pip install coveralls pep8 mock
script:

View File

@@ -8,8 +8,11 @@ Audio Modem Communication Library
:target: https://coveralls.io/r/romanz/amodem?branch=master
:alt: Code Coverage
.. image:: https://landscape.io/github/romanz/amodem/master/landscape.svg?style=flat
:target: https://landscape.io/github/romanz/amodem/master
:alt: Code Health
:target: https://landscape.io/github/romanz/amodem/master
:alt: Code Health
.. image:: https://readthedocs.org/projects/amodem/badge/?version=latest
:target: http://amodem.readthedocs.org/en/latest/
:alt: Documentation
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
@@ -55,7 +58,7 @@ The modem is using OFDM over an audio cable with the following parameters:
- Sampling rate: 8/16/32 kHz
- Baud rate: 1 kHz
- Symbol modulation: BPSK, 4-PSK, 16-QAM ,64-QAM, 256-QAM
- Symbol modulation: BPSK, 4-PSK, 16-QAM, 64-QAM, 256-QAM
- Carriers: 2-11 kHz (up to ten carriers)
This way, modem may achieve 80kbps bitrate = 10 kB/s (for best SNR).
@@ -142,7 +145,7 @@ and send me the resulting ``audio.raw`` file for debugging::
~/receiver $ arecord --format=S16_LE --channels=1 --rate=32000 audio.raw
You can see a video of the `calibration process <http://www.youtube.com/watch?v=jRUj2Ifk-Po>`_.
You can see a screencast of the `calibration process <https://asciinema.org/a/25065?autoplay=1>`_.
Usage
-----
@@ -226,7 +229,7 @@ After the receiver has finished, verify the received file's hash::
~/receiver $ sha256sum data.rx
008df57d4f3ed6e7a25d25afd57d04fc73140e8df604685bd34fcab58f5ddc01 data.rx
You can see a video of the `data transfer process <http://www.youtube.com/watch?v=GZQUtHB8so4>`_.
You can see a screencast of the `data transfer process <https://asciinema.org/a/25066?autoplay=1>`_.
Visualization
-------------

View File

@@ -72,10 +72,10 @@ def detector(config, src, frame_length=200):
else:
msg = 'too {0} signal'.format(errors[flags.index(False)])
yield common.AttributeHolder(dict(
yield dict(
freq=freq, rms=rms, peak=peak, coherency=coherency,
total=total, success=success, msg=msg
))
)
def volume_calibration(result_iterator, volume_ctl):
@@ -90,7 +90,7 @@ def volume_calibration(result_iterator, volume_ctl):
for index, result in enumerate(itertools.chain([None], result_iterator)):
if index % iters_per_update == 0:
if index > 0: # skip dummy (first result)
sign = 1 if (result.total < target_level) else -1
sign = 1 if (result['total'] < target_level) else -1
level = level + step * sign
level = min(max(level, min_level), max_level)
step = step * 0.5
@@ -111,12 +111,7 @@ def iter_window(iterable, size):
yield block
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
fmt = '{0.freq:6.0f} Hz: {0.msg:20s}'
if verbose:
fields = ['total', 'rms', 'coherency', 'peak']
fmt += ', '.join('{0}={{0.{0}:.4f}}'.format(f) for f in fields)
def recv_iter(config, src, volume_cmd=None, dump_audio=None):
volume_ctl = volume_controller(volume_cmd)
if dump_audio:
@@ -125,6 +120,19 @@ def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
result_iterator = volume_calibration(result_iterator, volume_ctl)
for _prev, curr, _next in iter_window(result_iterator, size=3):
# don't log errors during frequency changes
if _prev.success and _next.success and _prev.freq != _next.freq:
curr.msg = curr.msg if curr.success else 'frequency change'
log.info(fmt.format(curr))
if _prev['success'] and _next['success']:
if _prev['freq'] != _next['freq']:
if not curr['success']:
curr['msg'] = 'frequency change'
yield curr
def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
fmt = '{freq:6.0f} Hz: {msg:20s}'
log.info('verbose: %s', verbose)
if verbose:
fields = ['total', 'rms', 'coherency', 'peak']
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
for state in recv_iter(config, src, volume_cmd, dump_audio):
log.info(fmt.format(**state))

View File

@@ -1,3 +1,6 @@
''' Common package functionality.
'''
import itertools
import numpy as np
@@ -8,21 +11,25 @@ scaling = 32000.0 # out of 2**15
def load(fileobj):
''' Load signal from file object. '''
return loads(fileobj.read())
def loads(data):
''' Load signal from memory buffer. '''
x = np.frombuffer(data, dtype='int16')
x = x / scaling
return x
def dumps(sym):
''' Dump signal to memory buffer. '''
sym = sym.real * scaling
return sym.astype('int16').tostring()
def iterate(data, size, func=None, truncate=True, index=False):
''' Iterate over a signal, taking each time *size* elements. '''
offset = 0
data = iter(data)
@@ -40,6 +47,9 @@ def iterate(data, size, func=None, truncate=True, index=False):
def split(iterable, n):
''' Split an iterable of n-tuples into n iterables of scalars.
The k-th iterable will be equivalent to (i[k] for i in iter).
'''
def _gen(it, index):
for item in it:
yield item[index]
@@ -49,37 +59,29 @@ def split(iterable, n):
def icapture(iterable, result):
''' Appends each yielded item to result. '''
for i in iter(iterable):
result.append(i)
yield i
def take(iterable, n):
''' Take n elements from iterable, and return them as a numpy array. '''
return np.array(list(itertools.islice(iterable, n)))
# "Python 3" zip re-implementation for Python 2
def izip(iterables):
''' "Python 3" zip re-implementation for Python 2. '''
iterables = [iter(iterable) for iterable in iterables]
while True:
yield tuple([next(iterable) for iterable in iterables])
class Dummy(object):
''' Dummy placeholder object for testing and mocking. '''
def __getattr__(self, name):
return self
def __call__(self, *args, **kwargs):
return self
class AttributeHolder(object):
def __init__(self, d):
self.__dict__.update(d)
def __repr__(self):
items = sorted(self.__dict__.items())
args = ', '.join('{0}={1}'.format(k, v) for k, v in items)
return '{0}({1})'.format(self.__class__.__name__, args)

View File

@@ -28,6 +28,7 @@ class Receiver(object):
self.equalizer = equalizer.Equalizer(config)
self.carrier_index = config.carrier_index
self.output_size = 0 # number of bytes written to output stream
self.freq_err_gain = 0.01 * self.Tsym # integration feedback gain
def _prefix(self, symbols, gain=1.0):
S = common.take(symbols, len(equalizer.prefix))
@@ -140,7 +141,7 @@ class Receiver(object):
err = np.mean(np.angle(err))/(2*np.pi) if len(err) else 0
errors.clear()
sampler.freq -= 0.01 * err * self.Tsym
sampler.freq -= self.freq_err_gain * err
sampler.offset -= err
def _report_progress(self, noise, sampler):

View File

@@ -39,8 +39,8 @@ def test_too_strong():
calib.send(config, p, gain=1.001, limit=32)
p.buf.seek(0)
for r in calib.detector(config, src=p):
assert not r.success
assert r.msg == 'too strong signal'
assert not r['success']
assert r['msg'] == 'too strong signal'
def test_too_weak():
@@ -48,8 +48,8 @@ def test_too_weak():
calib.send(config, p, gain=0.01, limit=32)
p.buf.seek(0)
for r in calib.detector(config, src=p):
assert not r.success
assert r.msg == 'too weak signal'
assert not r['success']
assert r['msg'] == 'too weak signal'
def test_too_noisy():
@@ -57,8 +57,8 @@ def test_too_noisy():
signal = np.array([r.choice([-1, 1]) for i in range(int(config.Fs))])
src = BytesIO(common.dumps(signal * 0.5))
for r in calib.detector(config, src=src):
assert not r.success
assert r.msg == 'too noisy signal'
assert not r['success']
assert r['msg'] == 'too noisy signal'
def test_errors():
@@ -94,9 +94,9 @@ def test_drift(freq_err):
src = BytesIO(common.dumps(signal))
iters = 0
for r in calib.detector(config, src, frame_length=frame_length):
assert r.success is True
assert abs(r.rms - rms) < 1e-3
assert abs(r.total - rms) < 1e-3
assert r['success'] is True
assert abs(r['rms'] - rms) < 1e-3
assert abs(r['total'] - rms) < 1e-3
iters += 1
assert iters > 0
@@ -146,3 +146,15 @@ def test_recv_binary_search():
fmt = 'ctl {0:.0f}%'
expected = [mock.call(shell=True, args=fmt.format(100 * g)) for g in gains]
assert check_call.mock_calls == expected
def test_recv_freq_change():
p = ProcessMock()
calib.send(config, p, gain=0.5, limit=2)
offset = p.buf.tell() // 16
p.buf.seek(offset)
messages = [state['msg'] for state in calib.recv_iter(config, p)]
assert messages == [
'good signal', 'good signal', 'good signal',
'frequency change',
'good signal', 'good signal', 'good signal']

View File

@@ -54,14 +54,6 @@ def test_izip():
assert list(common.izip([x, y])) == list(zip(x, y))
def test_holder():
d = {'x': 1, 'y': 2.3}
a = common.AttributeHolder(d)
assert a.x == d['x']
assert a.y == d['y']
assert repr(a) == 'AttributeHolder(x=1, y=2.3)'
def test_configs():
default = config.Configuration()
fastest = config.fastest()

View File

@@ -1 +1 @@
'1.12.0'
'1.13'

View File

@@ -1,3 +0,0 @@
numpy
six
argcomplete

View File

@@ -22,7 +22,7 @@ class PyTest(TestCommand):
def run_tests(self):
import sys
import pytest
sys.exit(pytest.main(['tests']))
sys.exit(pytest.main(['.']))
setup(
name='amodem',
@@ -48,6 +48,7 @@ setup(
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',

View File

@@ -7,7 +7,7 @@ deps=
pep8
coverage
pylint
six
six
commands=
pep8 amodem/ scripts/
pylint --extension-pkg-whitelist=numpy --report=no amodem --rcfile .pylintrc