1

Topic: Problems writing to Skylander RFID portal

For fun I wanted to to play around with the skylander rfid portal. This is an USB Hid device and the "protocol" is quite simple or so it appears from SKyDumper project (h**ps://github.com/capull0/SkyDumper). This tool SkyDumper works on my linux computer, but when I try to convert it to hidsharp it won't work.

To be more specific.
* I can detect the portal just fine and get the device.
* I can open a HidStream
* I then try to send the "restart" message. Basically report Zero and the letter R, the rest all zeros.
* As soon as this is send out the portal starts sending status messages which I receive continuously. So the message did wake up something.
* However the write errors out with an exception. On my linux laptop its and I/O exception hardcoded in the output source code of hidsharp and on windows its a timeout exception.

I'm no usb export and have little clue why this happens.

I converted portalIO.cpp to the C# code below. I must be missing something silly I think. No reason for it to work using the original cpp code and not thru HidSharp. The first write failure occurs on the call to restartportal. After that I am buried in status output from the portal but nothing else. Any further write messages give the same result

If anyone has a clue or suggestion on what to try next? That would be appreciated.

using System;
using System.Text;
using HidSharp;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using HidSharp.Reports;
using HidSharp.Reports.Input;

namespace SkyReaderLib
{
    public class PortalIO
    {
        public const int rw_buffer_size = 0x21;
        public const int TIMEOUT = 30000;
        public const int SKYLANDER_SIZE = 1024;
        public const int MAX_STR = 255;
        private HidDevice portal;
        private HidStream portalStream;

        private bool initialized = false;


        public event EventHandler<StatusChangedEventArgs> StatusEvent;


        public static IEnumerable<HidDevice> ListPortals()
        {

            IEnumerable<HidDevice> devices = DeviceList.Local.GetHidDevices()
                .Where(x => (x.VendorID == 0x12ba || x.VendorID == 0x54c || x.VendorID == 0x1430)
                    && (x.ProductID == 0x150 || x.ProductID == 0x967));

#if DEBUG
            if (devices.Any())
            {
                Console.WriteLine("Found portal usb devices:");
                foreach (HidDevice device in devices)
                {
                    Console.WriteLine($"\t{device.GetFriendlyName()}");
                }

            }
#endif
            return devices;
        }




        // Send a command to the portal
        private void Write(byte[] message)
        {

            try
            {
                byte[] buffer = new byte[portal.GetMaxOutputReportLength()];
                message.CopyTo(buffer, 0);
#if DEBUG
                Console.WriteLine("Issued report out:");
                Console.WriteLine(string.Join(" ", buffer));
#endif
                portalStream.Write(buffer);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Exception while issuing a report occured: {e}");
            }
       

        }

        private bool CheckResponse(out RWBlock response, byte expect)
        {
            response = new RWBlock();
#if DEBUG
            Console.WriteLine(">>> CheckResponse\n");
#endif

            int result = portalStream.Read(response.buffer, 0, rw_buffer_size);

            if (result < 0)
                throw new Exception("Could not read response");

#if DEBUG
            Console.WriteLine("CheckResponse read = {0} bytes\n", result);
#endif

            response.dwBytesTransferred = result;

   

            // found wireless USB but portal is not connected
            if (response.buffer[0] == 'Z')
                throw new Exception("Found portal dongle, but portal not connected");

            // Status says no skylander on portal
            // if (response->buffer[0] == 'Q' && response->buffer[1] == 0)
            //     throw 11;


#if DEBUG
            Console.WriteLine("<<< CheckResponse\n");
#endif
            return (response.buffer[0] != expect);

        }

        //
        public void PortalStatus()
        {
            byte[] buffer = new byte[portal.GetMaxOutputReportLength()];

            buffer[0] = 0;
            buffer[1] = (byte)'S';
           
            Write(buffer);
        }


        // Start portal
        public void RestartPortal()
        {
            byte[] buffer = new byte[portal.GetMaxOutputReportLength()];

            buffer[0] = 0;
            buffer[1] = (byte)'R';
            Write(buffer);
                       
        }

        // Antenna up / activate
        public void ActivatePortal(int active)
        {
            byte[] buffer = new byte[portal.GetMaxOutputReportLength()];
           
            buffer[0] = 0;
            buffer[1] = (byte)'A';
            buffer[2] = (byte)active;

            Write(buffer);
        }

        // Set the portal color
        public void SetPortalColor(byte r, byte g, byte b)
        {
            byte[] buffer = new byte[portal.GetMaxOutputReportLength()];

            buffer[0] = 0;
            buffer[1] = (byte)'C';
            buffer[2] = r; // R
            buffer[3] = g; // G
            buffer[4] = b; // B

            Write(buffer);
        }

        // Release hPortalInstance
        ~PortalIO()
        {
            ActivatePortal(0);
            portalStream.Close();
        }

        public void Flash()
        {

            for (; ; )
            {
                ActivatePortal(1);
                ActivatePortal(0);
            }
        }



        public PortalIO(HidDevice portal)
        {
            Console.WriteLine("Connecting to portal.\n");

            this.portal = portal;

            initialized = OpenPortal(portal);
            if (initialized)
            {
                RestartPortal();
                ActivatePortal(1);
                SetPortalColor(0xC8, 0xC8, 0xC8);
            }
            else
            {
                Console.WriteLine("Error initializing portal.");
            }
        }

        private bool OpenPortal(HidDevice portal)
        {
            Console.WriteLine(portal.GetMaxInputReportLength());
            ReportDescriptor descriptor = portal.GetReportDescriptor();

            HidDeviceInputReceiver reciever = descriptor.CreateHidDeviceInputReceiver();
            bool opened = portal.TryOpen(out portalStream);

            var x = portal.GetReportDescriptor();

            reciever.Start(portalStream);
            reciever.Received += OnReceiverReceived;
           
            portalStream.Closed += PortalStreamOnClosed;
           
            return opened;
        }

        private void PortalStreamOnClosed(object? sender, EventArgs e)
        {
            Console.WriteLine("Closed!");
        }

        private void OnReceiverReceived(object sender, EventArgs args)
        {
            try
            {
                var receiver = (HidDeviceInputReceiver)sender;

                if (receiver.Stream?.Device == null)
                {
                    return;
                }

                var length = receiver.Stream.Device.GetMaxInputReportLength();

                var buffer = new byte[length];

                if (receiver.TryRead(buffer, 0, out Report report))
                {
                   // Console.WriteLine($"Received report with id: {report.ReportID}, type: {report.ReportType}");

                    report.Read(buffer, 0, (bytes, offset, item, dataItem) =>
                    {
                       // Console.WriteLine(string.Join(" ", bytes));
                    });

                    switch((char)buffer[1])
                    {
                        case 'S':
                            StatusEvent?.Invoke(this, new StatusChangedEventArgs(buffer));
                            break;
                        default:
                            break;
                    }
                }
                else
                {
                    Console.WriteLine("Failed to read report.");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine($"Exception while reading report occured: {e}");
            }
        }




    }
}

2

Re: Problems writing to Skylander RFID portal

I don't have this device, but, just want to make sure, portal.GetMaxOutputReportLength() returns 33, yes?
(hid_write in the C version seems to assume this.)

3

Re: Problems writing to Skylander RFID portal

Zer wrote:

I don't have this device.


I could hope, but this was to be expected. You cannot have every device.

Zer wrote:

but, just want to make sure, portal.GetMaxOutputReportLength() returns 33, yes?


Yes is does return 33 both for in and output report.


Zer wrote:

hid_write in the C version seems to assume this.


It assumes a lot. But I did not write it smile. I'm just happy someone wrote it, as figuring out usb devices is not exactly a big skillset of mine smile

4

Re: Problems writing to Skylander RFID portal

Though its a little out of my league it tried to use a usb analyser to get a trace both with the c# and the c++ version.

The analyser gives the same report data in both cases and in both cases an actual "report" seems to be send.

But the analyser has a summerize windows and it show this for the working c++ version. Note the text at URB function.
  URB Class Interface issued
Device Object    FFFFD40A17A13CB0h
Driver Object    hhdusbh64

URB Function    URB_FUNCTION_CLASS_INTERFACE

Request    Set Report
Report Type    Output
Report Length    32

when I write using c# using hidsharp I get:


Device Object    FFFFD40A17A13CB0h
Driver Object    hhdusbh64

URB Function    URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER

Endpoint 01h    1 Out, Interrupt

Report Type    Output
Report Length    32

Seems to me there is some difference in the type of data transfer. The working c++ code does set report and hidsharp does not? Somewhere in this difference seems to be something the portal does not like.

5 (edited by bzuidgeest 2021-11-24 17:44:12)

Re: Problems writing to Skylander RFID portal

Using an googling around using the above discoveries, I found that microsoft advertises two methods to write to usb hid Writefile and HidD_SetOutputReport from hid.dll

The latter one is not among the ones exposed in nativemethods. But set/get feature from the same dll is there. So I added this function to nativemethods.

[DllImport("hid.dll", SetLastError = true)]
        public static extern bool HidD_SetOutputReport(IntPtr handle, byte[] lpReportBuffer, int ReportBufferLength);

change write in winhidstream to

fixed (byte* ptr = _writeBuffer)
                    {
                        int offset0 = 0;
                        while (count > 0)
                        {
                            var overlapped = stackalloc NativeOverlapped[1];
                            overlapped[0].EventHandle = @event;


                            bytesTransferred = 33;
                            NativeMethods.HidD_SetOutputReport(_handle, _writeBuffer, 33);

                            //bool result = NativeMethods.WriteFile(_handle, ptr + offset0, Math.Min(minOut, count), IntPtr.Zero, overlapped);

                            //int win32Error = Marshal.GetLastWin32Error();

                            //NativeMethods.OverlappedOperation(_handle, @event, WriteTimeout, _closeEventHandle,
                            // result,
                            // overlapped, out bytesTransferred);
                            count -= (int)bytesTransferred; offset0 += (int)bytesTransferred;
                        }
                    }

Crude change I know, but it works flawlessly. I guess the skylander rfid portal is real picky about exactly the type of transfer/message it gets. As I said, I know to little about usb to completely understand this. But I'am happy it works at least.

Is it possible to make a new version that exposes this setreport function somehow? Keeping de write intact, but adding a WriteReport function to the stream?

I also expect a similar change is needed on the linux side of things.... But I know even less where to look on that platform.

edit: Digging a little more it seems that writefile does different thing depending on the device. It uses the control endpoint to send reports unless it finds a interrupt out endpoint. (I don't know if this behavior is controllable).

I thing the rfid portal uses the interrupt pipe to send a constant stream of status updates. But that it still expects commands on the control endpoint or something like that.