From 04fc3d4135f82b7c6aaea6437b243a43a39f381c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 1 Nov 2014 12:40:46 +0200 Subject: [PATCH 1/6] Add audio modem integration for transaction sending & receiving http://www.flaticon.com/free-icon/speaker-outline_54951 Speaker icon made by Catalin Fertu from www.flaticon.com is licensed under CC BY 3.0 http://www.flaticon.com/free-icon/mic_10032 Microphone icon made by Elegant Themes from www.flaticon.com is licensed under CC BY 3.0 --- gui/qt/qrtextedit.py | 3 + icons.qrc | 2 + icons/microphone.png | Bin 0 -> 199 bytes icons/speaker.png | Bin 0 -> 392 bytes plugins/audio_modem.py | 123 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 icons/microphone.png create mode 100644 icons/speaker.png create mode 100644 plugins/audio_modem.py diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py index 8b7106e3..994733ab 100644 --- a/gui/qt/qrtextedit.py +++ b/gui/qt/qrtextedit.py @@ -1,4 +1,5 @@ from electrum.i18n import _ +from electrum.plugins import run_hook from PyQt4.QtGui import * from PyQt4.QtCore import * @@ -25,6 +26,7 @@ class ShowQRTextEdit(QRTextEdit): super(ShowQRTextEdit, self).__init__(text) self.setReadOnly(1) self.button.clicked.connect(self.qr_show) + run_hook('show_text_edit', self) def qr_show(self): from qrcodewidget import QRDialog @@ -49,6 +51,7 @@ class ScanQRTextEdit(QRTextEdit): if win: assert hasattr(win,"config"), "You must pass a window with access to the config to ScanQRTextEdit constructor." self.button.clicked.connect(self.qr_input) + run_hook('scan_text_edit', self) def qr_input(self): diff --git a/icons.qrc b/icons.qrc index 94fd43e3..e2197dd0 100644 --- a/icons.qrc +++ b/icons.qrc @@ -28,6 +28,8 @@ icons/network.png icons/dark_background.png icons/qrcode.png + icons/microphone.png + icons/speaker.png icons/trustedcoin.png diff --git a/icons/microphone.png b/icons/microphone.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a271c55810fd14f8915beaebd826b0e52a3ffa GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6`aE46Ln;{OUf7t)>?pu`q5b1j z1x3?MjY8rIER&d?-){K5!8v(1?{aoOF}=^nt~Wclu6bmuwo9D%u>U{y2FI|_n+j8W zC#6hkUBxm<>q^t<$rrjDHirBoc5!lIL8@MUQTpt6Hc|`>jI5q6 zjv*Qo=UzUqeI!tX^~3q!K_X%%(kG9Fc}DiBX-|5_|A2qP(L?_iq@^Y&uAZX%lX;4! zs_LfDx+zmcwO409)HMES^z2IHp5psEx8Jk%clpsh!%6t{iY-^Xc>dlvq^tGfAk(*k z{+pNBRy?%vw>};dT`PA!A}g~&@lAsK;z;YmGHwRn-sXvxOXvi7wQDXtpxvgNq^dM; zOA^C!&82Q$E6tT=SsZk0v{JixV7I)0ApcLr^bJ#^Q6A>ar(MdUfQkVzopr00*d;g8%>k literal 0 HcmV?d00001 diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py new file mode 100644 index 00000000..146cb135 --- /dev/null +++ b/plugins/audio_modem.py @@ -0,0 +1,123 @@ +from electrum.plugins import BasePlugin, hook +from electrum_gui.qt.util import WaitingDialog +from electrum.util import print_msg + +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +import subprocess +import traceback +import zlib +import json + +try: + subprocess.check_call(['amodem-cli', '--help']) + print_msg('Audio MODEM is enabled.') + amodem_available = True +except subprocess.CalledProcessError: + print_msg('Audio MODEM is not found.') + amodem_available = False + + +def send(parent, blob): + print_msg('Sending:', repr(blob)) + blob = zlib.compress(blob) + def sender_thread(): + try: + args = ['amodem-cli', 'send', '-vv'] + p = subprocess.Popen(args, stdin=subprocess.PIPE) + p.stdin.write(blob) + p.stdin.close() + p.wait() + except Exception: + traceback.print_exc() + p.kill() + + return WaitingDialog( + parent=parent, message='Sending transaction to Audio MODEM...', + run_task=sender_thread) + + +def recv(parent): + def receiver_thread(): + import subprocess + try: + args = ['amodem-cli', 'recv', '-vv'] + p = subprocess.Popen(args, stdout=subprocess.PIPE) + return p.stdout.read() + except Exception: + traceback.print_exc() + p.kill() + + def on_success(blob): + blob = zlib.decompress(blob) + print_msg('Received:', repr(blob)) + parent.setText(blob) + + return WaitingDialog( + parent=parent, message='Receiving transaction from Audio MODEM...', + run_task=receiver_thread, on_success=on_success) + + +class Plugin(BasePlugin): + + def fullname(self): + return 'Audio MODEM' + + def description(self): + return ('Provides support for air-gapped transaction signing.\n\n' + 'Requires http://github.com/romanz/amodem/') + + def is_available(self): + return amodem_available + + is_enabled = is_available + + @hook + def transaction_dialog(self, dialog): + b = QPushButton() + b.setIcon(QIcon(":icons/speaker.png")) + + def handler(): + blob = json.dumps(dialog.tx.as_dict()) + self.sender = send(parent=dialog, blob=blob) + self.sender.start() + b.clicked.connect(handler) + dialog.buttons.insertWidget(1, b) + + @hook + def scan_text_edit(self, parent): + def handler(): + self.receiver = recv(parent=parent) + self.receiver.start() + button = add_button(parent=parent, icon_name=':icons/microphone.png') + button.clicked.connect(handler) + + @hook + def show_text_edit(self, parent): + def handler(): + blob = str(parent.toPlainText()) + self.sender = send(parent=parent, blob=blob) + self.sender.start() + button = add_button(parent=parent, icon_name=':icons/speaker.png') + button.clicked.connect(handler) + + +def add_button(parent, icon_name): + audio_button = QToolButton(parent) + audio_button.setIcon(QIcon(icon_name)) + audio_button.setStyleSheet("QToolButton { border: none; padding: 0px; }") + audio_button.setVisible(True) + + parent_resizeEvent = parent.resizeEvent + + def resizeEvent(e): + result = parent_resizeEvent(e) + qr_button = parent.button + left = qr_button.geometry().left() - audio_button.sizeHint().width() + top = qr_button.geometry().top() + audio_button.move(left, top) + return result + + parent.resizeEvent = resizeEvent + return audio_button From 4acc09c91a815d515a4ad4b8fedcddd371fc86cd Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 30 Dec 2014 08:39:29 +0200 Subject: [PATCH 2/6] Use amodem as a Python package instead of subprocess. --- plugins/audio_modem.py | 91 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index 146cb135..f0fcec81 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -5,60 +5,20 @@ from electrum.util import print_msg from PyQt4.QtGui import * from PyQt4.QtCore import * -import subprocess import traceback import zlib import json +from io import BytesIO try: - subprocess.check_call(['amodem-cli', '--help']) + import amodem print_msg('Audio MODEM is enabled.') amodem_available = True -except subprocess.CalledProcessError: +except ImportError: print_msg('Audio MODEM is not found.') amodem_available = False -def send(parent, blob): - print_msg('Sending:', repr(blob)) - blob = zlib.compress(blob) - def sender_thread(): - try: - args = ['amodem-cli', 'send', '-vv'] - p = subprocess.Popen(args, stdin=subprocess.PIPE) - p.stdin.write(blob) - p.stdin.close() - p.wait() - except Exception: - traceback.print_exc() - p.kill() - - return WaitingDialog( - parent=parent, message='Sending transaction to Audio MODEM...', - run_task=sender_thread) - - -def recv(parent): - def receiver_thread(): - import subprocess - try: - args = ['amodem-cli', 'recv', '-vv'] - p = subprocess.Popen(args, stdout=subprocess.PIPE) - return p.stdout.read() - except Exception: - traceback.print_exc() - p.kill() - - def on_success(blob): - blob = zlib.decompress(blob) - print_msg('Received:', repr(blob)) - parent.setText(blob) - - return WaitingDialog( - parent=parent, message='Receiving transaction from Audio MODEM...', - run_task=receiver_thread, on_success=on_success) - - class Plugin(BasePlugin): def fullname(self): @@ -80,7 +40,7 @@ class Plugin(BasePlugin): def handler(): blob = json.dumps(dialog.tx.as_dict()) - self.sender = send(parent=dialog, blob=blob) + self.sender = self._send(parent=dialog, blob=blob) self.sender.start() b.clicked.connect(handler) dialog.buttons.insertWidget(1, b) @@ -88,7 +48,7 @@ class Plugin(BasePlugin): @hook def scan_text_edit(self, parent): def handler(): - self.receiver = recv(parent=parent) + self.receiver = self._recv(parent=parent) self.receiver.start() button = add_button(parent=parent, icon_name=':icons/microphone.png') button.clicked.connect(handler) @@ -97,11 +57,50 @@ class Plugin(BasePlugin): def show_text_edit(self, parent): def handler(): blob = str(parent.toPlainText()) - self.sender = send(parent=parent, blob=blob) + self.sender = self._send(parent=parent, blob=blob) self.sender.start() button = add_button(parent=parent, icon_name=':icons/speaker.png') button.clicked.connect(handler) + def _send(self, parent, blob): + def sender_thread(): + try: + modem_config = amodem.config.slowest() + audio_interface = amodem.audio.Interface(modem_config) + src = BytesIO(blob) + dst = audio_interface.player() + amodem.send.main(config=modem_config, src=src, dst=dst) + except Exception: + traceback.print_exc() + + print_msg('Sending:', repr(blob)) + blob = zlib.compress(blob) + return WaitingDialog( + parent=parent, message='Sending transaction to Audio MODEM...', + run_task=sender_thread) + + def _recv(self, parent): + def receiver_thread(): + try: + modem_config = amodem.config.slowest() + audio_interface = amodem.audio.Interface(modem_config) + src = audio_interface.recorder() + dst = BytesIO() + amodem.recv.main(config=modem_config, src=src, dst=dst) + return dst.getvalue() + except Exception: + traceback.print_exc() + + def on_success(blob): + if blob: + blob = zlib.decompress(blob) + print_msg('Received:', repr(blob)) + parent.setText(blob) + + return WaitingDialog( + parent=parent, message='Receiving transaction from Audio MODEM...', + run_task=receiver_thread, on_success=on_success) + def add_button(parent, icon_name): audio_button = QToolButton(parent) From a75fcd19eb143566c69ede352a361fe6f03a7c93 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 30 Dec 2014 16:12:00 +0200 Subject: [PATCH 3/6] Add bitrate settings for Audio MODEM --- plugins/audio_modem.py | 67 ++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index f0fcec81..ae3cf4fc 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -1,6 +1,7 @@ from electrum.plugins import BasePlugin, hook -from electrum_gui.qt.util import WaitingDialog +from electrum_gui.qt.util import WaitingDialog, EnterButton from electrum.util import print_msg +from electrum.i18n import _ from PyQt4.QtGui import * from PyQt4.QtCore import * @@ -13,14 +14,18 @@ from io import BytesIO try: import amodem print_msg('Audio MODEM is enabled.') - amodem_available = True except ImportError: + amodem = None print_msg('Audio MODEM is not found.') - amodem_available = False class Plugin(BasePlugin): + def __init__(self, config, name): + BasePlugin.__init__(self, config, name) + if self.is_available(): + self.modem_config = amodem.config.slowest() + def fullname(self): return 'Audio MODEM' @@ -29,10 +34,40 @@ class Plugin(BasePlugin): 'Requires http://github.com/romanz/amodem/') def is_available(self): - return amodem_available + return amodem is not None is_enabled = is_available + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self): + d = QDialog() + d.setWindowTitle("Settings") + + layout = QGridLayout(d) + layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) + + bitrates = list(sorted(amodem.config.bitrates.keys())) + + def _index_changed(index): + bitrate = bitrates[index] + self.modem_config = amodem.config.bitrates[bitrate] + + combo = QComboBox() + combo.addItems(map(str, bitrates)) + combo.currentIndexChanged.connect(_index_changed) + layout.addWidget(combo, 0, 1) + + ok_button = QPushButton(_("OK")) + ok_button.clicked.connect(d.accept) + layout.addWidget(ok_button, 1, 1) + + return bool(d.exec_()) + @hook def transaction_dialog(self, dialog): b = QPushButton() @@ -65,28 +100,27 @@ class Plugin(BasePlugin): def _send(self, parent, blob): def sender_thread(): try: - modem_config = amodem.config.slowest() - audio_interface = amodem.audio.Interface(modem_config) + audio_interface = amodem.audio.Interface(self.modem_config) src = BytesIO(blob) dst = audio_interface.player() - amodem.send.main(config=modem_config, src=src, dst=dst) + amodem.send.main(config=self.modem_config, src=src, dst=dst) except Exception: traceback.print_exc() print_msg('Sending:', repr(blob)) blob = zlib.compress(blob) - return WaitingDialog( - parent=parent, message='Sending transaction to Audio MODEM...', - run_task=sender_thread) + + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) + return WaitingDialog(parent=parent, message=msg, run_task=sender_thread) def _recv(self, parent): def receiver_thread(): try: - modem_config = amodem.config.slowest() - audio_interface = amodem.audio.Interface(modem_config) + audio_interface = amodem.audio.Interface(self.modem_config) src = audio_interface.recorder() dst = BytesIO() - amodem.recv.main(config=modem_config, src=src, dst=dst) + amodem.recv.main(config=self.modem_config, src=src, dst=dst) return dst.getvalue() except Exception: traceback.print_exc() @@ -97,9 +131,10 @@ class Plugin(BasePlugin): print_msg('Received:', repr(blob)) parent.setText(blob) - return WaitingDialog( - parent=parent, message='Receiving transaction from Audio MODEM...', - run_task=receiver_thread, on_success=on_success) + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) + return WaitingDialog(parent=parent, message=msg, + run_task=receiver_thread, on_success=on_success) def add_button(parent, icon_name): From 3fa20d0e334ae26000216f2457c0fd011dc998a5 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 30 Dec 2014 16:13:06 +0200 Subject: [PATCH 4/6] Add logging for Audio MODEM plugin --- plugins/audio_modem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index ae3cf4fc..4cf8409f 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -10,10 +10,13 @@ import traceback import zlib import json from io import BytesIO +import sys try: import amodem print_msg('Audio MODEM is enabled.') + amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) + amodem.log.setLevel(amodem.logging.INFO) except ImportError: amodem = None print_msg('Audio MODEM is not found.') From 7833055308c6139b8542fc07cc979c9ed4a1b93c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 6 Jan 2015 18:37:13 +0200 Subject: [PATCH 5/6] Update for amodem v1.5 - amodem does not depend on pyaudio (only on numpy) - use ctypes to access PortAudio API --- plugins/audio_modem.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index 4cf8409f..edb92cfe 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -11,9 +11,13 @@ import zlib import json from io import BytesIO import sys +import platform try: - import amodem + import amodem.audio + import amodem.recv + import amodem.send + import amodem.config print_msg('Audio MODEM is enabled.') amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) amodem.log.setLevel(amodem.logging.INFO) @@ -28,6 +32,9 @@ class Plugin(BasePlugin): BasePlugin.__init__(self, config, name) if self.is_available(): self.modem_config = amodem.config.slowest() + self.library_name = { + 'Linux': 'libportaudio.so' + }[platform.system()] def fullname(self): return 'Audio MODEM' @@ -100,13 +107,19 @@ class Plugin(BasePlugin): button = add_button(parent=parent, icon_name=':icons/speaker.png') button.clicked.connect(handler) + def _audio_interface(self): + return amodem.audio.Interface( + config=self.modem_config, + name=self.library_name + ) + def _send(self, parent, blob): def sender_thread(): try: - audio_interface = amodem.audio.Interface(self.modem_config) - src = BytesIO(blob) - dst = audio_interface.player() - amodem.send.main(config=self.modem_config, src=src, dst=dst) + with self._audio_interface() as interface: + src = BytesIO(blob) + dst = interface.player() + amodem.send.main(config=self.modem_config, src=src, dst=dst) except Exception: traceback.print_exc() @@ -120,11 +133,11 @@ class Plugin(BasePlugin): def _recv(self, parent): def receiver_thread(): try: - audio_interface = amodem.audio.Interface(self.modem_config) - src = audio_interface.recorder() - dst = BytesIO() - amodem.recv.main(config=self.modem_config, src=src, dst=dst) - return dst.getvalue() + with self._audio_interface() as interface: + src = interface.recorder() + dst = BytesIO() + amodem.recv.main(config=self.modem_config, src=src, dst=dst) + return dst.getvalue() except Exception: traceback.print_exc() From 2ab839f2427972c0cdb425fd48df1f05555cc778 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 7 Jan 2015 02:49:56 +0100 Subject: [PATCH 6/6] do not self-enable audio modem plugin --- plugins/audio_modem.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index edb92cfe..67b46aa5 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -18,7 +18,7 @@ try: import amodem.recv import amodem.send import amodem.config - print_msg('Audio MODEM is enabled.') + print_msg('Audio MODEM is available.') amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) amodem.log.setLevel(amodem.logging.INFO) except ImportError: @@ -46,8 +46,6 @@ class Plugin(BasePlugin): def is_available(self): return amodem is not None - is_enabled = is_available - def requires_settings(self): return True