| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | """ Panohead remote control. |
|---|
| 4 | |
|---|
| 5 | License |
|---|
| 6 | ======= |
|---|
| 7 | |
|---|
| 8 | - B{Papywizard} (U{http://www.papywizard.org}) is Copyright: |
|---|
| 9 | - (C) 2007-2011 Frédéric Mantegazza |
|---|
| 10 | |
|---|
| 11 | This software is governed by the B{CeCILL} license under French law and |
|---|
| 12 | abiding by the rules of distribution of free software. You can use, |
|---|
| 13 | modify and/or redistribute the software under the terms of the CeCILL |
|---|
| 14 | license as circulated by CEA, CNRS and INRIA at the following URL |
|---|
| 15 | U{http://www.cecill.info}. |
|---|
| 16 | |
|---|
| 17 | As a counterpart to the access to the source code and rights to copy, |
|---|
| 18 | modify and redistribute granted by the license, users are provided only |
|---|
| 19 | with a limited warranty and the software's author, the holder of the |
|---|
| 20 | economic rights, and the successive licensors have only limited |
|---|
| 21 | liability. |
|---|
| 22 | |
|---|
| 23 | In this respect, the user's attention is drawn to the risks associated |
|---|
| 24 | with loading, using, modifying and/or developing or reproducing the |
|---|
| 25 | software by the user in light of its specific status of free software, |
|---|
| 26 | that may mean that it is complicated to manipulate, and that also |
|---|
| 27 | therefore means that it is reserved for developers and experienced |
|---|
| 28 | professionals having in-depth computer knowledge. Users are therefore |
|---|
| 29 | encouraged to load and test the software's suitability as regards their |
|---|
| 30 | requirements in conditions enabling the security of their systems and/or |
|---|
| 31 | data to be ensured and, more generally, to use and operate it in the |
|---|
| 32 | same conditions as regards security. |
|---|
| 33 | |
|---|
| 34 | The fact that you are presently reading this means that you have had |
|---|
| 35 | knowledge of the CeCILL license and that you accept its terms. |
|---|
| 36 | |
|---|
| 37 | Module purpose |
|---|
| 38 | ============== |
|---|
| 39 | |
|---|
| 40 | Hardware |
|---|
| 41 | |
|---|
| 42 | Implements |
|---|
| 43 | ========== |
|---|
| 44 | |
|---|
| 45 | - MerlinOrionHardware |
|---|
| 46 | |
|---|
| 47 | @author: Frédéric Mantegazza |
|---|
| 48 | @copyright: (C) 2007-2011 Frédéric Mantegazza |
|---|
| 49 | @license: CeCILL |
|---|
| 50 | """ |
|---|
| 51 | |
|---|
| 52 | __revision__ = "$Id$" |
|---|
| 53 | |
|---|
| 54 | from PyQt4 import QtCore |
|---|
| 55 | |
|---|
| 56 | from papywizard.common.exception import HardwareError |
|---|
| 57 | from papywizard.common.loggingServices import Logger |
|---|
| 58 | from papywizard.hardware.abstractHardware import AbstractHardware |
|---|
| 59 | |
|---|
| 60 | ENCODER_ZERO = 0x800000 |
|---|
| 61 | |
|---|
| 62 | |
|---|
| 63 | class MerlinOrionHardware(AbstractHardware): |
|---|
| 64 | """ Merlin/Orion low-level hardware. |
|---|
| 65 | """ |
|---|
| 66 | def _init(self): |
|---|
| 67 | AbstractHardware._init(self) |
|---|
| 68 | self.__encoderFullCircle = None |
|---|
| 69 | |
|---|
| 70 | def __decodeAxisValue(self, strValue): |
|---|
| 71 | """ Decode value from axis. |
|---|
| 72 | |
|---|
| 73 | Values (position, speed...) returned by axis are |
|---|
| 74 | 32bits-encoded strings, low byte first. |
|---|
| 75 | |
|---|
| 76 | @param strValue: value returned by axis |
|---|
| 77 | @type strValue: str |
|---|
| 78 | |
|---|
| 79 | @return: value |
|---|
| 80 | @rtype: int |
|---|
| 81 | """ |
|---|
| 82 | value = 0 |
|---|
| 83 | for i in xrange(3): |
|---|
| 84 | value += eval("0x%s" % strValue[i*2:i*2+2]) * 2 ** (i * 8) |
|---|
| 85 | |
|---|
| 86 | return value |
|---|
| 87 | |
|---|
| 88 | def __encodeAxisValue(self, value): |
|---|
| 89 | """ Encode value for axis. |
|---|
| 90 | |
|---|
| 91 | Values (position, speed...) to send to axis must be |
|---|
| 92 | 32bits-encoded strings, low byte first. |
|---|
| 93 | |
|---|
| 94 | @param value: value |
|---|
| 95 | @type value: int |
|---|
| 96 | |
|---|
| 97 | @return: value to send to axis |
|---|
| 98 | @rtype: str |
|---|
| 99 | """ |
|---|
| 100 | strHexValue = "000000%s" % hex(value)[2:] |
|---|
| 101 | strValue = strHexValue[-2:] + strHexValue[-4:-2] + strHexValue[-6:-4] |
|---|
| 102 | |
|---|
| 103 | return strValue.upper() |
|---|
| 104 | |
|---|
| 105 | def __encoderToAngle(self, codPos): |
|---|
| 106 | """ Convert encoder value to degres. |
|---|
| 107 | |
|---|
| 108 | @param codPos: encoder position |
|---|
| 109 | @type codPos: int |
|---|
| 110 | |
|---|
| 111 | @return: position, in ° |
|---|
| 112 | @rtype: float |
|---|
| 113 | """ |
|---|
| 114 | return (codPos - ENCODER_ZERO) * 360. / self.__encoderFullCircle |
|---|
| 115 | |
|---|
| 116 | def __angleToEncoder(self, pos): |
|---|
| 117 | """ Convert degres to encoder value. |
|---|
| 118 | |
|---|
| 119 | @param pos: position, in ° |
|---|
| 120 | @type pos: float |
|---|
| 121 | |
|---|
| 122 | @return: encoder position |
|---|
| 123 | @rtype: int |
|---|
| 124 | """ |
|---|
| 125 | return int(pos * self.__encoderFullCircle / 360. + ENCODER_ZERO) |
|---|
| 126 | |
|---|
| 127 | def __sendCmd(self, cmd, param=""): |
|---|
| 128 | """ Send a command to the axis. |
|---|
| 129 | |
|---|
| 130 | @param cmd: command to send |
|---|
| 131 | @type cmd: str |
|---|
| 132 | |
|---|
| 133 | @return: answer |
|---|
| 134 | @rtype: str |
|---|
| 135 | """ |
|---|
| 136 | cmd = ":%s%d%s\r" % (cmd, self._axis, param) |
|---|
| 137 | #Logger().debug("MerlinOrionHardware.__sendCmd(): axis %d cmd=%s" % (self._axis, repr(cmd))) |
|---|
| 138 | for nbTry in xrange(self._nbRetry): |
|---|
| 139 | try: |
|---|
| 140 | answer = "" |
|---|
| 141 | self._driver.empty() |
|---|
| 142 | self._driver.write(cmd) |
|---|
| 143 | c = '' |
|---|
| 144 | while c not in ('=', '!'): |
|---|
| 145 | c = self._driver.read(1) |
|---|
| 146 | #Logger().debug("MerlinOrionHardware.__sendCmd(): c=%s" % repr(c)) |
|---|
| 147 | if c == '!': |
|---|
| 148 | c = self._driver.read(1) # Get error code |
|---|
| 149 | raise IOError("Error in command %s (err=%s)" % (repr(cmd), c)) |
|---|
| 150 | answer = "" |
|---|
| 151 | while True: |
|---|
| 152 | c = self._driver.read(1) |
|---|
| 153 | #Logger().debug("MerlinOrionHardware.__sendCmd(): c=%s" % repr(c)) |
|---|
| 154 | if c == '\r': |
|---|
| 155 | break |
|---|
| 156 | answer += c |
|---|
| 157 | |
|---|
| 158 | except IOError: |
|---|
| 159 | Logger().exception("MerlinOrionHardware.__sendCmd") |
|---|
| 160 | Logger().warning("MerlinOrionHardware.__sendCmd(): axis %d can't sent command %s. Retrying..." % (self._axis, repr(cmd))) |
|---|
| 161 | else: |
|---|
| 162 | break |
|---|
| 163 | else: |
|---|
| 164 | raise HardwareError("axis %d can't send command %s" % (self._axis, repr(cmd))) |
|---|
| 165 | #Logger().debug("MerlinOrionHardware.__sendCmd(): axis %d ans=%s" % (self._axis, repr(answer))) |
|---|
| 166 | |
|---|
| 167 | return answer |
|---|
| 168 | |
|---|
| 169 | def init(self): |
|---|
| 170 | """ Init the MerlinOrion hardware. |
|---|
| 171 | |
|---|
| 172 | Done only once per axis. |
|---|
| 173 | """ |
|---|
| 174 | self._driver.acquireBus() |
|---|
| 175 | try: |
|---|
| 176 | |
|---|
| 177 | # Stop motor |
|---|
| 178 | self.__sendCmd("L") |
|---|
| 179 | |
|---|
| 180 | # Check motor? |
|---|
| 181 | self.__sendCmd("F") |
|---|
| 182 | |
|---|
| 183 | # Get firmeware version |
|---|
| 184 | value = self.__sendCmd("e") |
|---|
| 185 | Logger().debug("MerlinOrionHardware.init(): firmeware version=%s" % value) |
|---|
| 186 | |
|---|
| 187 | # Get encoder full circle |
|---|
| 188 | value = self.__sendCmd("a") |
|---|
| 189 | self.__encoderFullCircle = self.__encoderFullCircle_ = self.__decodeAxisValue(value) |
|---|
| 190 | Logger().debug("MerlinOrionHardware.init(): encoder full circle=%s" % hex(self.__encoderFullCircle)) |
|---|
| 191 | |
|---|
| 192 | # Get sidereal rate |
|---|
| 193 | value = self.__sendCmd("D") |
|---|
| 194 | Logger().debug("MerlinOrionHardware.init(): sidereal rate=%s" % hex(self.__decodeAxisValue(value))) |
|---|
| 195 | |
|---|
| 196 | finally: |
|---|
| 197 | self._driver.releaseBus() |
|---|
| 198 | |
|---|
| 199 | def overwriteEncoderFullCircle(self, encoderFullCircle): |
|---|
| 200 | """ Overwrite firmware value. |
|---|
| 201 | """ |
|---|
| 202 | Logger().debug("MerlinOrionHardware.overwriteEncoderFullCircle(): encoderFullCircle=%x" % encoderFullCircle) |
|---|
| 203 | self.__encoderFullCircle = encoderFullCircle |
|---|
| 204 | |
|---|
| 205 | def useFirmwareEncoderFullCircle(self): |
|---|
| 206 | """ Use value from firmware. |
|---|
| 207 | """ |
|---|
| 208 | self.__encoderFullCircle = self.__encoderFullCircle_ |
|---|
| 209 | Logger().debug("MerlinOrionHardware.useFirmwareEncoderFullCircle(): encoderFullCircle=%x" % self.__encoderFullCircle) |
|---|
| 210 | |
|---|
| 211 | def read(self): |
|---|
| 212 | """ Read the axis position. |
|---|
| 213 | |
|---|
| 214 | @return: axis position, in ° |
|---|
| 215 | @rtype: float |
|---|
| 216 | """ |
|---|
| 217 | self._driver.acquireBus() |
|---|
| 218 | try: |
|---|
| 219 | value = self.__sendCmd("j") |
|---|
| 220 | finally: |
|---|
| 221 | self._driver.releaseBus() |
|---|
| 222 | pos = self.__encoderToAngle(self.__decodeAxisValue(value)) |
|---|
| 223 | return pos |
|---|
| 224 | |
|---|
| 225 | def drive(self, pos): |
|---|
| 226 | """ Drive the axis. |
|---|
| 227 | |
|---|
| 228 | @param pos: position to reach, in ° |
|---|
| 229 | @type pos: float |
|---|
| 230 | """ |
|---|
| 231 | strValue = self.__encodeAxisValue(self.__angleToEncoder(pos)) |
|---|
| 232 | self._driver.acquireBus() |
|---|
| 233 | try: |
|---|
| 234 | self.__sendCmd("L") |
|---|
| 235 | self.__sendCmd("G", "00") |
|---|
| 236 | self.__sendCmd("S", strValue) |
|---|
| 237 | self.__sendCmd("J") |
|---|
| 238 | finally: |
|---|
| 239 | self._driver.releaseBus() |
|---|
| 240 | |
|---|
| 241 | def stop(self): |
|---|
| 242 | """ Stop the axis. |
|---|
| 243 | """ |
|---|
| 244 | self._driver.acquireBus() |
|---|
| 245 | try: |
|---|
| 246 | self.__sendCmd("L") |
|---|
| 247 | finally: |
|---|
| 248 | self._driver.releaseBus() |
|---|
| 249 | |
|---|
| 250 | def startJog(self, dir_, speed): |
|---|
| 251 | """ Start the axis. |
|---|
| 252 | |
|---|
| 253 | @param dir_: direction ('+', '-') |
|---|
| 254 | @type dir_: str |
|---|
| 255 | |
|---|
| 256 | @param speed: speed |
|---|
| 257 | @type speed: int |
|---|
| 258 | """ |
|---|
| 259 | self._driver.acquireBus() |
|---|
| 260 | try: |
|---|
| 261 | self.__sendCmd("L") |
|---|
| 262 | if dir_ == '+': |
|---|
| 263 | self.__sendCmd("G", "30") |
|---|
| 264 | elif dir_ == '-': |
|---|
| 265 | self.__sendCmd("G", "31") |
|---|
| 266 | else: |
|---|
| 267 | raise ValueError("Axis %d dir. must be in ('+', '-')" % self._axis) |
|---|
| 268 | self.__sendCmd("I", self.__encodeAxisValue(speed)) |
|---|
| 269 | self.__sendCmd("J") |
|---|
| 270 | finally: |
|---|
| 271 | self._driver.releaseBus() |
|---|
| 272 | |
|---|
| 273 | def getStatus(self): |
|---|
| 274 | """ Get axis status. |
|---|
| 275 | |
|---|
| 276 | @return: axis status |
|---|
| 277 | @rtype: str |
|---|
| 278 | """ |
|---|
| 279 | self._driver.acquireBus() |
|---|
| 280 | try: |
|---|
| 281 | return self.__sendCmd("f") |
|---|
| 282 | finally: |
|---|
| 283 | self._driver.releaseBus() |
|---|
| 284 | |
|---|
| 285 | def setOutput(self, state): |
|---|
| 286 | """ Set output state. |
|---|
| 287 | |
|---|
| 288 | @param state: new state of the the output |
|---|
| 289 | @type state: bool |
|---|
| 290 | """ |
|---|
| 291 | self._driver.acquireBus() |
|---|
| 292 | try: |
|---|
| 293 | self.__sendCmd("O", str(int(state))) |
|---|
| 294 | finally: |
|---|
| 295 | self._driver.releaseBus() |
|---|