diff --git a/src/devices/Ft200X/Ft200X.csproj b/src/devices/Ft200X/Ft200X.csproj new file mode 100644 index 0000000000..6af3c788a8 --- /dev/null +++ b/src/devices/Ft200X/Ft200X.csproj @@ -0,0 +1,15 @@ + + + $(DefaultBindingTfms) + false + + + + + + + <_Parameter1>$(AssemblyName).Tests + + + + diff --git a/src/devices/Ft200X/Ft200X.slnx b/src/devices/Ft200X/Ft200X.slnx new file mode 100644 index 0000000000..4f16d496ce --- /dev/null +++ b/src/devices/Ft200X/Ft200X.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/devices/Ft200X/Ft200XDevice.cs b/src/devices/Ft200X/Ft200XDevice.cs new file mode 100644 index 0000000000..2492fac37d --- /dev/null +++ b/src/devices/Ft200X/Ft200XDevice.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Iot.Device.FtCommon; + +namespace Iot.Device.Ft200X +{ + /// + /// Provides USB-side access to the FT200X I2C slave's receive and transmit FIFOs. + /// + /// + /// This is not an I2C master or an I2cDevice. An external I2C master must + /// initiate all bus transfers. Reads and writes contain only payload bytes, + /// not I2C addresses or register commands. The device owns its D2XX connection. + /// + public sealed class Ft200XDevice : IDisposable + { + private readonly object _sync = new object(); + private readonly IFtXSeriesI2cSlaveTransport _transport; + private bool _disposed; + + /// + /// Opens the FT200X by serial number, without resetting it or purging its FIFOs. + /// + /// The FT200X descriptor returned by . + /// Positive D2XX read timeout, in milliseconds. + /// Positive D2XX write timeout, in milliseconds. + /// + /// For devices with a customized USB description, a descriptor from + /// FtCommon.GetDevices can be supplied. Verify that it is physically an + /// FT200X: D2XX reports the same FtXSeries type for several different chips. + /// A nonempty, unique serial number is required on all platforms. + /// + public Ft200XDevice(FtDevice deviceInformation, int readTimeoutMilliseconds = 1000, int writeTimeoutMilliseconds = 1000) + : this(Open(deviceInformation, readTimeoutMilliseconds, writeTimeoutMilliseconds)) + { + } + + internal Ft200XDevice(IFtXSeriesI2cSlaveTransport transport) + { + ArgumentNullException.ThrowIfNull(transport); + _transport = transport; + } + + /// + /// Gets the number of bytes currently available in the USB driver's receive queue. + /// These bytes were written to the FT200X by the external I2C master. + /// + /// This does not report free space or pending data in the USB-to-I2C FIFO. + public uint BytesAvailable + { + get + { + lock (_sync) + { + ThrowIfDisposed(); + return _transport.BytesAvailable; + } + } + } + + /// + /// Gets the I2C slave address stored in MTP memory, without programming it. + /// + /// + /// This is the programmed configuration, not a measurement of the active + /// address. A USB disconnect/reconnect is required after programming. + /// + public int ProgrammedI2cAddress + { + get + { + lock (_sync) + { + ThrowIfDisposed(); + FtXSeriesConfiguration configuration = _transport.ReadConfiguration(); + configuration.Validate(); + return configuration.Data.I2cSlaveAddress; + } + } + } + + /// + /// Enumerates FT-X devices whose USB description starts with FT200X. + /// + /// Descriptors of likely FT200X adapters. + /// + /// The default VID/PID 0403:6015 and D2XX device type are shared with other + /// FT-X chips. The description filter avoids selecting all FT-X devices. + /// Devices with customized descriptions must be selected using FtCommon.GetDevices. + /// + public static List GetDevices() + { + return FtCommon.FtCommon.GetDevices().Where(IsFt200X).ToList(); + } + + /// + /// Reads bytes that the external I2C master has written to the FT200X. + /// + /// Destination buffer; only the returned number of bytes is valid. + /// + /// The number of bytes read. D2XX waits for the requested length or the + /// read timeout, so a timeout can return fewer bytes, including zero. + /// An empty buffer returns zero without accessing USB. + /// + public int Read(Span buffer) + { + lock (_sync) + { + ThrowIfDisposed(); + if (buffer.IsEmpty) + { + return 0; + } + + int count = _transport.Read(buffer); + if (count < 0 || count > buffer.Length) + { + throw new IOException($"FT200X returned an invalid read count ({count})."); + } + + return count; + } + } + + /// + /// Queues bytes on USB for the external I2C master to read from the FT200X. + /// + /// Payload bytes, without an I2C address. An empty buffer does nothing. + /// + /// D2XX failed or accepted fewer bytes than requested. A partial write is + /// not retried automatically, since some bytes may already have been queued. + /// + /// + /// Success means the USB driver accepted the bytes, not that the external + /// master consumed them. Large writes can time out if the master is not + /// draining the FIFO. This method does not initiate I2C activity. + /// + public void Write(ReadOnlySpan buffer) + { + lock (_sync) + { + ThrowIfDisposed(); + if (buffer.IsEmpty) + { + return; + } + + int count = _transport.Write(buffer); + if (count != buffer.Length) + { + throw new IOException($"FT200X queued {count} of {buffer.Length} bytes. The write was not retried."); + } + } + } + + /// + /// Persistently programs the I2C slave address while preserving the other + /// FT-X configuration fields and USB descriptor strings. + /// + /// An unshifted 7-bit address from 0x08 to 0x77; reserved addresses are rejected. + /// + /// if a write was performed and verified, or + /// if the stored address already matched. + /// + /// + /// Stop the external I2C master and back up the configuration before use. + /// This consumes MTP endurance (specified as 2,000 cycles), so do not call + /// it routinely on startup. The entire exposed configuration is read back + /// and compared after programming. Once a write is attempted, this instance + /// is disposed even on failure; no automatic retry or rollback is attempted. + /// Disconnect/reconnect USB and open a new instance to activate the change. + /// A no-op leaves the instance open. A read failure before programming also + /// leaves it open. Requires a D2XX runtime exposing the FT-X EEPROM APIs. + /// + /// + /// Reading, programming, or verification failed. If programming was + /// attempted, inspect the stored configuration before trying again. + /// + public bool ProgramI2cAddress(int address) + { + if (address < 0x08 || address > 0x77) + { + throw new ArgumentOutOfRangeException(nameof(address), "Use a nonreserved 7-bit I2C address from 0x08 to 0x77."); + } + + lock (_sync) + { + ThrowIfDisposed(); + FtXSeriesConfiguration original = _transport.ReadConfiguration(); + original.Validate(); + if (original.Data.I2cSlaveAddress == address) + { + return false; + } + + FtXSeriesConfiguration updated = original.Clone(); + FtXSeriesEeprom data = updated.Data; + data.I2cSlaveAddress = (ushort)address; + updated.Data = data; + try + { + _transport.ProgramConfiguration(updated); + FtXSeriesConfiguration verified = _transport.ReadConfiguration(); + verified.Validate(); + if (!updated.HasSameContents(verified)) + { + throw new IOException("FT200X EEPROM verification failed. Disconnect/reconnect USB and inspect the configuration before retrying."); + } + + return true; + } + finally + { + // A reconnect is needed to reload MTP on all supported operating systems. + Dispose(); + } + } + } + + /// + public void Dispose() + { + lock (_sync) + { + if (!_disposed) + { + _disposed = true; + _transport.Dispose(); + } + } + } + + internal static bool IsFt200X(FtDevice device) + { + return device.Type == FtDeviceType.FtXSeries && device.Description.StartsWith("FT200X", StringComparison.OrdinalIgnoreCase); + } + + private static IFtXSeriesI2cSlaveTransport Open(FtDevice deviceInformation, int readTimeoutMilliseconds, int writeTimeoutMilliseconds) + { + ArgumentNullException.ThrowIfNull(deviceInformation); + if (deviceInformation.Type != FtDeviceType.FtXSeries) + { + throw new ArgumentException("An FT200X must be reported as an FT-X series device.", nameof(deviceInformation)); + } + + if (string.IsNullOrWhiteSpace(deviceInformation.SerialNumber) || deviceInformation.SerialNumber.Contains('\0')) + { + throw new ArgumentException("A nonempty FT200X serial number without NUL characters is required.", nameof(deviceInformation)); + } + + if (readTimeoutMilliseconds <= 0) + { + throw new ArgumentOutOfRangeException(nameof(readTimeoutMilliseconds)); + } + + if (writeTimeoutMilliseconds <= 0) + { + throw new ArgumentOutOfRangeException(nameof(writeTimeoutMilliseconds)); + } + + return new FtXSeriesI2cSlaveTransport(deviceInformation.SerialNumber, readTimeoutMilliseconds, writeTimeoutMilliseconds); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + } +} diff --git a/src/devices/Ft200X/README.md b/src/devices/Ft200X/README.md new file mode 100644 index 0000000000..a35c97f1d6 --- /dev/null +++ b/src/devices/Ft200X/README.md @@ -0,0 +1,187 @@ +# FT200X - USB to I2C slave FIFO + +The FTDI FT200X (FT200XD) connects a USB host to an **external I2C master**. This binding exposes the USB side through FTDI D2XX, using the same shared infrastructure as the [FT232H](../Ft232H/README.md) and [FT4222](../Ft4222/README.md) bindings. + +Internally, `FtCommon` separates the `IFtI2cSlaveTransport` payload FIFO contract from the derived `IFtXSeriesI2cSlaveTransport` contract for persistent FT-X configuration. The shared D2XX implementation can be reused by FT-X I2C slave bindings, including a future FT201X binding; discovery here remains FT200X-specific, and FT201X hardware has not been validated. An FT4222 slave implementation would require LibFT4222 and would not inherit FT-X MTP configuration operations. + +**The FT200X cannot act as an I2C master.** It cannot initiate transactions, address a sensor, or provide an `I2cBus` or `I2cDevice` for other bindings. Unlike the MPSSE-capable FT232H, it cannot generate master transfers by sending MPSSE commands. Use an FT232H or FT4222 if you need to drive I2C peripherals directly from a PC. + +## How data flows + +* `Write` queues payload bytes over USB. The external I2C master must read them from the FT200X at its configured slave address. +* The external master writes bytes to that slave address. `Read` retrieves those bytes over USB. +* `BytesAvailable` reports bytes waiting in the USB driver's receive queue, not free space in the chip or the amount of data waiting for the master. + +These are byte FIFOs, not messages or register transactions. I2C START/STOP boundaries are not preserved in the USB byte stream. Agree on framing, lengths, and acknowledgements with your master firmware. Only payload bytes belong in `Write`: do not prepend the slave address or MPSSE commands. + +The master supplies SCL, chooses the bus speed, and addresses the FT200X. Opening the binding does not change the slave address, CBUS configuration, or persistent MTP settings, and does not reset or purge the device. Persistent address programming is a separate, explicit operation. + +## Requirements and wiring + +Install the architecture-appropriate [FTDI D2XX driver/runtime](https://ftdichip.com/drivers/d2xx-drivers/) and make its native library available to the .NET process: + +* **Windows:** `ftd2xx.dll`, normally supplied by FTDI's driver installation. Keep the FTDI driver; do not replace it with WinUSB or a HID driver. +* **Linux:** `libftd2xx.so` and USB access permissions for the current user. Follow FTDI's instructions if the VCP driver has claimed the adapter. Do not unload drivers used by other devices indiscriminately. +* **macOS:** `libftd2xx.dylib`. Follow FTDI's instructions concerning competing VCP drivers and USB access. + +Match the native library architecture to the running .NET process. No native drivers or vendor binaries are distributed with this binding. + +Connect the external master's SDA, SCL, and GND to the FT200X board. Provide pull-ups and power appropriate for both devices' I/O voltages; check the board's schematic rather than assuming USB power implies 3.3 V signaling. Configure the master to use the FT200X's programmed 7-bit slave address. + +## Usage + +```csharp +using System; +using System.Linq; +using Iot.Device.Ft200X; + +var information = Ft200XDevice.GetDevices().Single(); +using var device = new Ft200XDevice( + information, readTimeoutMilliseconds: 1000, writeTimeoutMilliseconds: 1000); + +// The external I2C master must read these bytes from the FT200X. +device.Write(new byte[] { 0x01, 0x02, 0x03 }); + +// Retrieve bytes previously sent by the external I2C master. +byte[] buffer = new byte[16]; +int count = device.Read(buffer); +Console.WriteLine(Convert.ToHexString(buffer.AsSpan(0, count))); +``` + +Discovery filters FT-X descriptors by a USB description starting with `FT200X`. FT200X shares the default USB VID/PID `0403:6015` and the D2XX `FtXSeries` type with other FT-X chips; neither identifies it uniquely. For a customized description, select a descriptor using `FtCommon.GetDevices()` and pass it to the constructor **only after verifying the physical chip**. + +Opening uses the serial number, not the Windows-only D2XX location ID. Adapters must have nonempty, unique serial numbers. With multiple adapters, select by serial number instead of enumeration order. Only one application should open the adapter at a time. + +`Read` requests up to the buffer length and can return fewer bytes, including zero, after the D2XX timeout. Only the returned bytes are valid. This is not EOF. An empty read or write does nothing. Native D2XX failures throw `IOException`. Missing libraries or incompatible architectures surface the .NET loader error. + +`Write` requires D2XX to accept the entire buffer; otherwise it throws `IOException` with the accepted byte count. It never retries a partial write, which could duplicate bytes. Success does not confirm that the master consumed the payload. Without an active master draining the FIFO, sufficiently large writes will time out. Use application-level acknowledgements for delivery. + +All instance operations, including disposal, are serialized. An outstanding read may delay a write or disposal until its timeout. Disposing the device releases the native handle and is safe to repeat. + +## Programming the I2C address + +`ProgrammedI2cAddress` reads the address stored in MTP memory. `ProgramI2cAddress` changes it using D2XX's FT-X EEPROM configuration APIs, without requiring FT_PROG. These APIs are available on Windows, Linux, and macOS. Use a sufficiently recent native D2XX runtime; a missing entry point is surfaced as a loader error, not treated as success. + +```csharp +using var device = new Ft200XDevice(information); +Console.WriteLine($"Stored address: 0x{device.ProgrammedI2cAddress:X2}"); +bool changed = device.ProgramI2cAddress(0x23); +Console.WriteLine(changed + ? "Verified. Disconnect/reconnect USB, then open a new device instance." + : "Already programmed; no MTP write was needed."); +``` + +Only unshifted, nonreserved 7-bit addresses `0x08` through `0x77` are accepted. Verify that the physical chip is an FT200X, back up its configuration using FT_PROG, stop the external master, and ensure the selected address does not conflict with another device before programming. Maintain stable USB power. + +The method reads the current FT-X configuration and descriptor strings, changes only the address, programs once, then reads back and compares **all exposed configuration fields and all four descriptor strings**. USB VID/PID, serial number, power settings, CBUS functions, and other exposed settings are preserved. It does not erase the device or intentionally program the user-data area. This comparison is not a backup or byte-for-byte verification of the entire 2 KB MTP memory. + +If the address already matches, the method returns `false`, does not program memory, and leaves the connection open. Once programming is attempted, it always closes the connection, including on write or verification failure. Failures throw; there is no automatic retry or rollback that might consume more programming cycles. Inspect the stored configuration before deciding whether to retry. + +After programming, **physically disconnect/reconnect USB and open a new instance** to reload the configuration. The method deliberately does not use Windows-only `FT_CyclePort` or assume that an ordinary D2XX device reset reloads MTP. The stored address getter and a successful readback do not prove that the chip is already responding at the new address. Even a no-op can require a reconnect if an earlier programming operation has not yet been reloaded. + +The specified MTP endurance is **2,000 read/write cycles**. Address changes are repeatable, but are intended for occasional configuration, not rapid switching. Do not call programming automatically at sensor startup. + +## Sample and tests + +The [sample](samples/Program.cs) lists matching devices without opening them: + +```shell +dotnet run --project samples/Ft200X.sample.csproj +``` + +Use the displayed serial number for an explicit operation: + +```shell +dotnet run --project samples/Ft200X.sample.csproj -- --status SERIAL +dotnet run --project samples/Ft200X.sample.csproj -- --write SERIAL 01020304 +dotnet run --project samples/Ft200X.sample.csproj -- --read SERIAL 4 +dotnet run --project samples/Ft200X.sample.csproj -- --get-address SERIAL +``` + +Address programming requires the explicit `--confirm` flag: + +```shell +dotnet run --project samples/Ft200X.sample.csproj -- --program-address SERIAL 0x22 --confirm +``` + +This command programs real hardware. It is not part of normal discovery, status checks, or sensor startup. After a changed address is verified, follow the reconnect instruction before running the sensor. + +`--status` opens the adapter and reports its USB receive queue without consuming or transmitting payload data. `--write` and `--read` require cooperating master firmware; a short sample read reports the actual bytes and returns exit code 1. + +### Temperature sensor demo + +```shell +dotnet run --project samples/Ft200X.sample.csproj -- --sensor SERIAL +``` + +This mode mimics a fixed-temperature sensor with three logical 16-bit registers. It is an educational FIFO protocol, **not** an emulation of a standard sensor chip: it cannot service immediate register reads with `WriteRead` or a repeated START. The host PC must receive and process each request over USB before the master starts a separate read. + +Before running, back up the FT200X configuration with [FT_PROG](https://ftdichip.com/utilities/) and set its **7-bit I2C slave address to `0x22`**, either with FT_PROG or the explicit `--program-address` command above. Program only the intended adapter and disconnect/reconnect USB to reload the configuration. This is a persistent MTP setting. The `--sensor` mode does not automatically program or verify it. Make sure no other peripheral uses `0x22` on the same bus. Start with empty FIFOs and the master stopped, for example by power-cycling the adapter before starting the sample. The sample does not purge existing data. + +| Register | Meaning | Access | Initial value | +|----------|---------|--------|---------------| +| `0x00` | Sensor ID | Read-only | `0x2001` | +| `0x01` | Simulated temperature, signed hundredths of degrees Celsius | Read-only | `2350` = 23.50 C | +| `0x02` | Measurement enabled: `0` disabled, `1` enabled | Read/write | `1` | + +The temperature is constant, not a physical measurement. Reading it while disabled returns an explicit error rather than a fabricated temperature. + +Each request and response is exactly four bytes. Values are **big-endian**: + +* Request: `[command, register, value MSB, value LSB]`. Command `0x00` reads (both value bytes must be zero); `0x01` writes. +* Response: `[status, register, value MSB, value LSB]`. Success is status `0x00` with the read or updated value. On error the value bytes are zero. + +| Status | Meaning | +|--------|---------| +| `0x00` | Success | +| `0x01` | Invalid command | +| `0x02` | Unknown register | +| `0x03` | Write to a read-only register | +| `0x04` | Invalid value or nonzero read-request value | +| `0x05` | Temperature measurement disabled | + +Examples: + +| Operation | Master writes | Master later reads | +|-----------|---------------|--------------------| +| Read sensor ID | `00 00 00 00` | `00 00 20 01` | +| Read temperature | `00 01 00 00` | `00 01 09 2E` | +| Disable measurement | `01 02 00 00` | `00 02 00 00` | +| Read disabled temperature | `00 01 00 00` | `05 01 00 00` | +| Enable measurement | `01 02 00 01` | `00 02 00 01` | + +Send **one request at a time** and consume its entire response before sending another. USB read boundaries do not matter: the sample accumulates complete four-byte frames and retains partial requests. There is no automatic resynchronization after dropped bytes or restarting the sample mid-frame. Restart both ends with empty FIFOs after such an error. + +Here is the corresponding master-side .NET code, using a separate I2C master adapter or a board's native bus: + +```csharp +using System; +using System.Device.I2c; +using System.IO; +using System.Threading; + +using I2cDevice sensor = I2cDevice.Create(new I2cConnectionSettings(1, 0x22)); +sensor.Write(new byte[] { 0x00, 0x01, 0x00, 0x00 }); // Read temperature. +Thread.Sleep(100); // Demonstration delay for USB and the PC sample to process the request. +byte[] reply = new byte[4]; +sensor.Read(reply); // Separate transaction, not WriteRead. +if (reply[0] != 0 || reply[1] != 0x01) +{ + throw new IOException($"Unexpected sensor reply: {Convert.ToHexString(reply)}"); +} + +short hundredthsCelsius = (short)((reply[2] << 8) | reply[3]); +Console.WriteLine($"{hundredthsCelsius / 100.0:F2} C"); +``` + +Bus ID `1` is an example; use your master's bus or an `I2cDevice` from an FT232H/FT4222 bus. The 100 ms delay is illustrative, **not a readiness guarantee**. For initial testing, wait for the sample's `Reply:` message before triggering the master read. A production protocol needs explicit readiness signalling (for example, the chip's configured data-available CBUS signal) or another coordinated handshake. Do not poll by blindly consuming FIFO bytes or assume an empty FIFO behaves like an ordinary sensor register. Press Ctrl+C to stop. + +Open `Ft200X.slnx` to work with the binding, sample, and tests. Hardware-independent tests exercise discovery filtering, validation, partial transfers, error propagation, and lifetime: + +```shell +dotnet test tests/Ft200X.Tests.csproj +``` + +For hardware validation on Windows, Linux, and macOS, queue a short known payload and read it from the external master, then write a different payload from the master and verify the exact USB read result. Check short-read timeouts, disconnect errors, reopen, multiple-adapter selection, and backpressure with the master paused. USB-only discovery does not validate I2C communication. + +Protocol reference: [FT200XD datasheet](https://ftdichip.com/wp-content/uploads/2025/06/DS_FT200XD.pdf). diff --git a/src/devices/Ft200X/category.txt b/src/devices/Ft200X/category.txt new file mode 100644 index 0000000000..31846a735f --- /dev/null +++ b/src/devices/Ft200X/category.txt @@ -0,0 +1,3 @@ +i2c +usb +protocol diff --git a/src/devices/Ft200X/samples/Ft200X.sample.csproj b/src/devices/Ft200X/samples/Ft200X.sample.csproj new file mode 100644 index 0000000000..cc66ebb242 --- /dev/null +++ b/src/devices/Ft200X/samples/Ft200X.sample.csproj @@ -0,0 +1,9 @@ + + + Exe + $(DefaultSampleTfms) + + + + + diff --git a/src/devices/Ft200X/samples/Program.cs b/src/devices/Ft200X/samples/Program.cs new file mode 100644 index 0000000000..988cb3553a --- /dev/null +++ b/src/devices/Ft200X/samples/Program.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using Iot.Device.Ft200X; +using Iot.Device.Ft200X.Samples; +using Iot.Device.FtCommon; + +if (args.Length == 0) +{ + var devices = Ft200XDevice.GetDevices(); + foreach (FtDevice device in devices) + { + Console.WriteLine($"{device.SerialNumber}: {device.Description} ({device.Type})"); + } + + if (devices.Count == 0) + { + Console.Error.WriteLine("No FT200X found. Check D2XX installation, USB access, and the USB description."); + return 1; + } + + Console.WriteLine("FT200X is I2C slave-only. An external I2C master must initiate transfers."); + Console.WriteLine("Use --status SERIAL, --read SERIAL COUNT, --write SERIAL HEX, --sensor SERIAL, or --get-address SERIAL."); + Console.WriteLine("Explicit persistent programming: --program-address SERIAL HEX --confirm"); + return 0; +} + +if ((args.Length != 2 || (args[0] != "--status" && args[0] != "--sensor" && args[0] != "--get-address")) && + (args.Length != 3 || (args[0] != "--read" && args[0] != "--write")) && + (args.Length != 4 || args[0] != "--program-address" || args[3] != "--confirm")) +{ + Console.Error.WriteLine("Usage: --status SERIAL | --read SERIAL COUNT | --write SERIAL HEX | --sensor SERIAL | --get-address SERIAL | --program-address SERIAL HEX --confirm"); + return 1; +} + +byte[] payload = Array.Empty(); +int readCount = 0; +int address = 0; +if (args[0] == "--read") +{ + if (!int.TryParse(args[2], NumberStyles.None, CultureInfo.InvariantCulture, out readCount) || readCount < 1 || readCount > 65536) + { + Console.Error.WriteLine("COUNT must be between 1 and 65536 bytes."); + return 1; + } +} +else if (args[0] == "--write") +{ + if (args[2].Length == 0 || args[2].Length % 2 != 0 || !args[2].All(Uri.IsHexDigit)) + { + Console.Error.WriteLine("HEX must contain a nonempty, even number of hexadecimal digits, for example 01020304."); + return 1; + } + + payload = Convert.FromHexString(args[2]); +} +else if (args[0] == "--program-address") +{ + string text = args[2].StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? args[2].Substring(2) : args[2]; + if (!int.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out address) || address < 0x08 || address > 0x77) + { + Console.Error.WriteLine("HEX must be a nonreserved 7-bit address from 0x08 to 0x77, for example 0x22."); + return 1; + } + + Console.WriteLine("WARNING: This persistently programs MTP memory (2,000-cycle endurance)."); + Console.WriteLine("Verify the physical FT200X, back up its configuration, and stop the external I2C master first."); +} + +// Explicit serial selection also supports FT200X devices with customized descriptions. +FtDevice? selected = FtCommon.GetDevices().SingleOrDefault(device => device.Type == FtDeviceType.FtXSeries && device.SerialNumber == args[1]); +if (selected is null) +{ + Console.Error.WriteLine("No FT-X device with the specified serial number was found. Verify that your adapter is an FT200X."); + return 1; +} + +using var adapter = new Ft200XDevice(selected); +if (args[0] == "--program-address") +{ + bool changed = adapter.ProgramI2cAddress(address); + Console.WriteLine(changed + ? $"Programmed and verified address 0x{address:X2}; other configuration fields and USB strings verified unchanged. Connection closed. Disconnect/reconnect USB, then start a new sample instance." + : $"Stored address is already 0x{address:X2}. No MTP write performed. This does not verify the currently active address."); +} +else if (args[0] == "--get-address") +{ + Console.WriteLine($"Stored I2C address: 0x{adapter.ProgrammedI2cAddress:X2}. This reads MTP, not the active I2C address."); +} +else if (args[0] == "--sensor") +{ + RunSensor(adapter); +} +else if (args[0] == "--write") +{ + adapter.Write(payload); + Console.WriteLine($"Queued {payload.Length} bytes for the external I2C master. Delivery on I2C is not confirmed."); +} +else if (args[0] == "--read") +{ + byte[] buffer = new byte[readCount]; + int count = adapter.Read(buffer); + Console.WriteLine($"Received {count} bytes: {Convert.ToHexString(buffer.AsSpan(0, count))}"); + if (count != readCount) + { + Console.Error.WriteLine("The read timeout elapsed before the requested byte count was received."); + return 1; + } +} +else +{ + Console.WriteLine($"Opened FT200X. USB receive queue: {adapter.BytesAvailable} bytes."); +} + +return 0; + +static void RunSensor(Ft200XDevice adapter) +{ + Console.WriteLine("Temperature sensor demo: 23.50 C, ID register 0x00, temperature 0x01, configuration 0x02."); + Console.WriteLine("Configure the FT200X's 7-bit address to 0x22 using --program-address SERIAL 0x22 --confirm or FT_PROG, then reconnect USB."); + Console.WriteLine("This sample does not set or verify the chip's programmed address."); + Console.WriteLine("Master: write [command, register, value MSB, value LSB], wait, then read a 4-byte reply."); + Console.WriteLine("Commands: 0 = read, 1 = write. Reply: [status, register, value MSB, value LSB]."); + Console.WriteLine("Do not use WriteRead/repeated START. Allow USB processing time; see README for master code."); + Console.WriteLine("Send one request at a time and consume the entire reply before the next request. Ctrl+C stops."); + + var sensor = new TemperatureSensor(); + byte[] incoming = new byte[TemperatureSensor.FrameLength]; + byte[] response = new byte[TemperatureSensor.FrameLength]; + using var cancellation = new CancellationTokenSource(); + ConsoleCancelEventHandler cancelHandler = (_, e) => + { + e.Cancel = true; + cancellation.Cancel(); + }; + Console.CancelKeyPress += cancelHandler; + try + { + while (!cancellation.IsCancellationRequested) + { + uint available = adapter.BytesAvailable; + if (available == 0) + { + Thread.Sleep(5); + continue; + } + + int count = adapter.Read(incoming.AsSpan(0, (int)Math.Min(available, (uint)incoming.Length))); + for (int i = 0; i < count; i++) + { + if (sensor.TryProcessByte(incoming[i], response)) + { + adapter.Write(response); + Console.WriteLine($"Reply: {Convert.ToHexString(response)}"); + } + } + } + } + finally + { + Console.CancelKeyPress -= cancelHandler; + } +} diff --git a/src/devices/Ft200X/samples/TemperatureSensor.cs b/src/devices/Ft200X/samples/TemperatureSensor.cs new file mode 100644 index 0000000000..930e87605b --- /dev/null +++ b/src/devices/Ft200X/samples/TemperatureSensor.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; + +namespace Iot.Device.Ft200X.Samples +{ + /// + /// Processes FIFO request frames for a simulated temperature sensor with ID, temperature, and configuration registers. + /// + internal sealed class TemperatureSensor + { + /// + /// The size in bytes of each request and response frame. + /// + public const int FrameLength = 4; + + private const byte ReadCommand = 0; + private const byte WriteCommand = 1; + private const byte IdRegister = 0; + private const byte TemperatureRegister = 1; + private const byte ConfigurationRegister = 2; + private const byte InvalidCommand = 1; + private const byte UnknownRegister = 2; + private const byte ReadOnlyRegister = 3; + private const byte InvalidValue = 4; + private const byte MeasurementDisabled = 5; + private const ushort SensorId = 0x2001; + private const short TemperatureHundredthsCelsius = 2350; + + private readonly byte[] _request = new byte[FrameLength]; + private int _received; + private bool _enabled = true; + + /// + /// Accumulates a request byte and produces a response when a complete frame is available. + /// + /// The next request byte from the USB receive FIFO. + /// The four-byte destination for status, register, and a big-endian 16-bit value. + /// + /// when a response is ready, including protocol error responses; + /// otherwise , leaving the destination unchanged. + /// + /// The response buffer is not exactly four bytes long. + public bool TryProcessByte(byte value, Span response) + { + if (response.Length != FrameLength) + { + throw new ArgumentException("The sensor response must be four bytes long.", nameof(response)); + } + + // USB reads may split a request or combine several I2C writes. + _request[_received++] = value; + if (_received < FrameLength) + { + return false; + } + + _received = 0; + response.Clear(); + byte command = _request[0]; + byte register = _request[1]; + ushort requestedValue = BinaryPrimitives.ReadUInt16BigEndian(_request.AsSpan(2)); + response[1] = register; + if (command != ReadCommand && command != WriteCommand) + { + response[0] = InvalidCommand; + return true; + } + + if (register > ConfigurationRegister) + { + response[0] = UnknownRegister; + return true; + } + + if (command == WriteCommand) + { + if (register != ConfigurationRegister) + { + response[0] = ReadOnlyRegister; + return true; + } + + if (requestedValue > 1) + { + response[0] = InvalidValue; + return true; + } + + _enabled = requestedValue == 1; + } + else if (requestedValue != 0) + { + response[0] = InvalidValue; + return true; + } + + switch (register) + { + case IdRegister: + BinaryPrimitives.WriteUInt16BigEndian(response.Slice(2), SensorId); + break; + case TemperatureRegister: + if (_enabled) + { + BinaryPrimitives.WriteInt16BigEndian(response.Slice(2), TemperatureHundredthsCelsius); + } + else + { + response[0] = MeasurementDisabled; + } + + break; + case ConfigurationRegister: + response[3] = _enabled ? (byte)1 : (byte)0; + break; + } + + return true; + } + } +} diff --git a/src/devices/Ft200X/tests/Ft200X.Tests.csproj b/src/devices/Ft200X/tests/Ft200X.Tests.csproj new file mode 100644 index 0000000000..f06155e6da --- /dev/null +++ b/src/devices/Ft200X/tests/Ft200X.Tests.csproj @@ -0,0 +1,11 @@ + + + $(DefaultTestTfms) + false + false + + + + + + diff --git a/src/devices/Ft200X/tests/Ft200XDeviceTests.cs b/src/devices/Ft200X/tests/Ft200XDeviceTests.cs new file mode 100644 index 0000000000..2c41669e52 --- /dev/null +++ b/src/devices/Ft200X/tests/Ft200XDeviceTests.cs @@ -0,0 +1,522 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Iot.Device.FtCommon; +using Xunit; + +namespace Iot.Device.Ft200X.Tests +{ + public class Ft200XDeviceTests + { + [Theory] + [InlineData("FT200X USB I2C", FtDeviceType.FtXSeries, true)] + [InlineData("ft200x", FtDeviceType.FtXSeries, true)] + [InlineData("FT201X USB I2C", FtDeviceType.FtXSeries, false)] + [InlineData("FT230X Basic UART", FtDeviceType.FtXSeries, false)] + [InlineData("Custom adapter", FtDeviceType.FtXSeries, false)] + [InlineData("FT200X USB I2C", FtDeviceType.Ft232H, false)] + public void DiscoveryDoesNotSelectAllDevicesSharingTheVidPid(string description, FtDeviceType type, bool expected) + { + using var descriptor = Descriptor(description, type); + Assert.Equal(expected, Ft200XDevice.IsFt200X(descriptor)); + } + + [Fact] + public void RejectsNullDescriptorBeforeAccessingNativeLibrary() + { + Assert.Throws(() => new Ft200XDevice(deviceInformation: null!)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("ABC\0DEF")] + public void RequiresASerialNumberSuitableForNativeSelection(string serial) + { + using var descriptor = Descriptor(); + descriptor.SerialNumber = serial; + Assert.Throws(() => new Ft200XDevice(descriptor)); + } + + [Theory] + [InlineData(0, 1000)] + [InlineData(-1, 1000)] + [InlineData(1000, 0)] + [InlineData(1000, -1)] + public void RejectsInvalidTimeoutsBeforeAccessingNativeLibrary(int readTimeout, int writeTimeout) + { + using var descriptor = Descriptor(); + Assert.Throws(() => new Ft200XDevice(descriptor, readTimeout, writeTimeout)); + } + + [Fact] + public void RejectsNonXSeriesDevices() + { + using var descriptor = Descriptor(type: FtDeviceType.Ft232H); + Assert.Throws(() => new Ft200XDevice(descriptor)); + } + + [Theory] + [InlineData(1)] + [InlineData(512)] + [InlineData(513)] + [InlineData(65536)] + public void WritePassesOnlyPayloadBytesWithoutMasterCommands(int length) + { + var transport = new FakeTransport(); + using var device = new Ft200XDevice(transport); + byte[] payload = Enumerable.Range(0, length).Select(i => (byte)i).ToArray(); + device.Write(payload); + Assert.Equal(payload, transport.Written); + Assert.Equal(1, transport.WriteCalls); + } + + [Theory] + [InlineData(0)] + [InlineData(2)] + [InlineData(4)] + public void ReadReturnsTheActualCountIncludingTimeouts(int count) + { + var transport = new FakeTransport { Received = new byte[] { 1, 2, 3, 4 }.Take(count).ToArray() }; + using var device = new Ft200XDevice(transport); + byte[] buffer = Enumerable.Repeat((byte)0xFF, 4).ToArray(); + Assert.Equal(count, device.Read(buffer)); + Assert.Equal(transport.Received, buffer.Take(count)); + Assert.All(buffer.Skip(count), value => Assert.Equal(0xFF, value)); + Assert.Equal(1, transport.ReadCalls); + } + + [Theory] + [InlineData(-1)] + [InlineData(5)] + public void RejectsInvalidReadCounts(int count) + { + var transport = new FakeTransport { ReadCount = count }; + using var device = new Ft200XDevice(transport); + Assert.Throws(() => device.Read(new byte[4])); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(2)] + [InlineData(5)] + public void ShortOrInvalidWritesThrowWithoutRetrying(int count) + { + var transport = new FakeTransport { WriteCount = count }; + using var device = new Ft200XDevice(transport); + IOException error = Assert.Throws(() => device.Write(new byte[] { 1, 2, 3, 4 })); + Assert.Contains($"{count} of 4", error.Message); + Assert.Equal(1, transport.WriteCalls); + } + + [Fact] + public void EmptyTransfersDoNotAccessTransport() + { + var transport = new FakeTransport(); + using var device = new Ft200XDevice(transport); + device.Write(Array.Empty()); + Assert.Equal(0, device.Read(Array.Empty())); + Assert.Equal(0, transport.ReadCalls); + Assert.Equal(0, transport.WriteCalls); + } + + [Fact] + public void BytesAvailableReportsTheUsbReceiveQueueWithoutReadingIt() + { + var transport = new FakeTransport { QueuedBytes = 123 }; + using var device = new Ft200XDevice(transport); + Assert.Equal(123u, device.BytesAvailable); + Assert.Equal(0, transport.ReadCalls); + Assert.Equal(0, transport.WriteCalls); + } + + [Fact] + public void NativeFailuresPropagateWithoutRetriesOrSuccessFallbacks() + { + var error = new IOException("Simulated D2XX failure."); + var transport = new FakeTransport { Error = error }; + using var device = new Ft200XDevice(transport); + Assert.Same(error, Assert.Throws(() => device.Read(new byte[1]))); + Assert.Same(error, Assert.Throws(() => device.Write(new byte[1]))); + Assert.Same(error, Assert.Throws(() => device.BytesAvailable)); + Assert.Equal(1, transport.ReadCalls); + Assert.Equal(1, transport.WriteCalls); + } + + [Fact] + public void DisposalIsIdempotentAndAllOperationsRejectDisposedDevices() + { + var transport = new FakeTransport(); + var device = new Ft200XDevice(transport); + device.Dispose(); + device.Dispose(); + Assert.Equal(1, transport.DisposeCalls); + Assert.Throws(() => device.Read(new byte[1])); + Assert.Throws(() => device.Write(new byte[1])); + Assert.Throws(() => device.Read(Array.Empty())); + Assert.Throws(() => device.Write(Array.Empty())); + Assert.Throws(() => device.BytesAvailable); + Assert.Equal(0, transport.ReadCalls); + Assert.Equal(0, transport.WriteCalls); + } + + [Fact] + public async Task DisposalWaitsForAnOutstandingRead() + { + var transport = new FakeTransport(); + using var device = new Ft200XDevice(transport); + using var reading = new ManualResetEventSlim(); + using var release = new ManualResetEventSlim(); + using var disposing = new ManualResetEventSlim(); + transport.OnRead = () => + { + reading.Set(); + Assert.True(release.Wait(TimeSpan.FromSeconds(5))); + Assert.Equal(0, transport.DisposeCalls); + }; + + Task read = Task.Run(() => device.Read(new byte[1])); + Task? dispose = null; + try + { + Assert.True(reading.Wait(TimeSpan.FromSeconds(5))); + dispose = Task.Run(() => + { + disposing.Set(); + device.Dispose(); + }); + Assert.True(disposing.Wait(TimeSpan.FromSeconds(5))); + Assert.Equal(0, transport.DisposeCalls); + } + finally + { + release.Set(); + await read; + if (dispose is not null) + { + await dispose; + } + } + + Assert.Equal(1, transport.DisposeCalls); + } + + [Fact] + public void EepromLayoutMatchesTheD2xxNativeAbi() + { + Assert.Equal(16, Marshal.SizeOf()); + Assert.Equal(56, Marshal.SizeOf()); + Assert.Equal(10, Marshal.OffsetOf(nameof(FtEepromHeader.MaxPower)).ToInt32()); + Assert.Equal(16, Marshal.OffsetOf(nameof(FtXSeriesEeprom.AcSlowSlew)).ToInt32()); + Assert.Equal(40, Marshal.OffsetOf(nameof(FtXSeriesEeprom.I2cSlaveAddress)).ToInt32()); + Assert.Equal(44, Marshal.OffsetOf(nameof(FtXSeriesEeprom.I2cDeviceId)).ToInt32()); + Assert.Equal(54, Marshal.OffsetOf(nameof(FtXSeriesEeprom.DriverType)).ToInt32()); + } + + [Fact] + public void ReadingTheProgrammedAddressDoesNotWriteOrDispose() + { + var transport = new FakeTransport { Configuration = Configuration() }; + using var device = new Ft200XDevice(transport); + Assert.Equal(0x22, device.ProgrammedI2cAddress); + Assert.Equal(1, transport.ConfigurationReads); + Assert.Equal(0, transport.ConfigurationWrites); + Assert.Equal(0, transport.DisposeCalls); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(7)] + [InlineData(0x78)] + [InlineData(0x7F)] + [InlineData(0x80)] + [InlineData(0x123)] + public void ProgrammingRejectsReservedAndInvalidAddressesBeforeReadingMtp(int address) + { + var transport = new FakeTransport(); + using var device = new Ft200XDevice(transport); + Assert.Throws(() => device.ProgramI2cAddress(address)); + Assert.Equal(0, transport.ConfigurationReads); + Assert.Equal(0, transport.ConfigurationWrites); + Assert.Equal(0, transport.DisposeCalls); + } + + [Fact] + public void MatchingAddressSkipsProgrammingAndKeepsTheConnectionOpen() + { + var transport = new FakeTransport { Configuration = Configuration() }; + using var device = new Ft200XDevice(transport); + Assert.False(device.ProgramI2cAddress(0x22)); + Assert.Equal(1, transport.ConfigurationReads); + Assert.Equal(0, transport.ConfigurationWrites); + Assert.Equal(0, transport.DisposeCalls); + Assert.Equal(0u, device.BytesAvailable); + } + + [Theory] + [InlineData(0x08)] + [InlineData(0x23)] + [InlineData(0x77)] + public void ProgrammingChangesOnlyTheAddressAndVerifiesBeforeClosing(int address) + { + FtXSeriesConfiguration original = Configuration(); + var transport = new FakeTransport { Configuration = original }; + using var device = new Ft200XDevice(transport); + Assert.True(device.ProgramI2cAddress(address)); + Assert.Equal(2, transport.ConfigurationReads); + Assert.Equal(1, transport.ConfigurationWrites); + Assert.Equal(1, transport.DisposeCalls); + FtXSeriesEeprom programmed = transport.Configuration.Data; + Assert.Equal(address, programmed.I2cSlaveAddress); + programmed.I2cSlaveAddress = original.Data.I2cSlaveAddress; + Assert.Equal(original.Data, programmed); + Assert.Equal(original.Manufacturer, transport.Configuration.Manufacturer); + Assert.Equal(original.ManufacturerId, transport.Configuration.ManufacturerId); + Assert.Equal(original.Description, transport.Configuration.Description); + Assert.Equal(original.SerialNumber, transport.Configuration.SerialNumber); + Assert.Equal(0x22, original.Data.I2cSlaveAddress); + Assert.Throws(() => device.ProgrammedI2cAddress); + Assert.Throws(() => device.ProgramI2cAddress(address)); + Assert.Throws(() => device.Write(new byte[1])); + } + + [Theory] + [InlineData("address")] + [InlineData("cbus")] + [InlineData("vid")] + [InlineData("i2c-id")] + [InlineData("manufacturer")] + [InlineData("manufacturer-id")] + [InlineData("description")] + [InlineData("serial")] + public void VerificationDetectsUnintendedChangesWithoutRetrying(string field) + { + var transport = new FakeTransport { Configuration = Configuration() }; + transport.AfterProgram = configuration => + { + FtXSeriesEeprom data = configuration.Data; + switch (field) + { + case "address": + data.I2cSlaveAddress = 0x22; + break; + case "cbus": + data.Cbus0++; + break; + case "vid": + data.Common.VendorId++; + break; + case "i2c-id": + data.I2cDeviceId++; + break; + case "manufacturer": + configuration.Manufacturer[0]++; + break; + case "manufacturer-id": + configuration.ManufacturerId[0]++; + break; + case "description": + configuration.Description[0]++; + break; + case "serial": + configuration.SerialNumber[0]++; + break; + } + + configuration.Data = data; + }; + + using var device = new Ft200XDevice(transport); + Assert.Throws(() => device.ProgramI2cAddress(0x23)); + Assert.Equal(1, transport.ConfigurationWrites); + Assert.Equal(1, transport.DisposeCalls); + } + + [Theory] + [InlineData(1, 0, 0)] + [InlineData(2, 1, 1)] + public void ReadFailuresNeverTriggerAutomaticProgrammingOrRollback(int failedRead, int writes, int disposed) + { + var failure = new IOException("Simulated EEPROM read failure."); + var transport = new FakeTransport + { + Configuration = Configuration(), + ConfigurationReadError = failure, + FailConfigurationRead = failedRead + }; + using var device = new Ft200XDevice(transport); + Assert.Same(failure, Assert.Throws(() => device.ProgramI2cAddress(0x23))); + Assert.Equal(writes, transport.ConfigurationWrites); + Assert.Equal(disposed, transport.DisposeCalls); + } + + [Fact] + public void ProgramFailureClosesTheConnectionWithoutRetrying() + { + var failure = new IOException("Simulated EEPROM programming failure."); + var transport = new FakeTransport { Configuration = Configuration(), ConfigurationProgramError = failure }; + using var device = new Ft200XDevice(transport); + Assert.Same(failure, Assert.Throws(() => device.ProgramI2cAddress(0x23))); + Assert.Equal(1, transport.ConfigurationReads); + Assert.Equal(1, transport.ConfigurationWrites); + Assert.Equal(1, transport.DisposeCalls); + } + + [Theory] + [InlineData("type")] + [InlineData("manufacturer")] + [InlineData("manufacturer-id")] + [InlineData("description")] + [InlineData("serial")] + public void InvalidSnapshotsCannotBeProgrammed(string field) + { + FtXSeriesConfiguration configuration = Configuration(); + switch (field) + { + case "type": + FtXSeriesEeprom data = configuration.Data; + data.Common.DeviceType = FtDeviceType.Ft232H; + configuration.Data = data; + break; + case "manufacturer": + Array.Fill(configuration.Manufacturer, (byte)1); + break; + case "manufacturer-id": + Array.Fill(configuration.ManufacturerId, (byte)1); + break; + case "description": + Array.Fill(configuration.Description, (byte)1); + break; + case "serial": + Array.Fill(configuration.SerialNumber, (byte)1); + break; + } + + var transport = new FakeTransport { Configuration = configuration }; + using var device = new Ft200XDevice(transport); + Assert.Throws(() => device.ProgramI2cAddress(0x23)); + Assert.Equal(0, transport.ConfigurationWrites); + Assert.Equal(0, transport.DisposeCalls); + } + + private static FtXSeriesConfiguration Configuration() + { + byte[] bytes = Enumerable.Range(1, 56).Select(i => (byte)i).ToArray(); + FtXSeriesEeprom data = MemoryMarshal.Read(bytes); + data.Common.DeviceType = FtDeviceType.FtXSeries; + data.I2cSlaveAddress = 0x22; + var configuration = new FtXSeriesConfiguration { Data = data }; + // Keep non-ASCII descriptor bytes intact rather than decoding and re-encoding them. + new byte[] { 0xC4, 0x54, 0x44, 0x49 }.CopyTo(configuration.Manufacturer, 0); + new byte[] { 0x46, 0x54 }.CopyTo(configuration.ManufacturerId, 0); + new byte[] { 0x46, 0x54, 0x32, 0x30, 0x30, 0x58 }.CopyTo(configuration.Description, 0); + new byte[] { 0x31, 0x32, 0x33 }.CopyTo(configuration.SerialNumber, 0); + return configuration; + } + + private static FtDevice Descriptor(string description = "FT200X USB I2C", FtDeviceType type = FtDeviceType.FtXSeries) + { + return new FtDevice(0, type, 0x04036015, 0, "TEST123", description); + } + + private sealed class FakeTransport : IFtXSeriesI2cSlaveTransport + { + public byte[] Received { get; set; } = Array.Empty(); + + public byte[] Written { get; private set; } = Array.Empty(); + + public int? ReadCount { get; set; } + + public int? WriteCount { get; set; } + + public uint QueuedBytes { get; set; } + + public IOException? Error { get; set; } + + public Action? OnRead { get; set; } + + public int ReadCalls { get; private set; } + + public int WriteCalls { get; private set; } + + public int DisposeCalls { get; private set; } + + public FtXSeriesConfiguration Configuration { get; set; } = new FtXSeriesConfiguration(); + + public int ConfigurationReads { get; private set; } + + public int ConfigurationWrites { get; private set; } + + public int? FailConfigurationRead { get; set; } + + public IOException? ConfigurationReadError { get; set; } + + public IOException? ConfigurationProgramError { get; set; } + + public Action? AfterProgram { get; set; } + + public uint BytesAvailable => Error is null ? QueuedBytes : throw Error; + + public int Read(Span buffer) + { + ReadCalls++; + if (Error is not null) + { + throw Error; + } + + OnRead?.Invoke(); + Received.CopyTo(buffer); + return ReadCount ?? Received.Length; + } + + public int Write(ReadOnlySpan buffer) + { + WriteCalls++; + if (Error is not null) + { + throw Error; + } + + Written = buffer.ToArray(); + return WriteCount ?? buffer.Length; + } + + public FtXSeriesConfiguration ReadConfiguration() + { + ConfigurationReads++; + if (ConfigurationReadError is not null && ConfigurationReads == FailConfigurationRead) + { + throw ConfigurationReadError; + } + + return Configuration.Clone(); + } + + public void ProgramConfiguration(FtXSeriesConfiguration configuration) + { + ConfigurationWrites++; + if (ConfigurationProgramError is not null) + { + throw ConfigurationProgramError; + } + + Configuration = configuration.Clone(); + AfterProgram?.Invoke(Configuration); + } + + public void Dispose() + { + DisposeCalls++; + } + } + } +} diff --git a/src/devices/Ft200X/tests/FtXSeriesConfigurationTests.cs b/src/devices/Ft200X/tests/FtXSeriesConfigurationTests.cs new file mode 100644 index 0000000000..0e95e41296 --- /dev/null +++ b/src/devices/Ft200X/tests/FtXSeriesConfigurationTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Iot.Device.FtCommon; +using Xunit; + +namespace Iot.Device.Ft200X.Tests +{ + public class FtXSeriesConfigurationTests + { + [Fact] + public void ClonePreservesAllFieldsAndCopiesEntireDescriptorBuffers() + { + byte[] bytes = Enumerable.Range(1, 56).Select(i => (byte)i).ToArray(); + var original = new FtXSeriesConfiguration { Data = MemoryMarshal.Read(bytes) }; + Array.Fill(original.Manufacturer, (byte)0x80); + Array.Fill(original.ManufacturerId, (byte)0x81); + Array.Fill(original.Description, (byte)0x82); + Array.Fill(original.SerialNumber, (byte)0x83); + original.Manufacturer[1] = 0; + original.ManufacturerId[1] = 0; + original.Description[1] = 0; + original.SerialNumber[1] = 0; + + FtXSeriesConfiguration clone = original.Clone(); + + Assert.NotSame(original, clone); + Assert.Equal(original.Data, clone.Data); + Assert.True(original.HasSameContents(clone)); + Assert.NotSame(original.Manufacturer, clone.Manufacturer); + Assert.NotSame(original.ManufacturerId, clone.ManufacturerId); + Assert.NotSame(original.Description, clone.Description); + Assert.NotSame(original.SerialNumber, clone.SerialNumber); + Assert.Equal(original.Manufacturer, clone.Manufacturer); + Assert.Equal(original.ManufacturerId, clone.ManufacturerId); + Assert.Equal(original.Description, clone.Description); + Assert.Equal(original.SerialNumber, clone.SerialNumber); + + FtXSeriesEeprom data = clone.Data; + data.Cbus0++; + clone.Data = data; + clone.Manufacturer[0]++; + clone.ManufacturerId[0]++; + clone.Description[0]++; + clone.SerialNumber[0]++; + + Assert.Equal(MemoryMarshal.Read(bytes), original.Data); + Assert.Equal((byte)0x80, original.Manufacturer[0]); + Assert.Equal((byte)0x81, original.ManufacturerId[0]); + Assert.Equal((byte)0x82, original.Description[0]); + Assert.Equal((byte)0x83, original.SerialNumber[0]); + } + + [Fact] + public void ComparisonIgnoresDescriptorBytesAfterNullTerminators() + { + var original = new FtXSeriesConfiguration(); + FtXSeriesConfiguration clone = original.Clone(); + clone.Manufacturer[1] = 1; + clone.ManufacturerId[1] = 2; + clone.Description[1] = 3; + clone.SerialNumber[1] = 4; + + original.Validate(); + clone.Validate(); + Assert.True(original.HasSameContents(clone)); + } + } +} diff --git a/src/devices/Ft200X/tests/TemperatureSensorTests.cs b/src/devices/Ft200X/tests/TemperatureSensorTests.cs new file mode 100644 index 0000000000..aef1d314a6 --- /dev/null +++ b/src/devices/Ft200X/tests/TemperatureSensorTests.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Iot.Device.Ft200X.Samples; +using Xunit; + +namespace Iot.Device.Ft200X.Tests +{ + public class TemperatureSensorTests + { + [Theory] + [InlineData("00000000", "00002001")] + [InlineData("00010000", "0001092E")] + [InlineData("00020000", "00020001")] + [InlineData("02010000", "01010000")] + [InlineData("00FF0000", "02FF0000")] + [InlineData("01FF0000", "02FF0000")] + [InlineData("01000001", "03000000")] + [InlineData("01010001", "03010000")] + [InlineData("01020002", "04020000")] + [InlineData("01020100", "04020000")] + [InlineData("00010001", "04010000")] + [InlineData("00010100", "04010000")] + public void ImplementsRegisterMapAndExplicitErrorReplies(string requestHex, string expectedHex) + { + var sensor = new TemperatureSensor(); + Assert.Equal(Convert.FromHexString(expectedHex), Exchange(sensor, Convert.FromHexString(requestHex))); + } + + [Fact] + public void ConfigurationDisablesAndReenablesTemperatureReads() + { + var sensor = new TemperatureSensor(); + Assert.Equal(Convert.FromHexString("00020000"), Exchange(sensor, Convert.FromHexString("01020000"))); + Assert.Equal(Convert.FromHexString("00020000"), Exchange(sensor, Convert.FromHexString("00020000"))); + Assert.Equal(Convert.FromHexString("05010000"), Exchange(sensor, Convert.FromHexString("00010000"))); + Assert.Equal(Convert.FromHexString("00002001"), Exchange(sensor, Convert.FromHexString("00000000"))); + Assert.Equal(Convert.FromHexString("00020001"), Exchange(sensor, Convert.FromHexString("01020001"))); + Assert.Equal(Convert.FromHexString("0001092E"), Exchange(sensor, Convert.FromHexString("00010000"))); + } + + [Fact] + public void InvalidWriteDoesNotChangeConfiguration() + { + var sensor = new TemperatureSensor(); + Exchange(sensor, Convert.FromHexString("01020000")); + Assert.Equal(Convert.FromHexString("04020000"), Exchange(sensor, Convert.FromHexString("0102FFFF"))); + Assert.Equal(Convert.FromHexString("00020000"), Exchange(sensor, Convert.FromHexString("00020000"))); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void PartialRequestsProduceNoReplyAndRetainTheirBytes(int split) + { + var sensor = new TemperatureSensor(); + byte[] request = Convert.FromHexString("00010000"); + byte[] response = Convert.FromHexString("FFFFFFFF"); + for (int i = 0; i < split; i++) + { + Assert.False(sensor.TryProcessByte(request[i], response)); + } + + Assert.Equal(Convert.FromHexString("FFFFFFFF"), response); + for (int i = split; i < request.Length; i++) + { + Assert.Equal(i == request.Length - 1, sensor.TryProcessByte(request[i], response)); + } + + Assert.Equal(Convert.FromHexString("0001092E"), response); + } + + [Fact] + public void ConsecutiveFramesHaveIndependentRepliesWithoutStaleBytes() + { + var sensor = new TemperatureSensor(); + byte[] response = new byte[4]; + var replies = new List(); + foreach (byte value in Convert.FromHexString("000000000001000000FF000000020000")) + { + if (sensor.TryProcessByte(value, response)) + { + replies.Add(Convert.ToHexString(response)); + } + } + + Assert.Equal(new[] { "00002001", "0001092E", "02FF0000", "00020001" }, replies); + } + + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(5)] + public void InvalidResponseBufferDoesNotConsumeInput(int length) + { + var sensor = new TemperatureSensor(); + Assert.Throws(() => sensor.TryProcessByte(0xFF, new byte[length])); + Assert.Equal(Convert.FromHexString("00002001"), Exchange(sensor, Convert.FromHexString("00000000"))); + } + + private static byte[] Exchange(TemperatureSensor sensor, byte[] request) + { + byte[] response = new byte[4]; + for (int i = 0; i < request.Length; i++) + { + Assert.Equal(i == request.Length - 1, sensor.TryProcessByte(request[i], response)); + } + + return response; + } + } +} diff --git a/src/devices/FtCommon/FtCommon.cs b/src/devices/FtCommon/FtCommon.cs index 511f0a9d4b..91d4809b2d 100644 --- a/src/devices/FtCommon/FtCommon.cs +++ b/src/devices/FtCommon/FtCommon.cs @@ -28,6 +28,10 @@ public static List GetDevices() // Check device uint numOfDevices; ftStatus = FtFunction.FT_CreateDeviceInfoList(out numOfDevices); + if (ftStatus != FtStatus.Ok) + { + throw new IOException($"Can't enumerate FTDI devices, error {ftStatus}"); + } Debug.WriteLine($"Number of devices: {numOfDevices}"); if (numOfDevices == 0) diff --git a/src/devices/FtCommon/FtCommon.projitems b/src/devices/FtCommon/FtCommon.projitems index 2a3bbcf29c..f127e2eb76 100644 --- a/src/devices/FtCommon/FtCommon.projitems +++ b/src/devices/FtCommon/FtCommon.projitems @@ -14,12 +14,18 @@ + + + + + + \ No newline at end of file diff --git a/src/devices/FtCommon/FtEepromHeader.cs b/src/devices/FtCommon/FtEepromHeader.cs new file mode 100644 index 0000000000..2b8f82567c --- /dev/null +++ b/src/devices/FtCommon/FtEepromHeader.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Iot.Device.FtCommon +{ + /// + /// Represents the native FT_EEPROM_HEADER shared by FTDI EEPROM configuration structures. + /// See the native structure documentation. + /// + /// + /// The native structure is 16 bytes with 4-byte packing. Field offsets are byte offsets + /// from the start of this structure, not addresses in MTP memory. + /// Padding occupies offsets 9 and 15. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct FtEepromHeader + { + /// + /// Offset 0 (0x00): The FTDI chip type identifying the device-specific configuration layout. + /// + public FtDeviceType DeviceType; + + /// + /// Offset 4 (0x04): The USB vendor identifier. + /// + public ushort VendorId; + + /// + /// Offset 6 (0x06): The USB product identifier. + /// + public ushort ProductId; + + /// + /// Offset 8 (0x08): Nonzero to include the serial number in the USB descriptors. + /// + public byte SerialNumberEnabled; + + /// + /// Offset 10 (0x0A): The maximum USB current consumption in milliamperes as represented by D2XX. + /// + public ushort MaxPower; + + /// + /// Offset 12 (0x0C): Nonzero when the device is self-powered rather than USB bus-powered. + /// + public byte SelfPowered; + + /// + /// Offset 13 (0x0D): Nonzero when the USB device supports remote wakeup. + /// + public byte RemoteWakeup; + + /// + /// Offset 14 (0x0E): Nonzero to enable I/O pull-downs during USB suspend. + /// + public byte PullDownEnable; + } +} diff --git a/src/devices/FtCommon/FtFunction.cs b/src/devices/FtCommon/FtFunction.cs index 16d64443b6..6100fb1484 100644 --- a/src/devices/FtCommon/FtFunction.cs +++ b/src/devices/FtCommon/FtFunction.cs @@ -44,6 +44,16 @@ internal class FtFunction [DllImport("ftd2xx")] public static extern FtStatus FT_OpenEx(uint pvArg1, FtOpenType dwFlags, out SafeFtHandle ftHandle); + /// + /// Opens a device using its serial number or description. + /// + /// The serial number or description selected by the flags. + /// How to interpret the identifier. + /// The handle of the open device. + /// The status. + [DllImport("ftd2xx", EntryPoint = "FT_OpenEx", CharSet = CharSet.Ansi, ExactSpelling = true)] + public static extern FtStatus FT_OpenEx(string identifier, FtOpenType flags, out SafeFtHandle ftHandle); + /// /// Close the device. /// @@ -140,6 +150,38 @@ internal class FtFunction [DllImport("ftd2xx")] public static extern FtStatus FT_Write(SafeFtHandle ftHandle, in byte lpBuffer, uint dwBytesToWrite, ref uint lpdwBytesWritten); + /// + /// Reads the stored FT-X configuration fields and null-terminated USB descriptor strings. + /// + /// The open D2XX device handle. + /// The FT-X configuration, initialized with its device type. + /// The native configuration structure size in bytes. + /// The destination manufacturer string buffer. + /// The destination manufacturer ID string buffer. + /// The destination device description string buffer. + /// The destination serial number string buffer. + /// The native D2XX operation status. + [DllImport("ftd2xx", ExactSpelling = true)] + public static extern FtStatus FT_EEPROM_Read( + SafeFtHandle handle, ref FtXSeriesEeprom data, uint dataSize, + [Out] byte[] manufacturer, [Out] byte[] manufacturerId, [Out] byte[] description, [Out] byte[] serialNumber); + + /// + /// Programs the supplied FT-X configuration fields and null-terminated USB descriptor strings. + /// + /// The open D2XX device handle. + /// The FT-X configuration to persist. + /// The native configuration structure size in bytes. + /// The null-terminated manufacturer string buffer. + /// The null-terminated manufacturer ID string buffer. + /// The null-terminated device description string buffer. + /// The null-terminated serial number string buffer. + /// The native D2XX operation status. + [DllImport("ftd2xx", ExactSpelling = true)] + public static extern FtStatus FT_EEPROM_Program( + SafeFtHandle handle, ref FtXSeriesEeprom data, uint dataSize, + [In] byte[] manufacturer, [In] byte[] manufacturerId, [In] byte[] description, [In] byte[] serialNumber); + /// /// Sets chars for interruption. /// diff --git a/src/devices/FtCommon/FtXSeriesConfiguration.cs b/src/devices/FtCommon/FtXSeriesConfiguration.cs new file mode 100644 index 0000000000..25d1415e22 --- /dev/null +++ b/src/devices/FtCommon/FtXSeriesConfiguration.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; + +namespace Iot.Device.FtCommon +{ + /// + /// Holds an FT-X configuration snapshot and its null-terminated native USB descriptor buffers. + /// + internal sealed class FtXSeriesConfiguration + { + /// + /// Gets or sets the configuration fields represented by the native FT-X EEPROM structure. + /// + public FtXSeriesEeprom Data { get; set; } = new FtXSeriesEeprom + { + Common = new FtEepromHeader { DeviceType = FtDeviceType.FtXSeries } + }; + + /// + /// Gets the null-terminated manufacturer string buffer. + /// + /// The capacity exceeds the documented descriptor limits because D2XX accepts no buffer length. + public byte[] Manufacturer { get; } = new byte[256]; + + /// + /// Gets the null-terminated manufacturer ID string buffer. + /// + public byte[] ManufacturerId { get; } = new byte[256]; + + /// + /// Gets the null-terminated device description string buffer. + /// + public byte[] Description { get; } = new byte[256]; + + /// + /// Gets the null-terminated serial number string buffer. + /// + public byte[] SerialNumber { get; } = new byte[256]; + + /// + /// Verifies the device type and termination of all descriptor strings. + /// + /// The device type is unexpected or a descriptor lacks a null terminator. + public void Validate() + { + if (Data.Common.DeviceType != FtDeviceType.FtXSeries) + { + throw new IOException("The FT-X configuration contains an unexpected EEPROM device type."); + } + + GetStringBytes(Manufacturer); + GetStringBytes(ManufacturerId); + GetStringBytes(Description); + GetStringBytes(SerialNumber); + } + + /// + /// Creates an independent copy of the configuration and all descriptor buffers. + /// + /// A snapshot containing copies of the configuration fields and descriptor buffers. + public FtXSeriesConfiguration Clone() + { + var configuration = new FtXSeriesConfiguration { Data = Data }; + Manufacturer.CopyTo(configuration.Manufacturer, 0); + ManufacturerId.CopyTo(configuration.ManufacturerId, 0); + Description.CopyTo(configuration.Description, 0); + SerialNumber.CopyTo(configuration.SerialNumber, 0); + return configuration; + } + + /// + /// Compares the exposed configuration fields and descriptor strings, ignoring bytes after their null terminators. + /// + /// The configuration snapshot to compare. + /// Whether all configuration fields and descriptor string contents match. + /// A compared descriptor lacks a null terminator. + public bool HasSameContents(FtXSeriesConfiguration other) + { + return Data.Equals(other.Data) && + GetStringBytes(Manufacturer).SequenceEqual(GetStringBytes(other.Manufacturer)) && + GetStringBytes(ManufacturerId).SequenceEqual(GetStringBytes(other.ManufacturerId)) && + GetStringBytes(Description).SequenceEqual(GetStringBytes(other.Description)) && + GetStringBytes(SerialNumber).SequenceEqual(GetStringBytes(other.SerialNumber)); + } + + private static ReadOnlySpan GetStringBytes(byte[] buffer) + { + int length = buffer.AsSpan().IndexOf((byte)0); + if (length < 0) + { + throw new IOException("The FT-X configuration contains an unterminated EEPROM descriptor string."); + } + + return buffer.AsSpan(0, length); + } + } +} diff --git a/src/devices/FtCommon/FtXSeriesEeprom.cs b/src/devices/FtCommon/FtXSeriesEeprom.cs new file mode 100644 index 0000000000..669bb2598f --- /dev/null +++ b/src/devices/FtCommon/FtXSeriesEeprom.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Iot.Device.FtCommon +{ + /// + /// Represents the native FT_EEPROM_X_SERIES layout, including its separately aligned common header. + /// See the native structure documentation. + /// + /// + /// The native structure is 56 bytes with 4-byte packing. Field offsets are byte offsets + /// from the start of this structure, not addresses in MTP memory. + /// The common header occupies offsets 0 through 15; additional padding occupies offsets 42, 43, and 55. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct FtXSeriesEeprom + { + /// + /// Offset 0 (0x00): The device-independent EEPROM configuration fields. + /// + public FtEepromHeader Common; + + /// + /// Offset 16 (0x10): Nonzero to enable slow slew rates on AC bus pins. + /// + public byte AcSlowSlew; + + /// + /// Offset 17 (0x11): Nonzero to enable Schmitt inputs on AC bus pins. + /// + public byte AcSchmittInput; + + /// + /// Offset 18 (0x12): The AC bus drive current in milliamperes. + /// + public byte AcDriveCurrent; + + /// + /// Offset 19 (0x13): Nonzero to enable slow slew rates on AD bus pins. + /// + public byte AdSlowSlew; + + /// + /// Offset 20 (0x14): Nonzero to enable Schmitt inputs on AD bus pins. + /// + public byte AdSchmittInput; + + /// + /// Offset 21 (0x15): The AD bus drive current in milliamperes. + /// + public byte AdDriveCurrent; + + /// + /// Offset 22 (0x16): The native function selector for CBUS0. + /// + public byte Cbus0; + + /// + /// Offset 23 (0x17): The native function selector for CBUS1. + /// + public byte Cbus1; + + /// + /// Offset 24 (0x18): The native function selector for CBUS2. + /// + public byte Cbus2; + + /// + /// Offset 25 (0x19): The native function selector for CBUS3. + /// + public byte Cbus3; + + /// + /// Offset 26 (0x1A): The native function selector for CBUS4. + /// + public byte Cbus4; + + /// + /// Offset 27 (0x1B): The native function selector for CBUS5. + /// + public byte Cbus5; + + /// + /// Offset 28 (0x1C): The native function selector for CBUS6. + /// + public byte Cbus6; + + /// + /// Offset 29 (0x1D): Nonzero to invert the UART TXD signal on applicable FT-X devices. + /// + public byte InvertTxd; + + /// + /// Offset 30 (0x1E): Nonzero to invert the UART RXD signal on applicable FT-X devices. + /// + public byte InvertRxd; + + /// + /// Offset 31 (0x1F): Nonzero to invert the UART RTS signal on applicable FT-X devices. + /// + public byte InvertRts; + + /// + /// Offset 32 (0x20): Nonzero to invert the UART CTS signal on applicable FT-X devices. + /// + public byte InvertCts; + + /// + /// Offset 33 (0x21): Nonzero to invert the UART DTR signal on applicable FT-X devices. + /// + public byte InvertDtr; + + /// + /// Offset 34 (0x22): Nonzero to invert the UART DSR signal on applicable FT-X devices. + /// + public byte InvertDsr; + + /// + /// Offset 35 (0x23): Nonzero to invert the UART DCD signal on applicable FT-X devices. + /// + public byte InvertDcd; + + /// + /// Offset 36 (0x24): Nonzero to invert the UART RI signal on applicable FT-X devices. + /// + public byte InvertRi; + + /// + /// Offset 37 (0x25): Nonzero to enable battery charger detection. + /// + public byte BcdEnable; + + /// + /// Offset 38 (0x26): Nonzero to assert the CBUS power-enable signal when a charging port is detected. + /// + public byte BcdForceCbusPwren; + + /// + /// Offset 39 (0x27): Nonzero to disable sleep for battery charging. + /// + public byte BcdDisableSleep; + + /// + /// Offset 40 (0x28): The stored, unshifted I2C slave address. + /// + public ushort I2cSlaveAddress; + + /// + /// Offset 44 (0x2C): The stored I2C device identifier, distinct from the slave address. + /// + public uint I2cDeviceId; + + /// + /// Offset 48 (0x30): Nonzero to disable Schmitt triggering on the I2C pins. + /// + public byte I2cDisableSchmitt; + + /// + /// Offset 49 (0x31): The FT1248 clock polarity on applicable FT-X devices. + /// + public byte Ft1248Cpol; + + /// + /// Offset 50 (0x32): Nonzero to transfer FT1248 data least-significant bit first on applicable devices. + /// + public byte Ft1248Lsb; + + /// + /// Offset 51 (0x33): Nonzero to enable FT1248 flow control on applicable devices. + /// + public byte Ft1248FlowControl; + + /// + /// Offset 52 (0x34): Nonzero to suppress RS485 echo on applicable FT-X devices. + /// + public byte Rs485EchoSuppress; + + /// + /// Offset 53 (0x35): The native power-saving configuration flag. + /// + public byte PowerSaveEnable; + + /// + /// Offset 54 (0x36): The native driver selection value for D2XX or virtual COM port operation. + /// + public byte DriverType; + + } +} diff --git a/src/devices/FtCommon/FtXSeriesI2cSlaveTransport.cs b/src/devices/FtCommon/FtXSeriesI2cSlaveTransport.cs new file mode 100644 index 0000000000..5262ef0eeb --- /dev/null +++ b/src/devices/FtCommon/FtXSeriesI2cSlaveTransport.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Iot.Device.FtCommon +{ + /// + /// Implements FT-X I2C slave FIFO and MTP configuration access using the native D2XX library. + /// + /// + /// The caller must select an FT-X I2C slave device, such as an FT200XD or FT201X. + /// Other FT-X devices share the D2XX device type but do not provide the same I2C functionality. + /// The caller serializes operations and owns address validation and programming verification. + /// + internal sealed class FtXSeriesI2cSlaveTransport : IFtXSeriesI2cSlaveTransport + { + private readonly SafeFtHandle _handle; + + /// + /// Opens an FT-X I2C slave by serial number and configures its USB transfer timeouts. + /// + /// The unique serial number used to select the device. + /// The D2XX read timeout in milliseconds. + /// The D2XX write timeout in milliseconds. + internal FtXSeriesI2cSlaveTransport(string serialNumber, int readTimeoutMilliseconds, int writeTimeoutMilliseconds) + { + FtStatus status = FtFunction.FT_OpenEx(serialNumber, FtOpenType.OpenBySerialNumber, out SafeFtHandle handle); + if (status != FtStatus.Ok || handle is null || handle.IsInvalid) + { + handle?.Dispose(); + throw new IOException($"FT-X I2C slave failed to open, D2XX status: {status}."); + } + + _handle = handle; + try + { + CheckStatus(FtFunction.FT_SetTimeouts(_handle, (uint)readTimeoutMilliseconds, (uint)writeTimeoutMilliseconds), "set timeouts"); + } + catch + { + _handle.Dispose(); + throw; + } + } + + /// + public uint BytesAvailable + { + get + { + uint count = 0; + CheckStatus(FtFunction.FT_GetQueueStatus(_handle, ref count), "query receive queue"); + return count; + } + } + + /// + public int Read(Span buffer) + { + uint count = 0; + CheckStatus(FtFunction.FT_Read(_handle, in MemoryMarshal.GetReference(buffer), (uint)buffer.Length, ref count), "read"); + if (count > buffer.Length) + { + throw new IOException($"FT-X I2C slave returned an invalid read count ({count})."); + } + + return (int)count; + } + + /// + public int Write(ReadOnlySpan buffer) + { + uint count = 0; + CheckStatus(FtFunction.FT_Write(_handle, in MemoryMarshal.GetReference(buffer), (uint)buffer.Length, ref count), "write"); + if (count > buffer.Length) + { + throw new IOException($"FT-X I2C slave returned an invalid write count ({count})."); + } + + return (int)count; + } + + /// + /// Releases the owned native D2XX handle. + /// + public void Dispose() + { + _handle.Dispose(); + } + + /// + public FtXSeriesConfiguration ReadConfiguration() + { + var configuration = new FtXSeriesConfiguration(); + FtXSeriesEeprom data = configuration.Data; + CheckStatus( + FtFunction.FT_EEPROM_Read( + _handle, ref data, (uint)Marshal.SizeOf(), + configuration.Manufacturer, configuration.ManufacturerId, configuration.Description, configuration.SerialNumber), + "read EEPROM configuration"); + configuration.Data = data; + configuration.Validate(); + return configuration; + } + + /// + public void ProgramConfiguration(FtXSeriesConfiguration configuration) + { + configuration.Validate(); + FtXSeriesEeprom data = configuration.Data; + CheckStatus( + FtFunction.FT_EEPROM_Program( + _handle, ref data, (uint)Marshal.SizeOf(), + configuration.Manufacturer, configuration.ManufacturerId, configuration.Description, configuration.SerialNumber), + "program EEPROM configuration"); + } + + private static void CheckStatus(FtStatus status, string operation) + { + if (status != FtStatus.Ok) + { + throw new IOException($"FT-X I2C slave failed to {operation}, D2XX status: {status}."); + } + } + } +} diff --git a/src/devices/FtCommon/IFtI2cSlaveTransport.cs b/src/devices/FtCommon/IFtI2cSlaveTransport.cs new file mode 100644 index 0000000000..875d58fc5a --- /dev/null +++ b/src/devices/FtCommon/IFtI2cSlaveTransport.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Iot.Device.FtCommon +{ + /// + /// Provides USB-side payload access to an FTDI I2C slave's receive and transmit FIFOs. + /// + /// + /// An external I2C master initiates bus transfers. This contract does not configure + /// the slave address or preserve I2C transaction boundaries. + /// + internal interface IFtI2cSlaveTransport : IDisposable + { + /// + /// Gets the number of received payload bytes available to the USB host. + /// + uint BytesAvailable { get; } + + /// + /// Reads payload bytes received from the external I2C master. + /// + /// The destination buffer. + /// The actual byte count, which may be smaller than the buffer length on timeout. + int Read(Span buffer); + + /// + /// Queues payload bytes for the external I2C master to read. + /// + /// The payload to queue over USB. + /// The number of bytes accepted by the transport, not necessarily consumed by the master. + int Write(ReadOnlySpan buffer); + } +} diff --git a/src/devices/FtCommon/IFtXSeriesI2cSlaveTransport.cs b/src/devices/FtCommon/IFtXSeriesI2cSlaveTransport.cs new file mode 100644 index 0000000000..0d0b52886e --- /dev/null +++ b/src/devices/FtCommon/IFtXSeriesI2cSlaveTransport.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.FtCommon +{ + /// + /// Provides FIFO access and persistent configuration operations for FT-X I2C slave devices. + /// + internal interface IFtXSeriesI2cSlaveTransport : IFtI2cSlaveTransport + { + /// + /// Reads the stored FT-X configuration and USB descriptor strings without programming memory. + /// + /// A configuration snapshot independent of subsequent reads. + FtXSeriesConfiguration ReadConfiguration(); + + /// + /// Programs the supplied configuration into persistent MTP memory. + /// + /// The complete FT-X configuration and USB descriptor strings to program. + /// This consumes programming endurance and does not reload or verify the configuration. + void ProgramConfiguration(FtXSeriesConfiguration configuration); + } +}