RevPi programmieren mit VisualStudio in C#

Hier kannst du dein Revolution Pi Projekt der Community vorstellen
Post Reply
User avatar
fpf_baden
Posts: 20
Joined: 08 Jun 2017, 08:31
Location: Karlsruhe
Contact:

RevPi programmieren mit VisualStudio in C#

Post by fpf_baden »

Hallo,
da ich den RevPi gerne mit C# programmieren will habe ich eine Bibliothek erstellt,
die den Zugriff auf die Variablen ermöglicht.

Bisher sind dazu auch zwei Tools entstanden:
* PiTest - ein dem piTest ähnliches Kommandozeilenprogramm zum Test der Bibliothek
* VariableServer - EIn REST-API Server zum Abruf der Werte aus einer Web-Applikation (Ajax)

Die Bibliothek kann von Github heruntergeladen werden: https://github.com/FrankPfattheicher/RevolutionPi

Hier gerne Fragen und Anregungen !
User avatar
fpf_baden
Posts: 20
Joined: 08 Jun 2017, 08:31
Location: Karlsruhe
Contact:

Re: RevPi programmieren mit VisualStudio in C#

Post by fpf_baden »

Jetzt ist auch die Dokumentation online und die Bibliothek als Nuget-Paket verfügbar.
Heron
Posts: 40
Joined: 13 Jul 2017, 15:30

Re: RevPi programmieren mit VisualStudio in C#

Post by Heron »

Hallo fpf,

ich habe Deine Lib im Einsatz und finde sie super!

Ich nutze sie, um mit anderen .net/mono Programmen und auch per Ajax, auf Prozessvariablen zuzugreifen.

Ein paar kleine Anmerkungen habe ich:
1) bitte teste das Ganze einmal, in dem Du mehrere Geräte hinzufügst, u.a. auch die virtuellen Modbus Geräte.
- bei der Adressierung (aus dem VariableServer) muss die DeviceAddresse mit zu dem relativen Variablenoffset gerechnet werden.
2) In der VariablenInfo sollte der Value zBsp auf object gesetzt werden. Es gibt beim Modbus zBsp. auch IP Adressen, die nicht in ein "int64" gecastet werden können.
3) In der VariableInfo sollte der BitOffset mit eingelesen werden, um bei DO oder DI Geräte auch die Bits korrekt anzusprechen.

Für die Lib sieht meine VariableInfo so aus:

Code: Select all

 using System.Collections.Generic;
using System.Diagnostics;
using Newtonsoft.Json.Linq;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local

namespace IctBaden.RevolutionPi.Model
{
  [DebuggerDisplay("{" + nameof(Name) + "}")]
  public class VariableInfo
  {
    public VariableType Type { get; private set; }
    public int Index { get; private set; }
    public string Name { get; set; }
    public object DefaultValue { get; set; }          //orginal: long; kann aber auch eine IP Adresse beinhalten.... 
    public ushort Length { get; set; }                // length of the variable in bits. Possible values are 1, 8, 16 and 32
    public ushort Address { get; set; }               // Address of the byte in the process image
    public bool Export { get; set; }
    public string Unknown { get; set; }               //"0001"
    public string Comment { get; set; }
    public byte BitOffset { get; set; }               // 0-7 bit position, >= 8 whole byte

    public DeviceInfo Device { get; set; }

    public string LengthText
    {
      get
      {
        switch (Length)
        {
          case 1: return "BIT";
          case 8: return "BYTE";
          case 16: return "WORD";
          case 32: return "DWORD";
        }
        return $"[{Length} bits]";
      }
    }

    public VariableInfo(DeviceInfo device, VariableType type, int index, IList<JToken> json)
    {
      Device = device;
      Type = type;
      Index = index;
      Name = json[0].Value<string>();
      DefaultValue = json[1].Value<object>();        //orginal: long
      Length = json[2].Value<ushort>();
      Address = json[3].Value<ushort>();
      Export = json[4].Value<bool>();
      Unknown = json[5].Value<string>();
      Comment = json[6].Value<string>();
      try { BitOffset = json[7].Value<byte>(); } catch { BitOffset = 0; };
    }
  }
}
 
