.NET implementation

Apparently I'm a sucker for punishment so I'm trying to port some of your software over to .NET IoT with C#. My issue is that the library I'm using - UnoSquare's rather good interface - has provided me only so much. I can get your wheels turning, and I can get the speeds... But that's it. The reason appears to be because the .Read(int length) function is returning only an array of one byte that is [length] long.

I figure it has to do with how the library is reading the stream. But my question to you is: has anyone here, or that you've heard of, tried to do this yet? If so, what library(ies) did they use to interact with the I2C bus on the board?

I'm so close it's irritating.

Thanks.
-J

piborg's picture

This sounds like what I would expect, the library reads back as many individual bytes as it was asked for. This is because the data travelling across the I2C bus is just raw bytes, the library has no way of knowing what they mean.

If you are trying to read a value which is stored in multiple bytes (such as GetBatteryReading), you will need to re-combine the values into a single larger value once they have been read. Some values (like the battery voltage) also need a bit of decoding to get the values into standard units.

Here is an example for GetBatteryReading that should do the trick:

using Windows.Devices.I2c;

// Constants
const int I2C_MAX_LEN = 6;
const byte COMMAND_GET_BATT_VOLT = 21;
const double COMMAND_ANALOG_MAX = 0x3FF;
const double VOLTAGE_PIN_MAX = 36.3;
const double VOLTAGE_PIN_CORRECTION = 0.0;

private double GetBatteryReading() {
    // Build the command data, send it and read the reply
    byte[] recvBytes = new byte[I2C_MAX_LEN];
    byte[] sendBytes = new byte[] {COMMAND_GET_BATT_VOLT};
    try {
        ubI2C.Write(sendBytes);
        ubI2C.Read(recvBytes);
    } catch (Exception ex) {
        // Failed to read...
        return 0.0;
    }

    // Read out the battery level data (16-bit)
    int raw = ((int)recvBytes[1] << 8) + (int)recvBytes[2];

    // Convert the raw level to a voltage value
    double level = (double)raw / (double)COMMAND_ANALOG_MAX;
    level *= VOLTAGE_PIN_MAX;
    return level + VOLTAGE_PIN_CORRECTION;
}

So, this was awesome, thank you for the response! It doesn't, however, actually solve my problem. It's probably the library that I'm using, because here's what's going on:

log.WriteLog("---------------------------------------------------------------------");
byte[] recvBytes = new byte[6];
byte[] sendBytes = new byte[] { 0x99 };
foundDevice.Write(sendBytes);
recvBytes = foundDevice.Read(6);
int raw = ((int)recvBytes[1] << 8) + (int)recvBytes[2];
log.WriteLog("recvBytes[1]: " + recvBytes[1].ToString("X2"));
log.WriteLog("recvBytes[2]: " + recvBytes[2].ToString("X2"));
log.WriteLog("raw         : " + raw.ToString());
log.WriteLog("---------------------------------------------------------------------");

And the problem is, it produces this:

17/03/2021 20:01:13: ---------------------------------------------------------------------
17/03/2021 20:01:13: recvBytes[1]: 99
17/03/2021 20:01:13: recvBytes[2]: 99
17/03/2021 20:01:13: raw         : 39321
17/03/2021 20:01:13: ---------------------------------------------------------------------

The specific library that I'm using is this one:
https://github.com/unosquare/raspberryio

Frustrating as heck, because according to your Python code (assuming you were the one who wrote it) a hit on 0x99 should produce a response of:
0x99/0x15/0x00/0x00/0x00/0x00

But as you can see, it just responds 0x99/0x99/0x99/0x99/0x99/0x99.

I'm still too new to this part of things that I don't know if it's ME, or if it's the library I'm using.

But either way, any help is IMMENSELY appreciated.

Thanks.
-J

piborg's picture

