nbarikipoulos/poppy-robot-core

View on GitHub
lib/script/Script.js

Summary

Maintainability
A
1 hr
Test Coverage
'use strict'

/**
 * This object allows defining a set of actions to apply to target motors.
 * It allows selecting targeted motors, and then applying them a set of actions.
 *
 * Once instantiated, Script objects own a bunch of methods in oder to:
 * - select target motors,
 * - perform some actions,
 * - and other basic stuff such as waiting.
 *
 * Note contrary to the CLI mode, __no controls are performed on input values of
 * these methods and then, it is easy to corrupt motor registries__.
 * Such state will require a reboot of the robot.
 * @memberof module:poppy-robot-core
 * @inner
 * @example
 * const { Script } = require('poppy-robot-core')
 *
 * let script = new Script('all') // Select all motors
 *   .speed(100) // Set all motor speed to 100
 *   .stiff() // Make them programmatically "drivable"
 *   .goto(0) // Move all motors to 0 degree.
 *
 * let myOtherScript = new Script('m1', 'm3') // Only select the 'm1' and 'm2' motors
 *   .rotate(30) // rotate 'm1' and 'm3' by 30 degrees.
 *   .select('m4') // select the 'm4' motor for next action
 *   .rotate(20) // Rotate 'm4' by 20 degrees
 */
class Script {
  /**
   * Create a new Script Object.
   *
   * It could optionally set the targeted motor for the next actions of
   * this script.
   * @param {...string|Array<string>} motorNames  - the motor name(s) or 'all' to select all motors
   */
  constructor (...motorNames) {
    this._actionHandlers = [].concat(
      new ActionHandler(motorNames) // append a default ActionHandlers to this script
    )
  }

  /**
   * Select the target motor(s) for the next script actions.
   * It will define the targeted motor(s) until the next __select__ action, if any.
   * @param {...string|Array<string>} motorNames - the name(s) of the selected motor or 'all' to select all motors
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('all')
   *    .select('all') // Select all motors...
   *    .stiff() // Make them programmatically "drivable"
   *    . ...
   *    .select('m1','m2') // Next select only the motors 'm1' and 'm2'...
   *    .rotate(30) // and apply them a rotation by +30 degrees.
  */
  select (...motorNames) {
    const current = this._getCurrentActionHandler()
    if (!current.hasMotorSet()) { // Set the current action handler if it has no motor names...
      current.motors = motorNames
    } else { // ... Or create new one.
      this._actionHandlers.push(
        new ActionHandler(motorNames)
      )
    }

    return this
  }

  /**
   * Set the led value of the target motor(s).
   * @param {('off'|'red'|'green'|'blue'|'yellow'|'cyan'|'pink')} value - value for the 'led' register
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('all')
   *    .led('blue') // will set the led color to blue
  */
  led (value) {
    this._addAction('setLed', { led: value })
    return this
  }

  /**
   * Set the target position (register 'goal_position') of the selected motor(s).
   *
   * It will create an action that will move the selected motor(s) to the given position.
   * @param {Array.<integer>|integer} values - Either an array containing
   *   the target position for each motor or an integer if position is the same for all
   * @param {number=} duration - set the movement duration duration (in s)
   * @param {boolean=} [wait=false] - wait until motors reach their target positions.
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('m6')
   *    .goto(90) // Send a request in order to "open" the grip.
   *              // It does not wait the end of this movement
   *              // and next instructions will be send in the wake of it
   *    .select('m1', 'm2')
   *    .goto(0, 2.5) // move the motor 'm1' to position 0 degree in 2.5s
   *    .select('m2', 'm3')
   *    .goto([30, 90], true) // Send a instruction to move 'm2' and 'm3' to
   *                          // respectively position 30 and 90 degrees
   *                          // awaiting the end of the movement.
  */
  goto (values, duration, wait = false) {
    const param = _toParameters(
      { positions: values },
      duration,
      wait
    )

    this._addAction('move', param)

    return this
  }

