最近用C#写个一个通过树莓派(Raspberry Pi)3B+的蓝牙模块来扫描电子设备。当时实现的时候也是花了些时间。现在把代码整理一下,给需要的人参考一下!
目录
NuGet Packages
首先从NuGet上装几个package
- log4net - 用来记录日志,实在太好用了!
- SSH.NET - 用这个来实现通过SSH和树莓派进行通讯
- System.ValueTuple - 这个是比较新的特新,VS2017默认不支持

接下里,我们就直接看代码。
NetworkPinger
这个class是用来检测能不能ping上树莓派。
public interface INetworkPinger
{
bool CanPing(IPAddress ipAddress, int timeoutMilliseconds = 2000);
}
public class NetworkPinger : INetworkPinger
{
private readonly ILog _logger = LogManager.GetLogger(typeof(NetworkPinger));
public bool CanPing(IPAddress ipAddress, int timeoutMilliseconds = 2000)
{
try
{
_logger.Info($"Try to ping {ipAddress}...");
var ping = new Ping();
var pingReply = ping.Send(ipAddress, timeoutMilliseconds);
if (pingReply == null)
throw new Exception($"PingReply is null.");
if (pingReply.Status != IPStatus.Success)
throw new Exception($"The status of PingReply \"{pingReply.Status}\" is not \"{IPStatus.Success}\".");
_logger.Info($"Succeed in pinging {ipAddress}");
return true;
}
catch (Exception exception)
{
_logger.Warn($"Some error happened while pinging {ipAddress}.\n{exception}");
return false;
}
}
}
SshCommunication
这个class就是调用用Renci.SshNet实现SSH通讯的。
- 在调用参数中,有Func委托,委托也是实现松耦合的一种常用方式
public interface ISshCommunicator : IDisposable
{
void Open();
void Close();
string WriteAndRead(
string command,
int timeoutMilliseconds = SshCommunicator.DefaultReadTimeoutMilliseconds);
string WriteAndRead(
string command,
Func<string, bool> canStopEarlier,
int timeoutMilliseconds = SshCommunicator.DefaultReadTimeoutMilliseconds);
string ReadExistingString();
string Read(
Func<string, bool> canStopEarlier,
int timeoutMilliseconds = SshCommunicator.DefaultReadTimeoutMilliseconds);
}
public class SshCommunicator : ISshCommunicator
{
public const int DefaultReadTimeoutMilliseconds = 5000;
private bool _disposed = false;
private SshClient _sshClient = null;
private ShellStream _shellStream = null;
private readonly ILog _logger = LogManager.GetLogger(typeof(SshCommunicator));
private readonly ConnectionInfo _connectionInfo;
public SshCommunicator(string ipAddress, string userName, string password)
{
_connectionInfo =
new ConnectionInfo(
ipAddress,
userName,
new PasswordAuthenticationMethod(userName, password))
{
Encoding = Encoding.ASCII
};
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
Close();
_disposed = true;
}
public void Open()
{
Close();
_sshClient = new SshClient(_connectionInfo);
_sshClient.Connect();
_shellStream = _sshClient.CreateShellStream("BluetoothctlShell", 512, 128, 1920, 1080, 1024 * 10);
}
public void Close()
{
if (_sshClient == null)
return;
try
{
_logger.Info("Close SSH client...");
_shellStream.Close();
_sshClient.Disconnect();
}
catch (Exception exception)
{
_logger.Warn($"Some error happened while closing SSH client.\n{exception}");
}
finally
{
_shellStream = null;
_sshClient = null;
}
}
public string WriteAndRead(string command, int timeoutMilliseconds = DefaultReadTimeoutMilliseconds)
{
IgnoreExistingStringBeforeWrite();
Write(command);
return Read(timeoutMilliseconds);
}
private void IgnoreExistingStringBeforeWrite()
{
var existingString = ReadExistingString();
if (!string.IsNullOrEmpty(existingString))
_logger.Warn($"Ignore the below existing string before write.\n{existingString}");
}
public string WriteAndRead(
string command,
Func<string, bool> canStopEarlier,
int timeoutMilliseconds = DefaultReadTimeoutMilliseconds)
{
IgnoreExistingStringBeforeWrite();
Write(command);
return Read(canStopEarlier, timeoutMilliseconds);
}
private void Write(string command)
{
_logger.Info($"Write \"{command}\"...");
_shellStream.WriteLine(command);
}
public string Read(int timeoutMilliseconds = DefaultReadTimeoutMilliseconds)
{
_logger.Info($"Read the response within {timeoutMilliseconds} ms");
var stringBuilder = new StringBuilder();
var stopwatch = Stopwatch.StartNew();
while (true)
{
DelayBetweenLoops();
stringBuilder.Append(ReadExistingString());
if (stopwatch.ElapsedMilliseconds > timeoutMilliseconds)
{
_logger.Warn($"Timeout {timeoutMilliseconds} ms has happened. Stop reading more response.");
break;
}
}
_logger.Info($"The response is as below.\n{stringBuilder}");
return stringBuilder.ToString();
}
private static void DelayBetweenLoops()
{
Thread.Sleep(10);
}
public string Read(Func<string, bool> canStopEarlier, int timeoutMilliseconds = DefaultReadTimeoutMilliseconds)
{
if (canStopEarlier == null)
throw new ArgumentNullException(nameof(canStopEarlier));
_logger.Info($"Read the response within timeout {timeoutMilliseconds} ms");
var stringBuilder = new StringBuilder();
var stopwatch = Stopwatch.StartNew();
while (true)
{
DelayBetweenLoops();
stringBuilder.Append(ReadExistingString());
if (canStopEarlier(stringBuilder.ToString()))
{
_logger.Info("Expected response has been found. Stop reading more response.");
break;
}
if (stopwatch.ElapsedMilliseconds > timeoutMilliseconds)
{
throw new Exception($"Timeout {timeoutMilliseconds} ms has happened. The response is as below.\n{stringBuilder}");
}
}
_logger.Info($"The response is as below.\n{stringBuilder}");
return stringBuilder.ToString();
}
public string ReadExistingString()
{
var stringBuilder = new StringBuilder();
while (_shellStream.DataAvailable)
{
stringBuilder.Append(_shellStream.Read());
Thread.Sleep(50); // Need this delay !!!! Otherwise some response can be incomplete.
}
return stringBuilder.ToString();
}
}
BTDevice
这个class就是保存扫描到的蓝牙设备的地址,同时把每次扫到的RSSI也保存下来。
public class BTDevice
{
private readonly List<int> _rssis = new List<int>();
private readonly ILog _logger = LogManager.GetLogger(typeof(BTDevice));
public string BTAddress { get; }
public BTDevice(string btAddress)
{
BTAddress = btAddress;
}
public BTDevice(string btAddress, int rssi)
: this(btAddress)
{
AddRSSI(rssi);
}
public BTDevice(string btAddress, IEnumerable<int> rssis)
: this(btAddress)
{
AddRSSIs(rssis);
}
public void AddRSSI(int rssi)
=> _rssis.Add(rssi);
public void AddRSSIs(IEnumerable<int> rssis)
=> _rssis.AddRange(rssis);
public override string ToString() => $"{BTAddress}({string.Join(",", _rssis)})";
public IEnumerable<int> RSSIs => _rssis;
public bool HasAnyRSSI => _rssis.Any();
public int GetFinalReportingRSSI()
{
if (!HasAnyRSSI)
throw new Exception($"No any RSSI for {BTAddress}.");
var average = (int)_rssis.Average();
_logger.Info($"Take the average {average} of ({string.Join(",", _rssis)}) as final RSSI reported.");
return average;
}
}
BTDeviceParser
这个class会解析bluetoothctl返回的字符串,只针对期望的蓝牙地址。
public class BTDeviceParser
{
private string _scanResponse;
private string[] _btAddressesToScan;
public BTDevice[] Parse(string scanResponse, IEnumerable<string> btAddressesToScan)
{
_scanResponse = scanResponse;
_btAddressesToScan = btAddressesToScan.ToArray();
var btDevices = new List<BTDevice>();
foreach (var btAddress in _btAddressesToScan)
{
var success = TryParse(btAddress, out var btDevice);
if (success)
btDevices.Add(btDevice);
}
return btDevices.ToArray();
}
private int[] FindRssisFor(string btAddress)
{
var rssis = new List<int>();
var fullMatchPattern = btAddress + @" RSSI: \-?\d+";
var fullMatches = Regex.Matches(_scanResponse, fullMatchPattern);
if (fullMatches.Count > 0)
{
foreach (Match fullMatch in fullMatches)
{
var rssi = int.Parse(Regex.Match(fullMatch.Value, @"\-?\d+", RegexOptions.RightToLeft).Value);
rssis.Add(rssi);
}
}
return rssis.ToArray();
}
private bool HasFound(string btAddress)
{
return Regex.IsMatch(_scanResponse, btAddress);
}
private bool TryParse(string btAddress, out BTDevice btDevice)
{
btDevice = null;
var rssis = FindRssisFor(btAddress);
if (rssis.Length > 0)
{
btDevice = new BTDevice(btAddress, rssis);
return true;
}
else
{
if (HasFound(btAddress))
{
btDevice = new BTDevice(btAddress);
return true;
}
else
return false;
}
}
}
BTDevicesExtensions
这是IEnumerate<BTDevice>的扩展方法。有时实现扩展方法,可以让代码更具有可读性。
public static class BTDevicesExtensions
{
public static bool HasAllFound(this IEnumerable<BTDevice> btDevicesScanned, IEnumerable<string> btAddressesToScan)
{
if (btDevicesScanned == null)
throw new ArgumentNullException(nameof(btDevicesScanned));
if (btAddressesToScan == null)
throw new ArgumentNullException(nameof(btAddressesToScan));
var btAddressArrayToScan = btAddressesToScan.ToArray();
if (btAddressArrayToScan.Length < 1)
throw new ArgumentException("Should have one BT Address to scan at least", nameof(btAddressesToScan));
var btAddressesScanned =
btDevicesScanned
.Where(m => m.HasAnyRSSI)
.Select(m => m.BTAddress)
.ToArray();
return
btAddressArrayToScan
.All(m => btAddressesScanned.Contains(m));
}
}
BTScanExecutor
我们接下来看看通过bluetoothctl发送那些命令就可以实现蓝牙设备的扫描。
public class BTScanExecutor
{
private readonly string[] _btAddressesToScan;
private readonly ILog _logger = LogManager.GetLogger(typeof(BTScanExecutor));
private readonly INetworkPinger _networkPinger = new NetworkPinger();
private readonly Config _config;
private ISshCommunicator _sshCommunicator;
public BTScanExecutor(string[] btAddressesToScan)
: this(btAddressesToScan, new CachedConfigLoader())
{ }
public BTScanExecutor(string[] btAddressesToScan, IConfigLoader configLoader)
{
_btAddressesToScan = btAddressesToScan;
_config = configLoader.Load();
}
public BTDevice[] FindBTDevices()
{
ValidateRPiPingable();
return ExecuteScan();
}
private BTDevice[] ExecuteScan()
{
BTDevice[] btDevices;
using (_sshCommunicator = new SshCommunicator(_config.IpAddress, _config.UserName, _config.Password))
{
Open();
ReadStartPrompt();
EnterBluetoothctl();
PowerOnController();
btDevices = ScanBTDevices();
ListDevices();
ExitBluetoothctl();
CloseSsh();
}
return btDevices;
}
private BTDevice[] ScanBTDevices()
{
var overallBTDevices = new List<BTDevice>();
var scanner = new BTDeviceScanner(_sshCommunicator, _btAddressesToScan);
const int countOfTries = 3;
for (var i = 0; i < countOfTries; i++)
{
_logger.Info($"Start {i + 1}/{countOfTries} scan...");
if (i > 0)
_logger.Warn("Retry to scan BT devices !!!!");
var btDevices = scanner.ScanBTDevices();
foreach (var btDevice in btDevices)
{
var singleOrDefault = overallBTDevices.SingleOrDefault(m => m.BTAddress == btDevice.BTAddress);
if (singleOrDefault == null)
overallBTDevices.Add(btDevice);
else
singleOrDefault.AddRSSIs(btDevice.RSSIs);
}
_logger.Info($"Overall BT Devices detected:\n{string.Join("\n", overallBTDevices)}");
if (overallBTDevices.HasAllFound(_btAddressesToScan))
break;
}
return overallBTDevices.ToArray();
}
private void ValidateRPiPingable()
{
_logger.Info("Start to ping RPi...");
var pingable = _networkPinger.CanPing(IPAddress.Parse(_config.IpAddress), timeoutMilliseconds: 5000);
if (pingable)
_logger.Info("Succeed in pinging RPi");
else
throw new Exception($"Fail to ping {_config.IpAddress}");
}
private void Open()
{
_logger.Info("Open SSH");
_sshCommunicator.Open();
}
private void ReadStartPrompt()
{
_logger.Info("Read start prompt...");
_sshCommunicator.Read(s => s.Trim().EndsWith("pi@raspberrypi:~$"));
}
private void EnterBluetoothctl()
{
_logger.Info("Enter bluetoothctl...");
ReadAndIgnoreExistingString();
_sshCommunicator.WriteAndRead(
"bluetoothctl",
s => s.Contains("Agent registered"));
ReadAndIgnoreExistingString();
}
private void PowerOnController()
{
_logger.Info("Power on Controller...");
ReadAndIgnoreExistingString();
_sshCommunicator.WriteAndRead(
"power on",
s => s.Contains("Changing power on succeeded"));
ReadAndIgnoreExistingString();
}
private void ListDevices()
{
_logger.Info("List BT devices detected...");
ReadAndIgnoreExistingString();
_sshCommunicator.WriteAndRead(
"devices",
s => s.Contains("[bluetooth]"));
ReadAndIgnoreExistingString();
}
private void ExitBluetoothctl()
{
_logger.Info("Exit bluetoothctl...");
ReadAndIgnoreExistingString();
_sshCommunicator.WriteAndRead(
"exit",
s => s.Trim().EndsWith("pi@raspberrypi:~$"));
ReadAndIgnoreExistingString();
}
private void CloseSsh()
{
ReadAndIgnoreExistingString();
_logger.Info("Close SSH");
_sshCommunicator.Close();
}
private void ReadAndIgnoreExistingString()
{
var existingString = _sshCommunicator.ReadExistingString();
if (!string.IsNullOrEmpty(existingString))
_logger.Warn($"Some existing response has been read but ignored.\n{existingString}");
}
}
ResultDirectoryService
这个简单的类就是把扫描以文本的方式保存到制定目录。
public interface IHostExeService
{
string GetExePath();
string GetExeDirectory();
}
public class HostExeService : IHostExeService
{
public string GetExePath()
{
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule == null)
throw new Exception("The ProcessModule that was used to start the process is null!");
return processModule.FileName;
}
public string GetExeDirectory()
{
var filePath = GetExePath();
return Path.GetDirectoryName(filePath);
}
}
public interface IResultDirectoryService
{
void Clear();
void Save(BTDevice btDevice);
}
public class ResultDirectoryService : IResultDirectoryService
{
private readonly ILog _logger = LogManager.GetLogger(typeof(ResultDirectoryService));
private readonly IHostExeService _hostExeService = new HostExeService();
private readonly string _resultDirectory;
public ResultDirectoryService()
{
_resultDirectory = Path.Combine(_hostExeService.GetExeDirectory(), "Result");
}
public void Clear()
{
if (Directory.Exists(_resultDirectory))
{
foreach (var file in Directory.GetFiles(_resultDirectory))
File.Delete(file);
}
else
{
Directory.CreateDirectory(_resultDirectory);
}
}
public void Save(BTDevice btDevice)
{
var fileName = btDevice.BTAddress.Replace(":", "") + ".txt";
var filePath = Path.Combine(_resultDirectory, fileName);
_logger.Info($"Write {btDevice} to {filePath}");
File.WriteAllText(filePath, btDevice.GetFinalReportingRSSI().ToString());
}
}
BTDeviceScanner
public class BTDeviceScanner
{
private readonly ISshCommunicator _sshCommunicator;
private readonly string[] _btAddressesToScan;
private readonly Config _config;
private readonly ILog _logger = LogManager.GetLogger(typeof(BTDeviceScanner));
private readonly BTDeviceParser _btDeviceParser = new BTDeviceParser();
private Stopwatch _stopwatch;
private StringBuilder _scanOnStringBuilder;
private BTDevice[] _btDevices;
public BTDeviceScanner(
ISshCommunicator sshCommunicator,
string[] btAddressesToScan)
{
_sshCommunicator = sshCommunicator;
_btAddressesToScan = btAddressesToScan;
_config = new CachedConfigLoader().Load();
}
public BTDevice[] ScanBTDevices()
{
_scanOnStringBuilder = new StringBuilder();
_stopwatch = Stopwatch.StartNew();
TurnOnScan();
while (true)
{
Thread.Sleep(millisecondsTimeout: 500);
AppendExistingString();
UpdateBTDevices();
if (ShouldStopDueToAllHaveBeenFound())
break;
if (ShouldStopDueToAllHaveBeenFoundButNoneIsWithRssi())
break;
if (ShouldStopDueToScanTimeout())
break;
}
_logger.Info($"The final scan response is as below.\n{_scanOnStringBuilder}");
TurnOffScan();
return _btDevices;
}
private void UpdateBTDevices()
{
_btDevices =
_btDeviceParser
.Parse(_scanOnStringBuilder.ToString(), _btAddressesToScan);
}
private bool ShouldStopDueToScanTimeout()
{
var shouldStop = _stopwatch.ElapsedMilliseconds > _config.ScanMilliseconds;
if (shouldStop)
_logger.Warn($"Scan timeout {_config.ScanMilliseconds} ms has happened.");
return shouldStop;
}
private bool ShouldStopDueToAllHaveBeenFound()
{
var shouldStop = _btDevices.HasAllFound(_btAddressesToScan);
if (shouldStop)
_logger.Info("All BT devices have been found. Stop this scan.");
return shouldStop;
}
private bool ShouldStopDueToAllHaveBeenFoundButNoneIsWithRssi()
{
var response = _scanOnStringBuilder.ToString();
var shouldStop =
_btAddressesToScan
.Select(m => !Regex.IsMatch(response, m + @" RSSI: \-?\d+") && Regex.IsMatch(response, m))
.All(m => m);
if (shouldStop)
_logger.Warn("All BT addresses have been detected but none has RSSI reported. Stop this scan earlier.");
return shouldStop;
}
private void AppendExistingString()
{
var existingString = _sshCommunicator.ReadExistingString();
if (!string.IsNullOrEmpty(existingString))
{
_logger.Info($"More Scan On response was read.\n{existingString}");
_scanOnStringBuilder.Append(existingString);
}
}
private void TurnOnScan()
{
const int waitMilliseconds = 1000;
_logger.Info($"Turn on scan and wait {waitMilliseconds} ms");
_scanOnStringBuilder.Append(
_sshCommunicator.WriteAndRead("scan on", waitMilliseconds));
}
private void TurnOffScan()
{
_logger.Info("Turn off scan");
_sshCommunicator.WriteAndRead(
"scan off", s => s.Contains("Discovery stopped"));
}
}
TopProcessor
顶层处理函数是这个样子的。
public class TopProcessor
{
private readonly string[] _btAddressesToScan;
private readonly IResultDirectoryService _resultDirectoryService = new ResultDirectoryService();
private readonly ILog _logger = LogManager.GetLogger(typeof(TopProcessor));
public TopProcessor(string[] btAddressesToScan)
{
_btAddressesToScan = btAddressesToScan;
}
public void Process()
{
ClearResultDirectory();
var btDevicesScanned = ScanBTDevices();
SaveBTDevices(btDevicesScanned);
}
private void ClearResultDirectory()
{
_resultDirectoryService.Clear();
}
private BTDevice[] ScanBTDevices()
{
_logger.Info($"Scan these BT devices\n{string.Join("\n", _btAddressesToScan)}");
var scanExecutor = new BTScanExecutor(_btAddressesToScan);
return scanExecutor.FindBTDevices();
}
private void SaveBTDevices(BTDevice[] btDevicesScanned)
{
foreach (var btAddress in _btAddressesToScan)
{
var firstOrDefault = btDevicesScanned.FirstOrDefault(m => btAddress.ToString() == m.BTAddress.ToString());
if (firstOrDefault == null)
{
_logger.Warn($"[{btAddress}]: Can't detect this device");
}
else
{
if (firstOrDefault.HasAnyRSSI)
{
_logger.Info($"[{btAddress}]: Succeed in detecting this device");
_resultDirectoryService.Save(firstOrDefault);
}
else
_logger.Warn($"[{btAddress}]: Succeed in detecting this device, but no RSSI detected");
}
}
}
}
Console App
这个命令行程序的顶层代码
class Program
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(Program));
static void Main(string[] args)
{
var stopwatch = Stopwatch.StartNew();
try
{
Logger.Info("========== PROGRAM - START ==========");
if (args.Length < 1)
throw new ArgumentException("Should have one argument at least");
var btAddressesToFind = GetBTAddressesFrom(args[0]);
Process(btAddressesToFind);
}
catch (Exception exception)
{
Logger.Error($"!!!! Some error happened. !!!!\n{exception}");
}
finally
{
Logger.Info($"Elapsed milliseconds: {stopwatch.ElapsedMilliseconds}");
Logger.Info("========== PROGRAM - END ==========\n\n\n\n");
}
}
private static string[] GetBTAddressesFrom(string argument)
{
return
argument
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(m => m.Trim())
.ToArray();
}
private static void Process(string[] btAddressesToFind)
{
var processor = new TopProcessor(btAddressesToFind);
processor.Process();
}
}
如果有什么疑问,大家可以多交流交流。
本文介绍了如何使用C#通过树莓派3B+的蓝牙模块来扫描周围的电子设备。详细讲解了涉及的NuGet包、关键类如NetworkPinger、SshCommunication以及BTDeviceScanner等,并提供了相应的代码片段,帮助开发者理解和实现类似功能。

5965

被折叠的 条评论
为什么被折叠?



