unosquare / raspberryio

The Raspberry Pi's IO Functionality in an easy-to-use API for Mono/.NET/C#
https://unosquare.github.io/raspberryio
Other
672 stars 104 forks source link
camera captured-images dotnet dotnetcore gpio-pins i2c-bus mono peripherals raspberry raspberry-pi raspberry-pi-3 raspberry-pi-camera raspberry-pi-gpio resistor scl sda spi-channel

THIS REPO HAS BEEN ARCHIVED

NuGet Analytics Build status Build Status

RaspberryIO - Pi's hardware access from .NET

WE ARE LOOKING FOR A NEW HOME FOR THIS PROJECT. APPLY AT: https://adoptoposs.org/p/9f5b74b9-04f2-42b6-891f-c5294c9ef1c5

:star: Please star this project if you find it useful!

The Raspberry Pi's IO Functionality in an easy-to-use API for .NET (Mono/.NET Core). Our mission is to make .NET a first-class citizen in the Python-centric community of Raspberry Pi developers.

Table of contents

Features

This library enables developers to use the various Raspberry Pi's hardware modules:

Peripherals

We offer an additional package with helpful classes to use peripherals, many of them are from pull requests from our contributors. The current set of peripherals supported are:

Breaking changes

Version > 0.24.0

This version requires .NET core 3.0 to build and run.

Version ≥ 0.18.0

In the beginning, RaspberryIO was built around WiringPi library and all our classes, properties, enums, etc. was based on those ones used in WiringPi too.

Now, we are working on a more general version of RaspberryIO (Abstractions) that, could use any core library (WiringPi, PiGpio or even new ones). So, it was necessary to change certain properties and enums for more general ones.

Pinout numbering system

A breaking change in this new general version is the pinout numbering system. As we already explained above, RaspberryIO was using the WiringPi pinout numbering system, but now it uses the BCM pinout numbering system.

Note: The pin numbers are totally different in both systems, so we recommend you to double check carefully the physical pins where you connect any device.

Installation

Install basic Raspberry.IO package: NuGet version

PM> Install-Package Unosquare.Raspberry.IO

If you want to implement your own provider for RaspberryIO, you must use the following package to implement all the Pi providers: NuGet version

PM> Install-Package Unosquare.WiringPi

Note: For now, we have fully implemented the WiringPi library and we are working in the PiGpio implementation.

Note: The latest development builds require .NET core 3.0 to build and run. You should upgrade to the latest Version of Visual Studio first.

Install Raspberry.IO Peripherals package (Optional): NuGet version

PM> Install-Package Unosquare.RaspberryIO.Peripherals

Running the latest version of Mono

It is recommended that you install the latest available release of Mono because what is available in the Raspbian repo is quite old (3.X). These commands were tested using Raspbian Stretch. The version of Mono that is installed at the time of this writing is:

Mono JIT compiler version 5.20.1.19 (tarball Thu Apr 11 19:13 UTC 2019)

The commands to get Mono installed are the following:

For Debian Stretch

sudo apt install apt-transport-https dirmngr gnupg ca-certificates
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
echo "deb https://download.mono-project.com/repo/debian stable-raspbianstretch main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list
sudo apt update
sudo apt-get install mono-complete

Now, verify your version of Mono by running mono --version. Version 4.6 and above should be good enough.

Running .NET Core 3.1

This project can also run in .NET Core. To install .Net Core 3.1 sdk please execute the following commands:

$ sudo apt-get -y update
$ sudo apt-get -y install libunwind8 gettext
$ wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4cd2-bca4-ea63d2cc115c/7bf13840aa2ed88793b7315d5e0d74e6/dotnet-sdk-3.1.100-linux-arm.tar.gz
$ sudo mkdir -p /usr/share/dotnet
$ sudo tar -xvf dotnet-sdk-3.1.100-linux-arm.tar.gz -C /usr/share/dotnet
$ sudo ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

If you want to install just the runtime, use the following commands:

