In the previous section we saw how to directly convert the VL6180X Arduino library into a version that works as a CircuitPython module. You can follow those steps to get most libraries ported over and working with Python code. However you might want to go further and simplify the driver a bit so it's more friendly and easy to use with Python code. In this section we'll go through a few small simplifications that make using the driver easier.
First remember the CircuitPython driver code:
# The MIT License (MIT) # # Copyright (c) 2017 Tony DiCola for Adafruit Industries # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ `adafruit_vl6180x` ==================================================== This is a CircuitPython driver for the VL6180X time-of-flight distance sensor. * Author(s): Tony DiCola """ from adafruit_bus_device.i2c_device import I2CDevice VL6180X_DEFAULT_I2C_ADDR = const(0x29) VL6180X_REG_IDENTIFICATION_MODEL_ID = const(0x000) VL6180X_REG_SYSTEM_INTERRUPT_CONFIG = const(0x014) VL6180X_REG_SYSTEM_INTERRUPT_CLEAR = const(0x015) VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET = const(0x016) VL6180X_REG_SYSRANGE_START = const(0x018) VL6180X_REG_SYSALS_START = const(0x038) VL6180X_REG_SYSALS_ANALOGUE_GAIN = const(0x03F) VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI = const(0x040) VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO = const(0x041) VL6180X_REG_RESULT_ALS_VAL = const(0x050) VL6180X_REG_RESULT_RANGE_VAL = const(0x062) VL6180X_REG_RESULT_RANGE_STATUS = const(0x04d) VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO = const(0x04f) VL6180X_ALS_GAIN_1 = const(0x06) VL6180X_ALS_GAIN_1_25 = const(0x05) VL6180X_ALS_GAIN_1_67 = const(0x04) VL6180X_ALS_GAIN_2_5 = const(0x03) VL6180X_ALS_GAIN_5 = const(0x02) VL6180X_ALS_GAIN_10 = const(0x01) VL6180X_ALS_GAIN_20 = const(0x00) VL6180X_ALS_GAIN_40 = const(0x07) VL6180X_ERROR_NONE = const(0) VL6180X_ERROR_SYSERR_1 = const(1) VL6180X_ERROR_SYSERR_5 = const(5) VL6180X_ERROR_ECEFAIL = const(6) VL6180X_ERROR_NOCONVERGE = const(7) VL6180X_ERROR_RANGEIGNORE = const(8) VL6180X_ERROR_SNR = const(11) VL6180X_ERROR_RAWUFLOW = const(12) VL6180X_ERROR_RAWOFLOW = const(13) VL6180X_ERROR_RANGEUFLOW = const(14) VL6180X_ERROR_RANGEOFLOW = const(15) class Adafruit_VL6180X: def __init__(self, i2c, address=VL6180X_DEFAULT_I2C_ADDR): self._device = I2CDevice(i2c, address) def begin(self): """Initialize access to the sensor. Returns True if successful.""" if self._read_8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4: return False self._load_settings() self._write_8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00) return True def read_range(self): """Read the range of an object in front of sensor and return it in mm.""" # wait for device to be ready for range measurement while not (self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01): pass # Start a range measurement self._write_8(VL6180X_REG_SYSRANGE_START, 0x01); # Poll until bit 2 is set while not (self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04): pass # read range in mm range_ = self._read_8(VL6180X_REG_RESULT_RANGE_VAL) # clear interrupt self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) return range_ def read_lux(self, gain): """Read the lux (light value) from the sensor and return it.""" reg = self._read_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG) reg &= ~0x38 reg |= (0x4 << 3) # IRQ on ALS ready self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg) # 100 ms integration period self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0) self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100) # analog gain if gain > VL6180X_ALS_GAIN_40: gain = VL6180X_ALS_GAIN_40 self._write_8(VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain) # start ALS self._write_8(VL6180X_REG_SYSALS_START, 0x1) # Poll until "New Sample Ready threshold event" is set while 4 != ((self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7): pass # read lux! lux = self._read_16(VL6180X_REG_RESULT_ALS_VAL) # clear interrupt self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07); lux *= 0.32 # calibrated count/lux if gain == VL6180X_ALS_GAIN_1: pass elif gain == VL6180X_ALS_GAIN_1_25: lux /= 1.25 elif gain == VL6180X_ALS_GAIN_1_67: lux /= 1.76 elif gain == VL6180X_ALS_GAIN_2_5: lux /= 2.5 elif gain == VL6180X_ALS_GAIN_5: lux /= 5 elif gain == VL6180X_ALS_GAIN_10: lux /= 10 elif gain == VL6180X_ALS_GAIN_20: lux /= 20 elif gain == VL6180X_ALS_GAIN_40: lux /= 20 lux *= 100 lux /= 100 # integration time in ms return lux def read_range_status(self): """Retrieve the status/error from a previous range read.""" return self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4 def _load_settings(self): # private settings from page 24 of app note self._write_8(0x0207, 0x01) self._write_8(0x0208, 0x01) self._write_8(0x0096, 0x00) self._write_8(0x0097, 0xfd) self._write_8(0x00e3, 0x00) self._write_8(0x00e4, 0x04) self._write_8(0x00e5, 0x02) self._write_8(0x00e6, 0x01) self._write_8(0x00e7, 0x03) self._write_8(0x00f5, 0x02) self._write_8(0x00d9, 0x05) self._write_8(0x00db, 0xce) self._write_8(0x00dc, 0x03) self._write_8(0x00dd, 0xf8) self._write_8(0x009f, 0x00) self._write_8(0x00a3, 0x3c) self._write_8(0x00b7, 0x00) self._write_8(0x00bb, 0x3c) self._write_8(0x00b2, 0x09) self._write_8(0x00ca, 0x09) self._write_8(0x0198, 0x01) self._write_8(0x01b0, 0x17) self._write_8(0x01ad, 0x00) self._write_8(0x00ff, 0x05) self._write_8(0x0100, 0x05) self._write_8(0x0199, 0x05) self._write_8(0x01a6, 0x1b) self._write_8(0x01ac, 0x3e) self._write_8(0x01a7, 0x1f) self._write_8(0x0030, 0x00) # Recommended : Public registers - See data sheet for more detail self._write_8(0x0011, 0x10) # Enables polling for 'New Sample ready' # when measurement completes self._write_8(0x010a, 0x30) # Set the averaging sample period # (compromise between lower noise and # increased execution time) self._write_8(0x003f, 0x46) # Sets the light and dark gain (upper # nibble). Dark gain should not be # changed. self._write_8(0x0031, 0xFF) # sets the # of range measurements after # which auto calibration of system is # performed self._write_8(0x0040, 0x63) # Set ALS integration time to 100ms self._write_8(0x002e, 0x01) # perform a single temperature calibration # of the ranging sensor # Optional: Public registers - See data sheet for more detail self._write_8(0x001b, 0x09) # Set default ranging inter-measurement # period to 100ms self._write_8(0x003e, 0x31) # Set default ALS inter-measurement period # to 500ms self._write_8(0x0014, 0x24) # Configures interrupt on 'New Sample # Ready threshold event' def _write_8(self, address, data): # Write 1 byte of data from the specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF, data])) def _write_16(self, address, data): # Write a 16-bit big endian value to the specified 16-bit register # address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF, (data >> 8) & 0xFF, data & 0xFF])) def _read_8(self, address): # Read and return a byte from the specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF]), stop=False) result = bytearray(1) self._device.read_into(result) return result[0] def _read_16(self, address): # Read and return a 16-bit unsigned big endian value read from the # specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF]), stop=False) result = bytearray(2) self._device.read_into(result) return (result[0] << 8) | result[1]
Initializer Simplification
One easy thing you can do to simplify the Python driver is combine the initializer (the __init__ function in the class) and the begin function. In Arduino begin is typically used because constructors (the equivalent of an initializer in Arduino/C++) can be problematic when they talk to hardware and might fail in some way. If a failure occurs in a constructor it's hard to respond with an error--typically you need to throw an exception, but some Arduino or C++ code might not handle the exception well!
Luckily in Python it's easy to manage and catch exceptions so throwing them in an initializer is a good option. This means we can combine both the begin and __init__ function:
def __init__(self, i2c, address=VL6180X_DEFAULT_I2C_ADDR): self._device = I2CDevice(i2c, address) if self._read_8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4: raise RuntimeError('Could not find VL6180X, is it connected and powered?') self._load_settings() self._write_8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00)
Notice the initializer will now both create the I2CDevice (the core of communication with the sensor) and now do a few checks to verify the chip is present and load default settings. If an error occurs, like when verifying the chip model ID register value, now an exception is thrown back to the user. This way when the user's Python code creates the VL6180X instance it will immediately see an exception thrown if there's a problem communicating with the chip. More importantly though the user doesn't need to call begin anymore--there's one less function to learn about and call!
Properties
Properties in Python are functions on a class that you interact with as if they were attributes--that is to say instead of using parenthesis to tell Python you're calling code, you just reference the property by name and Python knows to call some code internally. Another great simplification for your driver is to look where you can turn functions into properties, both to read and write data.
For example the read_range function looks like this right now when a user calls it:
>>> vl6180x.read_range() 163
However as a property you can expose the range like this:
>>> vl6180x.range 163
Notice there's no need to call the code as a function, instead you just refer to the range property on the object.
If you haven't used them be sure to read more about properties to understand how and why to use them.
For this driver we can convert a couple functions into properties, specifically read_range and read_range_status. For the lux value it's a little trickier because it needs to be told what gain to use as a parameter. Properties aren't like functions and can't be given parameters, so the lux function is a good case where keeping it as a function that takes the necessary gain parameter is the best option.
For read_range change its function to look like this to expose it as a property:
@property def range(self): """Read the range of an object in front of sensor and return it in mm.""" # wait for device to be ready for range measurement while not (self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01): pass # Start a range measurement self._write_8(VL6180X_REG_SYSRANGE_START, 0x01); # Poll until bit 2 is set while not (self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04): pass # read range in mm range_ = self._read_8(VL6180X_REG_RESULT_RANGE_VAL) # clear interrupt self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) return range_
Notice none of the code inside the function changed, only the definition. The name was changed from read_range to just range--generally you don't want verbs in properties whereas they can be handy in functions. More importantly the @property decorator was added before the function definition, this is the key that tells Python to expose the function as a property.
You can convert the read_range_status in the same way:
@property def range_status(self): """Retrieve the status/error from a previous range read.""" return self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4
Again only the function name and @property decorator are changed--the code inside the property/function doesn't need to change.
Now try copying the updated adafruit_vl6180x.py to your board and creating an instance of it:
import board import busio i2c = busio.I2C(board.SCL, board.SDA) import adafruit_vl6180x vl6180x = adafruit_vl6180x.Adafruit_VL6180X(i2c) vl6180x.range vl6180x.range_status
You should see the properties work just like the functions did before. However the code is a bit simpler and easier since you don't need to add the parenthesis like when calling a function.
Although it's not shown here you can also add a setter to a property which enables you to write data using property syntax. See this page about Python properties for more details on creating a setter for a property. Typically if you have an attribute that can be both read and written to (like a configuration value or register) you'll want to expose it as a property with both a getter and setter.
As a review here's the final simplified driver file:
# The MIT License (MIT) # # Copyright (c) 2017 Tony DiCola for Adafruit Industries # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ `adafruit_vl6180x` ==================================================== This is a CircuitPython driver for the VL6180X time-of-flight distance sensor. * Author(s): Tony DiCola """ from adafruit_bus_device.i2c_device import I2CDevice VL6180X_DEFAULT_I2C_ADDR = const(0x29) VL6180X_REG_IDENTIFICATION_MODEL_ID = const(0x000) VL6180X_REG_SYSTEM_INTERRUPT_CONFIG = const(0x014) VL6180X_REG_SYSTEM_INTERRUPT_CLEAR = const(0x015) VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET = const(0x016) VL6180X_REG_SYSRANGE_START = const(0x018) VL6180X_REG_SYSALS_START = const(0x038) VL6180X_REG_SYSALS_ANALOGUE_GAIN = const(0x03F) VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI = const(0x040) VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO = const(0x041) VL6180X_REG_RESULT_ALS_VAL = const(0x050) VL6180X_REG_RESULT_RANGE_VAL = const(0x062) VL6180X_REG_RESULT_RANGE_STATUS = const(0x04d) VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO = const(0x04f) VL6180X_ALS_GAIN_1 = const(0x06) VL6180X_ALS_GAIN_1_25 = const(0x05) VL6180X_ALS_GAIN_1_67 = const(0x04) VL6180X_ALS_GAIN_2_5 = const(0x03) VL6180X_ALS_GAIN_5 = const(0x02) VL6180X_ALS_GAIN_10 = const(0x01) VL6180X_ALS_GAIN_20 = const(0x00) VL6180X_ALS_GAIN_40 = const(0x07) VL6180X_ERROR_NONE = const(0) VL6180X_ERROR_SYSERR_1 = const(1) VL6180X_ERROR_SYSERR_5 = const(5) VL6180X_ERROR_ECEFAIL = const(6) VL6180X_ERROR_NOCONVERGE = const(7) VL6180X_ERROR_RANGEIGNORE = const(8) VL6180X_ERROR_SNR = const(11) VL6180X_ERROR_RAWUFLOW = const(12) VL6180X_ERROR_RAWOFLOW = const(13) VL6180X_ERROR_RANGEUFLOW = const(14) VL6180X_ERROR_RANGEOFLOW = const(15) class Adafruit_VL6180X: def __init__(self, i2c, address=VL6180X_DEFAULT_I2C_ADDR): self._device = I2CDevice(i2c, address) if self._read_8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4: raise RuntimeError('Could not find VL6180X, is it connected and powered?') self._load_settings() self._write_8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00) @property def range(self): """Read the range of an object in front of sensor and return it in mm.""" # wait for device to be ready for range measurement while not (self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01): pass # Start a range measurement self._write_8(VL6180X_REG_SYSRANGE_START, 0x01); # Poll until bit 2 is set while not (self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04): pass # read range in mm range_ = self._read_8(VL6180X_REG_RESULT_RANGE_VAL) # clear interrupt self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) return range_ def read_lux(self, gain): """Read the lux (light value) from the sensor and return it.""" reg = self._read_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG) reg &= ~0x38 reg |= (0x4 << 3) # IRQ on ALS ready self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg) # 100 ms integration period self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0) self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100) # analog gain if gain > VL6180X_ALS_GAIN_40: gain = VL6180X_ALS_GAIN_40 self._write_8(VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain) # start ALS self._write_8(VL6180X_REG_SYSALS_START, 0x1) # Poll until "New Sample Ready threshold event" is set while 4 != ((self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7): pass # read lux! lux = self._read_16(VL6180X_REG_RESULT_ALS_VAL) # clear interrupt self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07); lux *= 0.32 # calibrated count/lux if gain == VL6180X_ALS_GAIN_1: pass elif gain == VL6180X_ALS_GAIN_1_25: lux /= 1.25 elif gain == VL6180X_ALS_GAIN_1_67: lux /= 1.76 elif gain == VL6180X_ALS_GAIN_2_5: lux /= 2.5 elif gain == VL6180X_ALS_GAIN_5: lux /= 5 elif gain == VL6180X_ALS_GAIN_10: lux /= 10 elif gain == VL6180X_ALS_GAIN_20: lux /= 20 elif gain == VL6180X_ALS_GAIN_40: lux /= 20 lux *= 100 lux /= 100 # integration time in ms return lux @property def range_status(self): """Retrieve the status/error from a previous range read.""" return self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4 def _load_settings(self): # private settings from page 24 of app note self._write_8(0x0207, 0x01) self._write_8(0x0208, 0x01) self._write_8(0x0096, 0x00) self._write_8(0x0097, 0xfd) self._write_8(0x00e3, 0x00) self._write_8(0x00e4, 0x04) self._write_8(0x00e5, 0x02) self._write_8(0x00e6, 0x01) self._write_8(0x00e7, 0x03) self._write_8(0x00f5, 0x02) self._write_8(0x00d9, 0x05) self._write_8(0x00db, 0xce) self._write_8(0x00dc, 0x03) self._write_8(0x00dd, 0xf8) self._write_8(0x009f, 0x00) self._write_8(0x00a3, 0x3c) self._write_8(0x00b7, 0x00) self._write_8(0x00bb, 0x3c) self._write_8(0x00b2, 0x09) self._write_8(0x00ca, 0x09) self._write_8(0x0198, 0x01) self._write_8(0x01b0, 0x17) self._write_8(0x01ad, 0x00) self._write_8(0x00ff, 0x05) self._write_8(0x0100, 0x05) self._write_8(0x0199, 0x05) self._write_8(0x01a6, 0x1b) self._write_8(0x01ac, 0x3e) self._write_8(0x01a7, 0x1f) self._write_8(0x0030, 0x00) # Recommended : Public registers - See data sheet for more detail self._write_8(0x0011, 0x10) # Enables polling for 'New Sample ready' # when measurement completes self._write_8(0x010a, 0x30) # Set the averaging sample period # (compromise between lower noise and # increased execution time) self._write_8(0x003f, 0x46) # Sets the light and dark gain (upper # nibble). Dark gain should not be # changed. self._write_8(0x0031, 0xFF) # sets the # of range measurements after # which auto calibration of system is # performed self._write_8(0x0040, 0x63) # Set ALS integration time to 100ms self._write_8(0x002e, 0x01) # perform a single temperature calibration # of the ranging sensor # Optional: Public registers - See data sheet for more detail self._write_8(0x001b, 0x09) # Set default ranging inter-measurement # period to 100ms self._write_8(0x003e, 0x31) # Set default ALS inter-measurement period # to 500ms self._write_8(0x0014, 0x24) # Configures interrupt on 'New Sample # Ready threshold event' def _write_8(self, address, data): # Write 1 byte of data from the specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF, data])) def _write_16(self, address, data): # Write a 16-bit big endian value to the specified 16-bit register # address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF, (data >> 8) & 0xFF, data & 0xFF])) def _read_8(self, address): # Read and return a byte from the specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF]), stop=False) result = bytearray(1) self._device.read_into(result) return result[0] def _read_16(self, address): # Read and return a 16-bit unsigned big endian value read from the # specified 16-bit register address. with self._device: self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF]), stop=False) result = bytearray(2) self._device.read_into(result) return (result[0] << 8) | result[1]
With these small changes to simplify the begin & initializer, and switch some functions to properties the users of the driver will need to write less code--awesome!
That's all there is to the basics of porting and simplifying an Arduino driver to work with Python and CircuitPython. Continue on for some helpful tips that you should consider if your port a driver of your own.
Text editor powered by tinymce.