Skip to content

Commit

Permalink
Merge pull request #26 from vapor-ware/mhink-ecblue
Browse files Browse the repository at this point in the history
ECblue fan controller.
  • Loading branch information
MatthewHink committed Jan 5, 2018
2 parents 1747970 + 77e0ce9 commit f4f6fa1
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 8 deletions.
2 changes: 2 additions & 0 deletions synse/devicebus/devices/i2c/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"""
# pylint: skip-file

# We need to import these classes in order to initialize them so that
# get_all_subclasses() finds them. Without them, device registration will fail.
from synse.devicebus.devices.i2c.i2c_device import I2CDevice
from synse.devicebus.devices.i2c.max116xx_adc_thermistor import (Max11608Thermistor,
Max11610Thermistor)
Expand Down
3 changes: 3 additions & 0 deletions synse/devicebus/devices/rs485/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
"""
# pylint: skip-file

# We need to import these classes in order to initialize them so that
# get_all_subclasses() finds them. Without them, device registration will fail.
from synse.devicebus.devices.rs485.f660_airflow import F660Airflow
from synse.devicebus.devices.rs485.gs3_2010_fan_controller import GS32010Fan
from synse.devicebus.devices.rs485.rs485_device import RS485Device
from synse.devicebus.devices.rs485.sht31_humidity import SHT31Humidity
from synse.devicebus.devices.rs485.ec_blue_fan_controller import ECblueFan
80 changes: 80 additions & 0 deletions synse/devicebus/devices/rs485/ec_blue_fan_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python
""" Synse ECblue Fan Control RS485 Device.
\\//
\/apor IO
-------------------------------
Copyright (C) 2015-17 Vapor IO
This file is part of Synse.
Synse is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Synse is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Synse. If not, see <http://www.gnu.org/licenses/>.
"""

import logging

from synse.devicebus.devices.rs485.fan_controller import FanController
from synse.protocols.modbus import modbus_common # nopep8

logger = logging.getLogger(__name__)


class ECblueFan(FanController):
""" Device subclass for ECblue fan controller using RS485 communications.
"""
_instance_name = 'ecblue'

def __init__(self, **kwargs):
super(ECblueFan, self).__init__(**kwargs)

logger.debug('ECblueFan kwargs: {}'.format(kwargs))

if self.hardware_type == 'emulator':
raise NotImplementedError('No emulator for ECblueFan.')

logger.debug('ECblueFan self: {}'.format(dir(self)))

def _get_direction(self):
"""Production only direction reads from ecblue_fan (vapor_fan).
:returns: String forward or reverse."""
client = self.create_modbus_client()
return modbus_common.get_fan_direction_ecblue(
client.serial_device, self.slave_address)

def _get_rpm(self):
"""Production only rpm reads from ecblue_fan (vapor_fan).
:returns: Integer rpm."""
client = self.create_modbus_client()
return modbus_common.get_fan_rpm_ecblue(
client.serial_device, self.slave_address)

def _initialize_min_max_rpm(self):
"""Initialize the max and min supported fan rpm settings."""
client = self.create_modbus_client()
# Maximum rpm supported by the fan motor.
self.max_rpm = modbus_common.get_fan_max_rpm_ecblue(
client.serial_device, self.slave_address)
# Minimum rpm setting allowed. For now this is 10% of the max. This is
# due to minimal back EMF at low rpms.
self.min_nonzero_rpm = self.max_rpm / 10

def _set_rpm(self, rpm_setting):
"""Set fan speed to the given RPM.
:param rpm_setting: The user supplied rpm setting.
returns: The modbus write result."""
client = self.create_modbus_client()
return modbus_common.set_fan_rpm_ecblue(
client.serial_device, self.slave_address,
rpm_setting, self.max_rpm)
17 changes: 12 additions & 5 deletions synse/protocols/modbus/dkmodbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ class dkmodbus(object):
_READ_FIFO_QUEUE = '\x18'
_READ_DEVICE_IDENTIFICATION = '\x28'

# For modbus responses, the first bit may be high in some but not all
# implementations.
_READ_HOLDING_REGISTERS_RESPONSE = '\x83'
_READ_INPUT_REGISTER_RESPONSE = '\x84'