$ sudo apt-get -y update
$ sudo apt-get -y install libunwind8 gettext
$ wget https://download.visualstudio.microsoft.com/download/pr/60d21925-7f8f-4004-9afe-aebd041d2d4a/0db2946738642d7f88f71f7800522e8c/dotnet-runtime-3.1.0-linux-arm.tar.gz
$ sudo mkdir -p /usr/share/dotnet
$ sudo tar -xvf dotnet-runtime-3.1.0-linux-arm.tar.gz -C /usr/share/dotnet
$ sudo ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

Now, verify your version of .Net Core by running dotnet --info. Visit https://aka.ms/dotnet-download (or https://dotnet.microsoft.com/download/dotnet-core/3.1) to check for newer versions of the .Net Core runtime (or SDK). The Raspberry Pi running the default Raspbian OS needs the versions named "Linux ARM32".

Usage

Before start using RaspberryIO, you must initialize Pi class (bootstrapping process) with the valid Abstractions implementation, in order to let Pi know what implementation is going to use:

 Pi.Init<BootstrapWiringPi>();

Note: This sample code uses the wiringpi-dotnet implementation, you can use this implementation adding the Unosquare.WiringPi nuget package to your project.

Run the app on the raspberry

Deploy using dotnet-sshdeploy

<SshDeployHost>172.16.17.54</SshDeployHost>
<SshDeployTargetPath>/home/pi/Playground</SshDeployTargetPath>
<SshDeployUsername>pi</SshDeployUsername>
<SshDeployPassword>raspberry</SshDeployPassword>
SSH Deployment Tool [Version 0.3.1.0]
(c)2015 - 2017 Unosquare SA de CV. All Rights Reserved.
For additional help, please visit https://github.com/unosquare/sshdeploy
Deploying...
    Configuration   Debug
    Framework       dotnetcore2.2
    Source Path     C:\raspberryio\src\Unosquare.RaspberryIO.Playground\bin\Debug\dotnetcore2.2\publish
    Excluded Files  .ready|.vshost.exe|.vshost.exe.config
    Target Address  172.16.17.54:22
    Username        pi
    Target Path     /home/pi/Playground
    Clean Target    NO
    Pre Deployment  
    Post Deployment 
Connecting to host 172.16.17.54:22 via SSH.
Connecting to host 172.16.17.54:22 via SFTP.

    Target Path '/home/pi/Playground' does not exist. -- Will attempt to create.
    Target Path '/home/pi/Playground' created successfully.
    Cleaning Target Path '/home/pi/Playground'

    Deploying 8 files.
    Finished deployment in 1.25 seconds.
Completed.

Deploy using Visual Studio "Publish" and WinSCP

Alternatively, you can use Visual Studio's Publish feature and a SCP (SSH Copy) application such as WinSCP or FileZilla. This uses the example project from this library:

Run the project

Obtaining Board and System Information

RaspberryIO contains useful utilities to obtain information about the board it is running on. You can simply call the Pi.Info.ToString() method to obtain a dump of all system properties as a single string, or you can use the individual properties such as Installed RAM, Processor Count, Raspberry Pi Version, Serial Number, etc. There's not a lot more to this.

Using the GPIO Pins

Pin reference for the B plus (B+) - Header P1

BCM Name Mode V L R V Mode Name BCM
3.3v 01 02 5v
2 SDA.1 ALT0 1 03 04 5V
3 SCL.1 ALT0 1 05 06 GND
4 GPIO. 7 IN 1 07 08 1 ALT0 TxD 14
GND 09 10 1 ALT0 RxD 15
17 GPIO. 0 IN 0 11 12 0 IN GPIO. 1 18
27 GPIO. 2 IN 0 13 14 GND
22 GPIO. 3 IN 0 15 16 0 IN GPIO. 4 23
3.3v 17 18 0 IN GPIO. 5 24
10 MOSI IN 0 19 20 GND
9 MISO IN 0 21 22 0 IN GPIO. 6 25
11 SCLK IN 0 23 24 1 IN CE0 8
GND 25 26 1 IN CE1 7
0 SDA.0 IN 1 27 28 1 IN SCL.0 1
5 GPIO.21 IN 1 29 30 GND
6 GPIO.22 IN 1 31 32 0 IN GPIO.26 12
13 GPIO.23 IN 0 33 34 GND
19 GPIO.24 IN 0 35 36 0 IN GPIO.27 16
26 GPIO.25 IN 0 37 38 0 IN GPIO.28 20
GND 39 40 0 IN GPIO.29 21

