diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc84959 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ + diff --git a/README.md b/README.md index e974f76..06406fe 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ NXT-Python is a package for controlling a LEGO NXT robot using the Python progra * Mac: [LightBlue](http://lightblue.sourceforge.net/) * USB Communications: * [PyUSB](https://walac.github.io/pyusb/) -Fantom Communications: - * [Pyfantom](http://pyfantom.ni.fr.eu.org/) ## Installation 1. Go and grab the latest version from the [releases](https://github.com/Eelviny/nxt-python/releases) page - **the master branch is too unstable to use** diff --git a/nxt/.gitignore b/nxt/.gitignore new file mode 100644 index 0000000..8d35cb3 --- /dev/null +++ b/nxt/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/nxt/bluesock.py b/nxt/bluesock.py index 68efdcd..d1da786 100644 --- a/nxt/bluesock.py +++ b/nxt/bluesock.py @@ -64,13 +64,13 @@ def send(self, data): def recv(self): data = self.sock.recv(2) - l0 = ord(data[0]) - l1 = ord(data[1]) + l0 = data[0] + l1 = data[1] plen = l0 + (l1 << 8) data = self.sock.recv(plen) if self.debug: print('Recv:', end=' ') - print(':'.join('%02x' % ord(c) for c in data)) + print(':'.join('%02x' % c for c in data)) return data def _check_brick(arg, value): diff --git a/nxt/devsock.py b/nxt/devsock.py index b420c71..3423144 100644 --- a/nxt/devsock.py +++ b/nxt/devsock.py @@ -31,13 +31,13 @@ def close(self): def send(self, data): l0 = len(data) & 0xFF l1 = (len(data) >> 8) & 0xFF - d = chr(l0) + chr(l1) + data + d = bytes((l0, l1)) + data self._device.write(d) def recv(self): data = self._device.read(2) - l0 = ord(data[0]) - l1 = ord(data[1]) + l0 = data[0] + l1 = data[1] plen = l0 + (l1 << 8) return self._device.read(plen) diff --git a/nxt/direct.py b/nxt/direct.py index 0947314..72a781d 100644 --- a/nxt/direct.py +++ b/nxt/direct.py @@ -156,7 +156,7 @@ def ls_write(opcode, port, tx_data, rx_bytes): tgram.add_u8(port) tgram.add_u8(len(tx_data)) tgram.add_u8(rx_bytes) - tgram.add_string(len(tx_data), tx_data) + tgram.add_bytes(tx_data) return tgram def ls_read(opcode, port): diff --git a/nxt/locator.py b/nxt/locator.py index 3d6251a..bed8f39 100644 --- a/nxt/locator.py +++ b/nxt/locator.py @@ -2,6 +2,7 @@ # Copyright (C) 2006, 2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner # Copyright (C) 2013 Dave Churchill, Marcus Wanner +# Copyright (C) 2015, 2016, 2017, 2018 Multiple Authors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,9 +19,11 @@ class BrickNotFoundError(Exception): pass + class NoBackendError(Exception): pass + class Method(): """Used to indicate which comm backends should be tried by find_bricks/ find_one_brick. Any or all can be selected.""" @@ -30,6 +33,7 @@ def __init__(self, usb=True, bluetooth=True, device=False): self.bluetooth = bluetooth self.device = device + def find_bricks(host=None, name=None, silent=False, method=Method()): """Used by find_one_brick to look for bricks ***ADVANCED USERS ONLY***""" methods_available = 0 @@ -87,7 +91,7 @@ def find_one_brick(host=None, name=None, silent=False, strict=None, debug=False, information will be read from if no brick location directives (host, name, strict, or method) are provided.""" if debug and silent: - silent=False + silent = False print("silent and debug can't both be set; giving debug priority") conf = read_config(confpath, debug) @@ -95,44 +99,61 @@ def find_one_brick(host=None, name=None, silent=False, strict=None, debug=False, host = conf.get('Brick', 'host') name = conf.get('Brick', 'name') strict = bool(int(conf.get('Brick', 'strict'))) - method = eval('Method(%s)' % conf.get('Brick', 'method')) + method_value = conf.get('Brick', 'method') + if method_value: + methods = map(lambda x: x.strip().split('='), + method_value.split(',')) + method = Method(**{k: v == 'True' for k, v in methods + if k in ('bluetooth', 'usb', 'device')}) if not strict: strict = True if not method: method = Method() + if debug: print("Host: %s Name: %s Strict: %s" % (host, name, str(strict))) print("USB: {} BT: {}".format(method.usb, method.bluetooth)) - for s in find_bricks(host, name, silent, method): - try: - if host and 'host' in dir(s) and s.host != host: - if debug: - print("Warning: the brick found does not match the host provided (s.host).") - if strict: continue - b = s.connect() - info = b.get_device_info() - print(info) - strict = False - if host and info[1] != host: - if debug: - print("Warning: the brick found does not match the host provided (get_device_info).") - if strict: - s.close() - continue - info = list(info) - info[0] = str(info[0]) - info[0] = info[0][2:(len(info[0])-1)] - info[0] = info[0].strip('\\x00') - if info[0] != name: - if debug: - print("Warning; the brick found does not match the name provided.") - if strict: - s.close() - continue - return b - except: + for s in find_bricks(host, name, silent, method): + try: + if host and 'host' in dir(s) and s.host != host: if debug: - traceback.print_exc() - print("Failed to connect to possible brick") + print("Warning: the brick found does not match the host provided (s.host).") + if strict: continue + b = s.connect() + info = b.get_device_info() + if debug: + print("info: " + str(info)) + + strict = False + + if host and info[1] != host: + if debug: + print("Warning: the brick found does not match the host provided (get_device_info).") + print(" host:" + str(host)) + print(" info[1]:" + info[1]) + if strict: + s.close() + continue + + info = list(info) + info[0] = str(info[0]) + info[0] = info[0][2:(len(info[0])-1)] + info[0] = info[0].strip('\\x00') + + if info[0] != name: + if debug: + print("Warning; the brick found does not match the name provided.") + print(" host:" + str(host)) + print(" info[0]:" + info[0]) + print(" name:" + str(name)) + if strict: + s.close() + continue + + return b + except: + if debug: + traceback.print_exc() + print("Failed to connect to possible brick") print("""No brick was found. Is the brick turned on? @@ -146,6 +167,7 @@ def server_brick(host, port = 2727): sock = ipsock.IpSock(host, port) return sock.connect() + def device_brick(filename): from . import devsock sock = devsock.find_bricks(filename=filename) @@ -161,6 +183,7 @@ def read_config(confpath=None, debug=False): conf.add_section('Brick') return conf + def make_config(confpath=None): conf = configparser.RawConfigParser() if not confpath: confpath = os.path.expanduser('~/.nxt-python') diff --git a/nxt/sensor/__init__.py b/nxt/sensor/__init__.py index 9220560..941629a 100644 --- a/nxt/sensor/__init__.py +++ b/nxt/sensor/__init__.py @@ -16,7 +16,7 @@ from .common import * from .analog import BaseAnalogSensor from .digital import BaseDigitalSensor, find_class -from .generic import Touch, Light, Sound, Ultrasonic, Color20 +from .generic import Touch, Light, Sound, Ultrasonic, Color20, Temperature from . import mindsensors MSSumoEyes = mindsensors.SumoEyes MSCompassv2 = mindsensors.Compassv2 diff --git a/nxt/sensor/digital.py b/nxt/sensor/digital.py index 85bbe69..dfabf09 100644 --- a/nxt/sensor/digital.py +++ b/nxt/sensor/digital.py @@ -25,7 +25,7 @@ def __init__(self, version, product_id, sensor_type): self.version = version self.product_id = product_id self.sensor_type = sensor_type - + def clarifybinary(self, instr, label): outstr = '' outstr += (label + ': `' + instr + '`\n') @@ -33,7 +33,7 @@ def clarifybinary(self, instr, label): outstr += (hex(ord(char))+', ') outstr += ('\n') return outstr - + def __str__(self): outstr = '' outstr += (self.clarifybinary(str(self.version), 'Version')) @@ -55,7 +55,7 @@ class BaseDigitalSensor(Sensor): 'factory_scale_factor': (0x12, 'B'), 'factory_scale_divisor': (0x13, 'B'), } - + def __init__(self, brick, port, check_compatible=True): """Creates a BaseDigitalSensor. If check_compatible is True, queries the sensor for its name, and if a wrong sensor class was used, prints @@ -72,8 +72,8 @@ def __init__(self, brick, port, check_compatible=True): if check_compatible: sensor = self.get_sensor_info() if not sensor in self.compatible_sensors: - print(('WARNING: Wrong sensor class chosen for sensor ' + - str(sensor.product_id) + ' on port ' + str(port) + '. ' + """ + print(('WARNING: Wrong sensor class chosen for sensor ' + + str(sensor.product_id) + ' on port ' + str(port + 1) + '. ' + """ You may be using the wrong type of sensor or may have connected the cable incorrectly. If you are sure you're using the correct sensor class for the sensor, this message is likely in error and you should disregard it and file a @@ -95,7 +95,7 @@ def _i2c_command(self, address, value, format): a tuple of values corresponding to the given format. """ value = struct.pack(format, *value) - msg = chr(self.I2C_DEV) + chr(address) + value + msg = bytes((self.I2C_DEV, address)) + value now = time() if self.last_poll+self.poll_delay > now: diff = now - self.last_poll @@ -109,7 +109,7 @@ def _i2c_query(self, address, format): module. See http://docs.python.org/library/struct.html#format-strings """ n_bytes = struct.calcsize(format) - msg = chr(self.I2C_DEV) + chr(address) + msg = bytes((self.I2C_DEV, address)) now = time() if self.last_poll+self.poll_delay > now: diff = now - self.last_poll @@ -125,7 +125,7 @@ def _i2c_query(self, address, format): raise I2CError('Read failure: Not enough bytes') data = struct.unpack(format, data[-n_bytes:]) return data - + def read_value(self, name): """Reads a value from the sensor. Name must be a string found in self.I2C_ADDRESS dictionary. Entries in self.I2C_ADDRESS are in the @@ -150,13 +150,13 @@ def write_value(self, name, value): """ address, fmt = self.I2C_ADDRESS[name] self._i2c_command(address, value, fmt) - + def get_sensor_info(self): version = self.read_value('version')[0].decode('windows-1252').split('\0')[0] product_id = self.read_value('product_id')[0].decode('windows-1252').split('\0')[0] sensor_type = self.read_value('sensor_type')[0].decode('windows-1252').split('\0')[0] return SensorInfo(version, product_id, sensor_type) - + @classmethod def add_compatible_sensor(cls, version, product_id, sensor_type): """Adds an entry in the compatibility table for the sensor. If version @@ -171,8 +171,8 @@ def add_compatible_sensor(cls, version, product_id, sensor_type): cls.compatible_sensors.append(SCompatibility(version, product_id, sensor_type)) add_mapping(cls, version, product_id, sensor_type) - - + + class SCompatibility(SensorInfo): """An object that helps manage the sensor mappings""" def __eq__(self, other): @@ -194,7 +194,7 @@ def add_mapping(cls, version, product_id, sensor_type): if product_id not in sensor_mappings: sensor_mappings[product_id] = {} models = sensor_mappings[product_id] - + if sensor_type is None: if sensor_type in models: raise ValueError('Already registered!') @@ -204,7 +204,7 @@ def add_mapping(cls, version, product_id, sensor_type): if sensor_type not in models: models[sensor_type] = {} versions = models[sensor_type] - + if version in versions: raise ValueError('Already registered!') else: diff --git a/nxt/sensor/generic.py b/nxt/sensor/generic.py index 255d6eb..10b2388 100644 --- a/nxt/sensor/generic.py +++ b/nxt/sensor/generic.py @@ -24,10 +24,10 @@ class Touch(BaseAnalogSensor): def __init__(self, brick, port): super(Touch, self).__init__(brick, port) self.set_input_mode(Type.SWITCH, Mode.BOOLEAN) - + def is_pressed(self): return bool(self.get_input_values().scaled_value) - + get_sample = is_pressed @@ -46,8 +46,8 @@ def set_illuminated(self, active): self.set_input_mode(type_, Mode.RAW) def get_lightness(self): - return self.get_input_values().scaled_value - + return self.get_input_values().scaled_value + get_sample = get_lightness @@ -64,10 +64,10 @@ def set_adjusted(self, active): else: type_ = Type.SOUND_DB self.set_input_mode(type_, Mode.RAW) - + def get_loudness(self): return self.get_input_values().scaled_value - + get_sample = get_loudness @@ -82,7 +82,7 @@ class Ultrasonic(BaseDigitalSensor): 'actual_scale_factor': (0x51, 'B'), 'actual_scale_divisor': (0x52, 'B'), }) - + class Commands: 'These are for passing to command()' OFF = 0x00 @@ -90,7 +90,7 @@ class Commands: CONTINUOUS_MEASUREMENT = 0x02 EVENT_CAPTURE = 0x03 #Optimize results when other Ultrasonic sensors running REQUEST_WARM_RESET = 0x04 - + def __init__(self, brick, port, check_compatible=True): super(Ultrasonic, self).__init__(brick, port, check_compatible) self.set_input_mode(Type.LOW_SPEED_9V, Mode.RAW) @@ -98,16 +98,15 @@ def __init__(self, brick, port, check_compatible=True): def get_distance(self): 'Function to get data from the ultrasonic sensor' return self.read_value('measurement_byte_0')[0] - + get_sample = get_distance - def get_measurement_units(self): return self.read_value('measurement_units')[0].decode('windows-1252').split('\0')[0] def get_all_measurements(self): "Returns all the past readings in measurement_byte_0 through 7" return self.read_value('measurements') - + def get_measurement_no(self, number): "Returns measurement_byte_number" if not 0 <= number < 8: @@ -117,11 +116,11 @@ def get_measurement_no(self, number): def command(self, command): self.write_value('command', (command, )) - + def get_interval(self): 'Get the sample interval for continuous measurement mode -- Unknown units' return self.read_value('continuous_measurement_interval') - + def set_interval(self, interval): """Set the sample interval for continuous measurement mode. Unknown units; default is 1""" @@ -146,9 +145,40 @@ def get_light_color(self): def get_reflected_light(self, color): self.set_light_color(color) return self.get_input_values().scaled_value - + def get_color(self): self.get_reflected_light(Type.COLORFULL) return self.get_input_values().scaled_value - + get_sample = get_color + + +class Temperature(BaseDigitalSensor): + """Object for LEGO MINDSTORMS NXT Temperature sensors""" + # This is actually a TI TMP275 chip: http://www.ti.com/product/tmp275 + I2C_DEV = 0x98 + I2C_ADDRESS = {'raw_value': (0x00, '>h')} + + def __init__(self, brick, port): + # This sensor does not follow the convention of having version/vendor/ + # product at I2C registers 0x00/0x08/0x10, so check_compatible is + # always False + super(Temperature, self).__init__(brick, port, False) + + def _get_raw_value(self): + """Returns raw unscaled value""" + # this is a 12-bit value + return self.read_value('raw_value')[0] >> 4 + + def get_deg_c(self): + """Returns the temperature in degrees C""" + v = self._get_raw_value() + # technically, 16 should be 0x7ff/128 but 16 is close enough + return round(v / 16, 1) + + def get_deg_f(self): + v = self._get_raw_value() + # technically, 16 should be 0x7ff/128 but 16 is close enough + return round(9 / 5 * v / 16 + 32, 1) + + get_sample = get_deg_c diff --git a/nxt/system.py b/nxt/system.py index 2b6ebe2..6f2733a 100644 --- a/nxt/system.py +++ b/nxt/system.py @@ -217,7 +217,7 @@ def get_device_info(opcode): def _parse_get_device_info(tgram): tgram.check_status() - name = tgram.parse_string(15) + name = tgram.parse_string(15).decode('utf-8').split('\0')[0] a0 = tgram.parse_u8() a1 = tgram.parse_u8() a2 = tgram.parse_u8() diff --git a/nxt/telegram.py b/nxt/telegram.py index fd8604f..3d0795f 100644 --- a/nxt/telegram.py +++ b/nxt/telegram.py @@ -64,12 +64,15 @@ def bytes(self): def is_reply(self): return self.typ == Telegram.TYPE_REPLY + def add_bytes(self, b): + self.pkt.write(pack('%ds' % len(b), b)) + def add_string(self, n_bytes, v): self.pkt.write(pack('%ds' % n_bytes, v.encode('windows-1252'))) def add_filename(self, fname): - #self.pkt.write(pack('20s', fname)) - self.pkt.write(pack('20s', fname.encode('windows-1252'))) + self.pkt.write(pack('20s', fname.encode('windows-1252'))) + def add_s8(self, v): self.pkt.write(pack('