# region public

def __init__(self, serial_device):
Expand Down Expand Up @@ -306,14 +311,16 @@ def _send_receive_packet(self, packet):
logger.debug('bytes_returned: {}'.format(bytes_returned))

function_code = cec_rx_packet[1]
# TODO: Double check the '\x04' (_READ_INPUT_REGISTER) in the next PR.
if function_code == dkmodbus._READ_HOLDING_REGISTERS \
or function_code == dkmodbus._READ_INPUT_REGISTER:
if function_code == dkmodbus._WRITE_MULTIPLE_REGISTERS \
or dkmodbus._READ_HOLDING_REGISTERS \
or function_code == dkmodbus._READ_INPUT_REGISTER \
or function_code == dkmodbus._READ_HOLDING_REGISTERS_RESPONSE \
or function_code == dkmodbus._READ_INPUT_REGISTER_RESPONSE:
logger.debug('returning read data')
result = cec_rx_packet[3:3 + bytes_returned]
logger.debug('result: {}'.format(hexlify(result)))
return result
logger.debug('just returning 0')
return 0
raise ValueError('Unexpected function code: {}'.format(
hexlify(function_code)))

# endregion
132 changes: 129 additions & 3 deletions synse/protocols/modbus/modbus_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,26 @@
The gs3 command line tool and the synse device bus use this code.
"""

import logging
import struct
from synse.protocols.modbus import dkmodbus # nopep8
from synse.protocols.conversions import conversions # nopep8

logger = logging.getLogger(__name__)


def get_fan_direction_ecblue(ser, slave_address):
"""Production only direction reads from ECblue fan (vapor_fan).
:param ser: Serial connection to the fan controller.
:param slave_address: The Modbus slave address of the device.
:return: The fan direction of forward or reverse.
"""
direction = read_holding_register(ser, slave_address, 1)
direction &= 8
if direction == 0:
return 'forward'
return 'reverse'


def get_fan_direction_gs3(ser, slave_address):
"""Get the current fan direction for a gs3 fan controller.
Expand All @@ -37,6 +54,31 @@ def get_fan_frequency_gs3(ser, slave_address):
return read_holding_register(ser, slave_address, 0x0002)


def get_fan_rpm_ecblue(ser, slave_address):
"""Production only rpm reads from ECblue fan (vapor_fan).
:param ser: Serial connection to the ECblue fan controller.
:param slave_address: The Modbus slave address of the device.
:return: The fan speed in rpm.
"""
# TODO: There may be another register for this.
# We are reading off of the Hz setting and there is going to be ramp up/down
# time which is not taken into account here.
max_rpm = get_fan_max_rpm_ecblue(ser, slave_address)
logger.debug('max_rpm: {}'.format(max_rpm))

# Get the hz setting in Hz * 10.
hz_setting = read_holding_register(ser, slave_address, 0x02)
logger.debug('raw Hz setting: 0x{:04x}'.format(hz_setting))
# Convert from hex to BCD.
hz_setting_string = '{:04x}'.format(hz_setting)
hz_setting_int = int(hz_setting_string)
hz_setting_float = float(hz_setting_int) / 10.0
ratio = hz_setting_float / 60.0
rpm = int(max_rpm * ratio)
logger.debug('rpm: {}'.format(rpm))
return rpm


def get_fan_rpm_gs3(ser, slave_address):
"""Get the current fan rpm for a gs3 fan controller.
:param ser: Serial connection to the gs3 fan controller.
Expand All @@ -58,6 +100,15 @@ def get_fan_rpm_to_hz_gs3(ser, slave_address, max_rpm):
return float(base_frequency) / float(max_rpm)


def get_fan_max_rpm_ecblue(ser, slave_address):
"""Get the maximum rpm of the fan motor through the ECblue fan controller.
:param ser: Serial connection to the ECblue fan controller.
:param slave_address: The Modbus slave address of the device.
:returns: The base maximum rpm of the fan motor.
"""
return read_holding_register(ser, slave_address, 0x08)


def get_fan_max_rpm_gs3(ser, slave_address):
"""Get the maximum rpm of the fan motor through the gs3 fan controller.
:param ser: Serial connection to the gs3 fan controller.
Expand All @@ -69,16 +120,76 @@ def get_fan_max_rpm_gs3(ser, slave_address):

