C# SDK¶
The C# SDK provides an async/await-based interface for structured message communication using .NET Standard 2.0+.
Features¶
- Async/await: Modern C# asynchronous patterns
- Event-based: Standard .NET event model for messages
- Cross-platform: Works on .NET Core, .NET 5+, Xamarin, MAUI
- Mobile-friendly: Generic serial interface for mobile apps
Installation¶
Generate C# code with SDK:
Note: The SDK is not included by default. Use the --sdk flag to generate SDK files.
Available Transports¶
UDP Transport¶
Uses standard UdpClient:
using StructFrame.Sdk;
var transport = new UdpTransport(new UdpTransportConfig
{
RemoteHost = "192.168.1.100",
RemotePort = 5000,
LocalPort = 5001,
LocalAddress = "0.0.0.0",
EnableBroadcast = false,
AutoReconnect = true,
ReconnectDelayMs = 1000,
MaxReconnectAttempts = 5,
});
TCP Transport¶
Uses standard TcpClient:
var transport = new TcpTransport(new TcpTransportConfig
{
Host = "192.168.1.100",
Port = 5000,
TimeoutMs = 5000,
AutoReconnect = true,
});
WebSocket Transport¶
Requires NetCoreServer NuGet package:
var transport = new WebSocketTransport(new WebSocketTransportConfig
{
Url = "ws://localhost:8080",
TimeoutMs = 5000,
});
Serial Transport¶
Uses System.IO.Ports.SerialPort:
var transport = new SerialTransport(new SerialTransportConfig
{
PortName = "COM3", // or "/dev/ttyUSB0" on Linux
BaudRate = 115200,
DataBits = 8,
Parity = System.IO.Ports.Parity.None,
StopBits = System.IO.Ports.StopBits.One,
});
Generic Serial Transport (Mobile)¶
For Xamarin/MAUI applications, implement IGenericSerialPort:
// Your platform-specific implementation
public class XamarinSerialPort : IGenericSerialPort
{
// Implement serial I/O for your platform
}
var serialPort = new XamarinSerialPort();
var transport = new GenericSerialTransport(serialPort);
SDK Usage¶
Using Parsers¶
The generated frame parsers handle the low-level framing and message extraction:
using StructFrame;
// Basic frame parser (includes length and CRC)
var parser = new BasicDefaultParser();
// Parse incoming bytes
foreach (byte b in incomingData)
{
var result = parser.ParseByte(b);
if (result.Valid)
{
Console.WriteLine($"Received msg_id={result.MsgId}, len={result.MsgLen}");
// Process result.MsgData
}
}
// Encode a message
var msgData = new byte[] {1, 2, 3, 4, 5};
var frame = parser.Encode(42, msgData);
Parsing Minimal Frames¶
Minimal frames don't include a length field or CRC. To parse them, provide a callback function that returns the expected message length for each message ID:
Note: Verify the exact API signature in your generated parser code. The callback parameter may vary.
using StructFrame;
// Define message size lookup
int? GetMsgLength(int msgId)
{
return msgId switch
{
1 => 10, // Status message is 10 bytes
2 => 20, // Command message is 20 bytes
3 => 5, // Sensor reading is 5 bytes
_ => null
};
}
// Create parser with callback
// The exact constructor signature depends on generated code
var parser = new TinyMinimalParser(GetMsgLength);
// Parse bytes
foreach (byte b in incomingData)
{
var result = parser.ParseByte(b);
if (result.Valid)
{
Console.WriteLine($"Minimal frame: msg_id={result.MsgId}, len={result.MsgLen}");
}
}
// Encode minimal frame
var data = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var frame = parser.Encode(1, data);
// Result: [0x70] [0x01] [0x01 0x02 ... 0x0A]
When to use minimal frames: - ✅ Fixed-size messages on trusted links - ✅ Bandwidth-constrained (LoRa, RF) - ✅ Minimal overhead (1-3 bytes) - ❌ No error detection (no CRC)
See the Minimal Frames Guide for complete details.
Profile-Based Parsing API¶
The C# SDK provides high-performance parsing classes that match the C++ gold standard implementation. These are optimized for specific frame profiles and provide convenient factory methods.
Available Profiles¶
| Profile | Header | Payload | Use Case |
|---|---|---|---|
ProfileStandard |
Basic | Default | General serial/UART communication |
ProfileSensor |
Tiny | Minimal | Low-bandwidth sensors, radio links |
ProfileIPC |
None | Minimal | Trusted inter-process communication |
ProfileBulk |
Basic | Extended | Firmware/file transfers |
ProfileNetwork |
Basic | ExtendedMultiSystemStream | Multi-node mesh networks |
BufferReader - Parse Multiple Frames from a Buffer¶
BufferReader iterates through a buffer containing one or more frames, automatically tracking the offset:
using StructFrame;
// Parse a buffer containing multiple ProfileStandard frames
var reader = FrameProfiles.CreateProfileStandardReader(bufferData);
while (reader.HasMore())
{
var result = reader.Next();
if (!result.Valid) break;
Console.WriteLine($"Message ID: {result.MsgId}, Length: {result.MsgLen}");
ProcessMessage(result.MsgData);
}
Console.WriteLine($"Processed {reader.Offset} bytes, {reader.Remaining} remaining");
For minimal profiles (no length field), provide a message length callback:
// Parse ProfileSensor frames (minimal payload)
var reader = FrameProfiles.CreateProfileSensorReader(bufferData, GetMessageLength);
while (reader.HasMore())
{
var result = reader.Next();
if (result.Valid)
{
ProcessMessage(result.MsgId, result.MsgData);
}
}
int? GetMessageLength(int msgId)
{
return msgId switch
{
1 => 10, // Status message
2 => 20, // Command message
_ => null
};
}
BufferWriter - Encode Multiple Frames¶
BufferWriter encodes multiple frames into a buffer with automatic offset tracking:
using StructFrame;
// Create writer with capacity
var writer = FrameProfiles.CreateProfileStandardWriter(4096);
// Write multiple messages
writer.Write(1, msg1Data);
writer.Write(2, msg2Data);
writer.Write(3, msg3Data);
// Get the encoded data
var encodedBuffer = writer.Data();
Console.WriteLine($"Encoded {writer.Size} bytes with {writer.Count} messages");
For network profiles with extra header fields:
var writer = FrameProfiles.CreateProfileNetworkWriter(4096);
writer.Write(1, data, seq: 1, sysId: 10, compId: 1);
AccumulatingReader - Unified Buffer and Streaming Parser¶
AccumulatingReader handles both buffer mode and byte-by-byte streaming, with support for partial messages across buffer boundaries:
Buffer Mode - Processing chunks of data:
using StructFrame;
var reader = FrameProfiles.CreateProfileStandardAccumulatingReader();
// Process incoming chunks (e.g., from network or file)
reader.AddData(chunk1);
while (true)
{
var result = reader.Next();
if (!result.Valid) break;
ProcessMessage(result.MsgId, result.MsgData);
}
// Add more data (handles partial messages automatically)
reader.AddData(chunk2);
while (true)
{
var result = reader.Next();
if (!result.Valid) break;
ProcessMessage(result.MsgId, result.MsgData);
}
Stream Mode - Byte-by-byte processing (UART/serial):
using StructFrame;
var reader = FrameProfiles.CreateProfileSensorAccumulatingReader(GetMessageLength);
// Process incoming bytes one at a time
serialPort.DataReceived += (sender, e) =>
{
while (serialPort.BytesToRead > 0)
{
var b = (byte)serialPort.ReadByte();
var result = reader.PushByte(b);
if (result.Valid)
{
// Complete message received
ProcessMessage(result.MsgId, result.MsgData);
}
}
};
Factory Methods¶
All profiles have factory methods for creating readers and writers:
using StructFrame;
// BufferReader factories
var reader = FrameProfiles.CreateProfileStandardReader(buffer);
var reader = FrameProfiles.CreateProfileSensorReader(buffer, getMsgLength);
var reader = FrameProfiles.CreateProfileIPCReader(buffer, getMsgLength);
var reader = FrameProfiles.CreateProfileBulkReader(buffer);
var reader = FrameProfiles.CreateProfileNetworkReader(buffer);
// BufferWriter factories
var writer = FrameProfiles.CreateProfileStandardWriter(capacity);
var writer = FrameProfiles.CreateProfileSensorWriter(capacity);
var writer = FrameProfiles.CreateProfileIPCWriter(capacity);
var writer = FrameProfiles.CreateProfileBulkWriter(capacity);
var writer = FrameProfiles.CreateProfileNetworkWriter(capacity);
// AccumulatingReader factories
var reader = FrameProfiles.CreateProfileStandardAccumulatingReader();
var reader = FrameProfiles.CreateProfileSensorAccumulatingReader(getMsgLength);
var reader = FrameProfiles.CreateProfileIPCAccumulatingReader(getMsgLength);
var reader = FrameProfiles.CreateProfileBulkAccumulatingReader();
var reader = FrameProfiles.CreateProfileNetworkAccumulatingReader();
Creating the SDK¶
### Generated SDK Interface
When using the `--sdk` or `--sdk_embedded` flag, struct-frame generates a high-level SDK interface for each package that provides type-safe methods for sending messages. This interface eliminates boilerplate code and provides two convenient ways to send each message type.
#### Features
- **Type-safe send methods**: One method per message type
- **Two overloads**: Send with individual fields or with a complete struct
- **Automatic framing**: Messages are automatically serialized and framed
- **Frame parser integration**: Works with any generated frame parser
#### Example
For a message defined as:
```proto
package robot_messages;
message RobotCommand {
option msgid = 1;
uint8 command_type = 1;
float speed = 2;
float direction = 3;
}
The generated SDK interface provides:
using StructFrame.RobotMessages;
using StructFrame.RobotMessages.Sdk;
// Create SDK interface with a frame parser and send function
var frameParser = new BasicDefault();
var sdkInterface = new RobotMessagesSdkInterface(
frameParser,
transport.SendAsync // or any Func<byte[], Task>
);
// Option 1: Send with individual field values
await sdkInterface.SendRobotCommand(
commandType: 1,
speed: 5.0f,
direction: 90.0f
);
// Option 2: Send with complete struct
var cmd = new RobotMessagesRobotCommand
{
CommandType = 1,
Speed = 5.0f,
Direction = 90.0f,
};
await sdkInterface.SendRobotCommand(cmd);
Integration with StructFrameSdk¶
The SDK interface works seamlessly with the main StructFrameSdk:
using StructFrame.Sdk;
using StructFrame.RobotMessages.Sdk;
// Create and configure SDK
var sdk = new StructFrameSdk(new StructFrameSdkConfig
{
Transport = transport,
FrameParser = new BasicDefault(),
Debug = true,
});
await sdk.ConnectAsync();
// Create SDK interface using the same frame parser and transport
var sdkInterface = new RobotMessagesSdkInterface(
new BasicDefault(), // Same frame parser
async (bytes) => await transport.SendAsync(bytes)
);
// Send using SDK interface
await sdkInterface.SendRobotCommand(1, 5.0f, 90.0f);
// Receive using standard SDK subscription
sdk.Subscribe<RobotCommand>(RobotCommand.MsgId, (msg, id) =>
{
Console.WriteLine($"Command: {msg.CommandType}, Speed: {msg.Speed}");
});
Creating the SDK¶
using StructFrame.Sdk;
var sdk = new StructFrameSdk(new StructFrameSdkConfig
{
Transport = transport,
FrameParser = new BasicDefault(),
Debug = true,
});
Connecting and Disconnecting¶
// Connect
await sdk.ConnectAsync();
// Check connection status
if (sdk.IsConnected)
{
Console.WriteLine("Connected!");
}
// Disconnect
await sdk.DisconnectAsync();
Subscribing to Messages¶
using RobotMessages;
// Subscribe to messages
Action unsubscribe = sdk.Subscribe<StatusMessage>(
StatusMessage.MsgId,
(message, msgId) =>
{
Console.WriteLine($"Temperature: {message.Temperature}°C");
Console.WriteLine($"Battery: {message.Battery}%");
}
);
// Unsubscribe when done
unsubscribe();
Sending Messages¶
using RobotMessages;
// Create and send message
var cmd = new CommandMessage
{
Command = "MOVE_FORWARD",
Speed = 50,
};
await sdk.SendAsync(cmd);
// Or send raw bytes
byte[] rawData = new byte[] { 1, 2, 3, 4 };
await sdk.SendRawAsync(CommandMessage.MsgId, rawData);
Automatic Message Deserialization¶
Register codecs for automatic deserialization:
// Create a codec wrapper
public class StatusMessageCodec : IMessageCodec<StatusMessage>
{
public byte MsgId => StatusMessage.MsgId;
public StatusMessage Deserialize(byte[] data)
{
return StatusMessage.CreateUnpack(data);
}
}
sdk.RegisterCodec(new StatusMessageCodec());
// Now messages are automatically deserialized
sdk.Subscribe<StatusMessage>(StatusMessage.MsgId, (message, msgId) =>
{
// message is already a StatusMessage instance
Console.WriteLine(message);
});
Complete Example¶
using System;
using System.Threading.Tasks;
using StructFrame.Sdk;
using RobotMessages;
public class RobotClient
{
public static async Task Main(string[] args)
{
// Create transport
var transport = new TcpTransport(new TcpTransportConfig
{
Host = "localhost",
Port = 8080,
AutoReconnect = true,
ReconnectDelayMs = 2000,
MaxReconnectAttempts = 10,
});
// Create SDK
var sdk = new StructFrameSdk(new StructFrameSdkConfig
{
Transport = transport,
FrameParser = new BasicDefault(),
Debug = true,
});
// Subscribe to status messages
sdk.Subscribe<StatusMessage>(StatusMessage.MsgId, (msg, id) =>
{
Console.WriteLine($"[Status] Temp: {msg.Temperature}°C, Battery: {msg.Battery}%");
});
// Connect
await sdk.ConnectAsync();
Console.WriteLine("Connected to robot");
// Send command
var cmd = new CommandMessage
{
Command = "MOVE_FORWARD",
Speed = 50,
};
await sdk.SendAsync(cmd);
// Handle errors
transport.ErrorOccurred += (sender, error) =>
{
Console.WriteLine($"Transport error: {error.Message}");
};
// Handle close
transport.ConnectionClosed += (sender, args) =>
{
Console.WriteLine("Connection closed");
};
// Keep alive
Console.WriteLine("Press Ctrl+C to exit");
await Task.Delay(-1);
}
}
ASP.NET Core Integration¶
Integrate with ASP.NET Core dependency injection:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register transport
services.AddSingleton<ITransport>(sp =>
{
return new TcpTransport(new TcpTransportConfig
{
Host = "localhost",
Port = 8080,
});
});
// Register SDK
services.AddSingleton<StructFrameSdk>(sp =>
{
var transport = sp.GetRequiredService<ITransport>();
return new StructFrameSdk(new StructFrameSdkConfig
{
Transport = transport,
FrameParser = new BasicDefault(),
});
});
// Register as hosted service
services.AddHostedService<RobotService>();
}
}
public class RobotService : BackgroundService
{
private readonly StructFrameSdk _sdk;
public RobotService(StructFrameSdk sdk)
{
_sdk = sdk;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _sdk.ConnectAsync();
// Subscribe to messages
_sdk.Subscribe<StatusMessage>(StatusMessage.MsgId, HandleStatus);
// Wait for cancellation
await Task.Delay(-1, stoppingToken);
await _sdk.DisconnectAsync();
}
private void HandleStatus(StatusMessage msg, byte msgId)
{
// Handle status
}
}
Xamarin/MAUI Example¶
using Xamarin.Forms;
using StructFrame.Sdk;
public class RobotPage : ContentPage
{
private StructFrameSdk _sdk;
public RobotPage()
{
InitializeComponent();
InitializeSdk();
}
private async void InitializeSdk()
{
// Platform-specific serial port
var serialPort = DependencyService.Get<IGenericSerialPort>();
var transport = new GenericSerialTransport(serialPort);
_sdk = new StructFrameSdk(new StructFrameSdkConfig
{
Transport = transport,
FrameParser = new BasicDefault(),
});
// Subscribe
_sdk.Subscribe<StatusMessage>(StatusMessage.MsgId, (msg, id) =>
{
Device.BeginInvokeOnMainThread(() =>
{
// Update UI
TempLabel.Text = $"{msg.Temperature}°C";
BatteryLabel.Text = $"{msg.Battery}%";
});
});
await _sdk.ConnectAsync();
}
private async void SendCommand_Clicked(object sender, EventArgs e)
{
var cmd = new CommandMessage { Command = "START" };
await _sdk.SendAsync(cmd);
}
}
Error Handling¶
try
{
await sdk.ConnectAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to connect: {ex.Message}");
}
// Event-based error handling
transport.ErrorOccurred += (sender, error) =>
{
Console.WriteLine($"Transport error: {error.Message}");
};
transport.ConnectionClosed += (sender, args) =>
{
Console.WriteLine("Connection closed");
};
NuGet Dependencies¶
Required¶
- .NET Standard 2.0+
- System.IO.Ports (for SerialTransport)
Optional¶
- NetCoreServer (for WebSocket and enhanced TCP/UDP)
Best Practices¶
-
Use async/await consistently:
-
Dispose properly:
-
Handle UI thread marshalling (Xamarin/WPF/WinForms):
-
Use CancellationToken:
Platform-Specific Notes¶
.NET Framework¶
- Requires .NET Framework 4.7.2+ for .NET Standard 2.0 support
- Use
System.IO.Portsfor serial communication
.NET Core / .NET 5+¶
- Full support for all features
- Cross-platform serial port support
Xamarin¶
- Implement
IGenericSerialPortfor platform-specific serial I/O - Use
Device.BeginInvokeOnMainThreadfor UI updates
MAUI¶
- Similar to Xamarin, use platform-specific implementations
- Use
MainThread.BeginInvokeOnMainThreadfor UI updates
UWP¶
- Serial port access requires capabilities in Package.appxmanifest
- Limited network socket support (use UWP-specific APIs)