In Startup.cs habe ich CORS mit aufgenommen, damit Edge und IE auch damit klar kommen ;)
CORS hinzufügen + vor "app.Use....":

Code: Select all

      config.EnableCors();
Aus der VariableController.cs sieht mein Code zum auslesen der Prozessvar aktuell so aus:

Code: Select all

    [Route("variables/{varname}")]
    [HttpGet]
    public async Task<IHttpActionResult> ReadVariable(string varname)
    {
      var config = Request.GetOwinContext().Get<PiConfiguration>("PiConfig");
      var control = Request.GetOwinContext().Get<PiControl>("PiControl");

      HttpResponseMessage httpResponse;

      try
      {
        IctBaden.RevolutionPi.Model.VariableInfo varInfo = null;


        foreach (IctBaden.RevolutionPi.Model.DeviceInfo pItem in config.Devices)
        {
          try
          {
            foreach (var variable in pItem.Variables)
            {
              if (variable.Name == varname)
              {
                varInfo = variable;
                break;                                        //den Ersten gefunden -> Ende
              }
            }
          }
          catch { }
        }

        if (varInfo == null)
          throw new Exception("varinfo ist leer -> Variable nicht vorhanden!!");


        //Es muss noch die Adresse des Hauptgerätes (Katalog) dazugerechnet werden, da die Adresse hier nur die relative Adresse ist.
        //
        int iDeviceOffset = varInfo.Device.Offset;
        int iByteLen = 0;

        switch(varInfo.Length)
        {
          case 1: iByteLen = 0; break;      //bit
          case 8: iByteLen = 1; break;
          case 16: iByteLen = 2; break;
          case 32: iByteLen = 3; break;
          default: iByteLen = -varInfo.Length / 8; break;      //strings, zBsp. IP Adresse (124)
        }

        //Console.WriteLine("[preread] name: " + varInfo.Name + ";addr: " + (iDeviceOffset + varInfo.Address) + "(" + varInfo.BitOffset + ");len: " + iByteLen);

        byte[] data = null;

        if (iByteLen > 0)
        {
          data = control.Read(iDeviceOffset + varInfo.Address, iByteLen);

          //Console.WriteLine("[readed] len: " + (data == null ? "null" : data.Length.ToString() + "; data: " + control.ConvertDataToValue(data, iByteLen)));
        }
        else if (iByteLen == 0)
        {
          data = new byte[1];
          data[0] = control.GetBitValue((ushort)((ushort)iDeviceOffset + varInfo.Address), varInfo.BitOffset) ? (byte)1 : (byte)0;

          //Console.WriteLine("[readed] bit: " + varInfo.BitOffset + "; data: " + data[0]);
        }
        else  // iByteLen < 0
        {
          data = control.Read(iDeviceOffset + varInfo.Address, -iByteLen);

          //Console.WriteLine("[readed] len: " + (data == null ? "null" : data.Length.ToString() + "; data: " + System.Text.Encoding.ASCII.GetString(data)));
        }

        if (data == null)
        {
          var error = new
          {
            error = "Could not read variable",
            name = varname
          };

          httpResponse = new HttpResponseMessage(HttpStatusCode.OK)   //ServiceUnavailable
          {
            Content = new StringContent(JsonConvert.SerializeObject(error, Formatting.Indented), Encoding.UTF8, "application/json")
          };
        }
        else
        {
          var read = new
          {
            name = varname,
            default_value = varInfo.DefaultValue,
            comment = varInfo.Comment,
            type = varInfo.Type.ToString(),
            lengthText = varInfo.LengthText,
            length = varInfo.Length,
            device_offset = iDeviceOffset,
            var_offset = varInfo.Address,
            var_dev_name = varInfo.Device.Name,
            var_dev_offset = varInfo.Device.Offset,
            data = data,
            value = control.ConvertDataToValue(data, iByteLen)
          };

          httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
          {
            Content = new StringContent(JsonConvert.SerializeObject(read, Formatting.Indented), Encoding.UTF8, "application/json")
          };
        }
      }
      catch (Exception ex)
      {
        var error = new
        {
          error = "Variable not found",
          name = varname,
          errortext = ex.ToString()
        };

        httpResponse = new HttpResponseMessage(HttpStatusCode.OK)  //NotFound
        {
          Content = new StringContent(JsonConvert.SerializeObject(error, Formatting.Indented), Encoding.UTF8, "application/json")
        };
      }

      var result = new ResponseMessageResult(httpResponse);
      return await Task.FromResult(result);
    }
