diff --git a/ev3/__init__.py b/ev3/__init__.py new file mode 100755 index 0000000..714bb81 --- /dev/null +++ b/ev3/__init__.py @@ -0,0 +1,12 @@ +import sys +import os +thisdir = os.path.dirname(os.path.abspath(__file__)) +print(thisdir) + +sys.path.append(thisdir) + +import ev3 +import message +import direct_command +import system_command +import async diff --git a/ev3/async.py b/ev3/async.py new file mode 100755 index 0000000..f0526bb --- /dev/null +++ b/ev3/async.py @@ -0,0 +1,91 @@ +"""A simple thread subclass for making ev3 function calls asynchronous. + +EXAMPLE USAGE: + import time + + from ev3 import * + + + finished = False + + + def keep_alive_finished(result): + global finished + print 'The keep_alive() function returned: ', result + finished = True + + + if ("__main__" == __name__): + try: + async_thread = async.AsyncThread() + + with ev3.EV3() as brick: + async_thread.put(brick.keep_alive, keep_alive_finished) + + while (not finished): + print 'Waiting...' + time.sleep(0.1) + + except ev3.EV3Error as ex: + print 'An error occurred: ', ex + + async_thread.stop() + +""" + + +import threading +import queue + + +class AsyncThread(threading.Thread): + """A simple thread subclass maintains a queue of functions to call.""" + + + _STOP_QUEUE_ITEM = 'STOP' + + + def __init__(self): + """Creates and starts a new thread.""" + super(AsyncThread, self).__init__() + + self._daemon = True + self._queue = Queue.Queue() + + self.start() + + + def run(self): + """This function is called automatically by the Thread class.""" + try: + while(True): + item = self._queue.get(block=True) + + if (self._STOP_QUEUE_ITEM == item): + break + + ev3_func, cb, args, kwargs = item + + cb(ev3_func(*args, **kwargs)) + + except KeyboardInterrupt: + pass + + + def stop(self): + """Instructs the thread to exit after the current function is + finished. + + """ + with self._queue.mutex: + self._queue.queue.clear() + + self._queue.put(self._STOP_QUEUE_ITEM) + + + def put(self, ev3_func, cb, *args, **kwargs): + """Adds a new function to the queue. The cb (callback) parameter should + be a function that accepts the result as its only parameter. + + """ + self._queue.put((ev3_func, cb, args, kwargs)) diff --git a/ev3/direct_command.py b/ev3/direct_command.py new file mode 100755 index 0000000..48a0ae0 --- /dev/null +++ b/ev3/direct_command.py @@ -0,0 +1,2213 @@ +"""A simple interface for executing bytecodes over a Bluetooth serial port. + +From the lms2012 source code documentation: + +Beside running user programs the VM is able to execute direct commands from +the Communication Module. In fact direct commands are small programs that +consist of regular byte codes and they are executed in parallel with a running +user program. Special care MUST be taken when writing direct commands because +the decision until now is NOT to restrict the use of "dangerous" codes and +constructions (loops in a direct command are allowed). + +If a new direct command from the same source is going to be executed an actual +running direct command is terminated. + +Because of a small header objects are limited to one VMTHREAD only - SUBCALLs +and BLOCKs are, of course, not possible. This header contains information about +the number of global variables (for response), number of local variables, and +command size. + +Direct commands that have data responses can place the data in the global +variable space. The global variable space is equal to the communication +response buffer. The composition of the direct command defines at which +offset the result is placed (global variable 0 is placed at offset 0 in +the buffer). + +Offsets in the response buffer (global variables) must be aligned (i.e. 32bit +variable offsets are divisible by 4, 16bit variable offsets are divisible by 2). + +All multi-byte words are little endian. + +Direct Command bytes: +------------------------------ +Byte 0 - 1: Command size +Byte 2 - 3: Message counter +Byte 4: CommandType +Byte 5 - 6: Number of global and local variables (compressed). + Byte 6 Byte 5 + 76543210 76543210 + -------- -------- + llllllgg gggggggg + gg gggggggg Global variables [0..MAX_COMMAND_GLOBALS] + llllll Local variables [0..MAX_COMMAND_LOCALS] +Byte 7 - n: Byte codes + +Direct Command response Bytes: +------------------------------ +Byte 0 - 1: Reply size +Byte 2 - 3: Message counter +Byte 4: ReplyType +Byte 5 - n: Response buffer (global variable values) + +""" + + +import ev3 +import message + + +MAX_CMD_LEN = 1019 # The size of the brick's txBuf is 1024 bytes but + # the header requires 5 bytes. +MAX_STR_LEN = 255 +MAX_VERSION_STR_LEN = 64 +MAX_LOCAL_VARIABLE_BYTES = 0xFFFFFFFF + +MAX_NAME_STR_LEN = 64 + +MOTOR_MIN_POWER = -100 +MOTOR_MAX_POWER = 100 + +MOTOR_MIN_SPEED = -100 +MOTOR_MAX_SPEED = 100 + +USB_CHAIN_LAYER_MASTER = 0 +USB_CHAIN_LAYER_SLAVE = 1 + +MOTOR_MIN_RATIO = -200 +MOTOR_MAX_RATIO = 200 + +MIN_VOLUME = 0 +MAX_VOLUME = 100 + +LCD_HEIGHT_PIXELS = 128 +LCD_WIDTH_PIXELS = 178 + + +class DirectCommandError(Exception): + """Subclass for reporting errors.""" + pass + + +class CommandType(object): + """Every System Command must be one of these two types.""" + DIRECT_COMMAND_REPLY = 0x00 + DIRECT_COMMAND_NO_REPLY = 0x80 + + +class ReplyType(object): + """Every reply to a System Command must be one of these two types.""" + DIRECT_REPLY = 0x02 + DIRECT_REPLY_ERROR = 0x04 + + +class OutputPort(object): + """These can be OR'd together to operate on multiple ports at once.""" + PORT_A = 0x01 + PORT_B = 0x02 + PORT_C = 0x04 + PORT_D = 0x08 + ALL = (PORT_A | PORT_B | PORT_C | PORT_D) + + +class InputPort(object): + """These can be OR'd together to operate on multiple ports at once.""" + PORT_1 = 0x00 + PORT_2 = 0x01 + PORT_3 = 0x02 + PORT_4 = 0x03 + PORT_A = 0x10 + PORT_B = 0x11 + PORT_C = 0x12 + PORT_D = 0x13 + + +class StopType(object): + """When an OutputPort is stopped it can be told to brake or coast.""" + COAST = 0 + BRAKE = 1 + + +class PolarityType(object): + """""" + BACKWARD = -1 + TOGGLE = 0 + FORWARD = 1 + + +class TouchMode(object): + """""" + TOUCH = 0 + BUMPS = 1 + + +class NXTLightMode(object): + """""" + REFLECT = 0 + AMBIENT = 1 + + +class NXTSoundMode(object): + """""" + DECIBELS = 0 + ADJUSTED_DECIBLES = 1 + + +class NXTColorMode(object): + """""" + REFLECTIVE = 0 + AMBIENT = 1 + COLOR = 2 + GREEN = 3 + BLUE = 4 + RAW = 5 + + +class NXTUltrasonicMode(object): + """""" + CM = 0 + INCHES = 1 + + +class NXTTemperatureMode(object): + """""" + CELSIUS = 0 + FAHRENHEIT = 1 + + +class MotorMode(object): + """""" + DEGREES = 0 + ROTATIONS = 1 + PERCENT = 2 + + +class UltrasonicMode(object): + """""" + CM = 0 + INCH = 1 + LISTEN = 2 + + +class GyroMode(object): + """""" + ANGLE = 0 + RATE = 1 + FAS = 2 + G_AND_A = 3 + + +class IRMode(object): + """""" + PROXIMITY = 0 + SEEK = 1 + REMOTE = 2 + REMOTE_A = 3 + SALT = 4 + CALIBRATION = 5 + + +class ColorMode(object): + """""" + RELECTIVE = 0 + AMBIENT = 1 + COLOR = 2 + + +class ColorSensorColor(object): + """These are the results that the EV3 color sensor can return when operating + in ColorMode.COLOR. + + """ + NONE = 0 + BLACK = 1 + BLUE = 2 + GREEN = 3 + YELLOW = 4 + RED = 5 + WHITE = 6 + BROWN = 7 + + +class LEDPattern(object): + """The brick user interface has several status LEDs.""" + OFF = 0 + GREEN = 1 + RED = 2 + ORANGE = 3 + FLASHING_GREEN = 4 + FLASHING_RED = 5 + FLASHING_ORANGE = 6 + GREEN_HEARTBEAT = 7 + RED_HEARTBEAT = 8 + ORANGE_HEARTBEAT = 9 + + +class DeviceType(object): + """These are the known device types. + + NOTE: These have only been partially confirmed. + + """ + NXT_TOUCH = 0x01 + NXT_LIGHT = 0x02 + NXT_SOUND = 0x03 + NXT_COLOR = 0x04 + NXT_ULTRASONIC = 0x05 + NXT_TEMPERATURE = 0x06 + TACHO = 0x07 # TYPE_TACHO in lms2012.h + MINI_TACHO = 0x08 # TYPE_MINITACHO in lms2012.h + NEW_TACHO = 0x09 # TYPE_NEWTACHO in lms2012.h + EV3_TOUCH = 0x10 + EV3_COLOR = 0x1D + EV3_ULTRASONIC = 0x1E + EV3_GYROSCOPE = 0x20 + EV3_INFRARED = 0x21 + SENSOR_INITIALIZING = 0x7D + PORT_EMPTY = 0x7E + ERROR_PORT = 0x7F + UNKNOWN = 0xFF + + +class LCDColor(object): + """The brick's LCD only displays two colors.""" + BACKGROUND = 0 + FOREGROUND = 1 + + +class ButtonType(object): + """The brick's user interface contains 6 buttons.""" + NO_BUTTON = 0 + UP_BUTTON = 1 + ENTER_BUTTON = 2 + DOWN_BUTTON = 3 + RIGHT_BUTTON = 4 + LEFT_BUTTON = 5 + BACK_BUTTON = 6 + ANY_BUTTON = 7 + + +class MathType(object): + """""" + EXP = 1 # e^x r = expf(x) + MOD = 2 # Modulo r = fmod(x,y) + FLOOR = 3 # Floor r = floor(x) + CEIL = 4 # Ceiling r = ceil(x) + ROUND = 5 # Round r = round(x) + ABS = 6 # Absolute r = fabs(x) + NEGATE = 7 # Negate r = 0.0 - x + SQRT = 8 # Squareroot r = sqrt(x) + LOG = 9 # Log r = log10(x) + LN = 10 # Ln r = log(x) + SIN = 11 + COS = 12 + TAN = 13 + ASIN = 14 + ACOS = 15 + ATAN = 16 + MOD8 = 17 # Modulo DATA8 r = x % y + MOD16 = 18 # Modulo DATA16 r = x % y + MOD32 = 19 # Modulo DATA32 r = x % y + POW = 20 # Exponent r = powf(x,y) + TRUNC = 21 # Truncate r = (float)((int)(x * pow(y))) / pow(y) + + +class BrowserType(object): + """""" + BROWSE_FOLDERS = 0 # Browser for folders + BROWSE_FOLDS_FILES = 1 # Browser for folders and files + BROWSE_CACHE = 2 # Browser for cached / recent files + BROWSE_FILES = 3 # Browser for files + + +class Icon(object): + """The icons on the brick are enumerated by value.""" + ICON_NONE = -1 + ICON_RUN = 0 + ICON_FOLDER = 1 + ICON_FOLDER2 = 2 + ICON_USB = 3 + ICON_SD = 4 + ICON_SOUND = 5 + ICON_IMAGE = 6 + ICON_SETTINGS = 7 + ICON_ONOFF = 8 + ICON_SEARCH = 9 + ICON_WIFI = 10 + ICON_CONNECTIONS = 11 + ICON_ADD_HIDDEN = 12 + ICON_TRASHBIN = 13 + ICON_VISIBILITY = 14 + ICON_KEY = 15 + ICON_CONNECT = 16 + ICON_DISCONNECT = 17 + ICON_UP = 18 + ICON_DOWN = 19 + ICON_WAIT1 = 20 + ICON_WAIT2 = 21 + ICON_BLUETOOTH = 22 + ICON_INFO = 23 + ICON_TEXT = 24 + ICON_QUESTIONMARK = 27 + ICON_INFO_FILE = 28 + ICON_DISC = 29 + ICON_CONNECTED = 30 + ICON_OBP = 31 + ICON_OBD = 32 + ICON_OPENFOLDER = 33 + ICON_BRICK1 = 34 + + +class FontType(object): + """""" + NORMAL_FONT = 0 + SMALL_FONT = 1 + LARGE_FONT = 2 + TINY_FONT = 3 + + +class DataFormat(object): + """Data formats that are used by the VM.""" + DATA8 = 0x00 + DATA16 = 0x01 + DATA32 = 0x02 + DATA_F = 0x03 # 32bit floating point value (single precision) + DATA_S = 0x04 # Zero terminated string + DATA_A = 0x05 # Array handle + DATA_V = 0x07 # Variable type + DATA_PCT = 0x10 # Percent (used in INPUT_READEXT) + DATA_RAW = 0x12 # Raw (used in INPUT_READEXT) + DATA_SI = 0x13 # SI unit (used in INPUT_READEXT) + # Values used by this Python module only: + HND = 0xFF # For compatibility with ParamTypes. + BOOL = 0xFE # For converting to Python values + + +class ParamType(object): + """Parameter types that are used by the VM.""" + PRIMPAR_LABEL = 0x20 + HND = 0x10 # 8bit handle index (i.e. pointer to a string) + ADR = 0x08 # 3bit address + LCS = 0x84 # Null terminated string + LAB1 = 0xA0 + LC0 = 0x00 # 6bit immediate + LC1 = 0x81 # 8bit immediate + LC2 = 0x82 # 16bit immediate + LC4 = 0x83 # 32bit immediate + LCA = 0x81 # 8bit pointer to local array + LV1 = 0xC1 # 8bit pointer to local value + LV2 = 0xC2 # 16bit pointer to local value + LV4 = 0xC3 # 32bit pointer to local value + LVA = 0xC1 # 8bit pointer to local array + GV0 = 0x60 # 5bit pointer to global value + GV1 = 0xE1 # 8bit pointer to global value + GV2 = 0xE2 # 16bit pointer to global value + GV4 = 0xE3 # 32bit pointer to global value + GVA = 0xE1 # 8bit pointer to global array + # Values used by this Python module only: + FLOAT = 0xFF # 32bit floating point value (single precision) + + +# Defines the number of bytes required to represent each DataFormat. +PARAM_TYPE_LENS = { ParamType.PRIMPAR_LABEL: None, + ParamType.HND: 1, + ParamType.ADR: 1, + ParamType.LCS: None, + ParamType.LAB1: 1, + ParamType.LC0: 0, + ParamType.LC1: 1, + ParamType.LC2: 2, + ParamType.LC4: 4, + ParamType.LCA: 1, + ParamType.LV1: 1, + ParamType.LV2: 2, + ParamType.LV4: 4, + ParamType.LVA: 1, + ParamType.GV0: 0, + ParamType.GV1: 1, + ParamType.GV2: 2, + ParamType.GV4: 4, + ParamType.GVA: 1, + ParamType.FLOAT: 4 } + + +DATA_FORMAT_LENS = { DataFormat.DATA8: 1, + DataFormat.DATA16: 2, + DataFormat.DATA32: 4, + DataFormat.DATA_F: 4, + DataFormat.DATA_S: None, + DataFormat.DATA_A: None, + DataFormat.DATA_V: None, + DataFormat.DATA_PCT: 1, + DataFormat.DATA_RAW: 4, + DataFormat.DATA_SI: 4, + DataFormat.HND: 1, + DataFormat.BOOL: 1 } + + +# There are two ways to specify an output in the c_output module. The first is +# as a bit mask and the second is by index. +OUTPUT_CHANNEL_TO_INDEX = { OutputPort.PORT_A: 0, + OutputPort.PORT_B: 1, + OutputPort.PORT_C: 2, + OutputPort.PORT_D: 3 } + + +class UIReadSubcode(object): + """""" + GET_VBATT = 1 + GET_IBATT = 2 + GET_OS_VERS = 3 + GET_EVENT = 4 + GET_TBATT = 5 + GET_IINT = 6 + GET_IMOTOR = 7 + GET_STRING = 8 + GET_HW_VERS = 9 + GET_FW_VERS = 10 + GET_FW_BUILD = 11 + GET_OS_BUILD = 12 + GET_ADDRESS = 13 + GET_CODE = 14 + KEY = 15 + GET_SHUTDOWN = 16 + GET_WARNING = 17 + GET_LBATT = 18 + TEXTBOX_READ = 21 + GET_VERSION = 26 + GET_IP = 27 + GET_POWER = 29 + GET_SDCARD = 30 + GET_USBSTICK = 31 + + +class UIWriteSubcode(object): + """""" + WRITE_FLUSH = 1 + FLOATVALUE = 2 + STAMP = 3 + PUT_STRING = 8 + VALUE8 = 9 + VALUE16 = 10 + VALUE32 = 11 + VALUEF = 12 + ADDRESS = 13 + CODE = 14 + DOWNLOAD_END = 15 + SCREEN_BLOCK = 16 + TEXTBOX_APPEND = 21 + SET_BUSY = 22 + SET_TESTPIN = 24 + INIT_RUN = 25 + UPDATE_RUN = 26 + LED = 27 + POWER = 29 + GRAPH_SAMPLE = 30 + TERMINAL = 31 + + +class UIButtonSubcode(object): + """""" + SHORTPRESS = 1 + LONGPRESS = 2 + WAIT_FOR_PRESS = 3 + FLUSH = 4 + PRESS = 5 + RELEASE = 6 + GET_HORZ = 7 + GET_VERT = 8 + PRESSED = 9 + SET_BACK_BLOCK = 10 + GET_BACK_BLOCK = 11 + TESTSHORTPRESS = 12 + TESTLONGPRESS = 13 + GET_BUMBED = 14 + GET_CLICK = 15 + + +class COMGetSubcodes(object): + """""" + GET_ON_OFF = 1 # Set, Get + GET_VISIBLE = 2 # Set, Get + GET_RESULT = 4 # Get + GET_PIN = 5 # Set, Get + SEARCH_ITEMS = 8 # Get + SEARCH_ITEM = 9 # Get + FAVOUR_ITEMS = 10 # Get + FAVOUR_ITEM = 11 # Get + GET_ID = 12 + GET_BRICKNAME = 13 + GET_NETWORK = 14 + GET_PRESENT = 15 + GET_ENCRYPT = 16 + CONNEC_ITEMS = 17 + CONNEC_ITEM = 18 + GET_INCOMING = 19 + GET_MODE2 = 20 + + +class COMSetSubcode(object): + """""" + SET_ON_OFF = 1 # Set, Get + SET_VISIBLE = 2 # Set, Get + SET_SEARCH = 3 # Set + SET_PIN = 5 # Set, Get + SET_PASSKEY = 6 # Set + SET_CONNECTION = 7 # Set + SET_BRICKNAME = 8 + SET_MOVEUP = 9 + SET_MOVEDOWN = 10 + SET_ENCRYPT = 11 + SET_SSID = 12 + SET_MODE2 = 13 + + +class InputDeviceSubcode(object): + """""" + GET_FORMAT = 2 + CAL_MINMAX = 3 + CAL_DEFAULT = 4 + GET_TYPEMODE = 5 + GET_SYMBOL = 6 + CAL_MIN = 7 + CAL_MAX = 8 + SETUP = 9 # Probably only for internal use. + CLR_ALL = 10 # Resets counters, angle, etc. + GET_RAW = 11 + GET_CONNECTION = 12 + STOP_ALL = 13 # Stops any attached motors? + GET_NAME = 21 + GET_MODENAME = 22 + SET_RAW = 23 + GET_FIGURES = 24 + GET_CHANGES = 25 + CLR_CHANGES = 26 + READY_PCT = 27 + READY_RAW = 28 + READY_SI = 29 + GET_MINMAX = 30 + GET_BUMPS = 31 + + +class ProgramInfoSubcode(object): + """""" + OBJ_STOP = 0 + OBJ_START = 4 + GET_STATUS = 22 + GET_SPEED = 23 + GET_PRGRESULT = 24 + SET_INSTR = 25 + + +class UIDrawSubcode(object): + """""" + UPDATE = 0 + CLEAN = 1 + PIXEL = 2 + LINE = 3 + CIRCLE = 4 + TEXT = 5 + ICON = 6 + PICTURE = 7 + VALUE = 8 + FILLRECT = 9 + RECT = 10 + NOTIFICATION = 11 + QUESTION = 12 + KEYBOARD = 13 + BROWSE = 14 + VERTBAR = 15 + INVERSERECT = 16 + SELECT_FONT = 17 + TOPLINE = 18 + FILLWINDOW = 19 + SCROLL = 20 + DOTLINE = 21 + VIEW_VALUE = 22 + VIEW_UNIT = 23 + FILLCIRCLE = 24 + STORE = 25 + RESTORE = 26 + ICON_QUESTION = 27 + BMPFILE = 28 + POPUP = 29 + GRAPH_SETUP = 30 + GRAPH_DRAW = 31 + TEXTBOX = 32 + + +class FileSubcode(object): + """""" + OPEN_APPEND = 0 + OPEN_READ = 1 + OPEN_WRITE = 2 + READ_VALUE = 3 + WRITE_VALUE = 4 + READ_TEXT = 5 + WRITE_TEXT = 6 + CLOSE = 7 + LOAD_IMAGE = 8 + GET_HANDLE = 9 + MAKE_FOLDER = 10 + GET_POOL = 11 + SET_LOG_SYNC_TIME = 12 + GET_FOLDERS = 13 + GET_LOG_SYNC_TIME = 14 + GET_SUBFOLDER_NAME = 15 + WRITE_LOG = 16 + CLOSE_LOG = 17 + GET_IMAGE = 18 + GET_ITEM = 19 + GET_CACHE_FILES = 20 + PUT_CACHE_FILE = 21 + GET_CACHE_FILE = 22 + DEL_CACHE_FILE = 23 + DEL_SUBFOLDER = 24 + GET_LOG_NAME = 25 + OPEN_LOG = 27 + READ_BYTES = 28 + WRITE_BYTES = 29 + REMOVE = 30 + MOVE = 31 + + +class ArraySubcode(object): + """""" + DELETE = 0 + CREATE8 = 1 + CREATE16 = 2 + CREATE32 = 3 + CREATEF = 4 + RESIZE = 5 + FILL = 6 + COPY = 7 + INIT8 = 8 + INIT16 = 9 + INIT32 = 10 + INITF = 11 + SIZE = 12 + READ_CONTENT = 13 + WRITE_CONTENT = 14 + READ_SIZE = 15 + + +class FilenameSubcode(object): + """""" + EXIST = 16 # MUST BE GREATER OR EQUAL TO "ARRAY_SUBCODES" + TOTALSIZE = 17 + SPLIT = 18 + MERGE = 19 + CHECK = 20 + PACK = 21 + UNPACK = 22 + GET_FOLDERNAME = 23 + + +class InfoSubcode(object): + """""" + SET_ERROR = 1 + GET_ERROR = 2 + ERRORTEXT = 3 + GET_VOLUME = 4 + SET_VOLUME = 5 + GET_MINUTES = 6 + SET_MINUTES = 7 + + +class SoundSubcode(object): + """""" + BREAK = 0 + TONE = 1 + PLAY = 2 + REPEAT = 3 + SERVICE = 4 + + +class StringSubcode(object): + """""" + GET_SIZE = 1 # Get string size + ADD = 2 # Add two strings + COMPARE = 3 # Compare two strings + DUPLICATE = 5 # Duplicate one string to another + VALUE_TO_STRING = 6 + STRING_TO_VALUE = 7 + STRIP = 8 + NUMBER_TO_STRING = 9 + SUB = 10 + VALUE_FORMATTED = 11 + NUMBER_FORMATTED = 12 + + +class TstSubcode(object): + """""" + TST_OPEN = 10 # Must >= "INFO_SUBCODES" + TST_CLOSE = 11 + TST_READ_PINS = 12 + TST_WRITE_PINS = 13 + TST_READ_ADC = 14 + TST_WRITE_UART = 15 + TST_READ_UART = 16 + TST_ENABLE_UART = 17 + TST_DISABLE_UART = 18 + TST_ACCU_SWITCH = 19 + TST_BOOT_MODE2 = 20 + TST_POLL_MODE2 = 21 + TST_CLOSE_MODE2 = 22 + TST_RAM_CHECK = 23 + + +class Opcode(object): + """All of the opcodes that are used by the VM.""" + ERROR = 0x00 + NOP = 0x01 + PROGRAM_STOP = 0x02 + PROGRAM_START = 0x03 + OBJECT_STOP = 0x04 + OBJECT_START = 0x05 + OBJECT_TRIG = 0x06 + OBJECT_WAIT = 0x07 + RETURN = 0x08 + CALL = 0x09 + OBJECT_END = 0x0A + SLEEP = 0x0B + PROGRAM_INFO = 0x0C + LABEL = 0x0D + PROBE = 0x0E + DO = 0x0F + # MATH + ADD8 = 0x10 + ADD16 = 0x11 + ADD32 = 0x12 + ADDF = 0x13 + SUB8 = 0x14 + SUB16 = 0x15 + SUB32 = 0x16 + SUBF = 0x17 + MUL8 = 0x18 + MUL16 = 0x19 + MUL32 = 0x1A + MULF = 0x1B + DIV8 = 0x1C + DIV16 = 0x1D + DIV32 = 0x1E + DIVF = 0x1F + # LOGIC + OR8 = 0x20 + OR16 = 0x21 + OR32 = 0x22 + AND8 = 0x24 + AND16 = 0x25 + AND32 = 0x26 + XOR8 = 0x28 + XOR16 = 0x29 + XOR32 = 0x2A + RL8 = 0x2C + RL16 = 0x2D + RL32 = 0x2E + # MOVE + INIT_BYTES = 0x2F + MOVE8_8 = 0x30 + MOVE8_16 = 0x31 + MOVE8_32 = 0x32 + MOVE8_F = 0x33 + MOVE16_8 = 0x34 + MOVE16_16 = 0x35 + MOVE16_32 = 0x36 + MOVE16_F = 0x37 + MOVE32_8 = 0x38 + MOVE32_16 = 0x39 + MOVE32_32 = 0x3A + MOVE32_F = 0x3B + MOVEF_8 = 0x3C + MOVEF_16 = 0x3D + MOVEF_32 = 0x3E + MOVEF_F = 0x3F + # BRANCH + JR = 0x40 + JR_FALSE = 0x41 + JR_TRUE = 0x42 + JR_NAN = 0x43 + # COMPARE + CP_LT8 = 0x44 + CP_LT16 = 0x45 + CP_LT32 = 0x46 + CP_LTF = 0x47 + CP_GT8 = 0x48 + CP_GT16 = 0x49 + CP_GT32 = 0x4A + CP_GTF = 0x4B + CP_EQ8 = 0x4C + CP_EQ16 = 0x4D + CP_EQ32 = 0x4E + CP_EQF = 0x4F + CP_NEQ8 = 0x50 + CP_NEQ16 = 0x51 + CP_NEQ32 = 0x52 + CP_NEQF = 0x53 + CP_LTEQ8 = 0x54 + CP_LTEQ16 = 0x55 + CP_LTEQ32 = 0x56 + CP_LTEQF = 0x57 + CP_GTEQ8 = 0x58 + CP_GTEQ16 = 0x59 + CP_GTEQ32 = 0x5A + CP_GTEQF = 0x5B + # SELECT + SELECT8 = 0x5C + SELECT16 = 0x5D + SELECT32 = 0x5E + SELECTF = 0x5F + SYSTEM = 0x60 + PORT_CNV_OUTPUT = 0x61 + PORT_CNV_INPUT = 0x62 + NOTE_TO_FREQ = 0x63 + # BRANCH + JR_LT8 = 0x64 + JR_LT16 = 0x65 + JR_LT32 = 0x66 + JR_LTF = 0x67 + JR_GT8 = 0x68 + JR_GT16 = 0x69 + JR_GT32 = 0x6A + JR_GTF = 0x6B + JR_EQ8 = 0x6C + JR_EQ16 = 0x6D + JR_EQ32 = 0x6E + JR_EQF = 0x6F + JR_NEQ8 = 0x70 + JR_NEQ16 = 0x71 + JR_NEQ32 = 0x72 + JR_NEQF = 0x73 + JR_LTEQ8 = 0x74 + JR_LTEQ16 = 0x75 + JR_LTEQ32 = 0x76 + JR_LTEQF = 0x77 + JR_GTEQ8 = 0x78 + JR_GTEQ16 = 0x79 + JR_GTEQ32 = 0x7A + JR_GTEQF = 0x7B + # VM + INFO = 0x7C + STRINGS = 0x7D + MEMORY_WRITE = 0x7E + MEMORY_READ = 0x7F + # UI + UI_FLUSH = 0x80 + UI_READ = 0x81 + UI_WRITE = 0x82 + UI_BUTTON = 0x83 + UI_DRAW = 0x84 + # TIMER + TIMER_WAIT = 0x85 + TIMER_READY = 0x86 + TIMER_READ = 0x87 + # BREAKPOINT + BP0 = 0x88 + BP1 = 0x89 + BP2 = 0x8A + BP3 = 0x8B + BP_SET = 0x8C + MATH = 0x8D + RANDOM = 0x8E + # TIMER + TIMER_READ_US = 0x8F + # UI + KEEP_ALIVE = 0x90 + # COM + COM_READ = 0x91 + COM_WRITE = 0x92 + # SOUND + SOUND = 0x94 + SOUND_TEST = 0x95 + SOUND_READY = 0x96 + # INPUT + INPUT_SAMPLE = 0x97 + INPUT_DEVICE_LIST = 0x98 + INPUT_DEVICE = 0x99 + INPUT_READ = 0x9A + INPUT_TEST = 0x9B + INPUT_READY = 0x9C + INPUT_READSI = 0x9D + INPUT_READEXT = 0x9E + INPUT_WRITE = 0x9F + # OUTPUT + OUTPUT_GET_TYPE = 0xA0 + OUTPUT_SET_TYPE = 0xA1 + OUTPUT_RESET = 0xA2 + OUTPUT_STOP = 0xA3 + OUTPUT_POWER = 0xA4 + OUTPUT_SPEED = 0xA5 + OUTPUT_START = 0xA6 + OUTPUT_POLARITY = 0xA7 + OUTPUT_READ = 0xA8 + OUTPUT_TEST = 0xA9 + OUTPUT_READY = 0xAA + OUTPUT_POSITION = 0xAB + OUTPUT_STEP_POWER = 0xAC + OUTPUT_TIME_POWER = 0xAD + OUTPUT_STEP_SPEED = 0xAE + OUTPUT_TIME_SPEED = 0xAF + OUTPUT_STEP_SYNC = 0xB0 + OUTPUT_TIME_SYNC = 0xB1 + OUTPUT_CLR_COUNT = 0xB2 + OUTPUT_GET_COUNT = 0xB3 + OUTPUT_PRG_STOP = 0xB4 + # MEMORY + FILE = 0xC0 + ARRAY = 0xC1 + ARRAY_WRITE = 0xC2 + ARRAY_READ = 0xC3 + ARRAY_APPEND = 0xC4 + MEMORY_USAGE = 0xC5 + FILENAME = 0xC6 + # READ + READ8 = 0xC8 + READ16 = 0xC9 + READ32 = 0xCA + READF = 0xCB + # WRITE + WRITE8 = 0xCC + WRITE16 = 0xCD + WRITE32 = 0xCE + WRITEF = 0xCF + # COM + COM_READY = 0xD0 + COM_READDATA = 0xD1 + COM_WRITEDATA = 0xD2 + COM_GET = 0xD3 + COM_SET = 0xD4 + COM_TEST = 0xD5 + COM_REMOVE = 0xD6 + COM_WRITEFILE = 0xD7 + MAILBOX_OPEN = 0xD8 + MAILBOX_WRITE = 0xD9 + MAILBOX_READ = 0xDA + MAILBOX_TEST = 0xDB + MAILBOX_READY = 0xDC + MAILBOX_CLOSE = 0xDD + # SPARE + TST = 0xFF + + +class DirectCommand(object): + """Handles variable allocation and parameters for commands that can consist + of arbitrary bytecodes. + + TODO: Better param verification? + + """ + + + # These are inserted into the _global_params_types list so that commands + # that return mulitple values can have their values bundled together into + # tuples before they are returned. + _REPLY_TUPLE_OPEN_TOKEN = '_(' + _REPLY_TUPLE_CLOSE_TOKEN = ')_' + + + def __init__(self): + """Constructs a new, empty object.""" + self._global_params_types = [] + + self._local_params_byte_count = 0 + self._global_params_byte_count = 0 + + # Allocate space for the CommandType. + self._msg = [0x00] + + # Allocate space for global and local param lengths. + self._msg.append(0x00) + self._msg.append(0x00) + + + def send(self, ev3_object): + """Sends the message and parses the reply.""" + if (2 == len(self._msg)): + raise DirectCommandError('Attempt to send an empty DirectCommand.') + + self._msg[1] = (self._global_params_byte_count & 0xFF) + self._msg[2] = ((self._local_params_byte_count << 2) | + ((self._global_params_byte_count >> 8) & 0x03)) + + if (self._global_params_byte_count): + self._msg[0] = CommandType.DIRECT_COMMAND_REPLY + reply = ev3_object.send_message_for_reply(self._msg) + + return self._parse_reply(reply) + else: + self._msg[0] = CommandType.DIRECT_COMMAND_NO_REPLY + ev3_object.send_message(self._msg) + + + def safe_add(fn): + """A wrapper for adding commands in a safe manner.""" + def checked_add(*args): + # Wrappers aren't bound methods so they can't reference 'self' + # directly. However, 'self' will be provided as the first parameter + # when the wrapped method is called. + _self = args[0] + + msg_len = len(_self._msg) + + global_params_types_len = len(_self._global_params_types) + + local_params_byte_count = _self._local_params_byte_count + global_params_byte_count = _self._global_params_byte_count + + fn(*args) + + if ((MAX_CMD_LEN < len(_self._msg)) or + (MAX_CMD_LEN < _self._global_params_byte_count) or + (MAX_LOCAL_VARIABLE_BYTES < _self._local_params_byte_count)): + del (_self._msg[msg_len:]) + + del (_self._global_params_types[global_params_types_len:]) + + _self._local_params_byte_count = local_params_byte_count + _self._global_params_byte_count = global_params_byte_count + + raise DirectCommandError('Not enough space to add the ' + + 'given func.') + + return checked_add + + + @safe_add + def add_timer_wait(self, milliseconds): + """Causes the thread to sleep for the specified number of milliseconds. + + """ + local_var_tuple = self._allocate_local_param(DataFormat.DATA32) + + self._msg.append(Opcode.TIMER_WAIT) + self._append_local_constant(milliseconds) + self._append_param(*local_var_tuple) + + self._msg.append(Opcode.TIMER_READY) + self._append_param(*local_var_tuple) + + + @safe_add + def add_ui_draw_update(self): + """Updates the screen (applies whatever drawing commands have been + issued since the last update). + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.UPDATE) + + + @safe_add + def add_ui_draw_clean(self): + """Fills the screen with LCDColor.BACKGROUND.""" + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.CLEAN) + + + @safe_add + def add_ui_draw_fillwindow(self, lcd_color, start_y, count): + """Fills the window with count rows of the given LCDColor starting at + row start_y. + + NOTE: Starting at 0 with a size of 0 will clear the window. This seems + to be the way the CLEAN command is implemented. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.FILLWINDOW) + self._append_param(lcd_color) + self._append_param(start_y, ParamType.LC2) + self._append_param(count, ParamType.LC2) + + + @safe_add + def add_ui_draw_pixel(self, lcd_color, xy): + """Draws a pixel at the given (x, y).""" + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.PIXEL) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + + + @safe_add + def add_ui_draw_line(self, lcd_color, start_xy, end_xy): + """Draws a line from the start (x, y) to the end (x, y).""" + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.LINE) + self._append_param(lcd_color) + self._append_param(start_xy[0], ParamType.LC2) + self._append_param(start_xy[1], ParamType.LC2) + self._append_param(end_xy[0], ParamType.LC2) + self._append_param(end_xy[1], ParamType.LC2) + + + @safe_add + def add_ui_draw_dotline(self, lcd_color, + start_xy, + end_xy, + on_pixels, + off_pixels): + """Draws a line from the start (x, y) to the end (x, y). The line will + be composed of a repeating pattern consisting of on_pixels followed by + off_pixels. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.DOTLINE) + self._append_param(lcd_color) + self._append_param(start_xy[0], ParamType.LC2) + self._append_param(start_xy[1], ParamType.LC2) + self._append_param(end_xy[0], ParamType.LC2) + self._append_param(end_xy[1], ParamType.LC2) + self._append_param(on_pixels, ParamType.LC2) + self._append_param(off_pixels, ParamType.LC2) + + + @safe_add + def add_ui_draw_rect(self, lcd_color, xy, width, height): + """Draws a rectangle with (x, y) as the top-left corner and with width + and height dimensions. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.RECT) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(width, ParamType.LC2) + self._append_param(height, ParamType.LC2) + + + @safe_add + def add_ui_draw_fillrect(self, lcd_color, xy, width, height): + """Draws a filled rectangle with (x, y) as the top-left corner and + with width and height dimensions. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.FILLRECT) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(width, ParamType.LC2) + self._append_param(height, ParamType.LC2) + + + @safe_add + def add_ui_draw_inverserect(self, xy, width, height): + """Draws a rectangle with (x, y) as the top-left corner and with width + and height dimensions. Any pixel that this rectangle overlaps will have + its color flipped. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.INVERSERECT) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(width, ParamType.LC2) + self._append_param(height, ParamType.LC2) + + + @safe_add + def add_ui_draw_circle(self, lcd_color, xy, radius): + """Draws a circle centered at (x, y) with the specified radius.""" + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.CIRCLE) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(radius, ParamType.LC2) + + + @safe_add + def add_ui_draw_fillcircle(self, lcd_color, xy, radius): + """Draws a filled circle centered at (x, y) with the specified radius. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.FILLCIRCLE) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(radius, ParamType.LC2) + + + @safe_add + def add_ui_draw_selectfont(self, font_type): + """Selects the FontType that will be used by following calls to + add_ui_draw_text. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.SELECT_FONT) + self._append_param(font_type) + + + @safe_add + def add_ui_draw_text(self, lcd_color, xy, text_str): + """Draws the given text with (x, y) as the top-left corner of the + bounding box. Use add_ui_draw_selectfont to select the font. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.TEXT) + self._append_param(lcd_color) + self._append_param(xy[0], ParamType.LC2) + self._append_param(xy[1], ParamType.LC2) + self._append_param(text_str, ParamType.LCS) + + + @safe_add + def add_ui_draw_topline(self, topline_enabled): + """Enables or disables the display of the menu bar at the top of the + screen that normally displays status icons such as the battery + indicator. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.TOPLINE) + self._append_param(int(topline_enabled)) + + + @safe_add + def add_ui_draw_store(self, ui_level_no=0): + """If ui_level_no is zero then this function saves the current screen + content so that it be restored later using add_ui_draw_restore. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.STORE) + self._append_param(ui_level_no) + + + @safe_add + def add_ui_draw_restore(self, ui_level_no=0): + """Restores the screen content that was previously saved using + add_ui_draw_store. + + """ + self._msg.append(Opcode.UI_DRAW) + self._msg.append(UIDrawSubcode.RESTORE) + self._append_param(ui_level_no) + + + @safe_add + def add_ui_button_pressed(self, button_type): + """Returns True if the specified ButtonType button is being pressed.""" + self._msg.append(Opcode.UI_BUTTON) + self._msg.append(UIButtonSubcode.PRESSED) + self._append_param(button_type) + self._append_reply_param(DataFormat.BOOL) + + + @safe_add + def add_keep_alive(self): + """Resets the sleep timer and returns the sleep timer's new value in + minutes. + + """ + self._msg.append(Opcode.KEEP_ALIVE) + self._append_reply_param(DataFormat.DATA8) + + + @safe_add + def add_input_device_get_typemode(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns the DeviceType and mode for the given input_port. The mode + value depends on the type of the device. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_TYPEMODE) + self._append_param(layer) + self._append_param(input_port) + self._global_params_types.append(self._REPLY_TUPLE_OPEN_TOKEN) + self._append_reply_param(DataFormat.DATA8) + self._append_reply_param(DataFormat.DATA8) + self._global_params_types.append(self._REPLY_TUPLE_CLOSE_TOKEN) + + + @safe_add + def add_input_device_get_name(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns a string describing the device that is located at the + specified port i.e. 'NONE' or 'US-DIST-CM'. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_NAME) + self._append_param(layer) + self._append_param(input_port) + self._append_param(MAX_NAME_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_input_device_get_modename(self, input_port, + mode, + layer=USB_CHAIN_LAYER_MASTER): + """Returns a string describing the specified mode of the device that is + located at the given port. For example, for an ultrasonic sensor mode + 0 will return 'US-DIST-CM' and mode 1 will return 'US-DIST-IN'. + + NOTE: Reading invalid modes can corrupt the reply buffer. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_MODENAME) + self._append_param(layer) + self._append_param(input_port) + self._append_param(mode) + self._append_param(MAX_NAME_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_input_device_get_minmax(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """""" + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_MINMAX) + self._append_param(layer) + self._append_param(input_port) + self._global_params_types.append(self._REPLY_TUPLE_OPEN_TOKEN) + self._append_reply_param(DataFormat.DATA_F) + self._append_reply_param(DataFormat.DATA_F) + self._global_params_types.append(self._REPLY_TUPLE_CLOSE_TOKEN) + + + @safe_add + def add_input_device_get_changes(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns the number of positive changes since the last time + CLR_CHANGES was called (i.e. the number of times that a touch sensor + has been pressed). + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_CHANGES) + self._append_param(layer) + self._append_param(input_port) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_input_device_get_bumps(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns the number of negative changes since the last time + CLR_CHANGES was called (i.e. the number of times that a touch sensor + has been released). + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.GET_BUMPS) + self._append_param(layer) + self._append_param(input_port) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_input_device_clr_changes(self, input_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns the number of negative changes since the last time + CLR_CHANGES was called (i.e. the number of times that a touch sensor + has been released). + + NOTE: Does not clear the accumulated angle measurement for the EV3 + gyro sensor. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.CLR_CHANGES) + self._append_param(layer) + self._append_param(input_port) + + + @safe_add + def add_input_device_clr_all(self, layer=USB_CHAIN_LAYER_MASTER): + """Clears all of the input device values.""" + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.CLR_ALL) + self._append_param(layer) + + + @safe_add + def add_input_device_ready_si(self, input_port, + mode=-1, + device_type=0, + layer=USB_CHAIN_LAYER_MASTER): + """Waits until the device on the specified InputPort is ready and then + returns its value as a standard unit. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.READY_SI) + self._append_param(layer) + self._append_param(input_port) + self._append_param(device_type) + self._append_param(mode) + self._append_param(1) # Number of values + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_input_device_ready_raw(self, input_port, + mode=-1, + device_type=0, + layer=USB_CHAIN_LAYER_MASTER): + """Waits until the device on the specified InputPort is ready and then + returns its value as a raw value. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.READY_RAW) + self._append_param(layer) + self._append_param(input_port) + self._append_param(device_type) + self._append_param(mode) + self._append_param(1) # Number of values + self._append_reply_param(DataFormat.DATA32) + + + @safe_add + def add_input_device_ready_percent(self, input_port, + mode=-1, + device_type=0, + layer=USB_CHAIN_LAYER_MASTER): + """Waits until the device on the specified InputPort is ready and then + returns its value as a percentage. + + """ + self._msg.append(Opcode.INPUT_DEVICE) + self._msg.append(InputDeviceSubcode.READY_PCT) + self._append_param(layer) + self._append_param(input_port) + self._append_param(device_type) + self._append_param(mode) + self._append_param(1) # Number of values + self._append_reply_param(DataFormat.DATA8) + + + @safe_add + def add_sound_tone(self, volume, + frequency_hz, + duration_ms, + layer=USB_CHAIN_LAYER_MASTER): + """Plays the tone at the given volume and frequency for the given + duration_ms milliseconds. + + """ + self._msg.append(Opcode.SOUND) + self._msg.append(SoundSubcode.TONE) + self._append_param(volume) + self._append_param(frequency_hz, ParamType.LC2) + self._append_param(duration_ms, ParamType.LC2) + + + @safe_add + def add_sound_play(self, volume, filename): + """Plays the sound file with the given name at the specified volume. + The default sound files are located in the '/home/root/lms2012/sys/ui/' + directory and include Startup.rsf, PowerDown.rsf, OverpowerAlert.rsf, + GeneralAlarm.rsf, DownloadSucces.rsf, and Click.rsf. + + NOTE: Do not include the '.rsf' extension in the filename. + + """ + self._msg.append(Opcode.SOUND) + self._msg.append(SoundSubcode.PLAY) + self._append_param(volume) + self._append_param(filename, ParamType.LCS) + + + @safe_add + def add_ui_read_get_fw_vers(self): + """Returns the FW version as a string in the form 'VX.XXX'.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_FW_VERS) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_hw_vers(self): + """Returns the HW version as a string in the form 'VX.XXX'.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_HW_VERS) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_fw_build(self): + """Returns the firmware build as a string in the form 'XXXXXXXXXX'.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_FW_BUILD) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_os_vers(self): + """Returns the OS version as a string in the form 'Linux X.X.XX'.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_OS_VERS) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_os_build(self): + """Returns the OS build as a string in the form 'XXXXXXXXXX'.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_OS_BUILD) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_version(self): + """Returns the Lego Mindstorms version as a string in the form + 'LMS2012 VX.XXX()'. + + """ + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_VERSION) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_ip(self): + """Returns the IP address as a string.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_IP) + self._append_param(MAX_VERSION_STR_LEN, ParamType.LC2) + self._append_reply_param((DataFormat.DATA_S, MAX_NAME_STR_LEN)) + + + @safe_add + def add_ui_read_get_vbatt(self): + """Gets the current battery voltage. According to the constants that are + defined in 'lms2012.h', the rechargeable battery should be in the range + of [6.0, 7.1] and normal batteries should be in the range of [4.5, 6.2]. + + """ + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_VBATT) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_ui_read_get_lbatt(self): + """Gets the current battery level as a percentage.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_LBATT) + self._append_reply_param(DataFormat.DATA_PCT) + + + @safe_add + def add_ui_read_get_ibatt(self): + """Gets the current battery discharge amperage.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_IBATT) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_ui_read_get_tbatt(self): + """Gets the current battery temperature rise.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_TBATT) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_ui_read_get_imotor(self): + """Gets the amount of current that the motors are using.""" + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_IMOTOR) + self._append_reply_param(DataFormat.DATA_F) + + + @safe_add + def add_ui_read_get_sdcard(self): + """Returns the following information about the SD card: + (, , ). The SD_CARD_OK value is + a boolean. + + """ + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_SDCARD) + self._global_params_types.append(self._REPLY_TUPLE_OPEN_TOKEN) + self._append_reply_param(DataFormat.BOOL) + self._append_reply_param(DataFormat.DATA32) + self._append_reply_param(DataFormat.DATA32) + self._global_params_types.append(self._REPLY_TUPLE_CLOSE_TOKEN) + + + @safe_add + def add_ui_read_get_usbstick(self): + """Returns the following information about the USB stick: + (, , ). The USB_STICK_OK value + is a boolean. + + """ + self._msg.append(Opcode.UI_READ) + self._msg.append(UIReadSubcode.GET_SDCARD) + self._global_params_types.append(self._REPLY_TUPLE_OPEN_TOKEN) + self._append_reply_param(DataFormat.BOOL) + self._append_reply_param(DataFormat.DATA32) + self._append_reply_param(DataFormat.DATA32) + self._global_params_types.append(self._REPLY_TUPLE_CLOSE_TOKEN) + + + @safe_add + def add_output_get_type(self, output_port, layer=USB_CHAIN_LAYER_MASTER): + """Returns the DeviceType of the device that is connected to the + specified OutputPort. + + """ + self._msg.append(Opcode.OUTPUT_GET_TYPE) + self._append_param(layer) + self._append_param(OUTPUT_CHANNEL_TO_INDEX[output_port]) + self._append_reply_param(DataFormat.DATA8) + + + @safe_add + def add_output_set_type(self, output_port, + output_type, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the DeviceType of the device that is connected to the + specified OutputPort. + + TODO: d_pwm.c says this only works with type TACHO or MINI_TACHO. + TODO: Debug this. Not sure how to verify that this works. + It seems to be implemented in d_pwm.c + + """ + self._msg.append(Opcode.OUTPUT_SET_TYPE) + self._append_param(layer) + self._append_param(output_type) + + + @safe_add + def add_output_reset(self, output_port_mask, + layer=USB_CHAIN_LAYER_MASTER): + """Resets the tacho count and timer of the motor(s) described by the + output_port_mask parameter. Should be called when initializing a + motor? + + """ + self._msg.append(Opcode.OUTPUT_RESET) + self._append_param(layer) + self._append_param(output_port_mask) + + + @safe_add + def add_output_stop(self, output_port_mask, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Stops the motor(s) described by the output_port_mask parameter. + The stop_byte parameter defines whether the motor will BRAKE or COAST. + + """ + self._msg.append(Opcode.OUTPUT_STOP) + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(stop_type) + + + @safe_add + def add_output_power(self, output_port_mask, + power, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the power for the motor(s) described by the output_port_mask + parameter. Power values should be in the range [-100, 100]. Note that + add_output_start needs to be called before the motor will start moving. + + """ + self._msg.append(Opcode.OUTPUT_POWER) + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(power) + + + @safe_add + def add_output_speed(self, output_port_mask, + speed, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the speed for the motor(s) described by the output_port_mask + parameter. Speed values should be in the range [-100, 100]. Note that + add_output_start needs to be called before the motor will start moving. + + """ + self._msg.append(Opcode.OUTPUT_SPEED); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(speed) + + + @safe_add + def add_output_start(self, output_port_mask, + layer=USB_CHAIN_LAYER_MASTER): + """Starts the motor(s) described by the output_port_mask + parameter. + + """ + self._msg.append(Opcode.OUTPUT_START) + self._append_param(layer) + self._append_param(output_port_mask) + + + @safe_add + def add_output_polarity(self, output_port_mask, + polarity_type, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the polarity for the motor(s) described by the + output_port_mask parameter. + + """ + self._msg.append(Opcode.OUTPUT_POLARITY) + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(polarity_type) + + + @safe_add + def add_output_read(self, output_port, layer=USB_CHAIN_LAYER_MASTER): + """Reads the speed and tacho pulses for the given output_port and + returns them as a tuple in the form (SPEED, TACHO_PULSES). + + """ + self._msg.append(Opcode.OUTPUT_READ) + self._append_param(layer) + self._append_param(OUTPUT_CHANNEL_TO_INDEX[output_port]) + self._global_params_types.append(self._REPLY_TUPLE_OPEN_TOKEN) + self._append_reply_param(DataFormat.DATA8) + self._append_reply_param(DataFormat.DATA32) + self._global_params_types.append(self._REPLY_TUPLE_CLOSE_TOKEN) + + + @safe_add + def add_output_ready(self, output_port_mask, + layer=USB_CHAIN_LAYER_MASTER): + """Waits for the outputs in the output_port_mask to report that + they are ready before executing the next opcode. For example, if two + consecutive motor commands are used with the same OutputPort putting + this opcode between them ensures that the first command finishes + before the second one is started. + + """ + self._msg.append(Opcode.OUTPUT_READY) + self._append_param(layer) + self._append_param(output_port_mask) + + + @safe_add + def add_output_position(self, output_port_mask, + position, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the position of the specied OutputPort(s).""" + self._msg.append(Opcode.OUTPUT_POSITION) + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(position, ParamType.LC4) + + + @safe_add + def add_output_step_power(self, output_port_mask, + power, + ramp_up_steps, + steps, + ramp_down_steps, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Ramps up the power for the motor(s) described by the + output_port_mask, holds for steps, and then ramps down. It is not + necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_STEP_POWER); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(power) + self._append_param(ramp_up_steps, ParamType.LC4) + self._append_param(steps, ParamType.LC4) + self._append_param(ramp_down_steps, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_time_power(self, output_port_mask, + power, + ramp_up_ms, + time_ms, + ramp_down_ms, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Ramps up the power for the motor(s) described by the + output_port_mask, holds for time_ms, and then ramps down. It is not + necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_TIME_POWER); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(power) + self._append_param(ramp_up_ms, ParamType.LC4) + self._append_param(time_ms, ParamType.LC4) + self._append_param(ramp_down_ms, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_step_speed(self, output_port_mask, + speed, + ramp_up_steps, + steps, + ramp_down_steps, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Ramps up the power for the motor(s) described by the + output_port_mask, holds for steps, and then ramps down. It is not + necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_STEP_SPEED); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(speed) + self._append_param(ramp_up_steps, ParamType.LC4) + self._append_param(steps, ParamType.LC4) + self._append_param(ramp_down_steps, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_time_speed(self, output_port_mask, + speed, + ramp_up_ms, + time_ms, + ramp_down_ms, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Ramps up the power for the motor(s) described by the + output_port_mask, holds for time_ms, and then ramps down. It is not + necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_TIME_SPEED); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(speed) + self._append_param(ramp_up_ms, ParamType.LC4) + self._append_param(time_ms, ParamType.LC4) + self._append_param(ramp_down_ms, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_step_sync(self, output_port_mask, + speed, + turn_ratio, + step, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the speed for the two given motors in the following fashion: + [-200, -101]: Turn right with right motor running in reverse + [-100, -1]: Turn right with right motor slowed + 0: Both motors in sync in the same direction + [1, 100]: Turn left with left motor slowed + [101, 200]: Turn left with left motor running in reverse + + It is not necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_STEP_SYNC); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(speed) + self._append_param(turn_ratio, ParamType.LC2) + self._append_param(step, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_time_sync(self, output_port_mask, + speed, + turn_ratio, + time, + stop_type, + layer=USB_CHAIN_LAYER_MASTER): + """Sets the speed for the two given motors in the following fashion: + [-200, -101]: Turn right with right motor running in reverse + [-100, -1]: Turn right with right motor slowed + 0: Both motors in sync in the same direction + [1, 100]: Turn left with left motor slowed + [101, 200]: Turn left with left motor running in reverse + + It is not necessary to call add_output_start in addition to this opcode. + + NOTE: The EV3 will NOT wait for this operation to complete before + executing the next opcode unless add_output_ready is used. + + """ + self._msg.append(Opcode.OUTPUT_TIME_SYNC); + self._append_param(layer) + self._append_param(output_port_mask) + self._append_param(speed) + self._append_param(turn_ratio, ParamType.LC2) + self._append_param(time, ParamType.LC4) + self._append_param(stop_type) + + + @safe_add + def add_output_clr_count(self, output_port_mask, + layer=USB_CHAIN_LAYER_MASTER): + """Clears the tacho count for the given OutputPort(s) when in sensor + mode. + + """ + self._msg.append(Opcode.OUTPUT_CLR_COUNT); + self._append_param(layer) + self._append_param(output_port_mask) + + + @safe_add + def add_output_get_count(self, output_port, + layer=USB_CHAIN_LAYER_MASTER): + """Returns the tacho count for the given OutputPort when in sensor + mode. + + """ + self._msg.append(Opcode.OUTPUT_GET_COUNT); + self._append_param(layer) + self._append_param(OUTPUT_CHANNEL_TO_INDEX[output_port]) + self._append_reply_param(DataFormat.DATA32) + + + @safe_add + def add_set_leds(self, led_pattern): + """Sets the LEDs on the front of the brick to the specified item in + the LEDPattern enumeration. + + """ + self._msg.append(Opcode.UI_WRITE) + self._msg.append(UIWriteSubcode.LED) + self._append_param(led_pattern) + + + def _parse_reply(self, buf): + result = [] + index = 0 + + if (ReplyType.DIRECT_REPLY_ERROR == buf[0]): + raise DirectCommandError('The DirectCommand failed.') + + if (self._global_params_byte_count != (len(buf) - 1)): + raise DirectCommandError('The data returned by the ' + + 'command was smaller than expected.') + + # The items in the reply are grouped into tuples. Each tuple represents + # the reply to a command that returns multiple values. + sub_tuple = None + for item in self._global_params_types: + value = None + length = 0 + + if (self._REPLY_TUPLE_OPEN_TOKEN == item): + sub_tuple = [] + continue + elif (self._REPLY_TUPLE_CLOSE_TOKEN == item): + result.append(tuple(sub_tuple)) + sub_tuple = None + continue + + if (isinstance(item, tuple)): + value, length = self._parse_param(buf, (index + 1), *item) + else: + # Ensure that the alignment is correct. + data_len = DATA_FORMAT_LENS[item] + + pad = (index % data_len) + if (0 != pad): + pad = (data_len - pad) + index += pad + + value, length = self._parse_param(buf, (index + 1), item) + + if (sub_tuple is not None): + sub_tuple.append(value) + else: + result.append(value) + + index += length + + return tuple(result) + + + def _parse_param(self, buf, index, data_format, data_len=None): + value = None + length = 1 + + if (DataFormat.DATA_S == data_format): + value = message.parse_null_terminated_str(buf, index, data_len) + length = data_len + elif (DataFormat.HND == data_format): + value = (buf[index] & ~ParamType.HND) + elif (DataFormat.DATA_F == data_format): + value = message.parse_float(buf, index) + length = DATA_FORMAT_LENS[DataFormat.DATA_F] + elif (DataFormat.BOOL == data_format): + value = bool(buf[index]) + length = 1 + else: + length = DATA_FORMAT_LENS[data_format] + + if (1 == length): + value = buf[index] + elif (2 == length): + value = message.parse_u16(buf, index) + elif (4 == length): + value = message.parse_u32(buf, index) + else: + raise DirectCommandError('Unexpected ParamType: %d' % + param_type) + + return (value, length) + + + def _append_reply_param(self, reply_format): + """Global parameters are stored in the tx buffer on the brick so + their values are returned in the message reply. + + """ + data_len = None + + if (not isinstance(reply_format, tuple)): + # Ensure that the alignment is correct. + data_len = DATA_FORMAT_LENS[reply_format] + + pad = (self._global_params_byte_count % data_len) + if (pad): + pad = (data_len - pad) + self._global_params_byte_count += pad + else: + data_len = reply_format[1] + + # Use as few bits as possible to save space in message buffer. + param_type = ParamType.GV1 + if (0xFFFF < self._global_params_byte_count): + param_type = ParamType.GV4 + elif (0xFF < self._global_params_byte_count): + param_type = ParamType.GV2 + + self._append_param(self._global_params_byte_count, param_type) + self._global_params_types.append(reply_format) + self._global_params_byte_count += data_len + + + def _allocate_local_param(self, data_format): + """Local parameters are essentially stack variables so they are NOT + included in the reply from the brick. This function returns an index + that can be used to access a new local variable of the given DataFormat. + + """ + # Ensure that the alignment is correct. + data_len = DATA_FORMAT_LENS[data_format] + + pad = (self._local_params_byte_count % data_len) + if (pad): + pad = (data_len - pad) + self._local_params_byte_count += pad + + # Use as few bits as possible to save space in message buffer. + param_type = ParamType.LV1 + if (0xFFFF < self._local_params_byte_count): + param_type = ParamType.LV4 + elif (0xFF < self._local_params_byte_count): + param_type = ParamType.LV2 + + index = self._local_params_byte_count + + self._local_params_byte_count += data_len + + return (index, param_type) + + + def _append_local_constant(self, val): + """"Appends an immediate value as a local constant.""" + param_type = None + + if (isinstance(val, int)): + num_bits = int.bit_length(val) + if (num_bits > 16): + param_type = ParamType.LC4 + elif (num_bits > 8): + param_type = ParamType.LC2 + elif (num_bits > 6): + param_type = ParamType.LC1 + else: + param_type = ParamType.LC0 + elif (isinstance(val, float)): + param_type = ParamType.FLOAT + elif (isinstance(val, str)): + param_type = ParamType.LCS + else: + raise NotImplementedError('Unknown local constant type.') + + self._append_param(val, param_type) + + + def _append_param(self, val, param_type=ParamType.LC1): + """All parameters need to be prefixed with their type so the VM knows + how to interpret the following data. The reply_format parameter is + used when a reply is expected. + + """ + if (ParamType.PRIMPAR_LABEL == param_type): + raise NotImplementedError('ParamType.PRIMPAR_LABEL') + elif (ParamType.LCS == param_type): + self._msg.append(param_type) + message.append_str(self._msg, val) + elif (ParamType.LC0 == param_type): + self._msg.append(ParamType.LC0 | (0x3F & val)) + elif (ParamType.HND == param_type): + self._msg.append(ParamType.HND | val) + elif (ParamType.ADR == param_type): + self._msg.append(ParamType.ADR | val) + elif (ParamType.GV0 == param_type): + self._msg.append(ParamType.GV0 | (0x1F & val)) + elif (ParamType.FLOAT == param_type): + self._msg.append(ParamType.LC4) + message.append_float(self._msg, val) + else: + length = PARAM_TYPE_LENS[param_type] + + self._msg.append(param_type) + + if (1 == length): + message.append_u8(self._msg, val) + elif (2 == length): + message.append_u16(self._msg, val) + elif (4 == length): + message.append_u32(self._msg, val) + else: + raise DirectCommandError('Unexpected ParamType:' + + ' %d' % param_type) diff --git a/ev3/ev3.py b/ev3/ev3.py new file mode 100755 index 0000000..8e21840 --- /dev/null +++ b/ev3/ev3.py @@ -0,0 +1,188 @@ +"""A wrapper for using Python to interact with a Lego Mindstorms EV3. + +Prerequisites: + + Make sure your sdp includes an SP (serial port): + % sdptool browse local + ... + Service Name: Serial Port + Service Description: COM Port + + If not: + % sdptool add SP + + Then: + % hcitool scan + ... + XX:XX:XX:XX:XX:XX EV3 + + % sudo rfcomm bind /dev/rfcomm0 XX:XX:XX:XX:XX:XX + + Now /dev/rfcomm0 can be opened and closed like a normal serial port. + The opposite action is: + % sudo rfcomm release /dev/rfcomm0 + + EXAMPLE USAGE: + from ev3 import * + + with ev3.EV3() as brick: + # Create DirectCommand objects and add commands to them. + cmd = direct_command.DirectCommand() + cmd.add_ui_draw_update() + cmd.send(brick) + + # Call single DirectCommand functions without creating DirectCommand + # objects each time. + brick.output_stop(direct_command.OutputPort.PORT_C, + direct_command.StopType.BRAKE) + + # Call single system_command functions. + brick.write_mailbox('foo', (0,1,2,3,4,5,6,7,8,9,0)) + +""" + + +import serial + +import message +import system_command +import direct_command + + +class KnownPaths(object): + """These are the default directories on the brick. All paths are + relative to 'lms2012/sys' by default. + + """ + DEFAULT_PATH = "." # lms2012/sys + PROJECTS_PATH = "../prjs" # lms2012/prjs + APPS_PATH = "../apps" # lms2012/apps + TOOLS_PATH = "../tools" # lms2012/tools + SOURCE_PATH = "../source" # lms2012/source + + +class EV3Error(Exception): + """Subclass for reporting errrors.""" + pass + + +class EV3(object): + """""" + DEFAULT_RFCOMM_PORT = '/dev/rfcomm0' + RFCOMM_BAUDRATE = 115200 + + + def __init__(self, port_str=DEFAULT_RFCOMM_PORT): + """Creates a new object but doesn't open the port.""" + self._port_str = port_str + self._port = None + + + def open(self): + """Opens the object's serial port.""" + if (self._port is None): + self._port = serial.Serial(port=self._port_str, + baudrate=self.RFCOMM_BAUDRATE, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=None, + xonxoff=False, + rtscts=False, + writeTimeout=None, + dsrdtr=False, + interCharTimeout=None) + + def close(self): + """Closes the object's serial port.""" + if (self._port is not None): + self._port.close() + self._port = None + + + def send_message(self, msg, message_counter=0x1234): + """Allows for sending raw messages to the EV3. The msg parameter should + be an array of byte values. The msg parameter should not include the + length/message_counter header. Raises an EV3Error if the specified + message is a type that expects a reply. + + """ + try: + message.send_message_no_reply(self._port, msg, message_counter) + except message.MessageError as ex: + raise EV3Error(ex.message) + + + def send_message_for_reply(self, msg, message_counter=0x1234): + """Allows for sending raw messages to the EV3. The msg parameter should + be an array of byte values. The msg parameter should not include the + length/message_counter header. Raises an EV3Error if the specified + message is a type that doesn't expect a reply. + + """ + try: + return message.send_message_for_reply(self._port, + msg, + message_counter) + except message.MessageError as ex: + raise EV3Error(ex.message) + + + def __dir__(self): + """Add in functions from the system_command module as well as methods + from the DirectCommand class because they can be called directly on an + EV3 object. + + """ + result = dir(type(self)) + result += list(self.__dict__) + result += [s for s in list(system_command.__dict__) + if not s.startswith('_')] + result += [s[4:] for s in list(direct_command.DirectCommand.__dict__) + if s.startswith('add_')] + return sorted(set(result)) + + + def __getattr__(self, name): + """A little bit of magic is used in order to make it easier to work with + EV3 objects. + + """ + if (hasattr(system_command, name)): + # This allows functions from the system_command module to be called + # from an EV3 object i.e. ev3.list_files(KnownPaths.PROJECTS_PATH). + def execute_sc(*args): + """This is just a wrapper around an individual function from the + system_command module. See the system_command module for more + information. + + """ + getattr(system_command, name)(self, *args) + + return execute_sc + + # This allows single functions from the DirectCommand class to be called + # from an EV3 object i.e. ev3.ui_draw_update(). + dc_name = ('add_' + name) + if (hasattr(direct_command.DirectCommand, dc_name)): + def execute_dc(*args): + """This is just a wrapper around an individual DirectCommand + method. See the DirectCommand class for more information. + + """ + dc = direct_command.DirectCommand() + getattr(dc, dc_name)(*args) + return dc.send(self) + + return execute_dc + + return Object.__getattr__(self, name) + + + def __enter__(self): + self.open() + return self + + + def __exit__(self, type, value, traceback): + self.close() diff --git a/ev3/message.py b/ev3/message.py new file mode 100755 index 0000000..bf58892 --- /dev/null +++ b/ev3/message.py @@ -0,0 +1,166 @@ +"""Handles the messaging with EV3 and contains several functions for dealing +with message variable types. + +""" + + +import struct + +import system_command +import direct_command + + +class MessageError(Exception): + """Subclass for reporting errors.""" + pass + + +def send_message_for_reply(port, msg, message_counter=0x1234): + """Sends the message and waits for a reply. The msg is expected to be a + sequence of bytes and it should not contain the length/message_counter + header. Returns an sequence of bytes without the length/message_counter + header. + + """ + if (not msg_expects_reply(msg)): + raise MessageError('The message is not a type that expects a reply.') + + # Message length includes the two message_counter bytes. + msg_len = (2 + len(msg)) + + msg_counter_lsb = (message_counter & 0xFF) + msg_counter_msb = ((message_counter >> 8) & 0xFF) + + buf = [(msg_len & 0xFF), + ((msg_len >> 8) & 0xFF), + msg_counter_lsb, + msg_counter_msb] + + buf += msg + _write_bytes(port, buf) + + expected_len = _read_bytes(port, 2) + expected_len = (expected_len[0] | (expected_len[1] << 8)) + + reply = _read_bytes(port, expected_len) + + if (reply[0] != msg_counter_lsb or reply[1] != msg_counter_msb): + raise MessageError('Reply message counter does not match.') + + return reply[2:] + + +def send_message_no_reply(port, msg, message_counter=0x1234): + """Sends the message without waiting for a reply.""" + if (msg_expects_reply(msg)): + raise MessageError('The message is a type that expects a reply.') + + # Message length includes the two message_counter bytes. + msg_len = (2 + len(msg)) + + msg_counter_lsb = (message_counter & 0xFF) + msg_counter_msb = ((message_counter >> 8) & 0xFF) + + buf = [(msg_len & 0xFF), + ((msg_len >> 8) & 0xFF), + msg_counter_lsb, + msg_counter_msb] + + buf += msg + _write_bytes(port, buf) + + +def msg_expects_reply(msg): + """Returns True if the given message is a type that expects a reply. The + given message should not include the length/message_counter header. + + """ + if (system_command.CommandType.SYSTEM_COMMAND_REPLY == msg[0]): + return True + + if (direct_command.CommandType.DIRECT_COMMAND_REPLY == msg[0]): + return True + + return False + + +def parse_u16(byte_seq, index): + """Parses a u32 value at the given index from the byte_seq.""" + return (byte_seq[index] | (byte_seq[index + 1] << 8)) + + +def parse_u32(byte_seq, index): + """Parses a u32 value at the given index from the byte_seq.""" + return (byte_seq[index] | + (byte_seq[index + 1] << 8) | + (byte_seq[index + 2] << 16) | + (byte_seq[index + 3] << 24)) + + +def parse_str(byte_seq, index, length=None): + """Parses a string of length chars.""" + if (length is None): + return ''.join([chr(c) for c in byte_seq[index:]]) + else: + return ''.join([chr(c) for c in byte_seq[index:(index + length)]]) + + +def parse_null_terminated_str(byte_seq, index, length): + """Parses a null-terminated string of up to length chars.""" + result = [] + + for i in range(index, (index + length)): + if (0x00 != byte_seq[i]): + result.append(chr(byte_seq[i])) + else: + break + + return ''.join(result) + + +def parse_float(byte_seq, index): + """Parses a 32bit floating point number.""" + str_value = ''.join([chr(c) for c in byte_seq[index:(index + 4)]]) + return struct.unpack('> 8) & 0xFF) + + +def append_u32(byte_list, value): + """Appends the given value to the list in little-endian order.""" + byte_list.append(value & 0xFF) + byte_list.append((value >> 8) & 0xFF) + byte_list.append((value >> 16) & 0xFF) + byte_list.append((value >> 24) & 0xFF) + + +def append_str(byte_list, str_value): + """Appends a null-terminated string.""" + if ('\0' != str_value[-1]): + str_value += '\0' + + for c in str_value: + byte_list.append(ord(c)) + + +def _read_bytes(port, num_bytes): + return [ord(i) for i in port.read(num_bytes)] + + +def _write_bytes(port, byte_seq): + port.write(byte_seq) diff --git a/ev3/system_command.py b/ev3/system_command.py new file mode 100755 index 0000000..92a21a1 --- /dev/null +++ b/ev3/system_command.py @@ -0,0 +1,421 @@ +"""System commands are defined as commands that aren't executed in byte code +(no VM intervention). + +All multi-byte words are little endian. + +System Command Bytes: +------------------------------ +Byte 0 - 1: Command size +Byte 2 - 3: Message counter +Byte 4: CommandType +Byte 5: Command +Byte 6 - n: payload + +System Command response Bytes: +------------------------------ +Byte 0 - 1: Reply size +Byte 2 - 3: Message counter +Byte 4: ReplyType +Byte 5: original Command +Byte 6: ReturnCode +Byte 7 - N: payload + +""" + + +import itertools + + +import message + + +MAX_REPLY_BYTES = 1014 # According to c_com.h comments. +MAX_TX_BYTES = 1016 + + +class SystemCommandError(Exception): + """Subclass for reporting errors.""" + pass + + +class CommandType(object): + """Every System Command must be one of these two types.""" + SYSTEM_COMMAND_REPLY = 0x01 + SYSTEM_COMMAND_NO_REPLY = 0x81 + + +class ReplyType(object): + """Every reply to a System Command must be one of these two types.""" + SYSTEM_REPLY = 0x03 + SYSTEM_REPLY_ERROR = 0x05 + + +class Command(object): + """Enumerated System Commands.""" + BEGIN_DOWNLOAD = 0x92 # Begin file down load + CONTINUE_DOWNLOAD = 0x93 # Continue file down load + BEGIN_UPLOAD = 0x94 # Begin file upload + CONTINUE_UPLOAD = 0x95 # Continue file upload + BEGIN_GETFILE = 0x96 # Begin get bytes from a file (while writing to the file) + CONTINUE_GETFILE = 0x97 # Continue get byte from a file (while writing to the file) + CLOSE_FILEHANDLE = 0x98 # Close file handle + LIST_FILES = 0x99 # List files + CONTINUE_LIST_FILES = 0x9A # Continue list files + CREATE_DIR = 0x9B # Create directory + DELETE_FILE = 0x9C # Delete + LIST_OPEN_HANDLES = 0x9D # List handles + WRITEMAILBOX = 0x9E # Write to mailbox + BLUETOOTHPIN = 0x9F # Transfer trusted pin code to brick + ENTERFWUPDATE = 0xA0 # Restart the brick in Firmware update mode + SETBUNDLEID = 0xA1 # Set Bundle ID for mode 2 + SETBUNDLESEEDID = 0xA2 # Set Bundle Seed ID for mode 2 + + +class ReturnCode(object): + """Enumerated System Command return codes.""" + SUCCESS = 0x00 + UNKNOWN_HANDLE = 0x01 + HANDLE_NOT_READY = 0x02 + CORRUPT_FILE = 0x03 + NO_HANDLES_AVAILABLE = 0x04 + NO_PERMISSION = 0x05 + ILLEGAL_PATH = 0x06 + FILE_EXITS = 0x07 + END_OF_FILE = 0x08 + SIZE_ERROR = 0x09 + UNKNOWN_ERROR = 0x0A + ILLEGAL_FILENAME = 0x0B + ILLEGAL_CONNECTION = 0x0C + + +def write_mailbox(ev3_obj, mailbox_name_str, msg): + """Writes a sequence of bytes to the mailbox with the given name.""" + if ('\0' != mailbox_name_str[-1]): + mailbox_name_str += '\0' + + if isinstance(msg, str): + byte_seq = (msg + '\0').encode('ascii') + else: + byte_seq = msg + + cmd = [] + + cmd += CommandType.SYSTEM_COMMAND_NO_REPLY.to_bytes(1, byteorder='little') + cmd += Command.WRITEMAILBOX.to_bytes(1, byteorder='little') + cmd += len(mailbox_name_str).to_bytes(1, byteorder='little') + cmd += mailbox_name_str.encode('ascii') + cmd += len(byte_seq).to_bytes(2, byteorder='little') + cmd += byte_seq + + ev3_obj.send_message(cmd) + + +def list_files(ev3_obj, path_str): + """Returns a tuple in the form (DIRS, FILES). DIRS is a tuple of directory + names (i.e. 'foo/'). FILES is a tuple of file information tuples in the form + (MD5_SUM, FILE_LENGTH, FILE_NAME). + + """ + dirs = [] + files = [] + list_str = '' + continue_list_str = '' + + if (not isinstance(path_str, str)): + raise ValueError('The path_str param must be of type str.') + + list_str, handle, needs_continue = _list_files(ev3_obj, path_str) + + if (needs_continue): + continue_list_str = _continue_list_files(ev3_obj, handle) + + result = (list_str + continue_list_str) + for line in result.strip().split('\n'): + if (line.endswith('/')): + # Directories have the format: '[DIR_NAME]/\n' + dirs.append(line) + elif (line): + # Files have the format: '[MD5] [HEX_SIZE] [FILE_NAME]\n' + fields = line.split() + files.append((fields[0], int(fields[1], 16), ' '.join(fields[2:]))) + + return (dirs, files) + + +def upload_file(ev3_obj, path_str, save_path_str=None): + """Uploads the file from the given path on the brick to the PC. If save_path_str + is not None then the file will be written to disk. Otherwise, the file data + will be returned as a tuple of bytes. + + """ + if (not isinstance(path_str, str)): + raise ValueError('The path_str param must be of type str.') + + result, handle, needs_continue = _upload_file(ev3_obj, path_str) + + if (needs_continue): + result += _continue_upload_file(ev3_obj, handle) + + if (save_path_str is not None): + with open(save_path_str, 'w') as out_file: + out_file.write(message.parse_str(result, 0, len(result))) + else: + return tuple(result) + + +def download_file_from_path(ev3_obj, save_path_str, file_path_str): + """Downloads the file from file_path_str on the PC to save_path_str on the brick. + + NOTE: This function creates intermediary directories automatically. + + """ + if (not isinstance(file_path_str, str)): + raise ValueError('The data_path_str param must be of type str.') + + with open(file_path_str, 'r') as read_file: + return download_file(ev3_obj, read_file.read(), save_path_str) + + +def download_file(ev3_obj, save_path_str, file_data): + """Downloads the file_data to save_path_str on the brick. + + NOTE: This function creates intermediary directories automatically. + + """ + if (not isinstance(save_path_str, str)): + raise ValueError('The save_path_str param must be of type str.') + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.BEGIN_DOWNLOAD) + + message.append_u32(cmd, len(file_data)) + message.append_str(cmd, save_path_str) + + reply = ev3_obj.send_message_for_reply(cmd) + + print ('reply: ' + str(reply)) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.BEGIN_DOWNLOAD): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + handle = reply[3] + + _continue_download_file(ev3_obj, handle, file_data) + + +def create_dir(ev3_obj, path_str): + """Creates the directory at the given path_str.""" + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_NO_REPLY) + cmd.append(Command.CREATE_DIR) + message.append_str(cmd, path_str) + ev3_obj.send_message(cmd) + + +def delete_path(ev3_obj, path_str): + """Deletes the file or directory specified by the given path_str. + + NOTE: Directories must be empty before they can be deleted. + + """ + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_NO_REPLY) + cmd.append(Command.DELETE_FILE) + message.append_str(cmd, path_str) + ev3_obj.send_message(cmd) + + +def delete_directory(ev3_obj, dir_path_str): + """Convenience function for deleting directories that may or may not be + empty. + + """ + if (not dir_path_str.endswith('/')): + dir_path_str += '/' + + directories, files = list_files(ev3_obj, dir_path_str) + + for f in files: + md5, length, file_name = f + delete_path_str(ev3_obj, (dir_path_str + file_name)) + + for d in directories: + # Ignore './' and '../'. + if (not d.startswith('.')): + delete_directory(ev3_obj, (dir_path_str + d)) + + delete_path_str(ev3_obj, dir_path_str) + + +def _list_files(ev3_obj, path_str): + handle = None + needs_continue = False + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.LIST_FILES) + + message.append_u16(cmd, MAX_REPLY_BYTES) + message.append_str(cmd, path_str) + + reply = ev3_obj.send_message_for_reply(cmd) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.LIST_FILES): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + list_size = message.parse_u32(reply, 3) + handle = reply[7] + + result = message.parse_str(reply, 8) + + return (result, handle, (reply[2] != ReturnCode.END_OF_FILE)) + + +def _continue_list_files(ev3_obj, handle): + result = [] + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.CONTINUE_LIST_FILES) + cmd.append(handle) + + message.append_u16(cmd, MAX_REPLY_BYTES) + + while (True): + reply = ev3_obj.send_message_for_reply(cmd) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.CONTINUE_LIST_FILES): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + handle = reply[3] + + result.append(message.parse_str(reply, 4)) + + if (reply[2] == ReturnCode.END_OF_FILE): + break + + return ''.join(result) + + +def _upload_file(ev3_obj, path_str): + handle = None + needs_continue = False + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.BEGIN_UPLOAD) + + message.append_u16(cmd, MAX_REPLY_BYTES) + message.append_str(cmd, path_str) + + reply = ev3_obj.send_message_for_reply(cmd) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.BEGIN_UPLOAD): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + data_size = message.parse_u32(reply, 3) + handle = reply[7] + + result = reply[8:] + + return (result, handle, (reply[2] != ReturnCode.END_OF_FILE)) + + +def _continue_upload_file(ev3_obj, handle): + result = [] + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.CONTINUE_UPLOAD) + cmd.append(handle) + + message.append_u16(cmd, MAX_REPLY_BYTES) + + while (True): + reply = ev3_obj.send_message_for_reply(cmd) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.CONTINUE_UPLOAD): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + handle = reply[3] + + result.append(reply[4:]) + + if (reply[2] == ReturnCode.END_OF_FILE): + break + + return itertools.chain.from_iterable(result) + + +def _continue_download_file(ev3_obj, handle, data): + offset = 0 + + data_len = len(data) + + cmd = [] + cmd.append(CommandType.SYSTEM_COMMAND_REPLY) + cmd.append(Command.CONTINUE_DOWNLOAD) + cmd.append(handle) + + if (MAX_TX_BYTES >= data_len): + offset = data_len + else: + offset = MAX_TX_BYTES + + map(cmd.append, data[:offset]) + + while (True): + reply = ev3_obj.send_message_for_reply(cmd) + + if (reply[0] == ReplyType.SYSTEM_REPLY_ERROR): + raise SystemCommandError('A command failed.') + + if (reply[1] != Command.CONTINUE_DOWNLOAD): + raise SystemCommandError('Sync error detected.') + + if (reply[2] == ReturnCode.UNKNOWN_ERROR): + raise SystemCommandError('An error occurred.') + + if (data_len == offset): + break + + del (cmd[3:]) + + if (MAX_TX_BYTES >= (data_len - offset)): + map(cmd.append, data[offset:]) + offset = data_len + else: + new_offset = (offset + MAX_TX_BYTES) + map(cmd.append, data[offset:new_offset]) + offset = new_offset