Skip to content

Commit c9e75cc

Browse files
committed
Cleanup and small enhancements
Restructured code to simplify maintenance and building Fixed message delay calculation Switched to kv language for interface description Enhanced logging handling of modules with Kivy Add application icon Streamlined windows binary
1 parent cf8de23 commit c9e75cc

19 files changed

Lines changed: 456 additions & 361 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ build
77
dist
88
__pycache__
99
# Asset sources
10-
sources
10+
assets

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Denon Remote
22
============
33

4-
![Screenshot](screenshot-v0.2.0.png)
4+
![Screenshot](screenshot-v0.3.0.png)
55

66
Author: Raphael Doursenaud <rdoursenaud@gmail.com>
77

@@ -85,6 +85,11 @@ Dependencies:
8585
- [ ] The Pythonic Way
8686
- [ ] Handle shutdown to power off the device
8787
- [x] PyInstaller
88+
- [x] Generate icon with [IconMaker](https://github.com/Inedo/iconmaker)
89+
- [x] [UPX](https://upx.github.io/) support
90+
- How to build:
91+
- Review [denonremote.spec](denonremote.spec)
92+
- Use `python -m PyInstaller denonremote.spec --upx-dir=c:\upx-3.96-win64`
8893
- [ ] VST plugin? (Not required if MIDI input is implemented but would be neat to have in the monitoring section of a
8994
DAW)
9095
- [ ] See [PyVST](https://pypi.org/project/pyvst/)

constraints.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

denonremote.spec

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,29 @@
22

33
from kivy_deps import sdl2, glew
44

5+
# Minimize dependencies bundling
6+
from kivy.tools.packaging.pyinstaller_hooks import get_deps_minimal, get_deps_all, hookspath, runtime_hooks
7+
58
block_cipher = None
69

710
added_files = [
8-
( 'Unicode_IEC_symbol.ttf', '.' ),
9-
('assets', 'assets')
11+
('denonremote\\fonts', 'fonts'),
12+
('denonremote\\images', 'images')
1013
]
1114

12-
a = Analysis(['main.py'],
13-
pathex=['G:\\raph\\Documents\\GitHub\\denonremote'],
14-
binaries=[],
15+
a = Analysis(['denonremote\\main.py'],
16+
pathex=['denonremote'],
1517
datas=added_files,
16-
hiddenimports=[],
17-
hookspath=[],
18+
hookspath=hookspath(),
1819
runtime_hooks=[],
19-
excludes=[],
2020
win_no_prefer_redirects=False,
2121
win_private_assemblies=False,
2222
cipher=block_cipher,
23-
noarchive=False)
23+
noarchive=False,
24+
**get_deps_all())
2425
pyz = PYZ(a.pure, a.zipped_data,
2526
cipher=block_cipher)
26-
exe = EXE(pyz, Tree('G:\\raph\\Documents\\GitHub\\denonremote\\'),
27+
exe = EXE(pyz, Tree('denonremote'),
2728
a.scripts,
2829
a.binaries,
2930
a.zipfiles,
@@ -37,4 +38,5 @@ exe = EXE(pyz, Tree('G:\\raph\\Documents\\GitHub\\denonremote\\'),
3738
upx=True,
3839
upx_exclude=[],
3940
runtime_tmpdir=None,
40-
console=False)
41+
console=False,
42+
icon='icon.ico')
File renamed without changes.
File renamed without changes.
Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@
66
from twisted.internet.protocol import ClientFactory
77
from twisted.protocols.basic import LineOnlyReceiver
88

9-
from config import GUI
10-
from denon.dn500av import DN500AVMessages, DN500AVFormat
9+
from denon.dn500av import DN500AVMessage, DN500AVFormat
1110

12-
if GUI:
13-
from kivy import Logger
14-
15-
logger = Logger
11+
logger = logging.getLogger(__name__)
1612

1713

1814
# TODO: Implement Serial ?
@@ -24,40 +20,35 @@ class DenonProtocol(LineOnlyReceiver):
2420
MAX_LENGTH = 135
2521
DELAY = 0.04 # in seconds. The documentation requires 200 ms. 40 ms seems safe.
2622
delimiter = b'\r'
27-
ongoing_calls = -1 # Delay handling. FIXME: should timeout after 200 ms.
28-
29-
logger = None
23+
ongoing_calls = 0 # Delay handling. FIXME: should timeout after 200 ms.
3024

3125
def sendLine(self, line):
3226
if b'?' in line:
3327
# A request is made. We need to delay the next calls
3428
self.ongoing_calls += 1
35-
self.logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
36-
self.logger.debug("Will send line: %s", line)
37-
return task.deferLater(reactor, delay=self.DELAY * self.ongoing_calls, callable=super().sendLine, line=line)
29+
logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
30+
logger.debug("Will send line: %s", line)
31+
if self.ongoing_calls:
32+
delay = self.DELAY * (self.ongoing_calls - 1)
33+
else:
34+
delay = self.DELAY
35+
return task.deferLater(reactor, delay=delay,
36+
callable=super().sendLine, line=line)
3837

3938
def lineReceived(self, line):
4039
if self.ongoing_calls:
4140
# We received a reply
4241
self.ongoing_calls -= 1
43-
self.logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
44-
receiver = DN500AVMessages(logger=self.logger)
42+
logger.debug("Ongoing calls for delay: %s", self.ongoing_calls)
43+
receiver = DN500AVMessage()
4544
receiver.parse_response(line)
46-
self.logger.info("Received line: %s", receiver.response)
47-
if self.factory.gui:
48-
self.factory.app.print_message(receiver.response)
49-
# FIXME: abstract
50-
# MUTE
51-
if receiver.command_code == 'MU':
52-
state = False
53-
if receiver.parameter_code == 'ON':
54-
state = True
55-
self.factory.app.update_volume_mute(state)
45+
logger.info("Received line: %s", receiver.response)
5646

57-
# VOLUME
58-
if receiver.command_code == 'MV':
59-
if receiver.subcommand_code is None:
60-
self.factory.app.update_volume(receiver.parameter_label)
47+
# FIXME: parse message into state
48+
49+
# FIXME: abstract away with a callback to the factory
50+
if self.factory.gui:
51+
self.factory.app.print_debug(receiver.response)
6152

6253
# POWER
6354
if receiver.command_code == 'PW':
@@ -66,10 +57,22 @@ def lineReceived(self, line):
6657
state = False
6758
self.factory.app.update_power(state)
6859

60+
# VOLUME
61+
if receiver.command_code == 'MV':
62+
if receiver.subcommand_code is None:
63+
self.factory.app.update_volume(receiver.parameter_label)
64+
65+
# MUTE
66+
if receiver.command_code == 'MU':
67+
state = False
68+
if receiver.parameter_code == 'ON':
69+
state = True
70+
self.factory.app.set_volume_mute(state)
71+
6972
# SOURCE
7073
if receiver.command_code == 'SI':
7174
source = receiver.parameter_code
72-
self.factory.app.update_source(source)
75+
self.factory.app.set_sources(source)
7376

7477
def connectionMade(self):
7578
if self.factory.gui:
@@ -79,7 +82,7 @@ def get_power(self):
7982
self.sendLine('PW?'.encode('ASCII'))
8083

8184
def set_power(self, state):
82-
self.logger.debug("Entering power callback")
85+
logger.debug("Entering power callback")
8386
if state:
8487
self.sendLine('PWON'.encode('ASCII'))
8588
else:
@@ -91,7 +94,7 @@ def get_volume(self):
9194
def set_volume(self, value):
9295
rawvalue = DN500AVFormat().mv_reverse_params.get(value)
9396
if rawvalue is None:
94-
self.logger.warning("Set volume value %s is invalid.", value)
97+
logger.warning("Set volume value %s is invalid.", value)
9598
else:
9699
message = 'MV' + rawvalue
97100
self.sendLine(message.encode('ASCII'))
@@ -118,7 +121,6 @@ class DenonClientFactory(ClientFactory):
118121

119122
def __init__(self):
120123
self.gui = False
121-
self.protocol.logger = logging.getLogger(__name__)
122124

123125

124126
class DenonClientGUIFactory(ClientFactory):
@@ -127,4 +129,6 @@ class DenonClientGUIFactory(ClientFactory):
127129
def __init__(self, app):
128130
self.gui = True
129131
self.app = app
130-
self.protocol.logger = Logger
132+
import kivy.logger
133+
global logger
134+
logger = kivy.logger.Logger
Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# -*- coding: utf-8 -*-
2-
from config import GUI
2+
"""
3+
Denon DN-500AV serial and IP communication protocol description
4+
"""
35

4-
if GUI:
5-
from kivy import Logger
6+
import logging
67

7-
logger = Logger
8+
logger = logging.getLogger(__name__)
89

910
commands = {
1011
'PW': "Power",
@@ -722,12 +723,10 @@ def compute_lfe_volume_label(value):
722723
# TODO: abstract device
723724

724725

725-
class DN500AVMessages():
726+
class DN500AVMessage:
726727
# From DN-500 manual (DN-500AVEM_ENG_CD-ROM_v00.pdf)
727728
# Pages 93-101 (99-107 in PDF form)
728729

729-
logger = None
730-
731730
command_code = None
732731
command_label = None
733732
subcommand_code = None
@@ -736,15 +735,15 @@ class DN500AVMessages():
736735
parameter_label = None
737736
response = None
738737

739-
def __init__(self, logger=None):
740-
self.logger = logger
738+
def __init__(self):
739+
pass
741740

742741
def parse_response(self, status_command):
743742
# Handle strings and bytes
744743
if type(status_command) is bytes:
745744
# FIXME: some parts can be UTF-8 encoded
746745
status_command = status_command.decode('ASCII')
747-
self.logger.debug("Received status command: %s", status_command)
746+
logger.debug("Received status command: %s", status_command)
748747

749748
# Commands are of known sizes. Try the largest first.
750749
for i in range(commands_max_size, commands_min_size - 1, -1):
@@ -754,19 +753,19 @@ def parse_response(self, status_command):
754753
break
755754

756755
if self.command_label is None:
757-
self.logger.error("Command unknown: %s", status_command)
756+
logger.error("Command unknown: %s", status_command)
758757
return
759758
else:
760-
self.logger.info("Parsed command %s: %s", self.command_code, self.command_label)
759+
logger.info("Parsed command %s: %s", self.command_code, self.command_label)
761760

762761
# Trim command from status command stream
763762
status_command = status_command[len(self.command_code):]
764763

765764
# Handle subcommands
766765
if commands_subcommands.get(self.command_code) is None:
767-
self.logger.debug("The command %s doesn't have any known subcommands.", self.command_code)
766+
logger.debug("The command %s doesn't have any known subcommands.", self.command_code)
768767
else:
769-
self.logger.debug("Searching for subcommands in: %s", status_command)
768+
logger.debug("Searching for subcommands in: %s", status_command)
770769

771770
# Subcommands are of known sizes. Try the largest first.
772771
for i in range(commands_subcommands_max_size[self.command_code],
@@ -777,49 +776,46 @@ def parse_response(self, status_command):
777776
break
778777

779778
if self.subcommand_label is None:
780-
self.logger.debug("Subcommand unknown. Probably a parameter: %s", status_command)
779+
logger.debug("Subcommand unknown. Probably a parameter: %s", status_command)
781780
self.subcommand_code = None
782781
else:
783-
self.logger.info("Parsed subcommand %s: %s", self.subcommand_code, self.subcommand_label)
782+
logger.info("Parsed subcommand %s: %s", self.subcommand_code, self.subcommand_label)
784783
# Trim subcommand from status command stream
785784
status_command = status_command[
786785
len(self.subcommand_code) + 1:] # Subcommands have a space before the parameter
787786

788787
# Handle parameters
789-
self.logger.debug("Searching for parameters in: %s", status_command)
788+
logger.debug("Searching for parameters in: %s", status_command)
790789
self.parameter_code = status_command
791790
if self.command_code == 'PS':
792791
self.parameter_label = commands_params[self.command_code][self.subcommand_code].get(self.parameter_code)
793792
else:
794793
self.parameter_label = commands_params[self.command_code].get(self.parameter_code)
795794
if self.parameter_label is None:
796-
self.logger.error("Parameter unknown: %s", status_command)
795+
logger.error("Parameter unknown: %s", status_command)
797796
self.parameter_code = None
798797
else:
799798
# Trim parameters from status command stream
800799
status_command = status_command[len(status_command):]
801800

802801
# Handle unexpected leftovers
803802
if status_command:
804-
self.logger.error("Unexpected unparsed data found: %s", status_command)
803+
logger.error("Unexpected unparsed data found: %s", status_command)
805804

806805
if self.subcommand_label:
807806
self.response = "%s, %s: %s" % (self.command_label, self.subcommand_label, self.parameter_label)
808807
else:
809808
self.response = "%s: %s" % (self.command_label, self.parameter_label)
810809

811810

812-
class DN500AVFormat():
813-
logger = None
814-
811+
class DN500AVFormat:
815812
mv_reverse_params = {}
816813

817-
def __init__(self, logger=None):
818-
self.logger = logger
814+
def __init__(self):
819815
self.mv_reverse_params = dict([(value, key) for key, value in mv_params.items()])
820816

821817
def get_raw_volume_value_from_db_value(self, value):
822-
self.logger.debug('value: %s', value)
818+
logger.debug('value: %s', value)
823819
raw_value = self.mv_reverse_params['value']
824-
self.logger.debug('rawvalue: %s', raw_value)
820+
logger.debug('rawvalue: %s', raw_value)
825821
return raw_value

0 commit comments

Comments
 (0)