Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add binding for IS31FL3730 LED driver #1930

Merged
merged 48 commits into from Feb 12, 2023
Merged

Conversation

richlander
Copy link
Member

@richlander richlander commented Sep 22, 2022

@ghost ghost added the area-device-bindings Device Bindings for audio, sensor, motor, and display hardware that can used with System.Device.Gpio label Sep 22, 2022
Copy link
Member

@gfoidl gfoidl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the draft, but hope you're OK with early feedback.

src/devices/Is31fl3730/Bonnet5x7x6.cs Outdated Show resolved Hide resolved
src/devices/Is31fl3730/Bonnet5x7x6.cs Outdated Show resolved Hide resolved

if (CurrentSetting > 0)
{
_i2cDevice.Write(new byte[] { LIGHTING_EFFECT_REGISTER, (byte)CurrentSetting });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I2CDevice has an overload that takes a ROS

public abstract void Write(ReadOnlySpan<byte> buffer);
, so this temp. allocation of the byte-array can be avoided.

Suggested change
_i2cDevice.Write(new byte[] { LIGHTING_EFFECT_REGISTER, (byte)CurrentSetting });
_i2cDevice.Write(stackalloc byte[] { LIGHTING_EFFECT_REGISTER, (byte)CurrentSetting });

Same below.

Comment on lines 161 to 168
if (value > 0)
{
buffer.AsSpan().Fill(255);
}
else
{
buffer.AsSpan().Fill(0);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (value > 0)
{
buffer.AsSpan().Fill(255);
}
else
{
buffer.AsSpan().Fill(0);
}
buffer.AsSpan().Fill(value > 0 ? 255 : 0);

More a matter of taste, but IL-size is also smaller (which I don't believe is a criteria here).

Or

Suggested change
if (value > 0)
{
buffer.AsSpan().Fill(255);
}
else
{
buffer.AsSpan().Fill(0);
}
if (value > 0)
{
buffer.AsSpan().Fill(255);
}
else
{
buffer.AsSpan().Clear();
}

Clear is (in general) better than Fill(0) (better optimization in the implementation).

Copy link
Member

@krwq krwq Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just FYI we prefer cleaner code to microoptimizations in this code-base except where it really matters. Reducing allocations - yes, microoptimization - not quite (except maybe direct GPIO operations). Here I2C transmission itself will likely take more than 99% of the time and this won't make any observable difference. I'd rather write this code as:

buffer.AsSpan().Fill(value > 0 ? 255 : 0);

Comment on lines 286 to 292
private void Write(byte address, byte[] value)
{
byte[] data = new byte[value.Length + 1];
data[0] = address;
value.CopyTo(data.AsSpan(1));
_i2cDevice.Write(data);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private void Write(byte address, byte[] value)
{
byte[] data = new byte[value.Length + 1];
data[0] = address;
value.CopyTo(data.AsSpan(1));
_i2cDevice.Write(data);
}
private void Write(byte address, ReadOnlySpan<byte> value)
{
Debug.Assert(value.Length < 256);
Span<byte> data = stackalloc byte[value.Length + ];
data[0] = address;
value.CopyTo(data.Slice(1));
_i2cDevice.Write(data);
}

Avoids the temp. allocation.


private void Write(byte address, byte value)
{
_i2cDevice.Write(new byte[] { address, value });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_i2cDevice.Write(new byte[] { address, value });
_i2cDevice.Write(stackalloc byte[] { address, value });

Copy link
Member

@Ellerbach Ellerbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments for this very nice addition :-)

/// <param name="i2cDevice">The <see cref="System.Device.I2c.I2cDevice"/> to create with.</param>
public BreakoutPair5x7(I2cDevice? i2cDevice = null)
{
_i2cDevice = i2cDevice is not null ? i2cDevice : I2cDevice.Create(new(1, Is31fl3730.DefaultI2cAddress));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not using this pattern and always leave the creation of the I2cDevice to the caller. Reason is because bus may be different, ownership as well and finally adaptors like FT4222 or Arduino. So don't pass the null as default and raise and exception if null.

{
_is31fl3730?.Dispose();
_is31fl3730 = null!;
_i2cDevice?.Dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the pattern we are using, as we don't create I2cDevice in the bindings, we don't dispose them. It's to the caller to take care of this.

{
// Function register
// table 2 in datasheet
private const byte CONFIGURATION_REGISTER = 0x0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than using constants, use a Register.cs file with enums in there being internal. Like this, you will be able to keep the register names like they are (all uppercase). And it's much better than constants.

/// <summary>
/// Represents a 8x8 LED matrix.
/// </summary>
Size8x8,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you are using explicit check of MatrixMode > 0 earlier, make the first element explicit to 0

src/devices/Is31fl3730/Is31fl3730.cs Show resolved Hide resolved
/// </summary>
public void UpdateDecimalPoint(int matrix, int value)
{
int matrixOneMask = 128;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const for those 2 would be better as you are using them like this

{
Span<byte> data = stackalloc byte[value.Length + 1];
data[0] = address;
value.CopyTo(data[1..]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you have to adjust the imports to add the new range (I love them btw :-))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there guidance on how to make this work for .NET Standard 2.0? Is this the right pattern? https://www.meziantou.net/how-to-use-csharp-8-indices-and-ranges-in-dotnet-standard-2-0-and-dotn.htm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with this: d82f7ff

/// <param name="x">The x dimension for the LED.</param>
/// <param name="y">The y dimension for the LED.</param>
/// <param name="value">The value to write.</param>
public void WriteLed(int matrix, int x, int y, int value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to consider as you only have 2 matrix: use an enum rather than an int?

/// <param name="i2cDevice">The <see cref="System.Device.I2c.I2cDevice"/> to create with.</param>
public BreakoutPair5x7(I2cDevice? i2cDevice = null)
{
_i2cDevice = i2cDevice is not null ? i2cDevice : I2cDevice.Create(new(1, Is31fl3730.DefaultI2cAddress));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same remark as for the other one, we don't create I2cDevice from the bindings.

{
_is31fl3730?.Dispose();
_is31fl3730 = null!;
_i2cDevice?.Dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And similar as my comment for the other one, no need to dispose as we don't create it in the binding

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed that guidance, however, when I came to supporting the Micro Dot pHAT, I found that there was a need to specify three I2C addresses. That's a bit of a pain. What do you think?

Here are two constructors: 0d1e95f#diff-c1aa6bb26153d700cceaa57064762e4c6dce51672088c47a5f883ded6f76c12eR28-R47

I'm happy to do whatever you think best. I wanted to demonstrate the UX of the two approaches.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I resolved this.

@richlander richlander marked this pull request as ready for review December 18, 2022 17:48
@pgrawehr
Copy link
Contributor

is this very different from #1926? Should the two be completely independent?

@richlander
Copy link
Member Author

Yes. They are very different. The two drivers do not work similarly. If you look at the datasheets, you'll see that there very little similarity w/no opportunity for code sharing. Still, good question.

Copy link
Contributor

@pgrawehr pgrawehr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good. Some general comments:

  • Documentation of some classes could be improved (was apparently just copy-pasted in some places)
  • Review the visibility of enums and constants. Ideally, bindings should not expose register numbers and other low-level constants.

Comment on lines 13 to 16
/// <summary>
/// Represents an IS31FL3731 LED Matrix driver
/// </summary>
public enum Current
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation is apparently wrong. Should be something about (maximum?) LED current.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I copied the text from the datasheet.

Comment on lines 13 to 16
/// <summary>
/// Represents an IS31FL3731 LED Matrix driver
/// </summary>
public enum DisplayMode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again: This documentation doesn't describe the enum

public DisplayMode DisplayMode { get; set; }

/// <summary>
/// Enables or disables auto-buggering.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment

/// <param name="x">The x dimension for the LED.</param>
/// <param name="y">The y dimension for the LED.</param>
/// <param name="value">The value to write.</param>
public void Write(int matrix, int x, int y, int value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since apparently, value is either 0 or 1, wouldn't it be better to use a bool or a PinValue type? Or is this designed to support displays with colors, too? (The method isn't virtual or from an interface, though)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 65 to 73
/// <summary>
/// Brightness of LED matrix (override default value (128; max brightness); set before calling Initialize method).
/// </summary>
public int Brightness { get; set; }

/// <summary>
/// Full current setting for each row output of LED matrix (override default value (40 mA)); set before calling Initialize method).
/// </summary>
public Current Current { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we don't allow setting Current and Brightness after Initialization? At the moment, setting these members after initialisation has no effect (not even an exception), but it looks like writing the associated registers should always be possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, the interest of having such properties is for them to be dynamic, so you can adjust them on the fly without having to call the initialize function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Good call.

/// <summary>
/// Matrix Values.
/// </summary>
public static class MatrixValues
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fixed.

/// <summary>
/// Register addresses for the Function Register.
/// </summary>
public static class FunctionRegister
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably no need to make this public.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fixed.

/// <summary>
/// Register addresses for the Configuration Register.
/// </summary>
public static class ConfigurationRegister
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this, too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and each enum should be in its own file

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@ghost ghost added the Needs: Author Feedback We are waiting for author to react to feedback (action required) label Jan 6, 2023
@pgrawehr pgrawehr assigned richlander and unassigned pgrawehr Jan 6, 2023
Copy link
Member

@Ellerbach Ellerbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments.

src/devices/Display/IMatrix.cs Outdated Show resolved Hide resolved
/// <summary>
/// 40 mA
/// </summary>
CmA40 = 0b0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even if those are just 0, maybe better to align with 4 0 like the rest?

src/devices/Is31fl3730/Current.cs Outdated Show resolved Hide resolved
src/devices/Is31fl3730/DotMatrix.cs Outdated Show resolved Hide resolved
src/devices/Is31fl3730/DotMatrix.cs Show resolved Hide resolved
src/devices/Is31fl3730/DotMatrix10x7.cs Show resolved Hide resolved
src/devices/Is31fl3730/Is31fl3730.cs Show resolved Hide resolved
Comment on lines 65 to 73
/// <summary>
/// Brightness of LED matrix (override default value (128; max brightness); set before calling Initialize method).
/// </summary>
public int Brightness { get; set; }

/// <summary>
/// Full current setting for each row output of LED matrix (override default value (40 mA)); set before calling Initialize method).
/// </summary>
public Current Current { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, the interest of having such properties is for them to be dynamic, so you can adjust them on the fly without having to call the initialize function

src/devices/Is31fl3730/Is31fl3730.cs Outdated Show resolved Hide resolved
/// <summary>
/// Register addresses for the Configuration Register.
/// </summary>
public static class ConfigurationRegister
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and each enum should be in its own file

@ghost ghost removed the Needs: Author Feedback We are waiting for author to react to feedback (action required) label Jan 12, 2023
@richlander
Copy link
Member Author

I'm having some namespace problems, as you can see with 0921724. Any advice on that would be appreciated.

Copy link
Member

@Ellerbach Ellerbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks great to me now!

@richlander
Copy link
Member Author

Thanks for all the help on these bindings. I learned a lot.

@pgrawehr
Copy link
Contributor

pgrawehr commented Feb 2, 2023

/azp run dotnet.iot

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

@pgrawehr pgrawehr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. You might wish to take a look at input validation, though.

Comment on lines 80 to 83
// Set high bit for the 128th item
// Set high bit low for the remaining 127 items
// And then set lower bits as appropriate, to represent 0-127
public void UpdateBrightness(byte value) => Write(Is31fl3730FunctionRegister.Pwm, value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation, not all values are valid (only 0-128). If that's correct, this method should contain some input validation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. That's a good point. Fair.

/// <summary>
/// Update current setting for each row output of LED matrix (default value is 40 mA).
/// </summary>
public void UpdateCurrent(Current value) => Write(Is31fl3730FunctionRegister.LightingEffect, (byte)value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're not doing that everywhere correctly, it would be good to check for valid values here as well. The user could pass any value that is not part of the enum.

@pgrawehr pgrawehr merged commit 58c8b14 into dotnet:main Feb 12, 2023
@richlander richlander deleted the Is31fl3730 branch February 13, 2023 02:13
@github-actions github-actions bot locked and limited conversation to collaborators Dec 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-device-bindings Device Bindings for audio, sensor, motor, and display hardware that can used with System.Device.Gpio
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants