diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 77439312..297e9d62 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -1,6 +1,6 @@ import logging from serial import Serial -from multiprocessing import Process, Pipe +from multiprocessing import Pipe, Process from mopidy.mixers import BaseMixer from mopidy.settings import MIXER_PORT @@ -10,16 +10,18 @@ logger = logging.getLogger('mopidy.mixers.nad') class NadMixer(BaseMixer): """ The NAD mixer was created using a NAD C 355BEE amplifier, but should also - work with other NAD amplifiers supporting the same RS-232 protocol. The C - 355BEE does not give you access to the current volume. It only supports - increasing or decreasing the volume one step at the time. Other NAD - amplifiers may support more advanced volume adjustment than what is + work with other NAD amplifiers supporting the same RS-232 protocol (v2.x). + The C 355BEE does not give you access to the current volume. It only + supports increasing or decreasing the volume one step at the time. Other + NAD amplifiers may support more advanced volume adjustment than what is currently used by this mixer. Sadly, this means that if you use the remote control to change the volume on the amplifier, Mopidy will no longer report the correct volume. To recalibrate the mixer, set the volume to 0, and then back again to the - level you want. + level you want. This will reset the amplifier to a known state, including + powering on the device, selecting the configured speakers and input + sources. """ def __init__(self): @@ -33,14 +35,30 @@ class NadMixer(BaseMixer): def _set_volume(self, volume): self._volume = volume if volume == 0: - self._pipe.send({'command': 'calibrate'}) + self._pipe.send({'command': 'reset_device'}) self._pipe.send({'command': 'set_volume', 'volume': volume}) class NadTalker(Process): + #: Timeout in seconds used for read/write operations. + #: + #: If you set the timeout too low, the reads will never get complete + #: confirmations and calibration will decrease volume forever. If you set + #: the timeout too high, stuff takes more time. + TIMEOUT = 0.2 + #: Number of volume levels the device supports NUM_STEPS = 40 + #: The amplifier source to use + SOURCE = 'Aux' + + #: State of speakers A + SPEAKERS_A = 'On' + + #: State of speakers B + SPEAKERS_B = 'Off' + #: Volume in range [0..NUM_STEPS]. :class:`None` before calibration. _nad_volume = None @@ -49,29 +67,63 @@ class NadTalker(Process): self.pipe = pipe def run(self): - # If you set the timeout too low, the reads will never get complete - # confirmations and calibration will decrease volume forever. - self._device = Serial(port=MIXER_PORT, baudrate=115200, timeout=0.2) - self._clear() - self._device_model = self._get_device_model() - self._calibrate() + self._open_connection() + self._set_device_to_known_state() while self.pipe.poll(None): message = self.pipe.recv() if message['command'] == 'set_volume': self._set_volume(message['volume']) - elif message['command'] == 'calibrate': - self._calibrate() + elif message['command'] == 'reset_device': + self._set_device_to_known_state() + + def _open_connection(self): + """ + Opens serial connection to the device. + + Communication settings: 115200 bps 8N1 + """ + self._device = Serial(port=MIXER_PORT, baudrate=115200, + timeout=self.TIMEOUT) + self._get_device_model() + + def _set_device_to_known_state(self): + self._power_device_on() + self._select_speakers() + self._select_input_source() + self._calibrate_volume() def _get_device_model(self): - self._write('Main.Model?') - result = '' - while len(result) < 2: - result = self._readline() - result = result.replace('Main.Model=', '') - logger.info(u'Connected to device of model "%s"' % result) - return result + model = self._ask_device('Main.Model') + logger.info(u'Connected to device of model "%s"' % model) + return model - def _calibrate(self): + def _power_device_on(self): + logger.info(u'Powering device on') + while self._ask_device('Main.Power') != 'On': + self._command_device('Main.Power', 'On') + + def _select_speakers(self): + logger.info(u'Setting speakers A "%s"', self.SPEAKERS_A) + while self._ask_device('Main.SpeakerA') != self.SPEAKERS_A: + self._command_device('Main.SpeakerA', self.SPEAKERS_A) + logger.info(u'Setting speakers B "%s"', self.SPEAKERS_B) + while self._ask_device('Main.SpeakerB') != self.SPEAKERS_B: + self._command_device('Main.SpeakerB', self.SPEAKERS_B) + + def _select_input_source(self): + logger.info(u'Selecting input source "%s"', self.SOURCE) + while self._ask_device('Main.Source') != self.SOURCE: + self._command_device('Main.Source', self.SOURCE) + + def _ask_device(self, key): + self._write('%s?' % key) + return self._readline().replace('%s=' % key, '') + + def _command_device(self, key, value): + self._write('%s=%s' % (key, value)) + self._readline() + + def _calibrate_volume(self): """ The NAD C 355BEE amplifier has 40 different volume levels. We have no way of asking on which level we are. Thus, we must calibrate the mixer @@ -83,24 +135,21 @@ class NadTalker(Process): if self._decrease_volume(): steps_left -= 1 self._nad_volume = 0 - logger.info(u'Calibration of NAD amplifier done') + logger.info(u'Done calibrating NAD amplifier') def _set_volume(self, volume): - logger.debug(u'Setting volume to %d' % volume) - self._set_nad_volume(int(round(volume * self.NUM_STEPS / 100.0))) - - def _set_nad_volume(self, target_volume): """ Increase or decrease the amplifier volume until it matches the given - target volume. Only calls to increase and decrease that returns - :class:`True` are counted against the internal volume. + target volume. """ + logger.debug(u'Setting volume to %d' % volume) + target_nad_volume = int(round(volume * self.NUM_STEPS / 100.0)) if self._nad_volume is None: - raise Exception(u'Calibration needed') - while target_volume > self._nad_volume: + return # Calibration needed + while target_nad_volume > self._nad_volume: if self._increase_volume(): self._nad_volume += 1 - while target_volume < self._nad_volume: + while target_nad_volume < self._nad_volume: if self._decrease_volume(): self._nad_volume -= 1 @@ -112,17 +161,17 @@ class NadTalker(Process): self._write('Main.Volume-') return self._readline() == 'Main.Volume-' - def _clear(self): - """Clear input and output buffers.""" - self._device.flushInput() - self._device.flushOutput() - def _write(self, data): - """Write and flush data to device.""" + """ + Write data to device. + + Prepends and appends a newline to the data, as recommended by the NAD + documentation. + """ if not self._device.isOpen(): self._device.open() - self._device.write('\r%s\r' % data) - self._device.flush() + self._device.write('\n%s\n' % data) + logger.debug('Write: %s', data) def _readline(self): """ @@ -131,5 +180,7 @@ class NadTalker(Process): """ if not self._device.isOpen(): self._device.open() - result = self._device.readline(eol='\r') - return result.strip() + result = self._device.readline(eol='\n').strip() + if result: + logger.debug('Read: %s', result) + return result