C#實現(xiàn)串口通信的四種靈活策略和避坑指南
前言
工業(yè)控制、物聯(lián)網(wǎng)設(shè)備通信中,是否遇到過這樣的場景:向設(shè)備發(fā)送一個簡單的查詢指令,卻發(fā)現(xiàn)返回的數(shù)據(jù)總是"分批到達"?明明應(yīng)該收到完整的20字節(jié)響應(yīng),卻只能收到幾個零散的數(shù)據(jù)包?
別急,這不是你的代碼有問題!
這是串口通信中最常見的"分包接收"現(xiàn)象。設(shè)備可能一次發(fā)送10字節(jié),下一次發(fā)送剩余的10字節(jié),而我們的程序卻不知道什么時候才算接收完成。
今天我們就來徹底解決這個讓無數(shù) C# 開發(fā)頭疼的問題!
為什么會分包接收
根本原因
串口通信是異步的,數(shù)據(jù)傳輸會受到以下因素影響:
硬件緩沖區(qū)大小限制
設(shè)備處理速度差異
網(wǎng)絡(luò)延遲(對于串口轉(zhuǎn)以太網(wǎng)設(shè)備)
系統(tǒng)調(diào)度
這些因素導(dǎo)致原本連續(xù)的數(shù)據(jù)流被 操作系統(tǒng)或中間設(shè)備拆分成多個小塊,逐次送達應(yīng)用程序。
傳統(tǒng)方案的痛點
// ? 錯誤示例:只能收到第一包數(shù)據(jù) serialPort.Write(command, 0, command.Length); Thread.Sleep(100); // 固定等待時間 byte[] buffer = new byte[1024]; int count = serialPort.Read(buffer, 0, 1024); // 可能只讀到部分數(shù)據(jù)
這種寫法的問題包括:
固定等待時間不可靠
無法判斷數(shù)據(jù)是否接收完整
容易丟失后續(xù)數(shù)據(jù)包
四種靈活接收策略
為應(yīng)對不同應(yīng)用場景,我們設(shè)計了以下四種策略:
方案一:數(shù)據(jù)間隔超時判斷(?推薦)
適用場景:不知道數(shù)據(jù)長度,但設(shè)備發(fā)送完畢后會有明顯時間間隔。
public byte[] SendQueryWithGapTimeout(byte[] command, int gapTimeoutMs = 100, int maxWaitMs = 3000)
{
// 清空緩沖區(qū)并開始接收
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
// 發(fā)送指令
serialPort.Write(command, 0, command.Length);
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
// ?? 關(guān)鍵邏輯:有數(shù)據(jù)且間隔超時則認為接收完成
if (receivedBuffer.Count > 0 &&
(DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
return receivedBuffer.ToArray();
}
}
}
return null;
}
實際業(yè)務(wù)中,這個能解決大部分問題。
方案二:結(jié)束符判斷
適用場景:數(shù)據(jù)以特定字符結(jié)尾(如 \r\n、\0 等)。
public byte[] SendQueryWithEndMarker(byte[] command, byte[] endMarker, int maxWaitMs = 3000)
{
// ... 發(fā)送邏輯相同 ...
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
lock (bufferLock)
{
if (receivedBuffer.Count >= endMarker.Length)
{
// ?? 檢查緩沖區(qū)末尾是否包含結(jié)束標(biāo)記
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
return receivedBuffer.ToArray();
}
}
}
}
}
方案三:協(xié)議幀結(jié)構(gòu)判斷
適用場景:數(shù)據(jù)有固定幀頭和長度字段(如 Modbus 協(xié)議)。
public byte[] SendQueryWithFrameProtocol(byte[] command, byte frameHeader, int lengthFieldOffset, int lengthFieldSize = 1)
{
// ... 發(fā)送邏輯 ...
while (/* 超時檢查 */)
{
lock (bufferLock)
{
if (receivedBuffer.Count > lengthFieldOffset + lengthFieldSize)
{
// 檢查幀頭
if (receivedBuffer[0] == frameHeader)
{
// ?? 從長度字段獲取數(shù)據(jù)長度
int dataLength = lengthFieldSize == 1 ?
receivedBuffer[lengthFieldOffset] :
(receivedBuffer[lengthFieldOffset] << 8) | receivedBuffer[lengthFieldOffset + 1];
int expectedFrameLength = lengthFieldOffset + lengthFieldSize + dataLength;
if (receivedBuffer.Count >= expectedFrameLength)
{
return receivedBuffer.Take(expectedFrameLength).ToArray();
}
}
}
}
}
}
方案四:組合策略(??推薦)
最靈活的方案,同時使用多種判斷條件:
public byte[] SendQueryWithCombinedStrategy(byte[] command,
int gapTimeoutMs = 100, // 數(shù)據(jù)間隔超時
byte[] endMarker = null, // 結(jié)束標(biāo)記
int? maxLength = null, // 最大長度限制
int maxWaitMs = 3000) // 總超時時間
{
// ... 發(fā)送邏輯 ...
while (/* 總超時檢查 */)
{
lock (bufferLock)
{
if (receivedBuffer.Count == 0) continue;
// ?? 條件1:達到最大長度限制
if (maxLength.HasValue && receivedBuffer.Count >= maxLength.Value)
return receivedBuffer.ToArray();
// ?? 條件2:發(fā)現(xiàn)結(jié)束標(biāo)記
if (endMarker != null && /* 檢查結(jié)束標(biāo)記邏輯 */)
return receivedBuffer.ToArray();
// ?? 條件3:數(shù)據(jù)間隔超時
if ((DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
return receivedBuffer.ToArray();
}
}
}
核心機制:數(shù)據(jù)接收事件
所有策略都依賴于統(tǒng)一的數(shù)據(jù)接收事件處理:
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (!isWaitingForResponse) return;
try
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte[bytesToRead];
int bytesRead = serialPort.Read(buffer, 0, bytesToRead);
lock (bufferLock)
{
receivedBuffer.AddRange(buffer);
lastReceiveTime = DateTime.Now; // ?? 更新最后接收時間
Console.WriteLine($"收到數(shù)據(jù)包 ({bytesRead} 字節(jié)): {BitConverter.ToString(buffer, 0, bytesRead)}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"數(shù)據(jù)接收異常: {ex.Message}");
}
}
完整代碼
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppFlexSerialPort
{
internal class FlexibleSerialPort
{
private SerialPort serialPort;
private List<byte> receivedBuffer;
private readonly object bufferLock = new object();
private Timer timeoutTimer;
private bool isWaitingForResponse = false;
private DateTime lastReceiveTime;
private readonly int dataGapTimeout = 100; // 數(shù)據(jù)間隔超時時間(ms)
public FlexibleSerialPort()
{
receivedBuffer = new List<byte>();
}
/// <summary>
/// 初始化串口
/// </summary>
public bool InitializePort(string portName = "COM1", int baudRate = 9600,
Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One)
{
try
{
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.ReadTimeout = 1000;
serialPort.WriteTimeout = 1000;
serialPort.DataReceived += OnDataReceived;
serialPort.Open();
Console.WriteLine($"串口 {portName} 已成功打開");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"串口初始化失敗: {ex.Message}");
return false;
}
}
/// <summary>
/// 方案1: 基于數(shù)據(jù)間隔超時判斷接收完成
/// 適用于:不知道數(shù)據(jù)長度,但設(shè)備發(fā)送完后會有明顯的時間間隔
/// </summary>
public byte[] SendQueryWithGapTimeout(byte[] command, int gapTimeoutMs = 100, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打開");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
try
{
// 發(fā)送查詢指令
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已發(fā)送查詢指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
DateTime lastCheckTime = DateTime.Now;
int lastBufferSize = 0;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
// 如果有數(shù)據(jù)且數(shù)據(jù)間隔超過指定時間,認為接收完成
if (receivedBuffer.Count > 0 &&
(DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"基于間隔超時判斷接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
}
}
// 最大等待時間超時
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"最大等待時間超時,收到 {result.Length} 字節(jié)");
return result;
}
}
Console.WriteLine("接收超時,未收到任何數(shù)據(jù)");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"發(fā)送指令失敗: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案2: 基于結(jié)束符判斷接收完成
/// 適用于:數(shù)據(jù)以特定字符或字節(jié)序列結(jié)尾
/// </summary>
public byte[] SendQueryWithEndMarker(byte[] command, byte[] endMarker, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打開");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已發(fā)送查詢指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count >= endMarker.Length)
{
// 檢查緩沖區(qū)末尾是否包含結(jié)束標(biāo)記
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"發(fā)現(xiàn)結(jié)束標(biāo)記,接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
}
}
}
// 超時處理
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"等待結(jié)束標(biāo)記超時,收到 {result.Length} 字節(jié)");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"發(fā)送指令失敗: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案3: 基于協(xié)議幀結(jié)構(gòu)判斷接收完成
/// 適用于:數(shù)據(jù)有固定的幀頭和長度字段
/// </summary>
public byte[] SendQueryWithFrameProtocol(byte[] command, byte frameHeader, int lengthFieldOffset,
int lengthFieldSize = 1, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打開");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已發(fā)送查詢指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count > lengthFieldOffset + lengthFieldSize)
{
// 檢查幀頭
if (receivedBuffer[0] == frameHeader)
{
// 獲取數(shù)據(jù)長度
int dataLength = 0;
if (lengthFieldSize == 1)
{
dataLength = receivedBuffer[lengthFieldOffset];
}
else if (lengthFieldSize == 2)
{
dataLength = (receivedBuffer[lengthFieldOffset] << 8) | receivedBuffer[lengthFieldOffset + 1];
}
int expectedFrameLength = lengthFieldOffset + lengthFieldSize + dataLength;
if (receivedBuffer.Count >= expectedFrameLength)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.Take(expectedFrameLength).ToArray();
Console.WriteLine($"根據(jù)幀長度判斷接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
}
}
}
}
// 超時處理
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"幀協(xié)議解析超時,收到 {result.Length} 字節(jié)");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"發(fā)送指令失敗: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案4: 組合策略 - 最靈活的方案
/// 同時使用多種判斷條件,任一條件滿足就結(jié)束接收
/// </summary>
public byte[] SendQueryWithCombinedStrategy(byte[] command,
int gapTimeoutMs = 100,
byte[] endMarker = null,
int? maxLength = null,
int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打開");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已發(fā)送查詢指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count == 0) continue;
// 條件1: 檢查最大長度限制
if (maxLength.HasValue && receivedBuffer.Count >= maxLength.Value)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"達到最大長度限制,接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
// 條件2: 檢查結(jié)束標(biāo)記
if (endMarker != null && receivedBuffer.Count >= endMarker.Length)
{
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"發(fā)現(xiàn)結(jié)束標(biāo)記,接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
}
// 條件3: 檢查數(shù)據(jù)間隔超時
if ((DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"數(shù)據(jù)間隔超時,接收完成,共收到 {result.Length} 字節(jié)");
return result;
}
}
}
// 最大等待時間超時
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"最大等待時間超時,收到 {result.Length} 字節(jié)");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"發(fā)送指令失敗: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 數(shù)據(jù)接收事件處理
/// </summary>
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (!isWaitingForResponse) return;
try
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte[bytesToRead];
int bytesRead = serialPort.Read(buffer, 0, bytesToRead);
lock (bufferLock)
{
receivedBuffer.AddRange(buffer);
lastReceiveTime = DateTime.Now; // 更新最后接收時間
Console.WriteLine($"收到數(shù)據(jù)包 ({bytesRead} 字節(jié)): {BitConverter.ToString(buffer, 0, bytesRead)}");
Console.WriteLine($"當(dāng)前緩沖區(qū)總計: {receivedBuffer.Count} 字節(jié)");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"數(shù)據(jù)接收處理異常: {ex.Message}");
}
}
/// <summary>
/// 關(guān)閉串口
/// </summary>
public void Close()
{
try
{
isWaitingForResponse = false;
if (serialPort != null && serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("串口已關(guān)閉");
}
}
catch (Exception ex)
{
Console.WriteLine($"關(guān)閉串口異常: {ex.Message}");
}
}
public static string[] GetAvailablePorts()
{
return SerialPort.GetPortNames();
}
}
}
namespace AppFlexSerialPort
{
internal class Program
{
static void Main(string[] args)
{
FlexibleSerialPort comm = new FlexibleSerialPort();
try
{
if (comm.InitializePort("COM1", 9600))
{
byte[] queryCommand = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A };
Console.WriteLine("=== 方案1: 基于數(shù)據(jù)間隔判斷 ===");
byte[] response1 = comm.SendQueryWithGapTimeout(queryCommand, 150, 3000);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案2: 基于結(jié)束符判斷 ===");
byte[] endMarker = { 0x0D, 0x0A }; // CR LF
byte[] response2 = comm.SendQueryWithEndMarker(queryCommand, endMarker);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案3: 基于協(xié)議幀結(jié)構(gòu)判斷 ===");
byte[] response3 = comm.SendQueryWithFrameProtocol(queryCommand, 0x01, 2, 1);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案4: 組合策略 ===");
byte[] response4 = comm.SendQueryWithCombinedStrategy(
queryCommand,
gapTimeoutMs: 100, // 數(shù)據(jù)間隔100ms
endMarker: new byte[] { 0x0A }, // 或者以LF結(jié)尾
maxLength: 50, // 或者最多50字節(jié)
maxWaitMs: 3000 // 最多等待3秒
);
}
Console.WriteLine("按任意鍵退出...");
Console.ReadKey();
}
catch (Exception ex)
{
Console.WriteLine($"程序異常: {ex.Message}");
}
finally
{
comm.Close();
}
}
}
}
結(jié)果如下:


性能優(yōu)化與實踐
關(guān)鍵參數(shù)調(diào)優(yōu)
// ? 推薦配置 int gapTimeoutMs = 100; // 100-200ms適合大多數(shù)設(shè)備 int maxWaitMs = 3000; // 總超時3秒,避免程序卡死 Thread.Sleep(10); // 輪詢間隔10ms,平衡CPU占用和響應(yīng)速度
線程安全保障
private readonly object bufferLock = new object();
// 所有緩沖區(qū)操作都要加鎖
lock (bufferLock)
{
receivedBuffer.Clear();
receivedBuffer.AddRange(buffer);
// ... 其他緩沖區(qū)操作
}
常見提醒
1、忘記清空緩沖區(qū):每次查詢前必須 receivedBuffer.Clear()
2、超時時間設(shè)置不當(dāng):間隔超時太短會截斷數(shù)據(jù),太長會影響響應(yīng)速度
3、線程安全問題:DataReceived 事件在不同線程中執(zhí)行,必須加鎖保護
4、資源釋放:程序結(jié)束前記得調(diào)用 Close() 方法
適用場景對比
| 方案 | 適用場景 | 優(yōu)點 | 缺點 |
|---|---|---|---|
| 間隔超時 | 通用場景 | 簡單可靠 | 需要調(diào)試最佳間隔時間 |
| 結(jié)束符判斷 | 文本協(xié)議 | 精確判斷 | 需要明確的結(jié)束符 |
| 幀結(jié)構(gòu)判斷 | 二進制協(xié)議 | 最精確 | 需要了解協(xié)議細節(jié) |
| 組合策略 | 復(fù)雜場景 | 最靈活 | 代碼稍復(fù)雜 |
總結(jié)
1、選擇合適的策略:對于90%的場景,數(shù)據(jù)間隔超時判斷就足夠了
2、參數(shù)調(diào)優(yōu)很重要:100-200ms 的間隔超時是經(jīng)驗值,需要根據(jù)實際設(shè)備調(diào)整
3、組合策略是王道:當(dāng)單一策略無法滿足需求時,組合策略提供了最大的靈活性
通過本文介紹的四種策略,開發(fā)者可以根據(jù)具體通信協(xié)議靈活選擇最適合的接收方式,徹底告別"分包接收"帶來的困擾。
到此這篇關(guān)于C#實現(xiàn)串口通信的四種靈活策略和避坑指南的文章就介紹到這了,更多相關(guān)C#串口通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# 使用鼠標(biāo)點擊對Chart控件實現(xiàn)數(shù)據(jù)提示效果
這篇文章主要介紹了C# 使用鼠標(biāo)點擊對Chart控件實現(xiàn)數(shù)據(jù)提示效果,文章給予上一篇的詳細內(nèi)容做延伸介紹,需要的小伙伴可任意參考一下2022-08-08
C#如何動態(tài)創(chuàng)建Label,及動態(tài)label事件
這篇文章主要介紹了C#如何動態(tài)創(chuàng)建Label,及動態(tài)label事件,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
Unity利用材質(zhì)自發(fā)光實現(xiàn)物體閃爍
這篇文章主要為大家詳細介紹了Unity利用材質(zhì)自發(fā)光實現(xiàn)物體閃爍,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04
C#實現(xiàn)圖表中鼠標(biāo)移動并顯示數(shù)據(jù)
這篇文章主要為大家詳細介紹了C#實現(xiàn)圖表中鼠標(biāo)移動并顯示數(shù)據(jù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02