The above diagram shows the pins of GPIO Header P1. There is an additional GPIO header on the Pi called P5. More info available here

In order to access the pins, use Pi.Gpio. The pins can have multiple behaviors and fortunately Pi.Gpio can be iterated, addressed by index, addressed by BCM pin number and provides the pins as publicly accessible properties.

Here is an example of addressing the pins in all the various ways:

public static void TestLedBlinking()
{
    // Get a reference to the pin you need to use.
    // Both methods below are equivalent
    var blinkingPin = Pi.Gpio[17];
    blinkingPin = Pi.Gpio[BcmPin.Gpio17];

    // Configure the pin as an output
    blinkingPin.PinMode = GpioPinDriveMode.Output;

    // perform writes to the pin by toggling the isOn variable
    var isOn = false;
    for (var i = 0; i < 20; i++)
    {
        isOn = !isOn;
        blinkingPin.Write(isOn);
        System.Threading.Thread.Sleep(500);
    }
}

Pin Information

All pins have handy properties and methods that you can use to drive them. For example, you can examine the Capabilities property to find out which features are available on the pin. You can also use the PinMode property to get or set the operating mode of the pin. Please note that the value of the PinMode property is by default set to Input and it will return the last mode you set the property to.

Digital Read and Write

It is very easy to read and write values to the pins. In general, it is a 2-step process.

Reading the value of a pin example:

Pi.Gpio.Pin27.PinMode = GpioPinDriveMode.Input;
// The below lines are reoughly equivalent
var isOn = Pi.Gpio.Pin27.Read(); // Reads as a boolean
var pinValue = Pi.Gpio.Pin27.ReadValue(); // Reads as a GpioPinValue

Writing to a pin example

Pi.Gpio.Pin27.PinMode = GpioPinDriveMode.Output;
// The below lines are reoughly equivalent
Pi.Gpio.Pin27.Write(true); // Writes a boolean
Pi.Gpio.Pin27.Write(GpioPinValue.High); // Writes a pin value

Hardware PWM

Simple code for led dimming:

   var pin = (GpioPin)Pi.Gpio[BcmPin.Gpio24];
   pin.PinMode = GpioPinDriveMode.PwmOutput;
   pin.PwmMode = PwmMode.Balanced;
   pin.PwmClockDivisor = 2; 
   while (true)
   {
      for (var x = 0; x <= 100; x++)
      {
         pin.PwmRegister = (int)pin.PwmRange / 100 * x;
         Thread.Sleep(10);
      }

      for (var x = 0; x <= 100; x++)
      {
         pin.PwmRegister = (int)pin.PwmRange - ((int)pin.PwmRange / 100 * x);
         Thread.Sleep(10);
      }
   }

PwmRange is the maximun value of the pulse width, than means 100% of pulse width. Changing this value allows you to have a more fine or coarse control of the pulse width (default 1024).

PwmRegister is the current pulse width. Changing this value allows you to change the current pulse width and thus the duty cycle.

Duty Cycle is equals to PwmRegister divide by PwmRange. Assuming a PwmRange value of 1024 (default), we have:

PwmRegister Duty Cycle
0 0%
256 25%
512 50%
768 75%
1024 100%

Note: Hardware PWM can be used only in GPIO12, GPIO13, GPIO18 and GPIO19.

Software PWM

