This commit is contained in:
2019-12-04 18:57:18 +01:00
parent 4692422c9a
commit 6263791dff
225 changed files with 33065 additions and 2 deletions
@@ -0,0 +1,20 @@
namespace Unosquare.RaspberryIO
{
using System;
/// <inheritdoc />
/// <summary>
/// Occurs when an exception is thrown in the <c>Bluetooth</c> component.
/// </summary>
public class BluetoothErrorException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="BluetoothErrorException"/> class.
/// </summary>
/// <param name="message">The message.</param>
public BluetoothErrorException(string message)
: base(message)
{
}
}
}
+134
View File
@@ -0,0 +1,134 @@
namespace Unosquare.RaspberryIO.Camera
{
using System;
using System.Linq;
using Swan;
/// <summary>
/// A simple RGB color class to represent colors in RGB and YUV colorspaces.
/// </summary>
public class CameraColor
{
/// <summary>
/// Initializes a new instance of the <see cref="CameraColor"/> class.
/// </summary>
/// <param name="r">The red.</param>
/// <param name="g">The green.</param>
/// <param name="b">The blue.</param>
public CameraColor(int r, int g, int b)
: this(r, g, b, string.Empty)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CameraColor"/> class.
/// </summary>
/// <param name="r">The red.</param>
/// <param name="g">The green.</param>
/// <param name="b">The blue.</param>
/// <param name="name">The well-known color name.</param>
public CameraColor(int r, int g, int b, string name)
{
RGB = new[] { Convert.ToByte(r.Clamp(0, 255)), Convert.ToByte(g.Clamp(0, 255)), Convert.ToByte(b.Clamp(0, 255)) };
var y = (R * .299000f) + (G * .587000f) + (B * .114000f);
var u = (R * -.168736f) + (G * -.331264f) + (B * .500000f) + 128f;
var v = (R * .500000f) + (G * -.418688f) + (B * -.081312f) + 128f;
YUV = new[] { (byte)y.Clamp(0, 255), (byte)u.Clamp(0, 255), (byte)v.Clamp(0, 255) };
Name = name;
}
#region Static Definitions
/// <summary>
/// Gets the predefined white color.
/// </summary>
public static CameraColor White => new CameraColor(255, 255, 255, nameof(White));
/// <summary>
/// Gets the predefined red color.
/// </summary>
public static CameraColor Red => new CameraColor(255, 0, 0, nameof(Red));
/// <summary>
/// Gets the predefined green color.
/// </summary>
public static CameraColor Green => new CameraColor(0, 255, 0, nameof(Green));
/// <summary>
/// Gets the predefined blue color.
/// </summary>
public static CameraColor Blue => new CameraColor(0, 0, 255, nameof(Blue));
/// <summary>
/// Gets the predefined black color.
/// </summary>
public static CameraColor Black => new CameraColor(0, 0, 0, nameof(Black));
#endregion
/// <summary>
/// Gets the well-known color name.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the red byte.
/// </summary>
public byte R => RGB[0];
/// <summary>
/// Gets the green byte.
/// </summary>
public byte G => RGB[1];
/// <summary>
/// Gets the blue byte.
/// </summary>
public byte B => RGB[2];
/// <summary>
/// Gets the RGB byte array (3 bytes).
/// </summary>
public byte[] RGB { get; }
/// <summary>
/// Gets the YUV byte array (3 bytes).
/// </summary>
public byte[] YUV { get; }
/// <summary>
/// Returns a hexadecimal representation of the RGB byte array.
/// Preceded by 0x and all in lowercase.
/// </summary>
/// <param name="reverse">if set to <c>true</c> [reverse].</param>
/// <returns>A string.</returns>
public string ToRgbHex(bool reverse)
{
var data = RGB.ToArray();
if (reverse) Array.Reverse(data);
return ToHex(data);
}
/// <summary>
/// Returns a hexadecimal representation of the YUV byte array.
/// Preceded by 0x and all in lowercase.
/// </summary>
/// <param name="reverse">if set to <c>true</c> [reverse].</param>
/// <returns>A string.</returns>
public string ToYuvHex(bool reverse)
{
var data = YUV.ToArray();
if (reverse) Array.Reverse(data);
return ToHex(data);
}
/// <summary>
/// Returns a hexadecimal representation of the data byte array.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>A string.</returns>
private static string ToHex(byte[] data) => $"0x{BitConverter.ToString(data).Replace("-", string.Empty).ToLowerInvariant()}";
}
}
@@ -0,0 +1,209 @@
namespace Unosquare.RaspberryIO.Camera
{
using Swan;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// The Raspberry Pi's camera controller wrapping raspistill and raspivid programs.
/// This class is a singleton.
/// </summary>
public class CameraController : SingletonBase<CameraController>
{
#region Private Declarations
private static readonly ManualResetEventSlim OperationDone = new ManualResetEventSlim(true);
private static readonly object SyncRoot = new object();
private static CancellationTokenSource _videoTokenSource = new CancellationTokenSource();
private static Task<Task>? _videoStreamTask;
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether the camera module is busy.
/// </summary>
/// <value>
/// <c>true</c> if this instance is busy; otherwise, <c>false</c>.
/// </value>
public bool IsBusy => OperationDone.IsSet == false;
#endregion
#region Image Capture Methods
/// <summary>
/// Captures an image asynchronously.
/// </summary>
/// <param name="settings">The settings.</param>
/// <param name="ct">The ct.</param>
/// <returns>The image bytes.</returns>
/// <exception cref="InvalidOperationException">Cannot use camera module because it is currently busy.</exception>
public async Task<byte[]> CaptureImageAsync(CameraStillSettings settings, CancellationToken ct = default)
{
if (Instance.IsBusy)
throw new InvalidOperationException("Cannot use camera module because it is currently busy.");
if (settings.CaptureTimeoutMilliseconds <= 0)
throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than 0");
try
{
OperationDone.Reset();
using var output = new MemoryStream();
var exitCode = await ProcessRunner.RunProcessAsync(
settings.CommandName,
settings.CreateProcessArguments(),
(data, proc) => output.Write(data, 0, data.Length),
null,
true,
ct).ConfigureAwait(false);
return exitCode != 0 ? Array.Empty<byte>() : output.ToArray();
}
finally
{
OperationDone.Set();
}
}
/// <summary>
/// Captures an image.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns>The image bytes.</returns>
public byte[] CaptureImage(CameraStillSettings settings) => CaptureImageAsync(settings).GetAwaiter().GetResult();
/// <summary>
/// Captures a JPEG encoded image asynchronously at 90% quality.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="ct">The ct.</param>
/// <returns>The image bytes.</returns>
public Task<byte[]> CaptureImageJpegAsync(int width, int height, CancellationToken ct = default)
{
var settings = new CameraStillSettings
{
CaptureWidth = width,
CaptureHeight = height,
CaptureJpegQuality = 90,
CaptureTimeoutMilliseconds = 300,
};
return CaptureImageAsync(settings, ct);
}
/// <summary>
/// Captures a JPEG encoded image at 90% quality.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>The image bytes.</returns>
public byte[] CaptureImageJpeg(int width, int height) => CaptureImageJpegAsync(width, height).GetAwaiter().GetResult();
#endregion
#region Video Capture Methods
/// <summary>
/// Opens the video stream with a timeout of 0 (running indefinitely) at 1080p resolution, variable bitrate and 25 FPS.
/// No preview is shown.
/// </summary>
/// <param name="onDataCallback">The on data callback.</param>
/// <param name="onExitCallback">The on exit callback.</param>
public void OpenVideoStream(Action<byte[]> onDataCallback, Action? onExitCallback = null)
{
var settings = new CameraVideoSettings
{
CaptureTimeoutMilliseconds = 0,
CaptureDisplayPreview = false,
CaptureWidth = 1920,
CaptureHeight = 1080,
};
OpenVideoStream(settings, onDataCallback, onExitCallback);
}
/// <summary>
/// Opens the video stream with the supplied settings. Capture Timeout Milliseconds has to be 0 or greater.
/// </summary>
/// <param name="settings">The settings.</param>
/// <param name="onDataCallback">The on data callback.</param>
/// <param name="onExitCallback">The on exit callback.</param>
/// <exception cref="InvalidOperationException">Cannot use camera module because it is currently busy.</exception>
/// <exception cref="ArgumentException">CaptureTimeoutMilliseconds.</exception>
public void OpenVideoStream(CameraVideoSettings settings, Action<byte[]> onDataCallback, Action? onExitCallback = null)
{
if (Instance.IsBusy)
throw new InvalidOperationException("Cannot use camera module because it is currently busy.");
if (settings.CaptureTimeoutMilliseconds < 0)
throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than or equal to 0");
try
{
OperationDone.Reset();
_videoStreamTask = Task.Factory.StartNew(() => VideoWorkerDoWork(settings, onDataCallback, onExitCallback), _videoTokenSource.Token);
}
catch
{
OperationDone.Set();
throw;
}
}
/// <summary>
/// Closes the video stream of a video stream is open.
/// </summary>
public void CloseVideoStream()
{
lock (SyncRoot)
{
if (IsBusy == false)
return;
}
if (_videoTokenSource.IsCancellationRequested == false)
{
_videoTokenSource.Cancel();
_videoStreamTask?.Wait();
}
_videoTokenSource = new CancellationTokenSource();
}
private static async Task VideoWorkerDoWork(
CameraVideoSettings settings,
Action<byte[]> onDataCallback,
Action onExitCallback)
{
try
{
await ProcessRunner.RunProcessAsync(
settings.CommandName,
settings.CreateProcessArguments(),
(data, proc) => onDataCallback?.Invoke(data),
null,
true,
_videoTokenSource.Token).ConfigureAwait(false);
onExitCallback?.Invoke();
}
catch
{
// swallow
}
finally
{
Instance.CloseVideoStream();
OperationDone.Set();
}
}
#endregion
}
}
@@ -0,0 +1,82 @@
namespace Unosquare.RaspberryIO.Camera
{
using System.Globalization;
using Swan;
/// <summary>
/// Defines the Raspberry Pi camera's sensor ROI (Region of Interest).
/// </summary>
public struct CameraRect
{
/// <summary>
/// The default ROI which is the entire area.
/// </summary>
public static readonly CameraRect Default = new CameraRect { X = 0M, Y = 0M, W = 1.0M, H = 1.0M };
/// <summary>
/// Gets or sets the x in relative coordinates. (0.0 to 1.0).
/// </summary>
/// <value>
/// The x.
/// </value>
public decimal X { get; set; }
/// <summary>
/// Gets or sets the y location in relative coordinates. (0.0 to 1.0).
/// </summary>
/// <value>
/// The y.
/// </value>
public decimal Y { get; set; }
/// <summary>
/// Gets or sets the width in relative coordinates. (0.0 to 1.0).
/// </summary>
/// <value>
/// The w.
/// </value>
public decimal W { get; set; }
/// <summary>
/// Gets or sets the height in relative coordinates. (0.0 to 1.0).
/// </summary>
/// <value>
/// The h.
/// </value>
public decimal H { get; set; }
/// <summary>
/// Gets a value indicating whether this instance is equal to the default (The entire area).
/// </summary>
/// <value>
/// <c>true</c> if this instance is default; otherwise, <c>false</c>.
/// </value>
public bool IsDefault
{
get
{
Clamp();
return X == Default.X && Y == Default.Y && W == Default.W && H == Default.H;
}
}
/// <summary>
/// Clamps the members of this ROI to their minimum and maximum values.
/// </summary>
public void Clamp()
{
X = X.Clamp(0M, 1M);
Y = Y.Clamp(0M, 1M);
W = W.Clamp(0M, 1M - X);
H = H.Clamp(0M, 1M - Y);
}
/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString() => $"{X.ToString(CultureInfo.InvariantCulture)},{Y.ToString(CultureInfo.InvariantCulture)},{W.ToString(CultureInfo.InvariantCulture)},{H.ToString(CultureInfo.InvariantCulture)}";
}
}
@@ -0,0 +1,340 @@
namespace Unosquare.RaspberryIO.Camera
{
using System.Globalization;
using System.Text;
using Swan;
/// <summary>
/// A base class to implement raspistill and raspivid wrappers
/// Full documentation available at
/// https://www.raspberrypi.org/documentation/raspbian/applications/camera.md.
/// </summary>
public abstract class CameraSettingsBase
{
/// <summary>
/// The Invariant Culture shorthand.
/// </summary>
protected static readonly CultureInfo Ci = CultureInfo.InvariantCulture;
#region Capture Settings
/// <summary>
/// Gets or sets the timeout milliseconds.
/// Default value is 5000
/// Recommended value is at least 300 in order to let the light collectors open.
/// </summary>
public int CaptureTimeoutMilliseconds { get; set; } = 5000;
/// <summary>
/// Gets or sets a value indicating whether or not to show a preview window on the screen.
/// </summary>
public bool CaptureDisplayPreview { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether a preview window is shown in full screen mode if enabled.
/// </summary>
public bool CaptureDisplayPreviewInFullScreen { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether video stabilization should be enabled.
/// </summary>
public bool CaptureVideoStabilizationEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the display preview opacity only if the display preview property is enabled.
/// </summary>
public byte CaptureDisplayPreviewOpacity { get; set; } = 255;
/// <summary>
/// Gets or sets the capture sensor region of interest in relative coordinates.
/// </summary>
public CameraRect CaptureSensorRoi { get; set; } = CameraRect.Default;
/// <summary>
/// Gets or sets the capture shutter speed in microseconds.
/// Default -1, Range 0 to 6000000 (equivalent to 6 seconds).
/// </summary>
public int CaptureShutterSpeedMicroseconds { get; set; } = -1;
/// <summary>
/// Gets or sets the exposure mode.
/// </summary>
public CameraExposureMode CaptureExposure { get; set; } = CameraExposureMode.Auto;
/// <summary>
/// Gets or sets the picture EV compensation. Default is 0, Range is -10 to 10
/// Camera exposure compensation is commonly stated in terms of EV units;
/// 1 EV is equal to one exposure step (or stop), corresponding to a doubling of exposure.
/// Exposure can be adjusted by changing either the lens f-number or the exposure time;
/// which one is changed usually depends on the camera's exposure mode.
/// </summary>
public int CaptureExposureCompensation { get; set; } = 0;
/// <summary>
/// Gets or sets the capture metering mode.
/// </summary>
public CameraMeteringMode CaptureMeteringMode { get; set; } = CameraMeteringMode.Average;
/// <summary>
/// Gets or sets the automatic white balance mode. By default it is set to Auto.
/// </summary>
public CameraWhiteBalanceMode CaptureWhiteBalanceControl { get; set; } = CameraWhiteBalanceMode.Auto;
/// <summary>
/// Gets or sets the capture white balance gain on the blue channel. Example: 1.25
/// Only takes effect if White balance control is set to off.
/// Default is 0.
/// </summary>
public decimal CaptureWhiteBalanceGainBlue { get; set; } = 0M;
/// <summary>
/// Gets or sets the capture white balance gain on the red channel. Example: 1.75
/// Only takes effect if White balance control is set to off.
/// Default is 0.
/// </summary>
public decimal CaptureWhiteBalanceGainRed { get; set; } = 0M;
/// <summary>
/// Gets or sets the dynamic range compensation.
/// DRC changes the images by increasing the range of dark areas, and decreasing the brighter areas. This can improve the image in low light areas.
/// </summary>
public CameraDynamicRangeCompensation CaptureDynamicRangeCompensation { get; set; } =
CameraDynamicRangeCompensation.Off;
#endregion
#region Image Properties
/// <summary>
/// Gets or sets the width of the picture to take.
/// Less than or equal to 0 in either width or height means maximum resolution available.
/// </summary>
public int CaptureWidth { get; set; } = 640;
/// <summary>
/// Gets or sets the height of the picture to take.
/// Less than or equal to 0 in either width or height means maximum resolution available.
/// </summary>
public int CaptureHeight { get; set; } = 480;
/// <summary>
/// Gets or sets the picture sharpness. Default is 0, Range form -100 to 100.
/// </summary>
public int ImageSharpness { get; set; } = 0;
/// <summary>
/// Gets or sets the picture contrast. Default is 0, Range form -100 to 100.
/// </summary>
public int ImageContrast { get; set; } = 0;
/// <summary>
/// Gets or sets the picture brightness. Default is 50, Range form 0 to 100.
/// </summary>
public int ImageBrightness { get; set; } = 50; // from 0 to 100
/// <summary>
/// Gets or sets the picture saturation. Default is 0, Range form -100 to 100.
/// </summary>
public int ImageSaturation { get; set; } = 0;
/// <summary>
/// Gets or sets the picture ISO. Default is -1 Range is 100 to 800
/// The higher the value, the more light the sensor absorbs.
/// </summary>
public int ImageIso { get; set; } = -1;
/// <summary>
/// Gets or sets the image capture effect to be applied.
/// </summary>
public CameraImageEffect ImageEffect { get; set; } = CameraImageEffect.None;
/// <summary>
/// Gets or sets the color effect U coordinates.
/// Default is -1, Range is 0 to 255
/// 128:128 should be effectively a monochrome image.
/// </summary>
public int ImageColorEffectU { get; set; } = -1; // 0 to 255
/// <summary>
/// Gets or sets the color effect V coordinates.
/// Default is -1, Range is 0 to 255
/// 128:128 should be effectively a monochrome image.
/// </summary>
public int ImageColorEffectV { get; set; } = -1; // 0 to 255
/// <summary>
/// Gets or sets the image rotation. Default is no rotation.
/// </summary>
public CameraImageRotation ImageRotation { get; set; } = CameraImageRotation.None;
/// <summary>
/// Gets or sets a value indicating whether the image should be flipped horizontally.
/// </summary>
public bool ImageFlipHorizontally { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image should be flipped vertically.
/// </summary>
public bool ImageFlipVertically { get; set; }
/// <summary>
/// Gets or sets the image annotations using a bitmask (or flags) notation.
/// Apply a bitwise OR to the enumeration to include multiple annotations.
/// </summary>
public CameraAnnotation ImageAnnotations { get; set; } = CameraAnnotation.None;
/// <summary>
/// Gets or sets the image annotations text.
/// Text may include date/time placeholders by using the '%' character, as used by strftime.
/// Example: ABC %Y-%m-%d %X will output ABC 2015-10-28 20:09:33.
/// </summary>
public string ImageAnnotationsText { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the font size of the text annotations
/// Default is -1, range is 6 to 160.
/// </summary>
public int ImageAnnotationFontSize { get; set; } = -1;
/// <summary>
/// Gets or sets the color of the text annotations.
/// </summary>
/// <value>
/// The color of the image annotation font.
/// </value>
public CameraColor? ImageAnnotationFontColor { get; set; } = null;
/// <summary>
/// Gets or sets the background color for text annotations.
/// </summary>
/// <value>
/// The image annotation background.
/// </value>
public CameraColor? ImageAnnotationBackground { get; set; } = null;
#endregion
#region Interface
/// <summary>
/// Gets the command file executable.
/// </summary>
public abstract string CommandName { get; }
/// <summary>
/// Creates the process arguments.
/// </summary>
/// <returns>The string that represents the process arguments.</returns>
public virtual string CreateProcessArguments()
{
var sb = new StringBuilder();
sb.Append("-o -"); // output to standard output as opposed to a file.
sb.Append($" -t {(CaptureTimeoutMilliseconds < 0 ? "0" : CaptureTimeoutMilliseconds.ToString(Ci))}");
// Basic Width and height
if (CaptureWidth > 0 && CaptureHeight > 0)
{
sb.Append($" -w {CaptureWidth.ToString(Ci)}");
sb.Append($" -h {CaptureHeight.ToString(Ci)}");
}
// Display Preview
if (CaptureDisplayPreview)
{
if (CaptureDisplayPreviewInFullScreen)
sb.Append(" -f");
if (CaptureDisplayPreviewOpacity != byte.MaxValue)
sb.Append($" -op {CaptureDisplayPreviewOpacity.ToString(Ci)}");
}
else
{
sb.Append(" -n"); // no preview
}
// Picture Settings
if (ImageSharpness != 0)
sb.Append($" -sh {ImageSharpness.Clamp(-100, 100).ToString(Ci)}");
if (ImageContrast != 0)
sb.Append($" -co {ImageContrast.Clamp(-100, 100).ToString(Ci)}");
if (ImageBrightness != 50)
sb.Append($" -br {ImageBrightness.Clamp(0, 100).ToString(Ci)}");
if (ImageSaturation != 0)
sb.Append($" -sa {ImageSaturation.Clamp(-100, 100).ToString(Ci)}");
if (ImageIso >= 100)
sb.Append($" -ISO {ImageIso.Clamp(100, 800).ToString(Ci)}");
if (CaptureVideoStabilizationEnabled)
sb.Append(" -vs");
if (CaptureExposureCompensation != 0)
sb.Append($" -ev {CaptureExposureCompensation.Clamp(-10, 10).ToString(Ci)}");
if (CaptureExposure != CameraExposureMode.Auto)
sb.Append($" -ex {CaptureExposure.ToString().ToLowerInvariant()}");
if (CaptureWhiteBalanceControl != CameraWhiteBalanceMode.Auto)
sb.Append($" -awb {CaptureWhiteBalanceControl.ToString().ToLowerInvariant()}");
if (ImageEffect != CameraImageEffect.None)
sb.Append($" -ifx {ImageEffect.ToString().ToLowerInvariant()}");
if (ImageColorEffectU >= 0 && ImageColorEffectV >= 0)
{
sb.Append(
$" -cfx {ImageColorEffectU.Clamp(0, 255).ToString(Ci)}:{ImageColorEffectV.Clamp(0, 255).ToString(Ci)}");
}
if (CaptureMeteringMode != CameraMeteringMode.Average)
sb.Append($" -mm {CaptureMeteringMode.ToString().ToLowerInvariant()}");
if (ImageRotation != CameraImageRotation.None)
sb.Append($" -rot {((int)ImageRotation).ToString(Ci)}");
if (ImageFlipHorizontally)
sb.Append(" -hf");
if (ImageFlipVertically)
sb.Append(" -vf");
if (CaptureSensorRoi.IsDefault == false)
sb.Append($" -roi {CaptureSensorRoi}");
if (CaptureShutterSpeedMicroseconds > 0)
sb.Append($" -ss {CaptureShutterSpeedMicroseconds.Clamp(0, 6000000).ToString(Ci)}");
if (CaptureDynamicRangeCompensation != CameraDynamicRangeCompensation.Off)
sb.Append($" -drc {CaptureDynamicRangeCompensation.ToString().ToLowerInvariant()}");
if (CaptureWhiteBalanceControl == CameraWhiteBalanceMode.Off &&
(CaptureWhiteBalanceGainBlue != 0M || CaptureWhiteBalanceGainRed != 0M))
sb.Append($" -awbg {CaptureWhiteBalanceGainBlue.ToString(Ci)},{CaptureWhiteBalanceGainRed.ToString(Ci)}");
if (ImageAnnotationFontSize > 0)
{
sb.Append($" -ae {ImageAnnotationFontSize.Clamp(6, 160).ToString(Ci)}");
sb.Append($",{(ImageAnnotationFontColor == null ? "0xff" : ImageAnnotationFontColor.ToYuvHex(true))}");
if (ImageAnnotationBackground != null)
{
ImageAnnotations |= CameraAnnotation.SolidBackground;
sb.Append($",{ImageAnnotationBackground.ToYuvHex(true)}");
}
}
if (ImageAnnotations != CameraAnnotation.None)
sb.Append($" -a {((int)ImageAnnotations).ToString(Ci)}");
if (string.IsNullOrWhiteSpace(ImageAnnotationsText) == false)
sb.Append($" -a \"{ImageAnnotationsText.Replace("\"", "'")}\"");
return sb.ToString();
}
#endregion
}
}
@@ -0,0 +1,120 @@
namespace Unosquare.RaspberryIO.Camera
{
using System;
using System.Collections.Generic;
using System.Text;
using Swan;
/// <summary>
/// Defines a wrapper for the raspistill program and its settings (command-line arguments).
/// </summary>
/// <seealso cref="CameraSettingsBase" />
public class CameraStillSettings : CameraSettingsBase
{
private int _rotate;
/// <inheritdoc />
public override string CommandName => "raspistill";
/// <summary>
/// Gets or sets a value indicating whether the preview window (if enabled) uses native capture resolution
/// This may slow down preview FPS.
/// </summary>
public bool CaptureDisplayPreviewAtResolution { get; set; } = false;
/// <summary>
/// Gets or sets the encoding format the hardware will use for the output.
/// </summary>
public CameraImageEncodingFormat CaptureEncoding { get; set; } = CameraImageEncodingFormat.Jpg;
/// <summary>
/// Gets or sets the quality for JPEG only encoding mode.
/// Value ranges from 0 to 100.
/// </summary>
public int CaptureJpegQuality { get; set; } = 90;
/// <summary>
/// Gets or sets a value indicating whether the JPEG encoder should add raw bayer metadata.
/// </summary>
public bool CaptureJpegIncludeRawBayerMetadata { get; set; } = false;
/// <summary>
/// JPEG EXIF data
/// Keys and values must be already properly escaped. Otherwise the command will fail.
/// </summary>
public Dictionary<string, string> CaptureJpegExtendedInfo { get; } = new Dictionary<string, string>();
/// <summary>
/// Gets or sets a value indicating whether [horizontal flip].
/// </summary>
/// <value>
/// <c>true</c> if [horizontal flip]; otherwise, <c>false</c>.
/// </value>
public bool HorizontalFlip { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether [vertical flip].
/// </summary>
/// <value>
/// <c>true</c> if [vertical flip]; otherwise, <c>false</c>.
/// </value>
public bool VerticalFlip { get; set; } = false;
/// <summary>
/// Gets or sets the rotation.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Valid range 0-359.</exception>
public int Rotation
{
get => _rotate;
set
{
if (value < 0 || value > 359)
{
throw new ArgumentOutOfRangeException(nameof(value), "Valid range 0-359");
}
_rotate = value;
}
}
/// <inheritdoc />
public override string CreateProcessArguments()
{
var sb = new StringBuilder(base.CreateProcessArguments());
sb.Append($" -e {CaptureEncoding.ToString().ToLowerInvariant()}");
// JPEG Encoder specific arguments
if (CaptureEncoding == CameraImageEncodingFormat.Jpg)
{
sb.Append($" -q {CaptureJpegQuality.Clamp(0, 100).ToString(Ci)}");
if (CaptureJpegIncludeRawBayerMetadata)
sb.Append(" -r");
// JPEG EXIF data
if (CaptureJpegExtendedInfo.Count > 0)
{
foreach (var kvp in CaptureJpegExtendedInfo)
{
if (string.IsNullOrWhiteSpace(kvp.Key) || string.IsNullOrWhiteSpace(kvp.Value))
continue;
sb.Append($" -x \"{kvp.Key.Replace("\"", "'")}={kvp.Value.Replace("\"", "'")}\"");
}
}
}
// Display preview settings
if (CaptureDisplayPreview && CaptureDisplayPreviewAtResolution) sb.Append(" -fp");
if (Rotation != 0) sb.Append($" -rot {Rotation}");
if (HorizontalFlip) sb.Append(" -hf");
if (VerticalFlip) sb.Append(" -vf");
return sb.ToString();
}
}
}
@@ -0,0 +1,104 @@
namespace Unosquare.RaspberryIO.Camera
{
using System.Text;
/// <summary>
/// Represents the raspivid camera settings for video capture functionality.
/// </summary>
/// <seealso cref="CameraSettingsBase" />
public class CameraVideoSettings : CameraSettingsBase
{
private int _length;
/// <inheritdoc />
public override string CommandName => "raspivid";
/// <summary>
/// Use bits per second, so 10Mbits/s would be -b 10000000. For H264, 1080p30 a high quality bitrate would be 15Mbits/s or more.
/// Maximum bitrate is 25Mbits/s (-b 25000000), but much over 17Mbits/s won't show noticeable improvement at 1080p30.
/// Default -1.
/// </summary>
public int CaptureBitrate { get; set; } = -1;
/// <summary>
/// Gets or sets the framerate.
/// Default 25, range 2 to 30.
/// </summary>
public int CaptureFramerate { get; set; } = 25;
/// <summary>
/// Sets the intra refresh period (GoP) rate for the recorded video. H264 video uses a complete frame (I-frame) every intra
/// refresh period, from which subsequent frames are based. This option specifies the number of frames between each I-frame.
/// Larger numbers here will reduce the size of the resulting video, and smaller numbers make the stream less error-prone.
/// </summary>
public int CaptureKeyframeRate { get; set; } = 25;
/// <summary>
/// Sets the initial quantisation parameter for the stream. Varies from approximately 10 to 40, and will greatly affect
/// the quality of the recording. Higher values reduce quality and decrease file size. Combine this setting with a
/// bitrate of 0 to set a completely variable bitrate.
/// </summary>
public int CaptureQuantisation { get; set; } = 23;
/// <summary>
/// Gets or sets the profile.
/// Sets the H264 profile to be used for the encoding.
/// Default is Main mode.
/// </summary>
public CameraH264Profile CaptureProfile { get; set; } = CameraH264Profile.Main;
/// <summary>
/// Forces the stream to include PPS and SPS headers on every I-frame. Needed for certain streaming cases
/// e.g. Apple HLS. These headers are small, so don't greatly increase the file size.
/// </summary>
/// <value>
/// <c>true</c> if [interleave headers]; otherwise, <c>false</c>.
/// </value>
public bool CaptureInterleaveHeaders { get; set; } = true;
/// <summary>
/// Toggle fullscreen mode for video preview.
/// </summary>
public bool Fullscreen { get; set; } = false;
/// <summary>
/// Specifies the path to save video files.
/// </summary>
public string VideoFileName { get; set; }
/// <summary>
/// Video stream length in seconds.
/// </summary>
public int LengthInSeconds
{
get => _length;
set => _length = value * 1000;
}
/// <summary>
/// Switch on an option to display the preview after compression. This will show any compression artefacts in the preview window. In normal operation,
/// the preview will show the camera output prior to being compressed. This option is not guaranteed to work in future releases.
/// </summary>
/// <value>
/// <c>true</c> if [capture display preview encoded]; otherwise, <c>false</c>.
/// </value>
public bool CaptureDisplayPreviewEncoded { get; set; } = false;
/// <inheritdoc />
public override string CreateProcessArguments()
{
var sb = new StringBuilder(base.CreateProcessArguments());
if (Fullscreen)
sb.Append(" -f");
if (LengthInSeconds != 0)
sb.Append($" -t {LengthInSeconds}");
if (!string.IsNullOrEmpty(VideoFileName))
sb.Append($" -o {VideoFileName}");
return sb.ToString();
}
}
}
+423
View File
@@ -0,0 +1,423 @@
namespace Unosquare.RaspberryIO.Camera
{
using System;
/// <summary>
/// Defines the available encoding formats for the Raspberry Pi camera module.
/// </summary>
public enum CameraImageEncodingFormat
{
/// <summary>
/// The JPG
/// </summary>
Jpg,
/// <summary>
/// The BMP
/// </summary>
Bmp,
/// <summary>
/// The GIF
/// </summary>
Gif,
/// <summary>
/// The PNG
/// </summary>
Png
}
/// <summary>
/// Defines the different exposure modes for the Raspberry Pi's camera module.
/// </summary>
public enum CameraExposureMode
{
/// <summary>
/// The automatic
/// </summary>
Auto,
/// <summary>
/// The night
/// </summary>
Night,
/// <summary>
/// The night preview
/// </summary>
NightPreview,
/// <summary>
/// The backlight
/// </summary>
Backlight,
/// <summary>
/// The spotlight
/// </summary>
Spotlight,
/// <summary>
/// The sports
/// </summary>
Sports,
/// <summary>
/// The snow
/// </summary>
Snow,
/// <summary>
/// The beach
/// </summary>
Beach,
/// <summary>
/// The very long
/// </summary>
VeryLong,
/// <summary>
/// The fixed FPS
/// </summary>
FixedFps,
/// <summary>
/// The anti shake
/// </summary>
AntiShake,
/// <summary>
/// The fireworks
/// </summary>
Fireworks
}
/// <summary>
/// Defines the different AWB (Auto White Balance) modes for the Raspberry Pi's camera module.
/// </summary>
public enum CameraWhiteBalanceMode
{
/// <summary>
/// No white balance
/// </summary>
Off,
/// <summary>
/// The automatic
/// </summary>
Auto,
/// <summary>
/// The sun
/// </summary>
Sun,
/// <summary>
/// The cloud
/// </summary>
Cloud,
/// <summary>
/// The shade
/// </summary>
Shade,
/// <summary>
/// The tungsten
/// </summary>
Tungsten,
/// <summary>
/// The fluorescent
/// </summary>
Fluorescent,
/// <summary>
/// The incandescent
/// </summary>
Incandescent,
/// <summary>
/// The flash
/// </summary>
Flash,
/// <summary>
/// The horizon
/// </summary>
Horizon
}
/// <summary>
/// Defines the available image effects for the Raspberry Pi's camera module.
/// </summary>
public enum CameraImageEffect
{
/// <summary>
/// No effect
/// </summary>
None,
/// <summary>
/// The negative
/// </summary>
Negative,
/// <summary>
/// The solarise
/// </summary>
Solarise,
/// <summary>
/// The whiteboard
/// </summary>
Whiteboard,
/// <summary>
/// The blackboard
/// </summary>
Blackboard,
/// <summary>
/// The sketch
/// </summary>
Sketch,
/// <summary>
/// The denoise
/// </summary>
Denoise,
/// <summary>
/// The emboss
/// </summary>
Emboss,
/// <summary>
/// The oil paint
/// </summary>
OilPaint,
/// <summary>
/// The hatch
/// </summary>
Hatch,
/// <summary>
/// Graphite Pen
/// </summary>
GPen,
/// <summary>
/// The pastel
/// </summary>
Pastel,
/// <summary>
/// The water colour
/// </summary>
WaterColour,
/// <summary>
/// The film
/// </summary>
Film,
/// <summary>
/// The blur
/// </summary>
Blur,
/// <summary>
/// The saturation
/// </summary>
Saturation,
/// <summary>
/// The solour swap
/// </summary>
SolourSwap,
/// <summary>
/// The washed out
/// </summary>
WashedOut,
/// <summary>
/// The colour point
/// </summary>
ColourPoint,
/// <summary>
/// The colour balance
/// </summary>
ColourBalance,
/// <summary>
/// The cartoon
/// </summary>
Cartoon
}
/// <summary>
/// Defines the different metering modes for the Raspberry Pi's camera module.
/// </summary>
public enum CameraMeteringMode
{
/// <summary>
/// The average
/// </summary>
Average,
/// <summary>
/// The spot
/// </summary>
Spot,
/// <summary>
/// The backlit
/// </summary>
Backlit,
/// <summary>
/// The matrix
/// </summary>
Matrix
}
/// <summary>
/// Defines the different image rotation modes for the Raspberry Pi's camera module.
/// </summary>
public enum CameraImageRotation
{
/// <summary>
/// No rerotation
/// </summary>
None = 0,
/// <summary>
/// 90 Degrees
/// </summary>
Degrees90 = 90,
/// <summary>
/// 180 Degrees
/// </summary>
Degrees180 = 180,
/// <summary>
/// 270 degrees
/// </summary>
Degrees270 = 270
}
/// <summary>
/// Defines the different DRC (Dynamic Range Compensation) modes for the Raspberry Pi's camera module
/// Helpful for low light photos.
/// </summary>
public enum CameraDynamicRangeCompensation
{
/// <summary>
/// The off setting
/// </summary>
Off,
/// <summary>
/// The low
/// </summary>
Low,
/// <summary>
/// The medium
/// </summary>
Medium,
/// <summary>
/// The high
/// </summary>
High
}
/// <summary>
/// Defines the bit-wise mask flags for the available annotation elements for the Raspberry Pi's camera module.
/// </summary>
[Flags]
public enum CameraAnnotation
{
/// <summary>
/// The none
/// </summary>
None = 0,
/// <summary>
/// The time
/// </summary>
Time = 4,
/// <summary>
/// The date
/// </summary>
Date = 8,
/// <summary>
/// The shutter settings
/// </summary>
ShutterSettings = 16,
/// <summary>
/// The caf settings
/// </summary>
CafSettings = 32,
/// <summary>
/// The gain settings
/// </summary>
GainSettings = 64,
/// <summary>
/// The lens settings
/// </summary>
LensSettings = 128,
/// <summary>
/// The motion settings
/// </summary>
MotionSettings = 256,
/// <summary>
/// The frame number
/// </summary>
FrameNumber = 512,
/// <summary>
/// The solid background
/// </summary>
SolidBackground = 1024
}
/// <summary>
/// Defines the different H.264 encoding profiles to be used when capturing video.
/// </summary>
public enum CameraH264Profile
{
/// <summary>
/// BP: Primarily for lower-cost applications with limited computing resources,
/// this profile is used widely in videoconferencing and mobile applications.
/// </summary>
Baseline,
/// <summary>
/// MP: Originally intended as the mainstream consumer profile for broadcast
/// and storage applications, the importance of this profile faded when the High profile was developed for those applications.
/// </summary>
Main,
/// <summary>
/// HiP: The primary profile for broadcast and disc storage applications, particularly
/// for high-definition television applications (this is the profile adopted into HD DVD and Blu-ray Disc, for example).
/// </summary>
High
}
}
@@ -0,0 +1,113 @@
namespace Unosquare.RaspberryIO.Computer
{
using Swan;
using System;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// Settings for audio device.
/// </summary>
public class AudioSettings : SingletonBase<AudioSettings>
{
private const string DefaultControlName = "PCM";
private const int DefaultCardNumber = 0;
private readonly string[] _errorMess = { "Invalid", "Unable" };
/// <summary>
/// Gets the current audio state.
/// </summary>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <returns>An <see cref="AudioState"/> object.</returns>
/// <exception cref="InvalidOperationException">Invalid command, card number or control name.</exception>
public async Task<AudioState> GetState(int cardNumber = DefaultCardNumber, string controlName = DefaultControlName)
{
var volumeInfo = await ProcessRunner.GetProcessOutputAsync("amixer", $"-c {cardNumber} get {controlName}").ConfigureAwait(false);
var lines = volumeInfo.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (!lines.Any())
throw new InvalidOperationException("Invalid command.");
if (_errorMess.Any(x => lines[0].Contains(x)))
throw new InvalidOperationException(lines[0]);
var volumeLine = lines
.FirstOrDefault(x => x.Trim()
.StartsWith("Mono:", StringComparison.OrdinalIgnoreCase));
if (volumeLine == null)
throw new InvalidOperationException("Unexpected output from 'amixer'.");
var sections = volumeLine.Split(new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries);
var level = int.Parse(sections[3].Substring(1, sections[3].Length - 3),
System.Globalization.NumberFormatInfo.InvariantInfo);
var decibels = float.Parse(sections[4].Substring(1, sections[4].Length - 4),
System.Globalization.NumberFormatInfo.InvariantInfo);
var isMute = sections[5].Equals("[off]",
StringComparison.CurrentCultureIgnoreCase);
return new AudioState(cardNumber, controlName, level, decibels, isMute);
}
/// <summary>
/// Sets the volume percentage.
/// </summary>
/// <param name="level">The percentage level.</param>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="InvalidOperationException">Invalid card number or control name.</exception>
public Task SetVolumePercentage(int level, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) =>
SetAudioCommand($"{level}%", cardNumber, controlName);
/// <summary>
/// Sets the volume by decibels.
/// </summary>
/// <param name="decibels">The decibels.</param>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="InvalidOperationException">Invalid card number or control name.</exception>
public Task SetVolumeByDecibels(float decibels, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) =>
SetAudioCommand($"{decibels}dB", cardNumber, controlName);
/// <summary>
/// Increments the volume by decibels.
/// </summary>
/// <param name="decibels">The decibels to increment or decrement.</param>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="InvalidOperationException">Invalid card number or control name.</exception>
public Task IncrementVolume(float decibels, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) =>
SetAudioCommand($"{decibels}dB{(decibels < 0 ? "-" : "+")}", cardNumber, controlName);
/// <summary>
/// Toggles the mute state.
/// </summary>
/// <param name="mute">if set to <c>true</c>, mutes the audio.</param>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="InvalidOperationException">Invalid card number or control name.</exception>
public Task ToggleMute(bool mute, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) =>
SetAudioCommand(mute ? "mute" : "unmute", cardNumber, controlName);
private static async Task<string> SetAudioCommand(string command, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName)
{
var taskResult = await ProcessRunner.GetProcessOutputAsync("amixer", $"-q -c {cardNumber} -- set {controlName} {command}").ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(taskResult))
throw new InvalidOperationException(taskResult.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).First());
return taskResult;
}
}
}
@@ -0,0 +1,64 @@
namespace Unosquare.RaspberryIO.Computer
{
/// <summary>
/// Manage the volume of any sound device.
/// </summary>
public readonly struct AudioState
{
/// <summary>
/// Initializes a new instance of the <see cref="AudioState"/> struct.
/// </summary>
/// <param name="cardNumber">The card number.</param>
/// <param name="controlName">Name of the control.</param>
/// <param name="level">The volume level in percentaje.</param>
/// <param name="decibels">The volume level in decibels.</param>
/// <param name="isMute">if set to <c>true</c> the audio is mute.</param>
public AudioState(int cardNumber, string controlName, int level, float decibels, bool isMute)
{
CardNumber = cardNumber;
ControlName = controlName;
Level = level;
Decibels = decibels;
IsMute = isMute;
}
/// <summary>
/// Gets the card number.
/// </summary>
public int CardNumber { get; }
/// <summary>
/// Gets the name of the current control.
/// </summary>
public string ControlName { get; }
/// <summary>
/// Gets the volume level in percentage.
/// </summary>
public int Level { get; }
/// <summary>
/// Gets the volume level in decibels.
/// </summary>
public float Decibels { get; }
/// <summary>
/// Gets a value indicating whether the audio is mute.
/// </summary>
public bool IsMute { get; }
/// <summary>
/// Returns a <see cref="string" /> that represents the audio state.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents the audio state.
/// </returns>
public override string ToString() =>
"Device information: \n" +
$">> Name: {ControlName}\n" +
$">> Card number: {CardNumber}\n" +
$">> Volume (%): {Level}%\n" +
$">> Volume (dB): {Decibels:0.00}dB\n" +
$">> Mute: [{(IsMute ? "Off" : "On")}]\n\n";
}
}
+271
View File
@@ -0,0 +1,271 @@
namespace Unosquare.RaspberryIO.Computer
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Swan;
/// <summary>
/// Represents the Bluetooth information.
/// </summary>
public class Bluetooth : SingletonBase<Bluetooth>
{
private const string BcCommand = "bluetoothctl";
/// <summary>
/// Turns on the Bluetooth adapter.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns true or false depending if the controller was turned on.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to power on:.</exception>
public async Task<bool> PowerOn(CancellationToken cancellationToken = default)
{
try
{
var output = await ProcessRunner.GetProcessOutputAsync(BcCommand, "power on", null, cancellationToken)
.ConfigureAwait(false);
return output.Contains("succeeded");
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to power on: {ex.Message}");
}
}
/// <summary>
/// Turns off the bluetooth adapter.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns true or false depending if the controller was turned off.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to power off:.</exception>
public async Task<bool> PowerOff(CancellationToken cancellationToken = default)
{
try
{
var output = await ProcessRunner.GetProcessOutputAsync(BcCommand, "power off", null, cancellationToken)
.ConfigureAwait(false);
return output.Contains("succeeded");
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to power off: {ex.Message}");
}
}
/// <summary>
/// Gets the list of detected devices.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns the list of detected devices.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to retrieve devices:.</exception>
public async Task<IEnumerable<string>> ListDevices(CancellationToken cancellationToken = default)
{
try
{
using var cancellationTokenSource = new CancellationTokenSource(3000);
await ProcessRunner.GetProcessOutputAsync(BcCommand, "scan on", null, cancellationTokenSource.Token)
.ConfigureAwait(false);
await ProcessRunner.GetProcessOutputAsync(BcCommand, "scan off", null, cancellationToken)
.ConfigureAwait(false);
var devices = await ProcessRunner.GetProcessOutputAsync(BcCommand, "devices", null, cancellationToken)
.ConfigureAwait(false);
return devices.Trim().Split('\n').Select(x => x.Trim());
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to retrieve devices: {ex.Message}");
}
}
/// <summary>
/// Gets the list of bluetooth controllers.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns the list of bluetooth controllers.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to retrieve controllers:.</exception>
public async Task<IEnumerable<string>> ListControllers(CancellationToken cancellationToken = default)
{
try
{
var controllers = await ProcessRunner.GetProcessOutputAsync(BcCommand, "list", null, cancellationToken)
.ConfigureAwait(false);
return controllers.Trim().Split('\n').Select(x => x.Trim());
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to retrieve controllers: {ex.Message}");
}
}
/// <summary>
/// Pairs a specific device with a specific controller.
/// </summary>
/// <param name="controllerAddress">The mac address of the controller that will be used to pair.</param>
/// <param name="deviceAddress">The mac address of the device that will be paired.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns true or false if the pair was successfully.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to Pair:.</exception>
public async Task<bool> Pair(
string controllerAddress,
string deviceAddress,
CancellationToken cancellationToken = default)
{
try
{
// Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes.
await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Makes the controller visible to other devices.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken)
.ConfigureAwait(false);
// Readies the controller for pairing. Remember that you have three minutes after running this command to pair.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken)
.ConfigureAwait(false);
// Pairs the device with the controller.
var result = await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"pair {deviceAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken)
.ConfigureAwait(false);
return result.Contains("Paired: yes");
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to Pair: {ex.Message}");
}
}
/// <summary>
/// Performs a connection of a given controller with a given device.
/// </summary>
/// <param name="controllerAddress">The mac address of the controller that will be used to make the connection.</param>
/// <param name="deviceAddress">The mac address of the device that will be connected.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns true or false if the connection was successfully.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to connect:.</exception>
public async Task<bool> Connect(
string controllerAddress,
string deviceAddress,
CancellationToken cancellationToken = default)
{
try
{
// Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes.
await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Makes the controller visible to other devices.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken)
.ConfigureAwait(false);
// Readies the controller for pairing. Remember that you have three minutes after running this command to pair.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken)
.ConfigureAwait(false);
// Readies the device for pairing.
var result = await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"connect {deviceAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken)
.ConfigureAwait(false);
return result.Contains("Connected: yes");
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to connect: {ex.Message}");
}
}
/// <summary>
/// Sets the device to re-pair automatically when it is turned on, which eliminates the need to pair all over again.
/// </summary>
/// <param name="controllerAddress">The mac address of the controller will be used.</param>
/// <param name="deviceAddress">The mac address of the device will be added to the trust list devices.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns true or false if the operation was successful.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to add to trust devices list:.</exception>
public async Task<bool> Trust(
string controllerAddress,
string deviceAddress,
CancellationToken cancellationToken = default)
{
try
{
// Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes.
await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Makes the controller visible to other devices.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken)
.ConfigureAwait(false);
// Readies the controller for pairing. Remember that you have three minutes after running this command to pair.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken)
.ConfigureAwait(false);
// Sets the device to re-pair automatically when it is turned on, which eliminates the need to pair all over again.
var result = await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"trust {deviceAddress}", null, cancellationToken)
.ConfigureAwait(false);
// Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole.
await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken)
.ConfigureAwait(false);
return result.Contains("Trusted: yes");
}
catch (Exception ex)
{
throw new BluetoothErrorException($"Failed to add to trust devices list: {ex.Message}");
}
}
/// <summary>
/// Displays information about a particular device.
/// </summary>
/// <param name="deviceAddress">The mac address of the device which info will be retrieved.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns the device info.
/// </returns>
/// <exception cref="BluetoothErrorException">Failed to retrieve info for {deviceAddress}.</exception>
public async Task<string> DeviceInfo(string deviceAddress, CancellationToken cancellationToken = default)
{
var info = await ProcessRunner
.GetProcessOutputAsync(BcCommand, $"info {deviceAddress}", null, cancellationToken)
.ConfigureAwait(false);
return !string.IsNullOrEmpty(info)
? info
: throw new BluetoothErrorException($"Failed to retrieve info for {deviceAddress}");
}
}
}
@@ -0,0 +1,71 @@
namespace Unosquare.RaspberryIO.Computer
{
using System.Globalization;
using System.IO;
using Swan;
/// <summary>
/// The Official Raspberry Pi 7-inch touch display from the foundation
/// Some docs available here:
/// http://forums.pimoroni.com/t/official-7-raspberry-pi-touch-screen-faq/959.
/// </summary>
public class DsiDisplay : SingletonBase<DsiDisplay>
{
private const string BacklightFilename = "/sys/class/backlight/rpi_backlight/bl_power";
private const string BrightnessFilename = "/sys/class/backlight/rpi_backlight/brightness";
/// <summary>
/// Prevents a default instance of the <see cref="DsiDisplay"/> class from being created.
/// </summary>
private DsiDisplay()
{
// placeholder
}
/// <summary>
/// Gets a value indicating whether the Pi Foundation Display files are present.
/// </summary>
/// <value>
/// <c>true</c> if this instance is present; otherwise, <c>false</c>.
/// </value>
public bool IsPresent => File.Exists(BrightnessFilename);
/// <summary>
/// Gets or sets the brightness of the DSI display via filesystem.
/// </summary>
/// <value>
/// The brightness.
/// </value>
public byte Brightness
{
get =>
IsPresent
? byte.TryParse(File.ReadAllText(BrightnessFilename).Trim(), out var brightness) ? brightness : (byte)0 :
(byte)0;
set
{
if (IsPresent)
File.WriteAllText(BrightnessFilename, value.ToString(CultureInfo.InvariantCulture));
}
}
/// <summary>
/// Gets or sets a value indicating whether the backlight of the DSI display on.
/// This operation is performed via the file system.
/// </summary>
/// <value>
/// <c>true</c> if this instance is backlight on; otherwise, <c>false</c>.
/// </value>
public bool IsBacklightOn
{
get =>
IsPresent && (int.TryParse(File.ReadAllText(BacklightFilename).Trim(), out var value) &&
value == 0);
set
{
if (IsPresent)
File.WriteAllText(BacklightFilename, value ? "0" : "1");
}
}
}
}
@@ -0,0 +1,40 @@
namespace Unosquare.RaspberryIO.Computer
{
using System.Net;
/// <summary>
/// Represents a Network Adapter.
/// </summary>
public class NetworkAdapterInfo
{
/// <summary>
/// Gets the name.
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// Gets the IP V4 address.
/// </summary>
public IPAddress IPv4 { get; internal set; }
/// <summary>
/// Gets the IP V6 address.
/// </summary>
public IPAddress IPv6 { get; internal set; }
/// <summary>
/// Gets the name of the access point.
/// </summary>
public string AccessPointName { get; internal set; }
/// <summary>
/// Gets the MAC (Physical) address.
/// </summary>
public string MacAddress { get; internal set; }
/// <summary>
/// Gets a value indicating whether this instance is wireless.
/// </summary>
public bool IsWireless { get; internal set; }
}
}
@@ -0,0 +1,281 @@
namespace Unosquare.RaspberryIO.Computer
{
using Swan;
using Swan.Logging;
using Swan.Net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// Represents the network information.
/// </summary>
public class NetworkSettings : SingletonBase<NetworkSettings>
{
private const string EssidTag = "ESSID:";
/// <summary>
/// Gets the local machine Host Name.
/// </summary>
public string HostName => Network.HostName;
/// <summary>
/// Retrieves the wireless networks.
/// </summary>
/// <param name="adapter">The adapter.</param>
/// <returns>A list of WiFi networks.</returns>
public Task<List<WirelessNetworkInfo>> RetrieveWirelessNetworks(string adapter) => RetrieveWirelessNetworks(new[] { adapter });
/// <summary>
/// Retrieves the wireless networks.
/// </summary>
/// <param name="adapters">The adapters.</param>
/// <returns>A list of WiFi networks.</returns>
public async Task<List<WirelessNetworkInfo>> RetrieveWirelessNetworks(string[] adapters = null)
{
var result = new List<WirelessNetworkInfo>();
foreach (var networkAdapter in adapters ?? (await RetrieveAdapters()).Where(x => x.IsWireless).Select(x => x.Name))
{
var wirelessOutput = await ProcessRunner.GetProcessOutputAsync("iwlist", $"{networkAdapter} scanning").ConfigureAwait(false);
var outputLines =
wirelessOutput.Split('\n')
.Select(x => x.Trim())
.Where(x => string.IsNullOrWhiteSpace(x) == false)
.ToArray();
for (var i = 0; i < outputLines.Length; i++)
{
var line = outputLines[i];
if (line.StartsWith(EssidTag) == false) continue;
var network = new WirelessNetworkInfo
{
Name = line.Replace(EssidTag, string.Empty).Replace("\"", string.Empty)
};
while (true)
{
if (i + 1 >= outputLines.Length) break;
// should look for two lines before the ESSID acording to the scan
line = outputLines[i - 2];
if (!line.StartsWith("Quality=")) continue;
network.Quality = line.Replace("Quality=", string.Empty);
break;
}
while (true)
{
if (i + 1 >= outputLines.Length) break;
// should look for a line before the ESSID acording to the scan
line = outputLines[i - 1];
if (!line.StartsWith("Encryption key:")) continue;
network.IsEncrypted = line.Replace("Encryption key:", string.Empty).Trim() == "on";
break;
}
if (result.Any(x => x.Name == network.Name) == false)
result.Add(network);
}
}
return result
.OrderBy(x => x.Name)
.ToList();
}
/// <summary>
/// Setups the wireless network.
/// </summary>
/// <param name="adapterName">Name of the adapter.</param>
/// <param name="networkSsid">The network ssid.</param>
/// <param name="password">The password (8 characters as minimum length).</param>
/// <param name="countryCode">The 2-letter country code in uppercase. Default is US.</param>
/// <returns>True if successful. Otherwise, false.</returns>
public async Task<bool> SetupWirelessNetwork(string adapterName, string networkSsid, string password = null, string countryCode = "US")
{
// TODO: Get the country where the device is located to set 'country' param in payload var
var payload = $"country={countryCode}\nctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\nupdate_config=1\n";
if (!string.IsNullOrWhiteSpace(password) && password.Length < 8)
throw new InvalidOperationException("The password must be at least 8 characters length.");
payload += string.IsNullOrEmpty(password)
? $"network={{\n\tssid=\"{networkSsid}\"\n\tkey_mgmt=NONE\n\t}}\n"
: $"network={{\n\tssid=\"{networkSsid}\"\n\tpsk=\"{password}\"\n\t}}\n";
try
{
File.WriteAllText("/etc/wpa_supplicant/wpa_supplicant.conf", payload);
await ProcessRunner.GetProcessOutputAsync("pkill", "-f wpa_supplicant");
await ProcessRunner.GetProcessOutputAsync("ifdown", adapterName);
await ProcessRunner.GetProcessOutputAsync("ifup", adapterName);
}
catch (Exception ex)
{
ex.Log(nameof(NetworkSettings));
return false;
}
return true;
}
/// <summary>
/// Retrieves the network adapters.
/// </summary>
/// <returns>A list of network adapters.</returns>
public async Task<List<NetworkAdapterInfo>> RetrieveAdapters()
{
const string hWaddr = "HWaddr ";
const string ether = "ether ";
var result = new List<NetworkAdapterInfo>();
var interfacesOutput = await ProcessRunner.GetProcessOutputAsync("ifconfig");
var wlanOutput = (await ProcessRunner.GetProcessOutputAsync("iwconfig"))
.Split('\n')
.Where(x => x.Contains("no wireless extensions.") == false)
.ToArray();
var outputLines = interfacesOutput.Split('\n').Where(x => string.IsNullOrWhiteSpace(x) == false).ToArray();
for (var i = 0; i < outputLines.Length; i++)
{
// grab the current line
var line = outputLines[i];
// skip if the line is indented
if (char.IsLetterOrDigit(line[0]) == false)
continue;
// Read the line as an adapter
var adapter = new NetworkAdapterInfo
{
Name = line.Substring(0, line.IndexOf(' ')).TrimEnd(':')
};
// Parse the MAC address in old version of ifconfig; it comes in the first line
if (line.IndexOf(hWaddr, StringComparison.Ordinal) >= 0)
{
var startIndexHwd = line.IndexOf(hWaddr, StringComparison.Ordinal) + hWaddr.Length;
adapter.MacAddress = line.Substring(startIndexHwd, 17).Trim();
}
// Parse the info in lines other than the first
for (var j = i + 1; j < outputLines.Length; j++)
{
// Get the contents of the indented line
var indentedLine = outputLines[j];
// We have hit the next adapter info
if (char.IsLetterOrDigit(indentedLine[0]))
{
i = j - 1;
break;
}
// Parse the MAC address in new versions of ifconfig; it no longer comes in the first line
if (indentedLine.IndexOf(ether, StringComparison.Ordinal) >= 0 && string.IsNullOrWhiteSpace(adapter.MacAddress))
{
var startIndexHwd = indentedLine.IndexOf(ether, StringComparison.Ordinal) + ether.Length;
adapter.MacAddress = indentedLine.Substring(startIndexHwd, 17).Trim();
}
// Parse the IPv4 Address
GetIPv4(indentedLine, adapter);
// Parse the IPv6 Address
GetIPv6(indentedLine, adapter);
// we have hit the end of the output in an indented line
if (j >= outputLines.Length - 1)
i = outputLines.Length;
}
// Retrieve the wireless LAN info
var wlanInfo = wlanOutput.FirstOrDefault(x => x.StartsWith(adapter.Name));
if (wlanInfo != null)
{
adapter.IsWireless = true;
var essidParts = wlanInfo.Split(new[] { EssidTag }, StringSplitOptions.RemoveEmptyEntries);
if (essidParts.Length >= 2)
{
adapter.AccessPointName = essidParts[1].Replace("\"", string.Empty).Trim();
}
}
// Add the current adapter to the result
result.Add(adapter);
}
return result.OrderBy(x => x.Name).ToList();
}
/// <summary>
/// Retrieves the current network adapter.
/// </summary>
/// <returns>The name of the current network adapter.</returns>
public static async Task<string> GetCurrentAdapterName()
{
var result = await ProcessRunner.GetProcessOutputAsync("route").ConfigureAwait(false);
var defaultLine = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(l => l.StartsWith("default", StringComparison.OrdinalIgnoreCase));
return defaultLine?.Trim().Substring(defaultLine.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1);
}
/// <summary>
/// Retrieves current wireless connected network name.
/// </summary>
/// <returns>The connected network name.</returns>
public Task<string> GetWirelessNetworkName() => ProcessRunner.GetProcessOutputAsync("iwgetid", "-r");
private static void GetIPv4(string indentedLine, NetworkAdapterInfo adapter)
{
var addressText = ParseOutputTagFromLine(indentedLine, "inet addr:") ??
ParseOutputTagFromLine(indentedLine, "inet ");
if (addressText == null) return;
if (IPAddress.TryParse(addressText, out var outValue))
adapter.IPv4 = outValue;
}
private static void GetIPv6(string indentedLine, NetworkAdapterInfo adapter)
{
var addressText = ParseOutputTagFromLine(indentedLine, "inet6 addr:") ??
ParseOutputTagFromLine(indentedLine, "inet6 ");
if (addressText == null) return;
if (IPAddress.TryParse(addressText, out var outValue))
adapter.IPv6 = outValue;
}
private static string ParseOutputTagFromLine(string indentedLine, string tagName)
{
if (indentedLine.IndexOf(tagName, StringComparison.Ordinal) < 0)
return null;
var startIndex = indentedLine.IndexOf(tagName, StringComparison.Ordinal) + tagName.Length;
var builder = new StringBuilder(1024);
for (var c = startIndex; c < indentedLine.Length; c++)
{
var currentChar = indentedLine[c];
if (!char.IsPunctuation(currentChar) && !char.IsLetterOrDigit(currentChar))
break;
builder.Append(currentChar);
}
return builder.ToString();
}
}
}
+46
View File
@@ -0,0 +1,46 @@
namespace Unosquare.RaspberryIO.Computer
{
/// <summary>
/// Represents the OS Information.
/// </summary>
public class OsInfo
{
/// <summary>
/// System name.
/// </summary>
public string SysName { get; set; }
/// <summary>
/// Node name.
/// </summary>
public string NodeName { get; set; }
/// <summary>
/// Release level.
/// </summary>
public string Release { get; set; }
/// <summary>
/// Version level.
/// </summary>
public string Version { get; set; }
/// <summary>
/// Hardware level.
/// </summary>
public string Machine { get; set; }
/// <summary>
/// Domain name.
/// </summary>
public string DomainName { get; set; }
/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString() => $"{SysName} {Release} {Version}";
}
}
+394
View File
@@ -0,0 +1,394 @@
namespace Unosquare.RaspberryIO.Computer
{
/// <summary>
/// Defines the board revision codes of the different versions of the Raspberry Pi
/// http://www.raspberrypi-spy.co.uk/2012/09/checking-your-raspberry-pi-board-version/.
/// https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md.
/// </summary>
public enum PiVersion
{
/// <summary>
/// The unknown version
/// </summary>
Unknown = 0,
/// <summary>
/// The model B Rev1
/// </summary>
ModelBRev1 = 0x0002,
/// <summary>
/// The model B Rev1 ECN0001
/// </summary>
ModelBRev1ECN0001 = 0x0003,
/// <summary>
/// The model B Rev2 Sony
/// </summary>
ModelBRev2x04 = 0x0004,
/// <summary>
/// The model B Rev2 Qisda
/// </summary>
ModelBRev2x05 = 0x0005,
/// <summary>
/// The model B Rev2 Egoman
/// </summary>
ModelBRev2x06 = 0x0006,
/// <summary>
/// The model A Rev2 Egoman
/// </summary>
ModelAx07 = 0x0007,
/// <summary>
/// The model A Rev2 Sony
/// </summary>
ModelAx08 = 0x0008,
/// <summary>
/// The model A Rev2 Qisda
/// </summary>
ModelAx09 = 0x0009,
/// <summary>
/// The model B Rev2 (512MB) Egoman
/// </summary>
ModelBRev2x0d = 0x000d,
/// <summary>
/// The model B Rev2 (512MB) Sony
/// </summary>
ModelBRev2x0e = 0x000e,
/// <summary>
/// The model B Rev2 (512MB) Egoman
/// </summary>
ModelBRev2x0f = 0x000f,
/// <summary>
/// The model B+ Rev1 Sony
/// </summary>
ModelBPlus0x10 = 0x0010,
/// <summary>
/// The compute module 1 Sony
/// </summary>
ComputeModule0x11 = 0x0011,
/// <summary>
/// The model A+ Rev1.1 Sony
/// </summary>
ModelAPlus0x12 = 0x0012,
/// <summary>
/// The model B+ Rev1.2 Embest
/// </summary>
ModelBPlus0x13 = 0x0013,
/// <summary>
/// The compute module 1 Embest
/// </summary>
ComputeModule0x14 = 0x0014,
/// <summary>
/// The model A+ Rev1.1 Embest
/// </summary>
ModelAPlus0x15 = 0x0015,
/// <summary>
/// The model A+ Rev1.1 (512MB) Sony
/// </summary>
ModelAPlus1v1Sony = 900021,
/// <summary>
/// The model B+ Rev1.2 sony
/// </summary>
ModelBPlus1v2Sony = 900032,
/// <summary>
/// The Pi Zero Rev1.2 Sony
/// </summary>
PiZero1v2 = 0x900092,
/// <summary>
/// The Pi Zero Rev1.3 SOny
/// </summary>
PiZero1v3 = 0x900093,
/// <summary>
/// The Pi Zero W Rev1.1
/// </summary>
PiZeroW = 0x9000c1,
/// <summary>
/// The Pi 3 model A+ Sony
/// </summary>
Pi3ModelAPlus = 0x9020e0,
/// <summary>
/// The Pi Zero Rev1.2 Embest
/// </summary>
PiZero1v2Embest = 0x920092,
/// <summary>
/// The Pi Zero Rev1.3 Embest
/// </summary>
PiZero1v3Embest = 0x920093,
/// <summary>
/// The Pi 2 model B Rev1.0 Sony
/// </summary>
Pi2ModelB1v0Sony = 0xa01040,
/// <summary>
/// The Pi 2 model B Rev1.1 Sony
/// </summary>
Pi2ModelB1v1Sony = 0xa01041,
/// <summary>
/// The Pi 3 model B Rev1.2 Sony
/// </summary>
Pi3ModelBSony = 0xa02082,
/// <summary>
/// The compute module 3 Rev1.0 Sony
/// </summary>
ComputeModule3Sony = 0xa020a0,
/// <summary>
/// The Pi 3 model B+ Rev1.3 Sony
/// </summary>
Pi3ModelBPlusSony = 0xa020d3,
/// <summary>
/// The Pi 2 model B Rev1.1 Embest
/// </summary>
Pi2ModelB1v1Embest = 0xa21041,
/// <summary>
/// The Pi 2 model B Rev1.2 Embest
/// </summary>
Pi2ModelB1v2 = 0xa22042,
/// <summary>
/// The Pi 3 model B Rev1.2 Embest
/// </summary>
Pi3ModelBEmbest = 0xa22082,
/// <summary>
/// The compute module 3 Rev1.0 Embest
/// </summary>
ComputeModule3Embest = 0xa220a0,
/// <summary>
/// The Pi 3 model B Rev1.2 Sony Japan
/// </summary>
Pi3ModelBSonyJapan = 0xa32082,
/// <summary>
/// The Pi 3 model B Rev1.2 Stadium
/// </summary>
Pi3ModelBStadium = 0xa52082,
/// <summary>
/// The compute module 3+ Rev 1.0 Sony
/// </summary>
ComputeModule3PlusSony = 0xa02100,
/// <summary>
/// The Pi 4 model B 1GB, Sony
/// </summary>
Pi4ModelB1Gb = 0xa03111,
/// <summary>
/// The Pi 4 model B 2GB, Sony
/// </summary>
Pi4ModelB2Gb = 0xb03111,
/// <summary>
/// The Pi 4 model B 4GB, Sony
/// </summary>
Pi4ModelB4Gb = 0xc03111,
}
/// <summary>
/// Defines the board model accordingly to new-style revision codes.
/// </summary>
public enum BoardModel
{
/// <summary>
/// Model A
/// </summary>
ModelA = 0,
/// <summary>
/// Model B
/// </summary>
ModelB = 1,
/// <summary>
/// Model A+
/// </summary>
ModelAPlus = 2,
/// <summary>
/// Model B+
/// </summary>
ModelBPlus = 3,
/// <summary>
/// Model 2B
/// </summary>
Model2B = 4,
/// <summary>
/// Alpha (early prototype)
/// </summary>
Alpha = 5,
/// <summary>
/// Compute Module 1
/// </summary>
CM1 = 6,
/// <summary>
/// Model 3B
/// </summary>
Model3B = 8,
/// <summary>
/// Model Zero
/// </summary>
Zero = 9,
/// <summary>
/// Compute Module 3
/// </summary>
CM3 = 0xa,
/// <summary>
/// Model Zero W
/// </summary>
ZeroW = 0xc,
/// <summary>
/// Model 3B+
/// </summary>
Model3BPlus = 0xd,
/// <summary>
/// Model 3A+
/// </summary>
Model3APlus = 0xe,
/// <summary>
/// Reserved (Internal use only)
/// </summary>
InternalUse = 0xf,
/// <summary>
/// Compute Module 3+
/// </summary>
CM3Plus = 0x10,
/// <summary>
/// Model 4B
/// </summary>
Model4B = 0x11,
}
/// <summary>
/// Defines the processor model accordingly to new-style revision codes.
/// </summary>
public enum ProcessorModel
{
/// <summary>
/// The BCMM2835 processor.
/// </summary>
BCM2835,
/// <summary>
/// The BCMM2836 processor.
/// </summary>
BCM2836,
/// <summary>
/// The BCMM2837 processor.
/// </summary>
BCM2837,
/// <summary>
/// The BCM2711 processor.
/// </summary>
BCM2711,
}
/// <summary>
/// Defines the manufacturer accordingly to new-style revision codes.
/// </summary>
public enum Manufacturer
{
/// <summary>
/// Sony UK
/// </summary>
SonyUK,
/// <summary>
/// Egoman
/// </summary>
Egoman,
/// <summary>
/// Embest
/// </summary>
Embest,
/// <summary>
/// Sony Japan
/// </summary>
SonyJapan,
/// <summary>
/// Embest
/// </summary>
Embest2,
/// <summary>
/// Stadium
/// </summary>
Stadium,
}
/// <summary>
/// Defines the memory size accordingly to new-style revision codes.
/// </summary>
public enum MemorySize
{
/// <summary>
/// 256 MB
/// </summary>
Memory256,
/// <summary>
/// 512 MB
/// </summary>
Memory512,
/// <summary>
/// 1 GB
/// </summary>
Memory1024,
/// <summary>
/// 2 GB
/// </summary>
Memory2048,
/// <summary>
/// 4 GB
/// </summary>
Memory4096,
}
}
@@ -0,0 +1,390 @@
namespace Unosquare.RaspberryIO.Computer
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Abstractions;
using Native;
using Swan;
using Swan.DependencyInjection;
/// <summary>
/// Retrieves the RaspberryPI System Information.
///
/// http://raspberry-pi-guide.readthedocs.io/en/latest/system.html.
/// </summary>
public sealed class SystemInfo : SingletonBase<SystemInfo>
{
private const string CpuInfoFilePath = "/proc/cpuinfo";
private const string MemInfoFilePath = "/proc/meminfo";
private const string UptimeFilePath = "/proc/uptime";
private const int NewStyleCodesMask = 0x800000;
private BoardModel _boardModel;
private ProcessorModel _processorModel;
private Manufacturer _manufacturer;
private MemorySize _memorySize;
/// <summary>
/// Prevents a default instance of the <see cref="SystemInfo"/> class from being created.
/// </summary>
/// <exception cref="NotSupportedException">Could not initialize the GPIO controller.</exception>
private SystemInfo()
{
#region Obtain and format a property dictionary
var properties =
typeof(SystemInfo)
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(
p =>
p.CanWrite && p.CanRead &&
(p.PropertyType == typeof(string) || p.PropertyType == typeof(string[])))
.ToArray();
var propDictionary = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var prop in properties)
{
propDictionary[prop.Name.Replace(" ", string.Empty).ToLowerInvariant().Trim()] = prop;
}
#endregion
#region Extract CPU information
if (File.Exists(CpuInfoFilePath))
{
var cpuInfoLines = File.ReadAllLines(CpuInfoFilePath);
foreach (var line in cpuInfoLines)
{
var lineParts = line.Split(new[] { ':' }, 2);
if (lineParts.Length != 2)
continue;
var propertyKey = lineParts[0].Trim().Replace(" ", string.Empty);
var propertyStringValue = lineParts[1].Trim();
if (!propDictionary.ContainsKey(propertyKey)) continue;
var property = propDictionary[propertyKey];
if (property.PropertyType == typeof(string))
{
property.SetValue(this, propertyStringValue);
}
else if (property.PropertyType == typeof(string[]))
{
var propertyArrayValue = propertyStringValue.Split(' ');
property.SetValue(this, propertyArrayValue);
}
}
}
#endregion
ExtractMemoryInfo();
ExtractBoardVersion();
ExtractOS();
}
/// <summary>
/// Gets the library version.
/// </summary>
public Version LibraryVersion { get; private set; }
/// <summary>
/// Gets the OS information.
/// </summary>
/// <value>
/// The os information.
/// </value>
public OsInfo OperatingSystem { get; set; }
/// <summary>
/// Gets the Raspberry Pi version.
/// </summary>
public PiVersion RaspberryPiVersion { get; set; }
/// <summary>
/// Gets the board revision (1 or 2).
/// </summary>
/// <value>
/// The wiring pi board revision.
/// </value>
public int BoardRevision { get; set; }
/// <summary>
/// Gets the number of processor cores.
/// </summary>
public int ProcessorCount => int.TryParse(Processor, out var outIndex) ? outIndex + 1 : 0;
/// <summary>
/// Gets the installed ram in bytes.
/// </summary>
public int InstalledRam { get; private set; }
/// <summary>
/// Gets a value indicating whether this CPU is little endian.
/// </summary>
public bool IsLittleEndian => BitConverter.IsLittleEndian;
/// <summary>
/// Gets the CPU model name.
/// </summary>
public string ModelName { get; private set; }
/// <summary>
/// Gets a list of supported CPU features.
/// </summary>
public string[] Features { get; private set; }
/// <summary>
/// Gets the CPU implementer hex code.
/// </summary>
public string CpuImplementer { get; private set; }
/// <summary>
/// Gets the CPU architecture code.
/// </summary>
public string CpuArchitecture { get; private set; }
/// <summary>
/// Gets the CPU variant code.
/// </summary>
public string CpuVariant { get; private set; }
/// <summary>
/// Gets the CPU part code.
/// </summary>
public string CpuPart { get; private set; }
/// <summary>
/// Gets the CPU revision code.
/// </summary>
public string CpuRevision { get; private set; }
/// <summary>
/// Gets the hardware model number.
/// </summary>
public string Hardware { get; private set; }
/// <summary>
/// Gets the hardware revision number.
/// </summary>
public string Revision { get; private set; }
/// <summary>
/// Gets the revision number (accordingly to new-style revision codes).
/// </summary>
public int RevisionNumber { get; set; }
/// <summary>
/// Gets the board model (accordingly to new-style revision codes).
/// </summary>
/// /// <exception cref="InvalidOperationException">This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}.</exception>
public BoardModel BoardModel =>
NewStyleRevisionCodes ?
_boardModel :
throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead.");
/// <summary>
/// Gets processor model (accordingly to new-style revision codes).
/// </summary>
/// /// <exception cref="InvalidOperationException">This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}.</exception>
public ProcessorModel ProcessorModel =>
NewStyleRevisionCodes ?
_processorModel :
throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead.");
/// <summary>
/// Gets the manufacturer of the board (accordingly to new-style revision codes).
/// </summary>
/// <exception cref="InvalidOperationException">This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}.</exception>
public Manufacturer Manufacturer =>
NewStyleRevisionCodes ?
_manufacturer :
throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead.");
/// <summary>
/// Gets the size of the memory (accordingly to new-style revision codes).
/// </summary>
/// <exception cref="InvalidOperationException">This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}.</exception>
public MemorySize MemorySize =>
NewStyleRevisionCodes ?
_memorySize :
throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead.");
/// <summary>
/// Gets the serial number.
/// </summary>
public string Serial { get; private set; }
/// <summary>
/// Gets the system up-time (in seconds).
/// </summary>
public double Uptime
{
get
{
try
{
if (File.Exists(UptimeFilePath) == false) return 0;
var parts = File.ReadAllText(UptimeFilePath).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 1 && float.TryParse(parts[0], out var result))
return result;
}
catch
{
/* Ignore */
}
return 0;
}
}
/// <summary>
/// Gets the uptime in TimeSpan.
/// </summary>
public TimeSpan UptimeTimeSpan => TimeSpan.FromSeconds(Uptime);
/// <summary>
/// Indicates if the board uses the new-style revision codes.
/// </summary>
private bool NewStyleRevisionCodes { get; set; }
/// <summary>
/// Placeholder for processor index.
/// </summary>
private string Processor { get; set; }
/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString()
{
var properties = typeof(SystemInfo).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanRead && (
p.PropertyType == typeof(string) ||
p.PropertyType == typeof(string[]) ||
p.PropertyType == typeof(int) ||
p.PropertyType == typeof(bool) ||
p.PropertyType == typeof(TimeSpan)))
.ToArray();
var propertyValues2 = new List<string>
{
"System Information",
$"\t{nameof(LibraryVersion),-22}: {LibraryVersion}",
$"\t{nameof(RaspberryPiVersion),-22}: {RaspberryPiVersion}",
};
foreach (var property in properties)
{
if (property.PropertyType != typeof(string[]))
{
propertyValues2.Add($"\t{property.Name,-22}: {property.GetValue(this)}");
}
else if (property.GetValue(this) is string[] allValues)
{
var concatValues = string.Join(" ", allValues);
propertyValues2.Add($"\t{property.Name,-22}: {concatValues}");
}
}
return string.Join(Environment.NewLine, propertyValues2.ToArray());
}
private void ExtractOS()
{
try
{
Standard.Uname(out var unameInfo);
OperatingSystem = new OsInfo
{
DomainName = unameInfo.DomainName,
Machine = unameInfo.Machine,
NodeName = unameInfo.NodeName,
Release = unameInfo.Release,
SysName = unameInfo.SysName,
Version = unameInfo.Version,
};
}
catch
{
OperatingSystem = new OsInfo();
}
}
private void ExtractBoardVersion()
{
var hasSysInfo = DependencyContainer.Current.CanResolve<ISystemInfo>();
try
{
if (string.IsNullOrWhiteSpace(Revision) == false &&
int.TryParse(
Revision.ToUpperInvariant(),
NumberStyles.HexNumber,
CultureInfo.InvariantCulture,
out var boardVersion))
{
RaspberryPiVersion = PiVersion.Unknown;
if (Enum.IsDefined(typeof(PiVersion), boardVersion))
RaspberryPiVersion = (PiVersion)boardVersion;
if ((boardVersion & NewStyleCodesMask) == NewStyleCodesMask)
{
NewStyleRevisionCodes = true;
RevisionNumber = boardVersion & 0xF;
_boardModel = (BoardModel)((boardVersion >> 4) & 0xFF);
_processorModel = (ProcessorModel)((boardVersion >> 12) & 0xF);
_manufacturer = (Manufacturer)((boardVersion >> 16) & 0xF);
_memorySize = (MemorySize)((boardVersion >> 20) & 0x7);
}
}
if (hasSysInfo)
BoardRevision = (int)DependencyContainer.Current.Resolve<ISystemInfo>().BoardRevision;
}
catch
{
/* Ignore */
}
if (hasSysInfo)
LibraryVersion = DependencyContainer.Current.Resolve<ISystemInfo>().LibraryVersion;
}
private void ExtractMemoryInfo()
{
if (!File.Exists(MemInfoFilePath)) return;
var memInfoLines = File.ReadAllLines(MemInfoFilePath);
foreach (var line in memInfoLines)
{
var lineParts = line.Split(new[] { ':' }, 2);
if (lineParts.Length != 2)
continue;
if (lineParts[0].ToLowerInvariant().Trim().Equals("memtotal") == false)
continue;
var memKb = lineParts[1].ToLowerInvariant().Trim().Replace("kb", string.Empty).Trim();
if (!int.TryParse(memKb, out var parsedMem)) continue;
InstalledRam = parsedMem * 1024;
break;
}
}
}
}
@@ -0,0 +1,23 @@
namespace Unosquare.RaspberryIO.Computer
{
/// <summary>
/// Represents a wireless network information.
/// </summary>
public class WirelessNetworkInfo
{
/// <summary>
/// Gets the ESSID of the Wireless network.
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// Gets the network quality.
/// </summary>
public string Quality { get; internal set; }
/// <summary>
/// Gets a value indicating whether this instance is encrypted.
/// </summary>
public bool IsEncrypted { get; internal set; }
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Unosquare.RaspberryIO.Native
{
using System.Runtime.InteropServices;
internal static class Standard
{
internal const string LibCLibrary = "libc";
/// <summary>
/// Fills in the structure with information about the system.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The result.</returns>
[DllImport(LibCLibrary, EntryPoint = "uname", SetLastError = true)]
public static extern int Uname(out SystemName name);
}
}
@@ -0,0 +1,47 @@
namespace Unosquare.RaspberryIO.Native
{
using System.Runtime.InteropServices;
/// <summary>
/// OS uname structure.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct SystemName
{
/// <summary>
/// System name.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string SysName;
/// <summary>
/// Node name.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string NodeName;
/// <summary>
/// Release level.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string Release;
/// <summary>
/// Version level.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string Version;
/// <summary>
/// Hardware level.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string Machine;
/// <summary>
/// Domain name.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
public string DomainName;
}
}
+141
View File
@@ -0,0 +1,141 @@
namespace Unosquare.RaspberryIO
{
using Abstractions;
using Camera;
using Computer;
using Swan;
using Swan.DependencyInjection;
using System;
using System.Threading.Tasks;
/// <summary>
/// Our main character. Provides access to the Raspberry Pi's GPIO, system and board information and Camera.
/// </summary>
public static class Pi
{
private const string MissingDependenciesMessage = "You need to load a valid assembly (WiringPi or PiGPIO).";
private static readonly object SyncLock = new object();
private static bool _isInit;
private static SystemInfo _info;
/// <summary>
/// Initializes static members of the <see cref="Pi" /> class.
/// </summary>
static Pi()
{
lock (SyncLock)
{
Camera = CameraController.Instance;
PiDisplay = DsiDisplay.Instance;
Audio = AudioSettings.Instance;
Bluetooth = Bluetooth.Instance;
}
}
/// <summary>
/// Provides information on this Raspberry Pi's CPU and form factor.
/// </summary>
public static SystemInfo Info => _info ??= SystemInfo.Instance;
/// <summary>
/// Provides access to the Raspberry Pi's GPIO as a collection of GPIO Pins.
/// </summary>
public static IGpioController Gpio =>
ResolveDependency<IGpioController>();
/// <summary>
/// Provides access to the 2-channel SPI bus.
/// </summary>
public static ISpiBus Spi =>
ResolveDependency<ISpiBus>();
/// <summary>
/// Provides access to the functionality of the i2c bus.
/// </summary>
public static II2CBus I2C =>
ResolveDependency<II2CBus>();
/// <summary>
/// Provides access to timing functionality.
/// </summary>
public static ITiming Timing =>
ResolveDependency<ITiming>();
/// <summary>
/// Provides access to threading functionality.
/// </summary>
public static IThreading Threading =>
ResolveDependency<IThreading>();
/// <summary>
/// Provides access to the official Raspberry Pi Camera.
/// </summary>
public static CameraController Camera { get; }
/// <summary>
/// Provides access to the official Raspberry Pi 7-inch DSI Display.
/// </summary>
public static DsiDisplay PiDisplay { get; }
/// <summary>
/// Provides access to Raspberry Pi ALSA sound card driver.
/// </summary>
public static AudioSettings Audio { get; }
/// <summary>
/// Provides access to Raspberry Pi Bluetooth driver.
/// </summary>
public static Bluetooth Bluetooth { get; }
/// <summary>
/// Restarts the Pi. Must be running as SU.
/// </summary>
/// <returns>The process result.</returns>
public static Task<ProcessResult> RestartAsync() => ProcessRunner.GetProcessResultAsync("reboot");
/// <summary>
/// Restarts the Pi. Must be running as SU.
/// </summary>
/// <returns>The process result.</returns>
public static ProcessResult Restart() => RestartAsync().GetAwaiter().GetResult();
/// <summary>
/// Halts the Pi. Must be running as SU.
/// </summary>
/// <returns>The process result.</returns>
public static Task<ProcessResult> ShutdownAsync() => ProcessRunner.GetProcessResultAsync("halt");
/// <summary>
/// Halts the Pi. Must be running as SU.
/// </summary>
/// <returns>The process result.</returns>
public static ProcessResult Shutdown() => ShutdownAsync().GetAwaiter().GetResult();
/// <summary>
/// Initializes an Abstractions implementation.
/// </summary>
/// <typeparam name="T">An implementation of <see cref="IBootstrap"/>.</typeparam>
public static void Init<T>()
where T : IBootstrap
{
lock (SyncLock)
{
if (_isInit) return;
Activator.CreateInstance<T>().Bootstrap();
_isInit = true;
}
}
private static T ResolveDependency<T>()
where T : class
{
if (!_isInit)
throw new InvalidOperationException($"You must first initialize {nameof(Pi)} referencing a valid {nameof(IBootstrap)} implementation.");
return DependencyContainer.Current.CanResolve<T>()
? DependencyContainer.Current.Resolve<T>()
: throw new InvalidOperationException(MissingDependenciesMessage);
}
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>The Raspberry Pi's IO Functionality in an easy-to use API for Mono/.NET Core
This library enables developers to use the various Raspberry Pi's hardware modules including the Camera to capture images and video, the GPIO pins, and both, the SPI and I2C buses.</Description>
<Copyright>Unosquare (c) 2016-2019</Copyright>
<AssemblyTitle>Unosquare Raspberry IO</AssemblyTitle>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AssemblyName>Unosquare.RaspberryIO</AssemblyName>
<PackageId>Unosquare.Raspberry.IO</PackageId>
<Version>0.26.0</Version>
<Authors>Unosquare</Authors>
<PackageIconUrl>https://github.com/unosquare/raspberryio/raw/master/logos/raspberryio-logo-32.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/unosquare/raspberryio</PackageProjectUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/unosquare/raspberryio/master/LICENSE</PackageLicenseUrl>
<PackageTags>Raspberry Pi GPIO Camera SPI I2C Embedded IoT Mono C# .NET</PackageTags>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Swan\Swan.csproj" />
<ProjectReference Include="..\Unosquare.RaspberryIO.Abstractions\Unosquare.RaspberryIO.Abstractions.csproj" />
</ItemGroup>
</Project>