blob: 22514b7faf716a8da186a45f8d0f5327d7fe1b35 [file] [log] [blame]
# Lint as: python2, python3
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This module provides interface to control button motors."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import logging
import os
import time
from . import chameleon_common # pylint: disable=W0611
from chameleond.utils import chameleon_io as io
from six.moves import range
"""
Holds bit offsets for motor ports.
Currently all motors use the same I/O expander.
If some motor needs to use another I/O expander, or some ports of a
motor need to use another I/O expander, index needs to be saved in
MotorPorts too.
"""
MotorPorts = collections.namedtuple(
'MotorPorts', ['step', 'direction', 'enable'])
"""
--------------- All the way to the top
{} |
{} | num_pulses_reset
{} |
{} V
{} -------- Reset position
{} ^
{} | num_pulses_move
{} V
{} -------- Touch position
----
| | Button
Motor parameter:
num_pulses_reset: Number of pulses to drive step motor to reset position
starting from all the way to the top.
num_pulses_move: Number of pulses to drive step motor to touch position from
reset position.
period_ms: Duration of each pulse in ms.
"""
MotorParams = collections.namedtuple(
'MotorParams', ['num_pulses_reset', 'num_pulses_move', 'period_ms'])
class ButtonFunction(object):
"""Button functions that motor touch/release."""
CALL = 'Call'
HANG_UP = 'Hang Up'
MUTE = 'Mute'
VOL_UP = 'Vol Up'
VOL_DOWN = 'Vol Down'
class MotorBoardException(Exception):
"""Errors in MotorBoard."""
class MotorBoardNotExistException(MotorBoardException):
"""Error that motor board does not exist."""
class MotorBoard(object):
"""Controls button motor through motor board.
A motor has three ports: Step, Direction, Enable.
There are five motors to control on motor board.
This class provides the interface to control these ports.
The ports are controlled through an I/O expander on I2C bus 3 address 0x23.
=============================================
I/O expander: address 0x23
bit offset 15 14 13 12 11 10 9 8
name p17 p16 p15 p14 p13 p12 p11 p10
bit offset 7 6 5 4 3 2 1 0
name p07 p06 p05 p04 p03 p02 p01 p00
=============================================
Common: Microstep resolution
Name name bit offset
MS1 p01 1
MS2 p02 2
MS3 p03 3
=============================================
Motor Function port name bit offset
0 Call step p04 4
dir p00 0
enable p05 5
1 Hangup step p06 6
dir p00 0
enable p07 7
2 Mute step p10 8
dir p00 0
enable p11 9
3 Vol Up step p12 10
dir p00 0
enable p13 11
4 Vol Down step p14 12
dir p00 0
enable p15 13
=============================================
"""
# Mapping from motor function to ports.
_MOTOR_PORT_MAP = {
ButtonFunction.CALL: MotorPorts(4, 0, 5),
ButtonFunction.HANG_UP: MotorPorts(6, 0, 7),
ButtonFunction.MUTE: MotorPorts(8, 0, 9),
ButtonFunction.VOL_UP: MotorPorts(10, 0, 11),
ButtonFunction.VOL_DOWN: MotorPorts(12, 0, 13)}
# TODO(cychiang) Tune the duration and parameters.
_MODEL_PARAMS_MAP = {
'Atrus': {
ButtonFunction.CALL: MotorParams(200, 250, 1),
ButtonFunction.HANG_UP: MotorParams(200, 250, 1),
ButtonFunction.MUTE: MotorParams(200, 250, 1),
ButtonFunction.VOL_UP: MotorParams(200, 200, 1),
ButtonFunction.VOL_DOWN: MotorParams(200, 200, 1),
},
'Jabra': {
ButtonFunction.CALL: MotorParams(350, 250, 1),
ButtonFunction.HANG_UP: MotorParams(350, 250, 1),
ButtonFunction.MUTE: MotorParams(350, 250, 1),
ButtonFunction.VOL_UP: MotorParams(350, 220, 1),
ButtonFunction.VOL_DOWN: MotorParams(350, 220, 1),
}
}
_MODEL_TAG_PATH = '/etc/default/motor_model'
def __init__(self, i2c_bus):
"""Constructs a MotorBoard.
Args:
i2c_bus: The I2cBus object.
"""
self._i2c_bus = i2c_bus
self._motor_io = io.IoExpander(self._i2c_bus,
io.IoExpander.SLAVE_ADDRESSES[3])
self._i2c_bus.AddSlave(self._motor_io)
self._motors = {}
def IsDetected(self):
"""Returns if the device can be detected."""
if self._motor_io.IsDetected():
return True
else:
logging.info('Can not access I2c slave on motor board. '
'Assume it is not connected')
return False
def InitDevice(self):
"""Runs the initialization sequence for the motor board.
Raises:
MotorBoardException: If motor board can not be initialized.
"""
self._ResetPorts()
model_name = self._GetModelName()
logging.info('Init motor board for model: %s', model_name)
self._CreateMotors(model_name)
logging.info('MotorBoard initialized')
def Reset(self):
"""Resets motor boards by releasing button and resetting port directions."""
for func, motor in list(self._motors.items()):
logging.info('Reset motor for %s function', func)
motor.Reset()
def _GetModelName(self):
"""Gets model name for this motor board.
Returns:
A string for model name.
Raises:
If model tag path does not exist or it contains unexpected model name.
"""
if not os.path.exists(self._MODEL_TAG_PATH):
raise MotorBoardException(
'Model path does not exist: %s' % self._MODEL_TAG_PATH)
with open(self._MODEL_TAG_PATH) as f:
model_name = f.readline().strip()
if model_name not in self._MODEL_PARAMS_MAP:
raise MotorBoardException('Model name not expected: %s' % model_name)
return model_name
def _ResetPorts(self):
"""Resets all ports on I/O expander to input direction.
The voltage level of all the ports are determined by their pull-up
or pull-down circuits.
"""
self._motor_io.SetDirection(0xffff)
def _CreateMotors(self, model_name):
"""Creates motors.
Args:
model_name: A key in self._MODEL_PARAMS_MAP.
"""
if model_name not in self._MODEL_PARAMS_MAP:
raise MotorBoardException('Unexpected model name %s' % model_name)
model_map = self._MODEL_PARAMS_MAP[model_name]
for func, params in list(model_map.items()):
ports = self._MOTOR_PORT_MAP[func]
self._motors[func] = Motor(self._motor_io, ports, params)
def _CheckFunction(self, func):
"""Check if a button function is supported on this motor board.
Args:
func: Button function for this motor. Defined in ButtonFunction.
Raises:
MotorBoardException if func is not supported on this motor board.
"""
if func not in self._motors:
raise MotorBoardException('Button function %s not suported' % func)
def Touch(self, func):
"""Let one of the motor moves to touch a button.
Args:
func: Button function for this motor. Defined in ButtonFunction.
"""
self._CheckFunction(func)
self._motors[func].Touch()
def Release(self, func):
"""Let one of the motor release button.
Args:
func: Button function for this motor. Defined in ButtonFunction.
"""
self._CheckFunction(func)
self._motors[func].Release()
class _MotorState(object):
"""Motor states."""
RELEASED = 'Released'
TOUCHED = 'Touched'
class _MotorDirection(object):
"""Motor moving direction."""
UP = 'Up'
DOWN = 'Down'
class MotorError(Exception):
"""Error in Motor."""
class Motor(object):
"""Class to control one motor."""
_MOTOR_PULSE_HIGH = 1
_MOTOR_PULSE_LOW = 0
_NUM_PULSES_RESET_TO_TOP = 800
def __init__(self, motor_io, motor_ports, params):
"""Initializes one Motor.
Args:
motor_io: A IoExpander object.
motor_ports: A MotorPorts for port bit offset.
params: A MotorParams namedtuple.
"""
self._motor_io = motor_io
self._ports = motor_ports
self._params = params
# TODO(cychiang) See if this assumption is true, or add a mechanism
# to reset the motor state when init.
self._state = _MotorState.RELEASED
def Reset(self):
"""Controls the motor to reset position."""
self._Enable(True)
# Moves all the way to the top.
self._SetDirection(_MotorDirection.UP)
self._Move(self._NUM_PULSES_RESET_TO_TOP)
# Moves down to reset position.
self._SetDirection(_MotorDirection.DOWN)
self._Move(self._params.num_pulses_reset)
self._Enable(False)
self._state = _MotorState.RELEASED
def Touch(self):
"""Controls the motor to touch the button."""
if self._state == _MotorState.TOUCHED:
return
self._Enable(True)
self._SetDirection(_MotorDirection.DOWN)
self._Move(self._params.num_pulses_move)
self._Enable(False)
self._state = _MotorState.TOUCHED
def Release(self):
"""Controls the motor to release the button."""
if self._state == _MotorState.RELEASED:
return
self._Enable(True)
self._SetDirection(_MotorDirection.UP)
self._Move(self._params.num_pulses_move)
self._Enable(False)
self._state = _MotorState.RELEASED
def _SetDirection(self, direction):
"""Sets motor movement direction.
Args:
direction: A direction defined in _MotorDirection.
Raises:
MotorError: Unexpected direction.
"""
# Up: set port to 0.
# Down: set port to 1.
value = None
if direction == _MotorDirection.UP:
value = 0
elif direction == _MotorDirection.DOWN:
value = 1
else:
raise MotorError('Unexpected direction: %s' % direction)
offset = self._ports.direction
self._motor_io.SetBit(offset, value)
def _Enable(self, enable):
"""Enables/Disables motor.
Note this function does not start motor movement. It just turns on motor
or let it sleep for power saving.
Args:
enable: True to enable; False to disable.
"""
# TODO(cychiang) Find out whether this port works or not.
# Enable: set port to 0.
# Disable: set port to 1.
value = 0 if enable else 1
offset = self._ports.enable
self._motor_io.SetBit(offset, value)
def _Move(self, num_pulse):
"""Moves motor by driving pulses on step port.
Drives num_pulse pulses with each pulse period_ms duration in ms.
Args:
num_pulse: Number of pulse to drive motor.
"""
offset = self._ports.step
half_period_sec = self._params.period_ms / 2 * 0.001
for _ in range(num_pulse):
self._motor_io.SetBit(offset, self._MOTOR_PULSE_LOW)
time.sleep(half_period_sec)
self._motor_io.SetBit(offset, self._MOTOR_PULSE_HIGH)
time.sleep(half_period_sec)