Simple code for led dimming:

   var range = 100;
   var pin = (GpioPin)Pi.Gpio[BcmPin.Gpio24];
   pin.PinMode = GpioPinDriveMode.Output;
   pin.StartSoftPwm(0, range);

   while (true)
   {
      for (var x = 0; x <= 100; x++)
      {
         pin.SoftPwmValue = range / 100 * x;
         Thread.Sleep(10);
      }

      for (var x = 0; x <= 100; x++)
      {
         pin.SoftPwmValue = range - (range / 100 * x);
         Thread.Sleep(10);
      }
   }

SoftPwmRange is the range of the pulse width, than means 100% of pulse width (We notice better performance using a range value of 100).

SoftPwmValue is the current pulse width. Changing this value allows you to change the current pulse width and thus the duty cycle.

Note: Software PWM can be used in any GPIO.

Tone Generation

You can emit tones by using SoftToneFrequency. Example:

// Get a reference to the pin
var passiveBuzzer = (GpioPin)Pi.Gpio[BcmPin.Gpio24];
// Set the frequency to Alto Do (523Hz)
passiveBuzzer.SoftToneFrequency = 523
// Wait 1 second
System.Threading.Thread.Sleep(1000);
// And stop
passiveBuzzer.SoftToneFrequency = 0;

Interrupts and Callbacks

Register an Interrupt Callback example:

using System;
using Unosquare.RaspberryIO;
using Unosquare.RaspberryIO.Gpio;

class Program
{
  // Define the implementation of the delegate;
  static void ISRCallback()
  {
     Console.WriteLine("Pin Activated...");         
  }

  static void Main(string[] args)
  {
        Console.WriteLine("Gpio Interrupts");
        var pin = Pi.Gpio.Pin24;
        pin.PinMode = GpioPinDriveMode.Input;
        pin.RegisterInterruptCallback(EdgeDetection.FallingEdge, ISRCallback);
        Console.ReadKey();
  }
}

Using the SPI Bus

I really liked the following description from Neil's Log Book: The SPI (Serial Peripheral Interface) protocol behaves like a ring buffer so that whenever the master sends a byte to the slave, the slave sends a byte back to the master. The slave can use this behavior to return a status byte, a response to a previous byte, or null data (the master may choose to read the returned byte or ignore it). The bus operates on a 4-wire interface.

RaspberryIO provides easy access to the 2 SPI channels available on the Raspberry. The functionality depends on Wiring Pi's SPI library. Please note that you may need to issue the command gpio load spi before starting your application (or as a System.Diagnostics.Process when your application starts) if the SPI kernel drivers have not been loaded.

In order to use an SPI channel you MUST always set the Channel0Frequency or Channel1Frequency (depending on the channel you want to use) before calling the SendReceive method. If the property is not set beforehand the SPI channel will fail initialization. See an example below:

Example of using the SPI Bus

Pi.Spi.Channel0Frequency = SpiChannel.MinFrequency;
var request = System.Text.Encoding.ASCII.GetBytes("HELLO!");
var response = Pi.Spi.Channel0.SendReceive(request);

Note: In order to enable the second SPI channel (SPI1) you need to add dtoverlay=spi1-1cs to the config file.

I2C to connect ICs

The Inter IC Bus (I2C) is a cousin of the SPI bus but it is somewhat more complex and it does not work as a ring buffer like the SPI bus. It also connects all of its slave devices in series and depends on 2 lines only. There is a nice tutorial on setting up and using the I2C bus at Robot Electronics. From their site: The physical bus is just two wires, called SCL and SDA. SCL is the clock line. It is used to synchronize all data transfers over the I2C bus. SDA is the data line. The SCL & SDA lines are connected to all devices on the I2C bus. There needs to be a third wire which is just the ground or 0 volts. There may also be a 5volt wire is power is being distributed to the devices. Both SCL and SDA lines are "open drain" drivers. What this means is that the chip can drive its output low, but it cannot drive it high. For the line to be able to go high you must provide pull-up resistors to the 5v supply. There should be a resistor from the SCL line to the 5v line and another from the SDA line to the 5v line. You only need one set of pull-up resistors for the whole I2C bus, not for each device.

