Source code for wni.motor.r356

# -*- coding: utf-8 -*-
"""Python code for controlling the Lin Engineering R356 Motor Controller"""

from __future__ import absolute_import, print_function, division

import contextlib
import glob
import logging
import math
import sys
import time

from .import commands

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


[docs]def decode_motor_response(resp): """Returns status as a string""" status_byte = resp[resp.find(b'/0') + 2] err_code = status_byte & 0x0f # descriptions from page 11 of Silverpak23C_R356_Commands.pdf descriptions = { 0: "No Error", 1: "Initialization error", 2: "Bad command", 3: "Bad operand", 5: "Communication error (internal communication error)", 7: "Not initialized (controller not initialized before attempting " "a move", 9: "Overload error (system could not keep up with commanded position", 11: "Move not allowed", 15: "Command overflow (Unit was busy executing another command", } return descriptions[err_code]
[docs]def fake_spin(fake, times=80, sleep=0.1): fake.revolve() for _ in range(times): print(fake.get_position()) time.sleep(sleep)
[docs]def compare(fake, real, times=80, sleep=0.1): real.zero() fake.zero() time.sleep(0.3) real.revolve() fake.revolve() time.sleep(0.2) for _ in range(times): print(real.get_position() - fake.get_position()) time.sleep(sleep)
[docs]class R356(object): """An R356 motor controller hooked up by USB. Default settings (from user manual) ======================== ======================================= Function Description ======================== ======================================= Running Current (m) 25% of 3.0 Amps (0.75 Amps) Holding Current (h) 10% of the running current Step Resolution (j) 256x Top Velocity (v) 305175 pps (microsteps / sec) Acceleration (L) L = 1000, 6103500 usteps/sec^2 Position 0 Microstep Smoothness (o) 1500 Outputs (J) Both are turned off, J0, Outputs 1 & 2 Baud Rate 9600 bps ======================== ======================================= """ MICROSTEPS = 256 # The number of steps that go by for one revolution of the motor. MOTOR_STEPS_PER_REVOLUTION = 200 # encoder "clicks per revolution" CPR = 2000 # reduction from the motor spinning to the radar spinning REDUCTION = 40 # Max current percentage that should go to the motor CURRENT = 40 TICKS_PER_REVOLUTION = CPR * 4 # multiply acceleration by this to get microsteps per second ACCELERATION_RATIO = 5850 ACCELERATION = 25 DEGREES_PER_SECOND = 60 def __init__(self, address=1, dev=None, baudrate=9600): """ address is a number: usually 1. """ if dev is None: devices = glob.glob('/dev/ttyUSB?') if devices: dev = devices[0] else: dev = '/dev/ttyUSB0' import serial self.address = address self.name = dev self._baudrate = baudrate self.start = commands.start(self.address) self.serial = serial.Serial(dev, baudrate, timeout=0.25, write_timeout=0.25) # self.serial = serial.Serial(dev, baudrate) self.program = [] self._current = None self._encoder_ratio = None self._acceleration = None self._fake = FakeR356() self.init_settings() self._velocity = self.get_slew_speed() self._fake.zero() @property def one_revolution_per_second(self): """Returns the command to velocity required for doing one revolution per second""" one_rps = (self.MOTOR_STEPS_PER_REVOLUTION * self._microsteps * self.REDUCTION) return one_rps def _rpm_to_velocity(self, desired_rpm): """Given a desired rpm, return the integer required to give to velocity command to achieve the rpm velocity. """ one_rps = self.one_revolution_per_second velocity = int(one_rps * desired_rpm / 60) return velocity def _degrees_per_sec_to_velocity(self, dps): one_rps = self.one_revolution_per_second velocity = one_rps * dps / 360 return velocity
[docs] def get_acceleration(self): return self._acceleration
[docs] def get_requested_velocity(self): return self._velocity
[docs] def get_degrees_per_second(self): v = self.get_slew_speed() self._velocity = v dps = self._velocity_to_degrees_per_sec(v) return dps
@property def max_velocity(self): """The highest allowed velocity. Anything higher could cause problems.""" one_rps = self.one_revolution_per_second return one_rps * 10
[docs] def spin(self): return self.positive(0)
[docs] def revolve(self, times=1): return self.positive(self.CPR * 4 * times)
[docs] def pnew_program(self): """ This is the first function that must be called when starting a new program. """ self.program = [] command = commands.start(self.address) self.program.append(command)
[docs] def run_current_program(self): self._run_program() return self.write_then_read(b''.join(self.program))
############################## ### Homing and Positioning ### ##############################
[docs] def pgo_home(self, num_steps=2000000): self.program.append(commands.home_and_initialize(num_steps))
[docs] def go_home(self, num_steps=2000000): return self.do(commands.home_and_initialize(num_steps))
[docs] def phome(self, num_steps=2000000): v = int(round(self.get_degrees_per_second())) self.pdegrees_per_second(20) self.pgo_home(num_steps) self.pdegrees_per_second(v) # if we let the position stay zero, then the motor cannot move # negatively at all. We need the position to read as a multiple of the # ticks per revolution so that we can move negatively. self.pset_position(self.TICKS_PER_REVOLUTION * 10) self._fake.zero() self._fake.degrees_per_second(v)
[docs] def home(self, num_steps=2000000): self.pnew_program() self.phome(num_steps) self.run_current_program() self._fake._reset()
[docs] def pzero(self): """Sets current position to 0""" self.pset_position(0)
[docs] def zero(self): """Sets current position to 0""" self.set_position(0)
[docs] def pset_position(self, pos): self.program.append(commands.set_current_position(pos)) self._fake._initial_position = pos
[docs] def set_position(self, pos): ret = self.do(commands.set_current_position(pos)) self._fake.set_position(pos) return ret
[docs] def pmove_to(self, pos): self.program.append(commands.move_to(pos))
[docs] def move_to(self, position): ret = self.do(commands.move_to(position)) self._fake.move_to(position) return ret
[docs] def phome_sensor_polarity(self, p): self.program.append(commands.set_home_sensor_polarity(p))
[docs] def home_sensor_polarity(self, p): return self.do(commands.set_home_sensor_polarity(p))
[docs] def ppositive(self, steps): self.program.append(commands.move_relative_positive(steps))
[docs] def positive(self, steps): ret = self.do(commands.move_relative_positive(steps)) self._fake.positive(steps) return ret
[docs] def pnegative(self, steps): self.program.append(commands.move_relative_negative(steps))
[docs] def negative(self, steps): if steps < 0: steps = abs(steps) ret = self.do(commands.move_relative_negative(steps)) self._fake.negative(steps) return ret
[docs] def pjog_distance(self, distance): self.program.append(commands.set_jog_distance(distance))
[docs] def jog_distance(self, distance): return self.do(commands.set_jog_distance(distance))
[docs] def pstop(self): self.program.append(commands.terminate_current_command())
[docs] def stop(self): ret = self.do(commands.terminate_current_command()) self._fake.stop() return ret
# def preverse(self, direction): # self.program.append(commands.reverse(direction)) # def reverse(self, direction): # ret = self.do(commands.reverse(direction)) # self._fake.reverse(direction) # return ret ######################### ### Velocity commands ### #########################
[docs] def pvelocity(self, v): self._velocity = v self.program.append(commands.set_velocity(v))
[docs] def velocity(self, v): """Sets the velocity immediately.""" if v > self.max_velocity: raise ValueError('It is dangerous to spin that fast.') self._velocity = v ret = self.do(commands.set_velocity(v)) self._fake.velocity(v) return ret
[docs] def pacceleration(self, a): self._acceleration = a self.program.append(commands.set_acceleration(a))
[docs] def acceleration(self, a): """Sets the acceleration immediately.""" self._acceleration = a ret = self.do(commands.set_acceleration(a)) self._fake.acceleration(a) return ret
####################### ### Setting current ### #######################
[docs] def pcurrent(self, current): self._current = current self.program.append(commands.set_current(current))
[docs] def current(self, current): self._current = current return self.do(commands.set_current(current))
[docs] def phold_current(self, current): self._hold_current = current self.program.append(commands.hold_current(current))
[docs] def hold_current(self, current): self._hold_current = current return self.do(commands.hold_current(current))
############################# ### Looping and Branching ### #############################
[docs] @contextlib.contextmanager def loop(self, repetitions): self.program.append(commands.begin_loop()) yield self.program.append(commands.end_loop(repetitions))
[docs] def pdelay(self, milliseconds): self.program.append(commands.delay(milliseconds))
[docs] def phalt_wait(self, value, pin): self.program.append(commands.halt_wait(value, pin))
[docs] def pskip_next_if(self, value, pin): self.program.append(commands.skip_next_if(value, pin))
[docs] def pmode(self, mode): self.program.append(commands.set_mode(mode))
[docs] def mode(self, mode): return self.do(commands.set_mode(mode))
################################################# ### Position Correction - Encoder Option Only ### #################################################
[docs] def pspecial_modes(self, mode): self.program.append(commands.special_modes(mode))
[docs] def special_modes(self, mode): return self.do(commands.special_modes(mode))
[docs] def pset_accuracy(self, a): self._accuracy = a self.program.append(commands.set_accuracy(a))
[docs] def set_accuracy(self, a): self._accuracy = a return self.do(commands.set_accuracy(a))
[docs] def pencoder_ratio(self, ratio): self._encoder_ratio = ratio self.program.append(commands.set_encoder_ratio(ratio))
[docs] def encoder_ratio(self, ratio): self._encoder_ratio = ratio return self.do(commands.set_encoder_ratio(ratio))
[docs] def poverload_timeout(self, timeout): self.program.append(commands.set_overload_timeout(timeout))
[docs] def overload_timeout(self, timeout): return self.do(commands.set_overload_timeout(timeout))
[docs] def precover_encoder(self): self.program.append(commands.recover_encoder())
[docs] def recover_encoder(self): return self.do(commands.recover_encoder())
################################## ### Program Storage and Recall ### ##################################
[docs] def pstore_program(self, program): self.program.append(commands.store_program(program))
[docs] def pexecute_program(self, program): self.program.append(commands.execute_program(program))
######################### ### Program Execution ### ######################### def _run_program(self): self.program.append(commands.run_program())
[docs] def prepeat_program(self): self.program.append(commands.repeat_program())
[docs] def repeat_program(self): return self.do(commands.repeat_program())
##################### ### Microstepping ### #####################
[docs] def pmicrosteps_per_step(self, microsteps): self._microsteps = microsteps self.program.append(commands.set_microsteps_per_step(microsteps))
[docs] def microsteps_per_step(self, microsteps): self._microsteps = microsteps ret = self.do(commands.set_microsteps_per_step(microsteps)) self._fake.microsteps_per_step(microsteps) return ret
[docs] def padjust_microsteps(self, adjustment): self.program.append(commands.adjust_microsteps(adjustment))
[docs] def adjust_microsteps(self, adjustment): ret = self.do(commands.adjust_microsteps(adjustment)) self._fake.adjust_microsteps(adjustment) return ret
################################ ### On/Off Drivers (Outputs) ### ################################
[docs] def pon_off_driver(self, value): self.program.append(commands.on_off_driver(value))
[docs] def on_off_driver(self, value): return self.do(commands.on_off_driver(value))
###################### ### Query Commands ### ###################### def _get_actual_position(self): actual = self.get_int(self.start + commands.get_position()) return actual
[docs] def get_actual_position(self): actual = self._get_actual_position() if self._fake._velocity is not None: estimated = self._fake.get_position() diff = actual - estimated orig = self._fake._initial_position * 1000 / self._fake._encoder_ratio self._fake.set_initial_position(orig + diff) return actual
[docs] def get_position(self): return self._fake.get_position()
[docs] def get_start_velocity(self): return self.get_int(self.start + commands.get_start_velocity())
[docs] def get_slew_speed(self): return self.get_int(self.start + commands.get_slew_speed())
[docs] def get_stop_speed(self): return self.get_int(self.start + commands.get_stop_speed())
[docs] def get_input_values(self): return self.get_int(self.start + commands.get_input_values())
[docs] def get_velocity(self): return self.get_int(self.start + commands.get_velocity())
[docs] def get_step_size(self): return self.get_int(self.start + commands.get_step_size())
[docs] def get_o_value(self): return self.get_int(self.start + commands.get_o_value())
[docs] def get_encoder_position(self): return self.get_int(self.start + commands.get_encoder_position())
[docs] def perase_programs(self): self.program.append(commands.erase_programs())
[docs] def erase_programs(self): return self.do(self.start + commands.erase_programs())
[docs] def get_current_command(self): return self.write_then_read(self.start + commands.get_current_command())
[docs] def get_firmware(self): return self.write_then_read(self.start + commands.get_firmware())
[docs] def get_controller_status(self): command = self.start + commands.get_controller_status() return self.write_then_read(command)
[docs] def send_number(self, n): return self.write_then_read(self.start + commands.send_number(n))
[docs] def pbaudrate(self, rate): return self.program.append(commands.set_baud_rate(rate))
[docs] def baudrate(self, rate): if rate not in (9600, 19200, 38400): raise ValueError(('baudrate "%d" not allowed: must be one of 9600, ' '19200, or 38400.') % rate) self._baudrate = rate return self.do(commands.set_baud_rate(rate))
### Functions for sending and receiving data to the controller
[docs] def programmify(self, command): """ Turn the specified command into a program by prepending the "start" command and appending the "run" command. This does not actually run the program, just returns bytes that are ready to run. """ return self.start + command + commands.run_program()
[docs] def write(self, data, append_newline=True): """Write data to the motor controller""" logger.debug(data.decode()) if append_newline: data += b'\r\n' self.serial.write(data)
[docs] def write_then_read(self, data): # We can clear the input data buffer since we just want the newest thing self.serial.reset_input_buffer() self.write(data) return self.serial.readline()
[docs] def get_int(self, data): string = self.write_then_read(data) first = string.find(b'/0') + 3 last = string.rfind(b'\x03\r\n') as_int = int(string[first:last]) return as_int
[docs] def read(self): return self.serial.read_all()
[docs] def do(self, command): return self.write_then_read(self.programmify((command)))
def __repr__(self): return '{}(address={}, dev="{}", baudrate={})'.format( type(self).__name__, self.address, self.name, self._baudrate) ######################################### ### Higher-level functions for humans ### #########################################
[docs] def pdegrees_per_second(self, dps): """Define velocity in degrees per second""" velocity = self._degrees_per_sec_to_velocity(dps) self.pvelocity(velocity)
[docs] def degrees_per_second(self, dps): """Define velocity in degrees per second""" velocity = self._degrees_per_sec_to_velocity(dps) self.velocity(velocity)
[docs] def prpm(self, desired_rpm): """Sets the velocity so that the desired rpm will be achieved.""" velocity = self._rpm_to_velocity(desired_rpm) self.pvelocity(velocity)
[docs] def rpm(self, desired_rpm): """Sets the velocity so that the desired rpm will be achieved.""" velocity = self._rpm_to_velocity(desired_rpm) self.velocity(velocity)
[docs] def pspin(self): self.program.append(commands.move_relative_positive(0)) self._fake.spin()
def _velocity_to_degrees_per_sec(self, velocity): one_rps = self.one_revolution_per_second dps = velocity * 360 / one_rps return dps
[docs] def init_settings(self, microsteps=None, current=None, degps=None, acc=None): """Set the settings we want the radar to operate under""" # encoder position is meaningful from 0 to 7999 because it's a # 2000-step encoder. # # encoder ratio is described in Appendix B of the R356 manual: # ratio = step_per_revolution * microsteps / (CPR * 4) * 1000 # for a 1.8 degree encoder, step_per_revolution = 200 # for our encoder, CPR = 2000 # # we also need to multiply by any gear reduction we're doing. microsteps = microsteps or self.MICROSTEPS current = current or self.CURRENT acc = acc or self.ACCELERATION self.pnew_program() self.pcurrent(current) encoder_ratio = ((self.MOTOR_STEPS_PER_REVOLUTION * microsteps) / (self.CPR * 4) * 1000 * self.REDUCTION ) self.pmicrosteps_per_step(microsteps) self.pencoder_ratio(encoder_ratio) if degps is not None: self.pdegrees_per_second(degps) self.pacceleration(acc) self.run_current_program()
@property def is_spinning(self): status = self.get_controller_status() if not status: # this is not documented by the motor, but it seems that sometimes # the motor decides not to respond. I suspect this happens # whenever the motor is processing a command and then arrives at # its home position. return True return status[3] != 96
class _Mock(object): def __getattr__(self, name): return _Mock() def __setattr__(self, name, val): pass def __call__(self, *args, **kwargs): pass
[docs]class FakeR356(R356): """A fake R356 motor so that testing can be done without a motor hooked up. This emulates a motor as best as I could. """ def __init__(self, address=1, dev='/dev/ttyUSB0', baudrate=9600): """ address is a number: usually 1. """ self.address = address self.name = dev self._baudrate = baudrate self.start = commands.start(self.address) self.serial = None self.program = [] self._current = None self._encoder_ratio = None self._velocity = 0 self._acceleration = None # these times are needed to know the position of the motor. # the motor starts accelerating self._t_s = None # acceleration stop time (when motor is at constant velocity) self._t_a_stop = None # deceleration start time (motor starts slowing down) self._t_d_start = None self._t_f = None self._initial_position = 0 self._fake = _Mock() self._direction = 1 self.init_settings() self.zero()
[docs] def go_home(self, num_steps=1000000): self.zero()
[docs] def zero(self): """Sets current position to 0""" self._initial_position = 0 self._t_s = time.time() self._t_a_stop = self._t_s self._t_d_start = self._t_s self._t_f = self._t_s
[docs] def get_degrees_per_second(self): return ticks_to_deg(self._velocity)
[docs] def set_initial_position(self, pos): self._initial_position = pos * self._encoder_ratio / 1000
def _calc_movement(self, steps): """Calculates the times that the motor's acceleration changes. The motor will speed up until it reaches the max velocity, then run at the max velocity, then slow down until the velocity is 0.""" # the times calculated here are just solving kinematic equations. Easy # peasy lemon squeazy. # start spinning self._t_s = time.time() # divide by 1000 because of the way encoder ratio is figured (check out # appendix B; mostly it was a guess that worked out honestly) steps *= self._encoder_ratio / 1000 # get the units to microsteps/s a = self._acceleration * self.ACCELERATION_RATIO v = self._velocity # final position, in units of microsteps x_half = steps / 2 # position when the max velocity is reached under constant acceleration x_vmax = 1/2 * v**2 / a if x_vmax >= x_half: # we're just going to accelerate until we're at x_half then # decelerate for the same amount of time (to stop again) # time for acceleration to stop t_a_stop = math.sqrt(2 * x_half / a) self._t_a_stop = self._t_s + t_a_stop self._t_d_start = self._t_a_stop self._t_f = self._t_d_start + t_a_stop else: # time required for acceleration to stop t_a_stop = v / a # distance traveled with constant velocity x_constv = steps - x_vmax*2 if v == 0: t_constv = 0 else: t_constv = x_constv / v self._t_a_stop = self._t_s + t_a_stop self._t_d_start = self._t_a_stop + t_constv self._t_f = self._t_d_start + t_a_stop assert self._t_f >= self._t_d_start >= self._t_a_stop >= self._t_s def _time_diffs(self): """Return the distance from self._t_s all the times are.""" t0 = 0 t1 = self._t_a_stop - self._t_s t2 = self._t_d_start - self._t_s t3 = self._t_f - self._t_s return t0, t1, t2, t3
[docs] def positive(self, steps): if self._velocity is not None: self.set_initial_position(self.get_position()) self._direction = 1 if steps == 0: # set stop time to 10000 revolutions steps = 8000 * 10000 self._calc_movement(steps)
[docs] def negative(self, steps): self.set_initial_position(self.get_position()) self._direction = -1 self._calc_movement(steps)
[docs] def stop(self): if self._velocity is not None: self._t_f = time.time() self.get_position() self.zero() self._reset()
def _reset(self): self.zero()
[docs] def get_position(self, now=None): """ This function, along with self._calc_movement(), are the most important functions for the FakeR356. """ if now is None: now = time.time() t0, t1, t2, t3 = self._time_diffs() t = now - self._t_s a = self._direction * self._acceleration * self.ACCELERATION_RATIO v = self._direction * self._velocity # initial x0 = self._initial_position # position when acceleration is over x1 = x0 + 1 / 2 * a * (t1)**2 # position when constant velocity part is over x2 = x1 + v * (self._t_d_start - self._t_a_stop) # final position # I'm cheating because I know that the distances of acceleration and # deceleration are the same. xf = x2 + (x1 - x0) if now >= self._t_f: # we're at the final position x = xf elif now >= self._t_d_start: # decelerating now # gotta get the correct velocity. assert t >= t2 if self._t_a_stop == self._t_d_start: v = t1 * a del_t = (t - t2) del_t_2 = del_t**2 x = x2 - 1/2*a*(del_t_2) + v*del_t elif now >= self._t_a_stop: assert t >= t1 # constant velocity x = x1 + v * (now - self._t_a_stop) else: # accelerating assert t >= t0 x = x0 + 1/2 * a * (now - self._t_s) ** 2 encoder_ticks = x / self._encoder_ratio * 1000 return int(round(encoder_ticks))
[docs] def get_actual_position(self): return self.get_position()
def __repr__(self): return '{}(address={}, dev="{}", baudrate={})'.format( type(self).__name__, self.address, self.name, self._baudrate)
[docs] def do(self, cmd): """Not going to actually do anything""" pass
[docs] def run_current_program(self): """Don't run the current program""" pass
@property def is_spinning(self): if self._t_f is None: return False now = time.time() return now < self._t_f
[docs]def ticks_to_deg(ticks): """Translates ticks to degrees""" return ((ticks * 360.0 / R356.TICKS_PER_REVOLUTION)) % 360
TPR = R356.TICKS_PER_REVOLUTION assert ticks_to_deg(TPR) == 0 assert ticks_to_deg(TPR / 2) == 180 assert ticks_to_deg(TPR / 4) == 90 assert ticks_to_deg(TPR + TPR / 4) == 90 assert ticks_to_deg(2 * TPR) == 0
[docs]def deg_to_ticks(degrees): """Translates degrees to ticks""" return int(round(degrees / 360.0 * R356.TICKS_PER_REVOLUTION))
assert deg_to_ticks(360) == TPR assert deg_to_ticks(720) == 2 * TPR del TPR
[docs]class Motor(object): """ An abstraction over the R356, so the user can think in degrees instead of stepper positions """ def __init__(self, motor, home_angle=0): """ Args: motor: BaseR356 object """ self.motor = motor self._degrees = 0 self.home_angle = home_angle self.position_valid = False # has anyone been warned about a bad position? self._warn_period = 100000 self._noticed_times = 0
[docs] def point(self, degrees): """From a stopped state, point a different direction.""" if not self.position_valid: logger.warning( "The motor's position is not in a known state; the returned " "position is inaccurate." ) desired = degrees % 360 current = self.get_actual_position() deg_needed = (desired - current) % 360 if deg_needed == 0: pass elif deg_needed < 180: self.positive(deg_needed) else: self.negative(360 - deg_needed)
[docs] def positive(self, degrees): ticks = deg_to_ticks(degrees) self.motor.positive(ticks)
[docs] def negative(self, degrees): ticks = deg_to_ticks(degrees) self.motor.negative(ticks)
def __get_pos(self, actual=False): if actual: pos = self.motor.get_actual_position() else: pos = self.motor.get_position() if not self.position_valid: if self._noticed_times % self._warn_period == 0: logger.warning( "The motor's position is not in a known state; the " " returned position is inaccurate." ) self._noticed_times += 1 # degrees, counterclockwise from north. degrees_ccw = (ticks_to_deg(pos) + self.home_angle) # azimuth angle is defined clockwise, not counter-clockwise degrees_cw = (-degrees_ccw) % 360 return degrees_cw
[docs] def get_position(self): """Return the current position, in degrees.""" return self.__get_pos(actual=False)
[docs] def get_actual_position(self): return self.__get_pos(actual=True)
[docs] def home(self): self.motor.home() self.position_valid = True
[docs] def spin(self): self.motor.spin()
[docs] def stop(self): self.position_valid = False self.motor.stop()
[docs] def get_acceleration(self): return self.motor.get_acceleration()
[docs] def acceleration(self, acc): self.motor.acceleration(acc)
[docs] def get_degrees_per_second(self): return self.motor.get_degrees_per_second()
[docs] def degrees_per_second(self, deg): return self.motor.degrees_per_second(deg)
# def reverse(self, direction): # self.motor.reverse(direction) @property def is_spinning(self): return self.motor.is_spinning
if __name__ == '__main__': logging.basicConfig( level=logging.DEBUG, format=logging.BASIC_FORMAT, stream=sys.stdout ) c = controller = R356() motor = Motor(c) fake = FakeR356() print('the motor is available as "motor"') print('the controller is available as "c"') print('the fake is available as "fake"') import IPython IPython.embed()