  /**
   * Create an action to rotate the selected motor(s) by x degrees.
   * @param {Array.<integer>|integer} values - Either an array containing
   *   the rotation value for each motor or an integer if the rotation is the same for all
   * @param {number=} duration - set the movement duration duration (in s)
   * @param {boolean=} [wait=false] - wait until the selected motors will end
   *   their rotations before executing the next action
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('m5')
   *    .rotate(-30) // Send instruction to rotate by -30 degrees the selected motors.
   *                 // It does not wait the end of this movement
   *                 // and next instructions will be send in the wake of it
   *    .select('m1', 'm6')
   *    .rotate([-90, 90], true) // Send an instruction in order to rotate
   *                      // the motors 'm1' and 'm6' by respectively -90 and 90 degrees
   *                      // awaiting the end of the movement
   *    .rotate([90, -90], 3, true) // then rotate 'm1' and 'm6' by 90 and -90 degrees in 3s.
   */
  rotate (values, duration, wait = false) {
    const param = _toParameters(
      { angles: values },
      duration,
      wait
    )

    this._addAction('rotate', param)

    return this
  }

  /**
   * Set the speed (register 'moving_speed') of the selected motor(s).
   * @param {integer} value - the speed value. It should be included into
   *    the [0,1023] range (speed is more or less 0.666 degree.s-1 per unit).
   *    Note using 0 set the speed to the highest possible value.
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('all')
   *    .speed(100) // Set the speed of all motor to 100
  */
  speed (value) {
    this._addAction('setSpeed', { speed: value })
    return this
  }

  /**
   * "Release" selected motor(s) _i.e._ make them movable by hand _i.e._ set their 'compliant' register to 'true'.
   * @return {module:poppy-robot-core~Script}
   * @example
   * let endScript = new Script('all')
   *    .compliant()
  */
  compliant () {
    this._compliant(true)
    return this
  }

  /**
   * "Handle" programmatically selected motor(s) _i.e._ set their 'compliant' register to 'false'.
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script('all')
   *    .stiff()
  */
  stiff () {
    this._compliant(false)
    return this
  }

  _compliant (value) {
    this._addAction('setCompliant', { compliant: value })
  }

  /**
   * The wait method. It allows to stop the script execution during a given
   * delay.
   * @param {number} value - wait delay (in s)
   * @return {module:poppy-robot-core~Script}
   * @example
   * let script = new Script()
   *    .select('m2')
   *    .goto(-90) // we do not wait the end of movement
   *    .wait(1) // Wait 1 second before executing the next action
   *    .select('m3')
   *    .goto(90)
  */
  wait (value) {
    this._addAction('wait', { wait: value })
    return this
  }

  /** @private */
  _addAction (id, param) {
    const currentElement = this._getCurrentActionHandler()
    currentElement.addAction(id, param)
  }

  /** @private */
  _getCurrentActionHandler () {
    return this._actionHandlers[
      this._actionHandlers.length - 1
    ]
  }
}

/** @private */
class ActionHandler {
  constructor (motorNames) {
    this.motors = motorNames
    this._actions = []
  }

  hasMotorSet () { return this.motors.length !== 0 }

  get motors () { return this._motorNames }
  set motors (motorNames) {
    this._motorNames = motorNames.flat(2)
  }

  get actions () { return this._actions }

  addAction (id, param) {
    this._actions.push({ id, param })
  }
}

// ////////////////////////////////
// ////////////////////////////////
// Utility functions
// ////////////////////////////////
// ////////////////////////////////

const _toParameters = (value, duration, wait) => {
  const hasDuration = typeof duration === 'number'
  const wait_ = hasDuration ? wait : duration

  const duration_ = hasDuration ? { duration } : undefined

  return {
    ...value,
    ...duration_,
    wait: wait_
  }
}

// ////////////////////////////////
// ////////////////////////////////
// Public API
// ////////////////////////////////
// ////////////////////////////////

module.exports = Script