RaspberryIO provides easy access to the I2C bus available on the Raspberry. Please note that you may need to issue the command gpio load i2c before starting your application (or as a System.Diagnostics.Process when your application starts) if the I2C kernel drivers have not been loaded. The default baud rate is 100Kbps. If you wish to initialize the bus at a different baud rate you may issue, for example, gpio load i2c 200. This will load the bus at 200kbps.

In order to detect I2C devices, you could use the i2cdetect system command. Just remember that on a Rev 1 Raspberry Pi it's device 0, and on a Rev. 2 it's device 1. e.g.

i2cdetect -y 0 # Rev 1
i2cdetect -y 1 # Rev 2

Example of using the I2C Bus:

// Register a device on the bus
var myDevice = Pi.I2C.AddDevice(0x20);

// Simple Write and Read (there are algo register read and write methods)
myDevice.Write(0x44);
var response = myDevice.Read();

// List registered devices on the I2C Bus
foreach (var device in Pi.I2C.Devices)
{
    Console.WriteLine($"Registered I2C Device: {device.DeviceId}");
}

Timing and Threading

Timing

System calls to provide various timing and sleeping functions.

Getting the number of microseconds or milliseconds since system boot:

// Getting the number of microseconds since system boot.
var micros = Pi.Timing.Microseconds;

// Getting the number of milliseconds since system boot.
var millis = Pi.Timing.Milliseconds;

Pausing program execution for a certain number of microseconds or milliseconds.

// Pausing program execution for 50 microseconds.
Pi.Timing.SleepMicroseconds(50);

// Pausing program execution for 100 milliseconds.
Pi.Timing.SleepMilliseconds(100);

Threading

Allows the creation of a thread which is another function in your program that runs concurrently with your main program. An example may be to have this function wait for an interrupt while your program carries on doing other tasks. The thread can indicate an event, or action by using global variables to communicate back to the main program, or other threads.

Basic thread creation:

    static void Main()
    {
        ...
        Pi.Threading.StartThread(ThreadWorker);
        ...
    }

    private void ThreadWorker()
    {
        // Thread body
    }

Passing data to a thread:


    static void Main()
    {
        ...
        var threadName = "Thread 1";
        var namePointer = (IntPtr)Marshal.StringToHGlobalAnsi(threadName);
        var threadHandle = Pi.Threading.StartThreadEx(ThreadWorker, namePointer);
        ...

        Pi.Threading.StopThreadEx(threadHandle);
        ...
    }

    private void ThreadWorker(IntPtr state)
    {
        var threadName = Marshal.PtrToStringAnsi(state);

        // Thread body
    }

Note: Not all underlying libraries support all methods for creating threads. WiringPi, for example, does only support basic thread creation.

Serial Ports (UART)

Where is the serial port API? Well, it is something we will most likely add in the future. For now, you can simply use the built-in SerialPort class the .NET framework provides.

The Camera Module

The Pi.Camera module uses raspivid and raspistill to access the camera so they must be installed in order for your program to work properly. raspistill arguments are specified in an instance of the CameraStillSettings class, while the raspivid arguments are specified in an instance of the CameraVideoSettings class.

Capturing Images

The Pi.Camera.CaptureImage* methods simply return an array of bytes containing the captured image. There are synchronous and asynchronous flavors of these methods so you can use the familiar async and await pattern to capture your images. All raspistill arguments (except for those that control user interaction such as -k) are available via the CameraStillSettings. To start, create a new instance of the CameraStillSettings class and pass it on to your choice of the Pi.Camera.CaptureImage* methods. There are shortcut methods available that simply take a JPEG image at the given Width and Height. By default, the shortcut methods set the JPEG quality at 90%.

Example using a shortcut method:

static void TestCaptureImage()
{
    var pictureBytes = Pi.Camera.CaptureImageJpeg(640, 480);
    var targetPath = "/home/pi/picture.jpg";
    if (File.Exists(targetPath))
        File.Delete(targetPath);

    File.WriteAllBytes(targetPath, pictureBytes);
    Console.WriteLine($"Took picture -- Byte count: {pictureBytes.Length}");
}