Hoffe Dir damit ein Stück geholfen zu haben.....


Gruß,
Heron
User avatar
volker
Posts: 1046
Joined: 09 Nov 2016, 15:41

Re: RevPi programmieren mit VisualStudio in C#

Post by volker »

Hallo Heron, hallo fpf,
ich lese mit Begeisterung, was ihr das so "treibt". Super! Es freut mich zu sehen, dass unsere Community solche Projekte, die aus Forumsfragen hervorgehen und dann dank der Arbeit und zeit Einzelner zu schönen Ergebnissen führen, letztlich dann auch gemeinsam getestet und verbessert werden.
Am 28. September haben wir unseren ersten RevPi Day in Bremen, bei dem sich die Community und KUNBUS Entwickler treffen. ich würde mich sehr freuen, wenn Ihr auch dort dabei sein könntet. Vielleicht hätte fpf auch Lust, seine Bibliothek in einem kurzen Vortrag den anderen Teilnehmern vorzustellen? wenn ja, dann kontaktiere mich doch bitte mal über PM.
Danke für Eure Zeit und Erfahrung, die Ihr hier der Community zur Verfügung stellt!
Unser RevPi Motto: Don't just claim it - make it!
Heron
Posts: 40
Joined: 13 Jul 2017, 15:30

Re: RevPi programmieren mit VisualStudio in C#

Post by Heron »

Hallo Volker,

danke für die Lorbeeren und die Einladung. Ich kann noch nicht sagen, ob ich teilnehmen kann..

Hallo fpf,

anbei noch ein kleiner Snippet für Deinen OWIN WebServer.

Der stürzt regelmäßig ab. Spätestens, wenn man am Client (Browser) mehrere Vars anfordert. Wenn man noch gleich F5 ein paarmal drückt, schafft man es in 5 Sekunden.

In der Startup habe ich eingefügt:

Code: Select all

        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        config.EnsureInitialized();
        config.MessageHandlers.Add(new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));
        config.EnableCors();    //ggf. mehr tun: Handler hier: http://www.developerhandbook.com/category/c-sharp/

//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
        object httpListener;
        if (app.Properties.TryGetValue(typeof(HttpListener).FullName, out httpListener)
            && httpListener is HttpListener)
        {
          // HttpListener should not return exceptions that occur
          // when sending the response to the client
          ((HttpListener)httpListener).IgnoreWriteExceptions = true;
        }
//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        app.Use((context, next) =>
        {
        .....

Gruß,
Heron
User avatar
fpf_baden
Posts: 20
Joined: 08 Jun 2017, 08:31
Location: Karlsruhe
Contact:

Re: RevPi programmieren mit VisualStudio in C#

Post by fpf_baden »

Hallo Heron,

freut mich, dass Dir die Bibliothek etwas bringt :-)

Habe jetzt versucht alle Deine Erweiterungen einzubringen.

Noch ein paar Fragen:
Warum soll der Service keine HTTP-Fehlercodes sondern immer OK zurück melden? (das hab ich nicht übernommen)
Wo hast DU die Spezifikation der rcs Datei gefunden? Habe vergeblich gesucht...

Ich dachte auch schon an einen Zusätzlichen Endpunkt der NUR den Variablenwert liefert, ev. auch für eine Liste von Werten...
z.B. so: POST variables
und payload: [ Name1, Name2, Name3, ... ]

Gruß Frank
Heron
Posts: 40
Joined: 13 Jul 2017, 15:30

Re: RevPi programmieren mit VisualStudio in C#

Post by Heron »

Hallo Frank,

nach langem Suchen habe ich nun auch herausgefunden, warum die ioctl - Aufrufe nicht funktionieren (Read/Write BitValue, Reset, CounterReset, ..).

Die generierten "Magic Numbers" funktionieren (zumindest bei mir) nicht.