I think you are right, it looks like the library is reading one byte per transaction instead of reading many bytes as a single transaction :(

We have only tried .NET on the Raspberry Pi when using Windows IoT, which has the Windows.Devices.I2c library available. That library reads all the bytes requested in a single transaction .

Looking at the https://github.com/unosquare/raspberryio library I think it may be a limitation of how the underlying WiringPi code it is built on works.

I had a quick look around and this library looks quite promising: https://github.com/mshmelev/RPi.I2C.Net
It implements its own native library for talking to the I2C driver which looks like it behaves the same way as our Python code :)

Oh, you ROCK! I'll look at this after my work day. Cheers, mate!

Hey. I don't... actually know what to call you other than @piborg.

So I checked out that repo at the link. It's older than dirt, but it "sort of" works. I've got some really, really weird crap going on with it that I can't yet explain, but overall, we've got FORWARD MOTION! LOL And we can stop! LOLer.

I'm getting some strange things happening, though, that I've had to confirm through your Python code. Namely, when setting 0x08 to 0x80, my A side wheels are going forward. Great. The next step - even if I send an all-stop first - is to send 0x11 a 0x80... and BOTH my A and B wheels are now going forward. I don't understand this yet.

Seen it before?

*IF* you're interested (or anyone else here) I'll throw my code up on GitHub along with a fork of the original code.

Let me know.

Thanks!
-J

piborg's picture

This is actually just a simple mix-up between decimal values (base 10) and hexadecimal values (base 16).

The commands involved here are:

Command                 Decimal     Hexadecimal
COMMAND_SET_A_FWD       8           0x08
COMMAND_SET_B_FWD       11          0x0B
COMMAND_SET_ALL_FWD     17          0x11

For the first command both 8 and 0x08 are the same value - COMMAND_SET_A_FWD.
For the second command 11 and 0x11 are different values - 11 is COMMAND_SET_B_FWD, 0x11 is COMMAND_SET_ALL_FWD.

Any number starting "0x" is interpreted as hexadecimal (uses digits 0-F) instead of decimal. You can mix and match between the two, just make sure each one has the right value :)

Most calculator programs have some tool to convert between decimal and hexadecimal. There are also online tools such as: https://www.rapidtables.com/convert/number/decimal-to-hex.html

Sharing the code would be great, I am sure it would be helpful for others who want to use .NET as well.

My name is Arron :)

...was I REALLY that tired?

Dang it, I thought I checked that. Twice. Ugh. I'll try that, thank you for putting up with me, Arron. I'm James.

And I'm good at this, I swear to God....

I mean, I do this as a DAY JOB... *smdh*

-J

Hey:

My repo is here, but it's Mono based, not Dotnet, just yet. I'm building this out and hoping to migrate it over to .Net Core soon.

https://github.com/crazycga/RPi.I2C.Net

It'll have a different name. But so far, I have code that runs motors, gets values, etc. I'm having some problems with setting LED values, but I'm working on them.

Thanks!
-J

...I will have a native .Net Core 3.1 implementation. Stay tuned!

piborg's picture

Looks like your library is coming along nicely :)

The LED colour control should be fairly simple, but as a summary:

  • COMMAND_SET_LED1 / COMMAND_GET_LED1 control the on-board LED.
  • COMMAND_SET_LED2 / COMMAND_GET_LED2 control the second LED if you have the additional lid with battery clip for an AA holder.
  • COMMAND_SET_LEDS controls both of the LEDs.
  • The levels are sent as a byte each for R, G, B (in that order), all between 0 and 255.
  • COMMAND_SET_LED_BATT_MON / COMMAND_GET_LED_BATT_MON control the LED colour mode, 0 for manual control, 1 for colour based on battery voltage.
  • To set colours the COMMAND_SET_LED_BATT_MON needs to be set to manual control first.
  • The board powers up in colour based on battery voltage mode.
  • If the GET commands do not read back the right values try adding a slight delay between the command write and the read back.
  • COMMAND_SET_BATT_LIMITS / COMMAND_GET_BATT_LIMITS can be used to control the battery voltage range.

