diff --git a/README.md b/README.md index 06d04f0..1721d0a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Checkout the libraries listed below for real-world examples. * Classes for describing devices, registers and individual bit fields within registers in a fashion which maps closely with the datasheet * Value translation from real world numbers (such as `512ms`) to register values (such as `0b111`) and back again -* Automatic generation of accessors for every BitField- add a `mode` field and you'll get `get_mode` and `set_mode` methods on your Register. +* Read registers into a namedtuple of fields using `get` +* Write multiple register fields in a transaction using `set` with keyword arguments * Support for treating multiple-bytes as a single value, or single register with multiple values # Built With i2cdevice @@ -71,3 +72,25 @@ ltr559 = Device(I2C_ADDR, bit_width=8, registers=( )) ``` +## Reading Registers + +One configured a register's fields can be read into a namedtuple using the `get` method: + +```python +register_values = ltr559.get('ALS_CONTROL') +gain = register_values.gain +sw_reset = register_values.sw_reset +mode = register_values.mode +``` + +## Writing Registers + +The namedtuple returned from `get` is immutable and does not attempt to map values back to the hardware, in order to write one or more fields to a register you must use `set` with a keyword argument for each field: + +```python +ltr559.set('ALS_CONTROL', + gain=4, + sw_reset=1) +``` + +This will read the register state from the device, update the bitfields accordingly and write the result back. diff --git a/examples/ltr559.py b/examples/ltr559.py index 9865c6b..c7d320b 100644 --- a/examples/ltr559.py +++ b/examples/ltr559.py @@ -1,7 +1,7 @@ import time import sys sys.path.insert(0, "../library/") -from i2cdevice import Device, Register, BitField +from i2cdevice import Device, Register, BitField, MockSMBus from i2cdevice.adapter import Adapter, LookupAdapter, U16ByteSwapAdapter I2C_ADDR = 0x23 @@ -26,7 +26,7 @@ def _decode(self, value): return ((value & 0xFF00) >> 8) | ((value & 0x000F) << 8) -ltr559 = Device(I2C_ADDR, bit_width=8, registers=( +ltr559 = Device(I2C_ADDR, i2c_dev=MockSMBus(0, default_registers={0x86: 0x92}), bit_width=8, registers=( Register('ALS_CONTROL', 0x80, fields=( BitField('gain', 0b00011100, adapter=LookupAdapter({1: 0b000, 2: 0b001, 4: 0b011, 8: 0b011, 48: 0b110, 96: 0b111})), @@ -129,26 +129,28 @@ def _decode(self, value): if __name__ == "__main__": - with ltr559.PART_ID as PART_ID: - assert PART_ID.get_part_number() == 0x09 - assert PART_ID.get_revision() == 0x02 + part_id = ltr559.get('PART_ID') + + assert part_id.part_number == 0x09 + assert part_id.revision == 0x02 print(""" Found LTR-559. Part ID: 0x{:02x} Revision: 0x{:02x} """.format( - ltr559.PART_ID.get_part_number(), - ltr559.PART_ID.get_revision()) - ) + part_id.part_number, + part_id.revision + )) print(""" Soft Reset """) - ltr559.ALS_CONTROL.set_sw_reset(1) + ltr559.set('ALS_CONTROL', sw_reset=1) + ltr559.set('ALS_CONTROL', sw_reset=0) try: while True: - status = ltr559.ALS_CONTROL.get_sw_reset() + status = ltr559.get('ALS_CONTROL').sw_reset print("Status: {}".format(status)) if status == 0: break @@ -157,58 +159,69 @@ def _decode(self, value): pass print("Setting ALS threshold") - # Modifying the fields of this register without a "with" statement will trigger - # two successive read/modify/write operations. Use "with" to optimise these out. - ltr559.ALS_THRESHOLD.set_lower(0x0001) - ltr559.ALS_THRESHOLD.set_upper(0xFFEE) + + # The `set` method can handle writes to multiple register fields + # specify each field value as a keyword argument + ltr559.set('ALS_THRESHOLD', + lower=0x0001, + upper=0xFFEE) + print("{:08x}".format(ltr559.values['ALS_THRESHOLD'])) - with ltr559.ALS_THRESHOLD as ALS_THRESHOLD: - print("LOWER: ", ALS_THRESHOLD.get_lower()) - assert ALS_THRESHOLD.get_lower() == 0x0001 - assert ALS_THRESHOLD.get_upper() == 0xFFEE + + # The `get` method returns register values as an immutable + # namedtuple, and fields will be available as properties + # You must use `set` to write a register. + als_threshold = ltr559.get('ALS_THRESHOLD') + assert als_threshold.lower == 0x0001 + assert als_threshold.upper == 0xFFEE print("Setting PS threshold") - ltr559.PS_THRESHOLD.set_lower(0) - ltr559.PS_THRESHOLD.set_upper(500) + + ltr559.set('PS_THRESHOLD', + lower=0, + upper=500) + print("{:08x}".format(ltr559.values['PS_THRESHOLD'])) - with ltr559.PS_THRESHOLD as PS_THRESHOLD: - assert PS_THRESHOLD.get_lower() == 0 - assert PS_THRESHOLD.get_upper() == 500 + + ps_threshold = ltr559.get('PS_THRESHOLD') + assert ps_threshold.lower == 0 + assert ps_threshold.upper == 500 print("Setting integration time and repeat rate") - ltr559.PS_MEAS_RATE.set_rate_ms(100) - ltr559.ALS_MEAS_RATE.set_integration_time_ms(50) - ltr559.ALS_MEAS_RATE.set_repeat_rate_ms(50) - with ltr559.ALS_MEAS_RATE as ALS_MEAS_RATE: - assert ALS_MEAS_RATE.get_integration_time_ms() == 50 - assert ALS_MEAS_RATE.get_repeat_rate_ms() == 50 + ltr559.set('PS_MEAS_RATE', rate_ms=100) + ltr559.set('ALS_MEAS_RATE', + integration_time_ms=50, + repeat_rate_ms=50) + + als_meas_rate = ltr559.get('ALS_MEAS_RATE') + assert als_meas_rate.integration_time_ms == 50 + assert als_meas_rate.repeat_rate_ms == 50 print(""" Activating sensor """) - ltr559.INTERRUPT.set_mode('als+ps') - ltr559.PS_CONTROL.set_active(True) - ltr559.PS_CONTROL.set_saturation_indicator_enable(1) + ltr559.set('INTERRUPT', mode='als+ps') + ltr559.set('PS_CONTROL', + active=True, + saturation_indicator_enable=1) - with ltr559.PS_LED as PS_LED: - PS_LED.set_current_ma(50) - PS_LED.set_duty_cycle(1.0) - PS_LED.set_pulse_freq_khz(30) - PS_LED.write() # *MUST* be called to write the value when in context mode + ltr559.set('PS_LED', + current_ma=50, + duty_cycle=1.0, + pulse_freq_khz=30) - ltr559.PS_N_PULSES.set_count(1) + ltr559.set('PS_N_PULSES', count=1) - with ltr559.ALS_CONTROL as ALS_CONTROL: - ALS_CONTROL.set_mode(1) - ALS_CONTROL.set_gain(4) - ALS_CONTROL.write() + ltr559.set('ALS_CONTROL', + mode=1, + gain=4) - with ltr559.ALS_CONTROL as ALS_CONTROL: - assert ALS_CONTROL.get_mode() == 1 - assert ALS_CONTROL.get_gain() == 4 + als_control = ltr559.get('ALS_CONTROL') + assert als_control.mode == 1 + assert als_control.gain == 4 - ltr559.PS_OFFSET.set_offset(69) + ltr559.set('PS_OFFSET', offset=69) als0 = 0 als1 = 0 @@ -220,21 +233,17 @@ def _decode(self, value): try: while True: - # By default any read from a register field will trigger a read from hardware - # and any write to a register field will trigger a write to hardware. - # Using the "with" statement overrides this behavior by locking the register - # value during the with context so that its value is only read once on context entry. - with ltr559.ALS_PS_STATUS as ALS_PS_STATUS: - ps_int = ALS_PS_STATUS.get_ps_interrupt() or ALS_PS_STATUS.get_ps_data() - als_int = ALS_PS_STATUS.get_als_interrupt() or ALS_PS_STATUS.get_als_data() + als_ps_status = ltr559.get('ALS_PS_STATUS') + ps_int = als_ps_status.ps_interrupt or als_ps_status.ps_data + als_int = als_ps_status.als_interrupt or als_ps_status.als_data if ps_int: - ps0 = ltr559.PS_DATA.get_ch0() + ps0 = ltr559.get('PS_DATA').ch0 if als_int: - with ltr559.ALS_DATA as ALS_DATA: - als0 = ALS_DATA.get_ch0() - als1 = ALS_DATA.get_ch1() + als_data = ltr559.get('ALS_DATA') + als0 = als_data.ch0 + als1 = als_data.ch1 ratio = 1000 if als0 + als0 > 0: diff --git a/library/CHANGELOG.txt b/library/CHANGELOG.txt index 672a658..e885a33 100644 --- a/library/CHANGELOG.txt +++ b/library/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.0.6 +----- + +* New API methods set and get + 0.0.5 ----- diff --git a/library/README.rst b/library/README.rst index 2cf14b9..b2f1638 100644 --- a/library/README.rst +++ b/library/README.rst @@ -33,9 +33,9 @@ Features within registers in a fashion which maps closely with the datasheet - Value translation from real world numbers (such as ``512ms``) to register values (such as ``0b111``) and back again -- Automatic generation of accessors for every BitField- add a ``mode`` - field and you'll get ``get_mode`` and ``set_mode`` methods on your - Register. +- Read registers into a namedtuple of fields using ``get`` +- Write multiple register fields in a transaction using ``set`` with + keyword arguments - Support for treating multiple-bytes as a single value, or single register with multiple values @@ -94,6 +94,36 @@ keyword argument: ALS_DATA )) +Reading Registers +----------------- + +One configured a register's fields can be read into a namedtuple using +the ``get`` method: + +.. code:: python + + register_values = ltr559.get('ALS_CONTROL') + gain = register_values.gain + sw_reset = register_values.sw_reset + mode = register_values.mode + +Writing Registers +----------------- + +The namedtuple returned from ``get`` is immutable and does not attempt +to map values back to the hardware, in order to write one or more fields +to a register you must use ``set`` with a keyword argument for each +field: + +.. code:: python + + ltr559.set('ALS_CONTROL', + gain=4, + sw_reset=1) + +This will read the register state from the device, update the bitfields +accordingly and write the result back. + .. |Build Status| image:: https://travis-ci.com/pimoroni/i2cdevice-python.svg?branch=master :target: https://travis-ci.com/pimoroni/i2cdevice-python .. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/i2cdevice-python/badge.svg?branch=master diff --git a/library/i2cdevice/__init__.py b/library/i2cdevice/__init__.py index 00db495..a040081 100644 --- a/library/i2cdevice/__init__.py +++ b/library/i2cdevice/__init__.py @@ -1,4 +1,7 @@ -__version__ = '0.0.5' +from collections import namedtuple + +__version__ = '0.0.6' + def _mask_width(value, bit_width=8): """Get the width of a bitwise mask @@ -56,8 +59,11 @@ def _int_to_bytes(value, length, endianness='big'): class MockSMBus: - def __init__(self, i2c_bus): + def __init__(self, i2c_bus, default_registers=None): self.regs = [0 for _ in range(255)] + if default_registers is not None: + for index in default_registers.keys(): + self.regs[index] = default_registers.get(index) def write_i2c_block_data(self, i2c_address, register, values): self.regs[register:register + len(values)] = values @@ -79,7 +85,6 @@ class _RegisterProxy(object): """ def __init__(self, device, register): - object.__init__(self) self.device = device self.register = register @@ -115,11 +120,14 @@ def __init__(self, name, address, fields=None, bit_width=8, read_only=False, vol self.bit_width = bit_width self.read_only = read_only self.volatile = volatile + self.is_read = False self.fields = {} for field in fields: self.fields[field.name] = field + self.namedtuple = namedtuple(self.name, sorted(self.fields)) + class BitField(): """Store information about a field or flag in an i2c register""" @@ -171,7 +179,9 @@ def unlock_register(self, name): def read_register(self, name): register = self.registers[name] - self.values[register.name] = self._i2c_read(register.address, register.bit_width) + if register.volatile or not register.is_read: + self.values[register.name] = self._i2c_read(register.address, register.bit_width) + register.is_read = True return self.values[register.name] def write_register(self, name): @@ -194,6 +204,34 @@ def next_address(self): self._i2c_address = self._i2c_addresses[next_addr] return self._i2c_address + def set(self, register, **kwargs): + """Write one or more fields on a device register. + + Accepts multiple keyword arguments, one for each field to write. + + :param register: Name of register to write. + + """ + self.read_register(register) + self.lock_register(register) + for field in kwargs.keys(): + value = kwargs.get(field) + self.set_field(register, field, value) + self.write_register(register) + self.unlock_register(register) + + def get(self, register): + """Get a namedtuple containing register fields. + + :param register: Name of register to retrieve + + """ + result = {} + self.read_register(register) + for field in self.registers[register].fields: + result[field] = self.get_field(register, field) + return self.registers[register].namedtuple(**result) + def get_field(self, register, field): register = self.registers[register] field = register.fields[field] diff --git a/library/i2cdevice/adapter.py b/library/i2cdevice/adapter.py index 394dc28..e94c85b 100644 --- a/library/i2cdevice/adapter.py +++ b/library/i2cdevice/adapter.py @@ -20,8 +20,10 @@ def __init__(self, lookup_table, snap=True): self.snap = snap def _decode(self, value): - index = list(self.lookup_table.values()).index(value) - return list(self.lookup_table.keys())[index] + for k, v in self.lookup_table.items(): + if v == value: + return k + raise ValueError("{} not in lookup table".format(value)) def _encode(self, value): if self.snap and type(value) in [int, float]: diff --git a/library/setup.py b/library/setup.py index 6e33737..0318c4f 100644 --- a/library/setup.py +++ b/library/setup.py @@ -39,7 +39,7 @@ setup( name='i2cdevice', - version='0.0.5', + version='0.0.6', author='Philip Howard', author_email='phil@pimoroni.com', description="""Python DSL for interacting with SMBus-compatible i2c devices""", diff --git a/library/tests/test_mocksmbus.py b/library/tests/test_mocksmbus.py index 7c61396..7848da0 100644 --- a/library/tests/test_mocksmbus.py +++ b/library/tests/test_mocksmbus.py @@ -5,3 +5,9 @@ def test_smbus_io(): bus = MockSMBus(1) bus.write_i2c_block_data(0x00, 0x00, [0xff, 0x00, 0xff]) assert bus.read_i2c_block_data(0x00, 0x00, 3) == [0xff, 0x00, 0xff] + + +def test_smbus_default_regs(): + bus = MockSMBus(1, default_registers={0x60: 0x99, 0x88: 0x51}) + assert bus.read_i2c_block_data(0x00, 0x60, 1) == [0x99] + assert bus.read_i2c_block_data(0x00, 0x88, 1) == [0x51] diff --git a/library/tests/test_registerproxy.py b/library/tests/test_registerproxy.py index a2d4af1..d992a74 100644 --- a/library/tests/test_registerproxy.py +++ b/library/tests/test_registerproxy.py @@ -2,6 +2,7 @@ def test_register_proxy(): + """This API pattern has been depricated in favour of set/get.""" bus = MockSMBus(1) device = Device(0x00, i2c_dev=bus, registers=( Register('test', 0x00, fields=( diff --git a/library/tests/test_set_and_get.py b/library/tests/test_set_and_get.py new file mode 100644 index 0000000..4cd9bab --- /dev/null +++ b/library/tests/test_set_and_get.py @@ -0,0 +1,33 @@ +from i2cdevice import MockSMBus, Device, Register, BitField + + +def test_set_regs(): + bus = MockSMBus(1) + device = Device(0x00, i2c_dev=bus, registers=( + Register('test', 0x00, fields=( + BitField('test', 0xFF), + )), + )) + device.set('test', test=123) + + assert device.get('test').test == 123 + + assert bus.regs[0] == 123 + + +def test_get_regs(): + bus = MockSMBus(1) + device = Device(0x00, i2c_dev=bus, registers=( + Register('test', 0x00, fields=( + BitField('test', 0xFF00), + BitField('monkey', 0x00FF), + ), bit_width=16), + )) + device.set('test', test=0x66, monkey=0x77) + + reg = device.get('test') + reg.test == 0x66 + reg.monkey == 0x77 + + assert bus.regs[0] == 0x66 + assert bus.regs[1] == 0x77 diff --git a/library/tox.ini b/library/tox.ini index aa96216..4c42212 100644 --- a/library/tox.ini +++ b/library/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35},qa +envlist = py{27,35,36},qa skip_missing_interpreters = True [testenv] @@ -22,3 +22,4 @@ deps = check-manifest flake8 rstcheck + pygments