Example using a CaptureImage method:

static byte[] TestCaptureImage()
{
    var settings = new CameraStillSettings
    {
        CaptureWidth = 640,
        CaptureHeight = 480,
        CaptureJpegQuality = 90,
        CaptureTimeoutMilliseconds = 300
    };

    return CaptureImage(settings);
}

Capturing Video

Capturing video streams is somewhat different but it is still very easy to do. The concept behind it is to Open a video stream providing your own callback. When opening the stream Raspberry IO will spawn a separate thread and will not block the execution of your code, but it will continually call your callback method containing the bytes that are being read from the camera until the Close method is called or until the timeout is reached.

Example of capturing a stream of H.264 video

static void TestCaptureVideo()
{
    // Setup our working variables
    var videoByteCount = 0;
    var videoEventCount = 0;
    var startTime = DateTime.UtcNow;

    // Configure video settings
    var videoSettings = new CameraVideoSettings()
    {
        CaptureTimeoutMilliseconds = 0,
        CaptureDisplayPreview = false,
        ImageFlipVertically = true,
        CaptureExposure = CameraExposureMode.Night,
        CaptureWidth = 1920,
        CaptureHeight = 1080
    };

    try
    {
        // Start the video recording
        Pi.Camera.OpenVideoStream(videoSettings,
            onDataCallback: (data) => { videoByteCount += data.Length; videoEventCount++; },
            onExitCallback: null);

        // Wait for user interaction
        startTime = DateTime.UtcNow;
        Console.WriteLine("Press any key to stop reading the video stream . . .");
        Console.ReadKey(true);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"{ex.GetType()}: {ex.Message}");
    }
    finally
    {
        // Always close the video stream to ensure raspivid quits
        Pi.Camera.CloseVideoStream();

        // Output the stats
        var megaBytesReceived = (videoByteCount / (1024f * 1024f)).ToString("0.000");
        var recordedSeconds = DateTime.UtcNow.Subtract(startTime).TotalSeconds.ToString("0.000");
        Console.WriteLine($"Capture Stopped. Received {megaBytesReceived} Mbytes in {videoEventCount} callbacks in {recordedSeconds} seconds");
    }            
}

Audio settings

Basic audio settings have been implemented in RaspberryIO:

Users set an audio card, an audio device and an audio command to perform an audio action. Example of audio tasks:

await Pi.PiVolumeControl.SetVolumePercentage(85);

await Pi.PiVolumeControl.SetVolumeByDecibels(-1.00f);

The code above sets the volume level in two different formats: Percentage or Decibels. The first method sets the volume on percentage (0% - 100%) and the second sets the volume level on decibels(dB) (-101.32dB - 4.00dB).

Users can consult the current audio settings by using the method GetState. An example is shown below:

var currentState = await Pi.Audio.GetState();
Console.WriteLine(currentState);

The same result can be achieved by setting the volume level to 0% or -9999.99dB.

Handy Notes

In order to setup Wi-Fi, run: sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

A good file should look like this:

country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid="your_real_wifi_ssid"
    scan_ssid=1
    psk="your_real_password"
}

And then restart the services as follows:

sudo systemctl daemon-reload
sudo systemctl restart dhcpcd

You can also configure most boot options by running: sudo raspi-config

Related Projects and Nugets

Name Author Description
WiringPi.net Unosquare Provides complete managed access to the popular wiringpi C library
PiGpio.net Unosquare Provides complete managed access to the popular pigpio C library
Raspberry Abstractions Unosquare Allows you to implement your own provider for RaspberryIO.
Raspberry# IO raspberry-sharp Raspberry# IO is a .NET/Mono IO Library for Raspberry Pi. This project is an initiative of the Raspberry# Community.
WiringPi.Net Daniel Riches A simple C# wrapper for Gordon's WiringPi library.
PiSharp Andy Bradford Pi# is a library to expose the GPIO functionality of the Raspberry Pi computer to the C# and Visual Basic.Net languages