/// <summary>
        /// Sets RGB values of the high intensity LED together (in one operation).
        /// </summary>
        /// <remarks>
        /// Automatically inverts the value, providing a natural behaviour where a higher number
        /// produces a higher LED intensity. Normally, when written as raw PWM values, the output
        /// is inverted due to common anode.
        /// </remarks>
        /// <param name="red">Red value in the range 0-<see cref="NxpPca9685ChannelValue.Maximum"/>.</param>
        /// <param name="green">Green value in the range 0-<see cref="NxpPca9685ChannelValue.Maximum"/>.</param>
        /// <param name="blue">Blue value in the range 0-<see cref="NxpPca9685ChannelValue.Maximum"/>.</param>
        public void SetLed(int red, int green, int blue)
        {
            // Validate
            if (red < 0 || red > NxpPca9685ChannelValue.Maximum)
            {
                throw new ArgumentOutOfRangeException(nameof(red));
            }
            if (green < 0 || green > NxpPca9685ChannelValue.Maximum)
            {
                throw new ArgumentOutOfRangeException(nameof(green));
            }
            if (blue < 0 || blue > NxpPca9685ChannelValue.Maximum)
            {
                throw new ArgumentOutOfRangeException(nameof(blue));
            }

            // Invert values
            var inverseRed   = ~red & 0x0fff;
            var inverseGreen = ~green & 0x0fff;
            var inverseBlue  = ~blue & 0x0fff;

            // Build block of data which sets all three register high and low values
            var redBytes   = NxpPca9685ChannelValue.FromLength(inverseRed).ToByteArray();
            var greenBytes = NxpPca9685ChannelValue.FromLength(inverseGreen).ToByteArray();
            var blueBytes  = NxpPca9685ChannelValue.FromLength(inverseBlue).ToByteArray();
            var data       = new byte[1 + ChannelSize * 3];

            data[0] = ChannelStartAddress;
            Array.ConstrainedCopy(blueBytes, 0, data, 1, ChannelSize);
            Array.ConstrainedCopy(greenBytes, 0, data, 1 + ChannelSize, ChannelSize);
            Array.ConstrainedCopy(redBytes, 0, data, 1 + ChannelSize * 2, ChannelSize);

            // Set new value
            Hardware.Write(data);

            // Update properties
            ReadLed();
        }
 /// <summary>
 /// Creates an instance with the specified values.
 /// </summary>
 public NxpPca9685Channel(int index, NxpPca9685ChannelValue value)
 {
     Index = index;
     Value = value;
     Value.Changed += OnValueChanged;
 }
        /// <summary>
        /// Writes the "on" and "off" values of a channel together, and updates it in <see cref="Channels"/>.
        /// </summary>
        /// <param name="index">Zero based channel number (0-15) or 16 for the "all call" channel.</param>
        /// <param name="value"><see cref="NxpPca9685ChannelValue"/> to write.</param>
        /// <returns>
        /// Updated channel value or null when all channels were updated.
        /// </returns>
        public NxpPca9685Channel WriteChannel(int index, NxpPca9685ChannelValue value)
        {
            // Validate
            if (index < 0 | index > ChannelCount) throw new ArgumentOutOfRangeException(nameof(index));

            // Validate
            if (value.On > NxpPca9685ChannelValue.Maximum + 1 ||
            value.Off > NxpPca9685ChannelValue.Maximum + 1)
                throw new ArgumentOutOfRangeException(nameof(value));

            // Calculate register address
            var register = GetChannelAddress(index);

            // Convert and write value
            var bytes = value.ToByteArray();
            Hardware.WriteBytes(register, bytes);

            // Read and return result
            if (index < ChannelCount)
                return ReadChannel(index);
            ReadAllChannels();
            return null;
        }