def read_holding_register(ser, slave_address, register):
"""Read a holding register from an RS485 device.
:param ser: Serial connection to the gs3 fan controller.
:param ser: Serial connection to the fan controller.
Contains baud rate, parity, and timeout.
:param slave_address: The Modbus slave address of the device.
:param register: The register to read (int).
:returns The register reading (int).
"""
client = dkmodbus.dkmodbus(ser)
register_data = client.read_holding_registers(slave_address, register, 1)
result = conversions.unpack_word(register_data)
print '0x{}'.format(result)
logger.debug('register_data: {}, type(register_data): {}'.format(
register_data, type(register_data)))
result = unpack_register_data(register_data)
logger.debug('read_holding_register result: 0x{:x} {}d'.format(result, result))
return result


def read_input_register(ser, slave_address, register):
"""Read an input register from an RS485 device.
:param ser: Serial connection to the fan controller.
Contains baud rate, parity, and timeout.
:param slave_address: The Modbus slave address of the device.
:param register: The register to read (int).
:returns The register reading (int).
"""
client = dkmodbus.dkmodbus(ser)
register_data = client.read_input_registers(slave_address, register, 1)
logger.debug('input register_data (raw): {} length: {}'.format(
register_data, len(register_data)))
result = unpack_register_data(register_data)
logger.debug('read_input_register result: 0x{}'.format(result))
return result


def set_fan_rpm_ecblue(ser, slave_address, rpm_setting, max_rpm):
"""
Set the fan speed in rpm on an ECblue fan controller.
:param ser: Serial connection to the gs3 fan controller.
Contains baud rate, parity, and timeout.
:param slave_address: The Modbus slave address of the device.
:param rpm_setting: The user supplied rpm setting.
:param max_rpm: The max rpm setting for the fan motor.
:return: The modbus write result.
"""
client = dkmodbus.dkmodbus(ser)

logger.debug('Setting fan speed. max_rpm: {}, rpm_setting: {}'.format(
max_rpm, rpm_setting))

percentage_setting = float(rpm_setting) / float(max_rpm)
logger.debug('percentage_setting: {}'.format(percentage_setting))
fan_setting_decimal = int(percentage_setting * float(600.0))
logger.debug('fan_setting_decimal: {}d 0x{:04x}'.format(
fan_setting_decimal, fan_setting_decimal))

fan_setting = (fan_setting_decimal / 100) << 8
logger.debug('fan_setting: {}d 0x{}'.format(fan_setting, fan_setting))
setting = ((fan_setting_decimal % 100) / 10) << 4
fan_setting += setting
logger.debug('fan_setting: {}d 0x{}'.format(fan_setting, fan_setting))
setting = (fan_setting_decimal % 10)
fan_setting += setting
logger.debug('fan_setting: {}d 0x{}'.format(fan_setting, fan_setting))

fan_setting = struct.pack('>H', fan_setting)

result = client.write_multiple_registers(
slave_address, # Slave address.
2, # Register to write to.
1, # Number of registers to write to.
2, # Number of bytes to write.
fan_setting) # Data to write.
return result


Expand Down Expand Up @@ -113,3 +224,18 @@ def set_fan_rpm_gs3(ser, slave_address, rpm_setting, max_rpm):
4, # Number of bytes to write.
packed_hz + '\x00\x01') # Frequency setting in Hz / data # 01 is on, # 00 is off.
return result


def unpack_register_data(register_data):
"""
Unpacks register_data from a byte string to int.
:param register_data: The data to unpack.
:return: The data as an int.
"""
length = len(register_data)
if length == 2:
return conversions.unpack_word(register_data)
elif length == 1:
return conversions.unpack_byte(register_data)
else:
raise ValueError('Unexpected length {}.'.format(length))

0 comments on commit f4f6fa1

Please sign in to comment.