Thanks, man. That's what I'm trying. I've validated that your Python script to wave the LEDs is working. But man, I'm getting some weird results here. I'll try putting in a delay, but what's happening that I just observed is that using a loop to go through colors 10 at a time, the LEDs are flashing... But only intermittently, and only when the LED monitoring is ON!!! Which has me puzzled. When I turn it off - and CONFIRM IT - it simply doesn't want to play fair with me.

I'll try the delay thing and get back to you.

Hey Arron:

I hate to be a PITA, but I can't figure out WTF is going on... This code is the exact same code used to pass values to the wheels, etc., but for whatever reason it is NOT working for the LEDs. Would you mind taking a quick look and see if I've missed something obvious, again? (note that I've collapsed some of the code to make it look right here, with the _ tag.)

public void WaveLEDs(Logger_class log = null)
{
	bool batSetting = false;
	batSetting = this.GetLEDBatteryMonitor(log);

	while (batSetting)
	{
		log.WriteLog("Battery was monitoring; resetting to off.");
		this.SetLEDBatteryMonitor(false, log);
		batSetting = this.GetLEDBatteryMonitor(log);
	}

	log.WriteLog("This routine will run through the colors of the LEDs.  Press any key to stop.");
	while (!Console.KeyAvailable)
	{
		for (int i = 0; i < 255; i = i + 10)
		{
			for (int j = 0; j < 255; j = j + 10)
			{
				for (int k = 0; k < 255; k = k + 10)
				{
					log.WriteLog("Set LED1: 01 " + i.ToString("X2") + " " + j.ToString("X2") + " " + k.ToString("X2") + _
						" 00 00  LED2: 03 " + i.ToString("X2") + " " + j.ToString("X2") + " " + k.ToString("X2") + " 00 00");
					this.SetLEDs(Convert.ToByte(i), Convert.ToByte(j), Convert.ToByte(k), log);
					log.WriteLog("Get LED1: " + this.BytesToString(this.GetLED1()) + " LED2: " + this.BytesToString(this.GetLED2()));
					if (Console.KeyAvailable) break;
				}
				if (Console.KeyAvailable) break;
			}
			if (Console.KeyAvailable) break;
		}
		if (Console.KeyAvailable) break;
	}
	Console.ReadKey(true);
}

And the code for ThunderBorg.SetLEDs:

public void SetLEDs(byte red, byte green, byte blue, Logger_class log = null)
{
	if (!_CheckInit())
	{
		return;
	}

	if (log != null)
	{
		log.WriteLog("Setting LEDs to " + red.ToString() + ", " + green.ToString() + ", " + blue.ToString() + "...");
	}

	using (var bus = I2CBus.Open("/dev/i2c-" + this._bus.ToString()))
	{
		if (log != null) 
		{
			log.WriteLog("Setting LEDs using command: " + this.BytesToString(new byte[] { COMMAND_SET_LEDS, red, green, blue }));
		}

		bus.WriteBytes(_TBorgAddress, new byte[] { COMMAND_SET_LEDS, red, green, blue });
		if (THROTTLE_CODE)
		{
			System.Threading.Thread.Sleep(200);
		}
		byte[] response = bus.ReadBytes(_TBorgAddress, I2C_MAX_LEN);

	}
}

The value for COMMAND_SET_LEDS:

public static readonly byte COMMAND_SET_LEDS            = 0x05;

The rest of the code is in my repo at https://github.com/crazycga/ThunderSharp

What I'm observing happen: the LED value seem to go though being SET, but does not appear to "TAKE":

20/03/2021 13:36:15: Setting LEDs using command: 05 00 00 0A
20/03/2021 13:36:16: Get LED1: 02 19 19 19 00 00  LED2: 04 FF 00 00 00 00
20/03/2021 13:36:16: Set LED1: 01 00 00 14 00 00  LED2: 03 00 00 14 00 00
20/03/2021 13:36:16: Setting LEDs to 0, 0, 20...
20/03/2021 13:36:16: Setting LEDs using command: 05 00 00 14
20/03/2021 13:36:16: Get LED1: 02 19 19 19 00 00  LED2: 04 FF 00 00 00 00
20/03/2021 13:36:16: Set LED1: 01 00 00 1E 00 00  LED2: 03 00 00 1E 00 00
20/03/2021 13:36:16: Setting LEDs to 0, 0, 30...
20/03/2021 13:36:16: Setting LEDs using command: 05 00 00 1E
20/03/2021 13:36:17: Get LED1: 02 19 19 19 00 00  LED2: 04 FF 00 00 00 00
20/03/2021 13:36:17: Set LED1: 01 00 00 28 00 00  LED2: 03 00 00 28 00 00

I kind of hate asking, but I'm running out of inspiration.

Thanks, man!
-J

piborg's picture

This is a bit of a guess, but this might be related to the passing of arrays between .NET and C.

You can get .NET to "convert" the array to and from a C style array for you using the [MarshalAs(UnmanagedType.LPArray)] attribute.

For example:

		[DllImport("libnativei2c.so", EntryPoint = "readBytes", SetLastError = true)]
		public static extern int ReadBytes(int busHandle, int addr, [MarshalAs(UnmanagedType.LPArray)] byte[] buf, int len);

You may need this on both the read and write functions, I don't remember for sure.

There is an explanation the attribute here:
https://docs.microsoft.com/en-us/dotnet/standard/native-interop/type-mar...

...and that didn't seem to make a difference, BUT, you've sent skittering off on a different tangent. I observed that the underlying code (C) for this library is accepting

byte* buf

and perhaps when it take that pointer and works on it, it might be losing something? I'm reaching, too, but I might just give it a try and see.

You da man, though! I can't express enough how much I appreciate your input, sir.

Thanks.
-J

I'll be go to hell, but I GOT IT!

You were absolutely (and accidentally) correct!

The I2C library that I farmed was a skootch too fast. It was written as:

if (write (busHandle, buf, len) != len)

So I changed it (because I wanted to enumerate the number of bytes actually written) and expanded it into an if-then clause:

    int rlen = write(busHandle, buf, len);
    if (rlen != len)
    {
        return -2;
    }

And I'll be damned but it started actually working! So you were right the first time, the code was moving too fast for the I2C response. But it was WAY deeper than in my C# code, it was way down in the C library I used. (LOL Told you I knew what I was doing. HAHAHAHA) I'll be updating my repo, testing it, then calling this the first ThunderSharp release. I haven't implemented all of the functions you have in the ThunderBorg.py library yet.

Which leads me to a question. Why - of all things - does that library contain output for the LED strip? (SK9822 / APA102C)

Thank you, thank you, thank you for your help, Arron! You da MAN!
-J

piborg's picture

Glad to hear that you managed to find the problem :)

The reason the delay is needed is that the ThunderBorg's PIC takes a short period of time to decode the command and load a response buffer with the reply. Without the delay the response buffer is read from before it is ready.

As for the APA102C code (COMMAND_WRITE_EXTERNAL_LED) it was added towards the end of development so we could add some RasPiO InsPiRing strips to a MonsterBorg for a bit of fun (see here for connections). Originally it was only intended for our internal use, but we left it in so others could use it if they wanted to. I would consider your library complete without it being implemented.

Thanks man. I've got ... two? more functions to implement and I should be "done". I haven't implemented the change address function (scared to, LOL) and ... okay, maybe one. I have to see. Unfortunately my day job interferes with my hobby.... Then I'll clean up the code, make the error handling consistent and then release.

Again, many thanks for the help!
-J

Subscribe to Comments for &quot;.NET implementation&quot;