Ich habe die _IO und _IOC direkt aus der ioctl.h übernommen:

Code: Select all

    private const uint _IOC_NRBITS = 8;
    private const uint _IOC_TYPEBITS = 8;

    /*
     * Let any architecture override either of the following before
     * including this file.
     */

    private const uint _IOC_SIZEBITS = 14;

    private const uint _IOC_NRSHIFT = 0;
    private const uint _IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS);
    private const uint _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS);
    private const uint _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS);

    /*
     * Direction bits, which any architecture can choose to override
     * before including this file.
     */

    private const uint _IOC_NONE = 0U;
    private const uint _IOC_WRITE = 1U;
    private const uint _IOC_READ = 2U;

    private static uint _IOC(uint dir, uint type, uint nr, uint size) => (((dir) << (int)_IOC_DIRSHIFT) | ((type) << (int)_IOC_TYPESHIFT) | ((nr) << (int)_IOC_NRSHIFT) | ((size) << (int)_IOC_SIZESHIFT));

    /* used to create numbers */
    private static uint _IO(uint type, uint nr) => _IOC(_IOC_NONE, (type), (nr), 0);


Damit kann ich dann auch den "ResetCounter" aufrufen:
DllImport:

Code: Select all

    [DllImport("libc", EntryPoint = "ioctl", SetLastError = true, CharSet = CharSet.Auto)]
    internal static extern int ioctl(int file, uint cmd, ref DIOResetCounter value);                          

der DIOResetCounter:

Code: Select all

  [StructLayout(LayoutKind.Sequential)]
  internal struct DIOResetCounter
  {
    /*
      typedef struct SDIOResetCounterStr
      {
	      uint8_t     i8uAddress;             // Address of module in current configuration
	      uint16_t    i16uBitfield;           // bitfield, if bit n is 1, reset counter/encoder on input n
      } SDIOResetCounter;
     */

    public byte i8uAddress;
    public System.UInt16 i16uBitfield;
  }

Und die Verwendung:

Code: Select all

    public int ResetCounter(byte iAddress, ushort iBit)
    {
      if (!IsOpen) return -1;
      
      DIOResetCounter pCounter;

      pCounter.i8uAddress = iAddress;
      pCounter.i16uBitfield = iBit;

      int iRetVal = Interop.ioctl(_piControlHandle, Interop.KB_DIO_RESET_COUNTER, ref pCounter);
      
      return iRetVal;
    }

Einzig ergibt sich aktuell die Schwierigkeit, das "iBit" zur Laufzeit zu ermitteln:
Über einen Variablenname zBsp. "inCounter" bekomme ich über die varInfo.Device.Position die Basisadresse des DIO. Leider habe ich aktuell noch keine Idee, wie ich zu der Variablen die Eingangsnummer bekomme (1 für Eingang 1, 2 für Eingang 2, usw.). Über die Adresse möchte ich nicht gehen, ändert Kunbus etwas an der RAP, stimmt die Adressierung, bzw. dessen Offset nicht mehr......

Falls da einer eine Idee hat, immer her damit!!


Gruß,
Heron
FerreiraMlg
Posts: 9
Joined: 13 Sep 2019, 07:42

Re: RevPi programmieren mit VisualStudio in C#

Post by FerreiraMlg »

I am not sure if this is somewhere else around here. I had to stop by to say Thank you!

Great and easy package!
Christoph
Posts: 1
Joined: 16 Sep 2021, 06:13

Re: RevPi programmieren mit VisualStudio in C#

Post by Christoph »

Hallo Frank,

erstmals vielen Dank für Deine Mühe. Es gibt allerdings einen Fehler im Interop.cs- der Rückgabewert von lseek ist int und nicht long, ich habe auf github einen Fehlerreport dazu erstellt. Eigenartigerweise ist bei allen meinen RevPis (RevPiDIO als auch RevPi Compact) noch nie ein Fehler beim Read aufgetreten, was natürlich reiner Zufall ist- bei unseren Partnern/Kunden ist es immer wieder, eigenartigerweise bei immer denselben Geräten häufiger, aufgetreten.

Liebe Grüße,
Christoph
Post Reply