Gondel Software

 1. Einleitung

 2. Sensoren, Filter und Interrupts

 3. Regler

 4. Motoren

 5. Kommunikation

 6. Abwurf

 7. Watchdog

 8. Flussdiagramm

 9. Anhang

--------------------------------------------------------------------------------------------------------

 

1. Einleitung

Im Folgenden wird die Funktionsweise der auf der Gondel laufenden Software beschrieben:

Im Wesentlichen besteht sie aus Softwarekomponenten, die zwei Regelkreise, Kommunikation und Steuerung der Aktorik implementieren.

Sie sieht vereinfacht so aus:

 

 

 

 

 2. Sensoren, Filter und Interrupts

Zunächst soll auf die Implementierung des Regelkreises, genauer der Verarbeitung der Sensordaten eingegangen werden:

Die Sensordaten stammen hauptsächlich von einer Inertial Measurement Unit (IMU), die Informationen über den Bewegungszustand der Gondel und über das Erdmagnetfeld liefert.
Die Höhenmessung wird mit dem Ultraschallmodul SRF02 vorgenommen.

Um Rohdaten aus der IMU via I2C auszulesen werden die entsprechend Arduino-Bibliotheken des I2Dev Projekts( http://www.i2cdevlib.com/ ) für das Gyroskop MPU6050 und für den Kompass HMC5883L verwendet. Für den Ultraschallsensor liegt ebenfalls eine passende Bibliothek vor.

Die Anwendung dieser Softwaremodule gestaltet sich oft ähnlich: Man deklariert ein Objekt von dem Typ der in der Bibliothek spezifiziert wird, und kann danach über Funktionsaufrufe mit dem Sensor kommunizieren. Näheres dazu ist in der entsprechenden Header-Datei zu finden:

Anmerkung: Alle folgenden Codebeispiele enthalten nur den für das Beispiel relevanten Code.
                   Teile die reusgeschnitten wurden, werden mit "..." gekennzeichnet.

 

Beispiel Gyro:

MPU6050 accelgyro(0x69); // <-- I2C Adresse

void setup(){
...
accelgyro.initialize();  // Gyro initialisieren
...
}

void loop(){
 ...
accelgyro.getMotion6(&ar.x, &ar.y, &ar.z, &gr.x, &gr.y, &gr.z); //Rohdaten in structs ar und gr einlesen
...
}

Für die anderen Sensoren wird analog verfahren.

Die somit ausgelesen Daten werden entweder in normalen variablen oder in Structs gespeichert.

In Falle der IMU werden die Rohdaten in der Datenstruktur IMURaw, die drei Werte vom Typ int16_t enthält, zur weiteren Verarbeitung zwischengespeichert. Die gefilterten Daten befinden sich in der Struktur IMUFilt:

 

// Rohdaten
typedef struct {
  int16_t x; 
  int16_t y;
  int16_t z;
}
IMURaw;

// Struct für die gefilterten Daten
typedef struct IMUFilt {
  float x; 
  float y;
  float z;
}
IMUFilt;

 

Da die Rohdaten fast immer mit Rauschen belastet sind, müssen sie gefiltert werden.

Damit dies komfortable geschehen kann wurde vom einem Teammitglied eine universelle Infinite Impulse Response (IIR) Klassen erstellt, die Filter in Form von Differenzengleichungen implementiert. Die für die Berechnungen erforderliche Zwischenspeicherung der  Ein- und Ausgangswerte erlauben zudem die effiziente Implementierung von Operationen wie z.B. Integration und Differentation.

 

// IIR.h

class IIR{ public: //IIR(); IIR(float *fbcoeffs,float *fwcoeffs, uint8_t fborder, uint8_t fworder); void setForwardCoeffs(float *nfwcoeffs); void setForwardCoeffs(uint8_t idx, float ncoeff); void setFeedbackCoeffs(float *nfbcoeffs); void setFeedbackCoeffs(uint8_t idx, float ncoeff); void setGain(float); float getGain(); float step(float x0); uint8_t getForwardOrder(); uint8_t getFeedbackOrder(); float *x ; float *y ; float getX(int8_t idx); // 0 -> yn; 1 -> y[n-1] and so on float getY(int8_t idx); // same ...
};

Die Anwendung soll am Beispiel des Filter, das für den Magnetometer zuständig ist, erläutert werden:

 

//magnetometer
#if MAGN_ENABLE

#define MAGN_SAMPLERATE_HZ 50.0f

#define LP_MAGN_ORDER 1
#define LP_MAGN_CUTOFF_HZ 2.0f
#define LP_MAGN_RC (1.0f/LP_MAGN_CUTOFF_HZ)

float lp_magn_alpha = ((1.0f/MAGN_SAMPLERATE_HZ)/(LP_MAGN_RC+(1.0f/MAGN_SAMPLERATE_HZ)));

float lp_magn_b[LP_MAGN_ORDER+1] = {
  lp_magn_alpha ,0.0f};
float lp_magn_a[LP_MAGN_ORDER+1] = {
  1.0f,(1.0f-lp_magn_alpha)};

IIR lowpass_magn_x(lp_magn_a,lp_magn_b,LP_MAGN_ORDER,LP_MAGN_ORDER);
IIR lowpass_magn_y(lp_magn_a,lp_magn_b,LP_MAGN_ORDER,LP_MAGN_ORDER);
IIR lowpass_magn_z(lp_magn_a,lp_magn_b,LP_MAGN_ORDER,LP_MAGN_ORDER);

float magn_head_rad = 0.0f;
float magn_head_deg = 0.0f;
float magn_head_rad_avg = 0.0f;
float magn_head_deg_avg = 0.0f;
float magn_offset_deg = 0.0f;

int magn_head_rad_avg_cnt = 1;
//int magn_head_deg_avg_cnt = 1;


#endif

 

Wichtige Filterparameter sind zum einen die Abtastrate und zum anderen die Grenzfrequenz. Aus diesen beiden Vorgaben lassen sich für einen Tiefpass erster Ordnun die Filterkoeffizient wie oben gezeigt berechnen. Dabei dient die Variable lp_magn_alpha dazu die Übersichtlichkeit zu steigern.

Die Koeffizienten für Feedback und Feedforward werden in Arrays, die jeweils um eins größer als die Ordnung des jeweiligen Stranges (y[n] oder x[n]) sein müssen, gespeichert und werden den Filterobjekten als Zeiger übergeben. Dies spart für den Fall, das alle Filter die gleichen Koeffizienten nutzen, nicht nur Speicherplatz sondern erlaubt zudem das einfache ändern der Koeffizienten.

 

  if(abs(mr.XAxis - lowpass_magn_x.getX(0)) < 1000.0f){ // Falschmessungen überspringen
      mf.XAxis =  lowpass_magn_x.step(mr.XAxis);
    }
    else{
      mf.XAxis =  lowpass_magn_x.step(mf.XAxis);

    }

 

Wenn das Filter mit den Nötigen Parametern versorgt wurde, kann mit der Funktion step(float x0) der nächste Zeitschritt berechnet werden. Das Argument ist die ungefilterte Eingangs Größe x[t0] zu übergeben. Danach wird der dazugehörige Wert y[t0] berechnet und ausgegeben. Es gilt diese Funktion mit der Abtastfrequenz aufzurufen.

Um dies zu realisieren wird der interne Hardware Timer folgendermaßen angesteuert:

 

void setup(){

TIMSK2 |= (1<<TOIE2);

}

ISR(TIMER2_OVF_vect){
  // Timer runs at 125khz

  if( ovfcnt == 9 ){ // 125khz/samplerate*256


#if MAGN_ENABLE == 1
    magnready=1;
#endif
    imuready = 1;
    motcont = 1;
    ovfcnt = 0;

  }
  else{

    ovfcnt++;
  }



} //ISR

 

Zunächst wird im Timer Interrupt Mask Register der Overflow Interrupt des Timer 2 aktiviert. Das bewirkt, das die ISR aufgerufen wird wenn der Zähler des Timers überläuft, also von 255 auf 0 springt.

Da hier die ISR jede alle 2,048 Millisekuden aufgerufen wird, sind Frequenzen bis ca. 488 Hz möglich.

Um nun eine Abtastrate von 50Hz zu erreichen, müssen die Flags alle 20ms gesetzt werden, d.h man braucht alle 20 ms einen Schaltvorgang. Man weis ausßerdem, dass der Timer nach 256 internen Schaltvorgängen überläuft. Daraus folgt:

n = f_timer / (256 * f_sample)

Schaltet man zur Veranschaulichung bei jedem Flag-Set die LED an PIN 13 um, ergibt sich folgender Signalverlauf:

 

1

 

Es steht also eine verlässliche Zeitbasis zur Verfürgung.

Innerhalb der ISR werden die entsprechenden Flags gesetzt, die dann in der Hauptschleife eine Aktion (z.B Sensor auslesen und filtern) auslösen.

Grob vereinfacht sieht die Hauptschleife folgendermaßen aus:

 

void loop(){

  if(imuready){

// Tue etwas mit der IMU

    imuready = 0;
}

  if(magnready){

// ...

magnready = 0;

}
...

}

Ein weiteres Synchronisationsproblem ist das Abstimmen des Ultraschallhöhensensors und denn Ultraschallsender vom IPS.

Um die Funktion des IPS zu gewährleisten, muss alle 200ms ein Ultraschallimpuls gesendet werden. Es muss zudem sichergestellt werden, das Höhensensor und IPS nicht gleichzeitig senden.

Die ISR wurde dazu um folgendes erweitert:

 

  if(ovfcnt2 == 4){ // alle 100 ms
      if( ovfcnt3 ){
#if IPS_TX_ENABLE
        ips_ready =1;
#endif
        ovfcnt3 = 0;
      }
      else{
#if ULTS_ENABLE 
        ultsready = 1;
#endif
        ovfcnt3 = 1;
      }
      ovfcnt2=0; 
    }
    else{
      ovfcnt2++;
    }

 

Das umschalten der Variable ovfcnt3 bewirkt das abwechselnde setzen von ipsready und ultsready. Durch die Wiederholfrequenz von 5Hz ist sichergestellt, dass der eine Impuls abgeklungen ist bevor der andere ausgesendet wird.

Für das senden der IPS Signale wird eine separate Schaltung mit einem zusätzlichen Microcontroller, das sich um das IR- und Ultraschallsignal  kümmert, verwendet. Die Ansteuerung der Schaltung erfolgt mit einem ca. 3ms langen LOW impuls am PIN 2.

Folgendes Flussdiagramm fasst  die Aufgaben der ISR zusammen:

 

 

3. Regler

Als Regler wird ein PID-Regler, der in einer eigenen Bibliothek implementiert wurde, verwendet. Die Besonderheit dieser Implementierung ist, das die Integration des Regelfehlers nach der Trapezregel erfolgt.

Klassendefinition:

 

#include <Arduino.h>


class PID{


public:

PID( float *ncf, float ng, int8_t nl,float nh);  // coeffs, gain , order, steplength

void setCoeffs(float * cf); // change the coeffcient array

void setIerr(float);

...

void reset(void); // set all internal values to zero

void setOutputLimits(float,float);


float *y; // last outputs
float *e; // last errors

float step(float,float);  // next timestep

private:
float gain; //overall gain
float* cf;  // coefficients
float h ; // time between samples

float ierr; // integrated error
...

};

 

Die Initialisierung erfolgt durch die Übergabe eines float-Arrays, das die Koeffizient für P-, D- und I-Anteil (in dieser Reihenfolge) enthält, eines floats für die Verstärkung (wird hier aber auf 1 gesetzt), der Länge der internen Arrays (für die Speicherung, der letzten errecheten Werte) und der Schrittweite für die Integration.

Um die Berechnungen anzustoßen muss die Funktion step() mit der Abtastfequent aufgerufen werden. Bei Abweichender Frequenz wird das Ergebnis verfälscht.

Zum zurücksetzen steht die Funktion reset() zur Verfügung. Diese setzt alle zur Laufzeit veränderlichen Werte auf Null.

Es ist recht einfach möglich die Koeffizienten auch zur Laufzeit zur verändern. Dadurch das die Koeffizienten in einem Array abgespeichert sind, können sie durch ein simples umlenken von einem Zeiger verändert werden:

 

void PID::setCoeffs(float *ncf){

cf = ncf;
}//setCoeffs

 

Beispiel Höhenregelung:

#define PID_H (1.0f/ACCEL_GYRO_SAMPLERATE_HZ)

#define PID_MOT_ALT_KP 0.7f
#define PID_MOT_ALT_KI 0.07f
#define PID_MOT_ALT_KD 200.0f

#define PID_MOT_ALT_GAIN PID_GAIN
#define PID_MOT_ALT_H PID_H

float  pid_mot_alt[3] ={
  PID_MOT_ALT_KP,PID_MOT_ALT_KI,PID_MOT_ALT_KD};

PID pid_alt(pid_mot_alt,PID_MOT_ALT_GAIN,3,PID_MOT_ALT_H);

 

Verwendet wird es  folgendermaßen:

dbg2f = pid_alt.step(reg_set_h,ults_h); // wird alle 20ms aufgerufen

setMotAlt((int)dbg2f); //Wert dem Motor übergeben

 

Die Variable reg_set_h enthält die Sollhöhe, während ults_h die durch den Ultraschallsensor  gemessene Istgröße bereitstellt.

4. Motoren

Für die Ansteuerung der Motorsteuergeräte sind drei Funktionen zuständig. Eine low level Funktion, die nur die übergeben Pins setzt und den PWM-Wert schreibt und zwei, die die jeweiligen Motoren abstrahieren:

 

void setMotSpeed(int sp,uint8_t in1,uint8_t in2 ,uint8_t pwmpin){ // sp in [-255, 255]
  if( sp == 0){
   
    //freerun
#if MOT_HALTMODE == MOT_FREERUN
    digitalWrite(in1,0);
    digitalWrite(in2,0);


#elif MOT_HALTMODE == MOT_SHORT    
    //short the motor to stop it near instantly 

    digitalWrite(in1,1);
    digitalWrite(in2,1);
#endif
  }
  else if(sp >0){
    digitalWrite(in1,1);
    digitalWrite(in2,0);
  }
  else{
    digitalWrite(in1,0);
    digitalWrite(in2,1);
  }
  sp = abs(sp);

  analogWrite(pwmpin,sp);
}// setMotorSpeed

#define MOT_R_PWM_PIN 9 //9
#define MOT_L_PWM_PIN 10//10
#define MOT_ALT_PWM_PIN 6//6

#define MOT_R_IN1_PIN 8//7
#define MOT_R_IN2_PIN 7//8

#define MOT_L_IN1_PIN 11//11
#define MOT_L_IN2_PIN 12//12

#define MOT_ALT_IN1_PIN 5//5
#define MOT_ALT_IN2_PIN 4//4

void setMotAlt(int sp0){
#define MOT_ALT_THRES 5
#define MOT_ALT_OFFSET 40 

    sp0 = constrain(sp0,-255,255);
 
  if(abs(sp0) <= MOT_ALT_THRES){
    sp0=0;
  }

  setMotSpeed(sp0,MOT_ALT_IN1_PIN,MOT_ALT_IN2_PIN,MOT_ALT_PWM_PIN);

}

 

Die Wahrheitstabelle für die Motorregler sieht folgender maßen aus:

 

IN1 IN2 Wirkung
0 0 Freilauf
0 1 Rückwärts
1 0 Vorwärts
1 1 Bremsen

 

Die Funktion "setMotSpeed" prüft zunächt das Vorzeichen des eingehenden PWM-Wertes und setzt nach obiger Tabelle die Richtungspins für den jeweiligen Motor.

Während die Steuerung der Höhenmotoren trivial ist, ist das winkelabhängige Lenken etwas komplizierter:

 

void setMotDirection(float angl,int sp0){
#define MOT_R_THRES 7
#define MOT_L_THRES 7
  sp0 = constrain(sp0,-128,128);
  angl = constrain(angl,-90,90);
  
#define MOT_REG_FACT 150.0f

  int spr =  ((int)(MOT_REG_FACT*sin(angl) +sp0)  );
  int spl = ((int)(MOT_REG_FACT*-sin(angl)  +sp0)  ); spr = constrain(spr,-255,255); spl = constrain(spl,-255,255); if(abs(spr) <= MOT_R_THRES){ spr = 0; } if(abs(spl) <= MOT_L_THRES){ spl = 0; } setMotSpeed(spr,MOT_R_IN1_PIN,MOT_R_IN2_PIN,MOT_R_PWM_PIN); setMotSpeed(spl,MOT_L_IN1_PIN,MOT_L_IN2_PIN,MOT_L_PWM_PIN); }

 

Um eine Schubdifferenz bei einem Winkel ungleich Null zu erzeugen, wird der Sinus des Winkels multipliziert mit einem Faktor, dem aktuellen Schub hinzuaddiert. Dem rechten Motor im Positiven und dem linken im Negativen. Dadurch dreht sich z.B. der Zeppelin beim einem positiven Winkel nach links, da der rechte Motor dann schneller dreht.

 

 5. Kommunikation

Die Kommunikation wird mit einer xBee-basierenden Funkstrecke realisiert. Der Vorteil vom xBee ist die transparente Realisierung einer drahtlosen seriellen Schnittstelle. Das bedeutet, dass das interne UART vom Arduino genutzt werden kann und man keine zusätzliche Software benötigt.

Um die Daten vom xBee in einen String einzulesen wird der Interrupthander des UART , also serialEvent(), folgendermaßen definiert:

 

void serialEvent(){
while(Serial.available() > 0){ char inChar = (char) Serial.read(); if(!serialready){ inString += inChar; if(inChar == '\n'){ serialready = 1; } } } }// SerialEvent

 

Der obige Code liest die  Zeichen, die am Uart verfügbar sind bis zum Linefeed (\n) in den String "inString" ein, und ignoriert danach alle restlichen Zeichen. So wird sichergestellt, dass nur ein Befehl in dem String steht und der Buffer geleert wird.

 

Als nächstes muss die Zeichenkette verarbeitet werden.

Dazu wird der String in einen anderen String namens "cmd" kopiert. Zuerst werden alle Zeichen vor dem ersten 'b' ignoriert d.h es werden alle Zeichen nach dem ersten 'b' kopiert. Anschließend werden überflüssige Leerzeichen mittels trim() entfernt.

 

cmd = inString.substring(inString.indexOf('b'));
inString = "";
    
cmd.trim();

 

Sicherheitshalber wird nochmal überprüft ob die Zeichenkette mit dem Buchstaben 'b' beginnt.

Jetzt erfolgt eine Unterscheidung zwischen festen Einwortbefehlen (z.B ben) und solchen die entweder Argumente erwarten (z.B. bwi 10) oder der syntaktische Aufbau des Befehlsnamen die aufzuführende Funktion charakterisiert (z.B. brg (gyro reset) oder brm (kompass reset)).

Davor werden drei char-Arrays mittels calloc alloziert. Der Befehl "calloc" arbeitet fast genauso wie malloc(). Der Unterschied ist, dass calloc den allozierten Speicher mit Nullen überschreibt was unerwünschten Zeichen in der Zeichenkette vorbeugt.

 

if((cmd.charAt(0) == 'b') || (cmd.charAt(0) == 'B' ) ){
#define CHRBUFSIZE 24
      char* xbuf = (char*) calloc( CHRBUFSIZE, sizeof(char));
      char* ybuf = (char*) calloc( CHRBUFSIZE, sizeof(char));
      char* zbuf = (char*) calloc( CHRBUFSIZE, sizeof(char));
      int xstart = 0;
      int ystart = 0;
      int zstart = 0;
      ...

 

Danach werden die Positionen der Leerzeichen in der Zeichenkette ermittelt und die Zeichen dazwischen in die obigen Arrays geschrieben.

 

     ...
     else if(cmd.substring(1).equals("en") || cmd.substring(1).equals("EN")){
     ...

      }
      else if(cmd.substring(1).equals("dis") || cmd.substring(1).equals("DIS")){
       ...
      }
      ...
  
      else
      {

        xstart = cmd.indexOf(" ");
        ystart = cmd.indexOf(" ", xstart+1);
        zstart = cmd.indexOf(" ", ystart+1);


        cmd.substring(xstart + 1,ystart ).toCharArray(xbuf,CHRBUFSIZE);
        cmd.substring(ystart +1,zstart ).toCharArray(ybuf,CHRBUFSIZE);
        cmd.substring(zstart+1).toCharArray(zbuf,CHRBUFSIZE);
        ...

 

Nun erfolgt eine zeichenweise Abarbeitung der Zeichenkette mit Hilfe mehrerer switch-case Strukturen.

Beispielsweise werden die Reglerkoeffizienten so geändert:

 

  switch((char)cmd.charAt(1)){
        case 'P':
        case 'p':
          {

            pidf[0] = atof(xbuf);
            pidf[1] = atof(ybuf);
            pidf[2] = atof(zbuf);

            switch((char)cmd.charAt(2)){
            case 'r':
            case 'R':
              {
                switch((char)cmd.charAt(3)){

                case '1':
                case 'n':
                case 'N':
                case '2':
                case 'c':
                case 'C':
                case '3':
                case 'a':
                case 'A':
                  {

                    for(byte i = 0 ; i<3;i++){
                      pid_mot_rl[i] = pidf[i];
                    }              
                   break;
                  }
                default:
                  
                  break;
                }
                break;
              }
            case 'H':
            case 'h':
              {
                ...

 Ähnlich sind auch die Strukturen für die übrigen Befehle.

 

 6. Abwurf

Die Ansteuerung des Abwurfeinrichtung wurde, da nur ein Pin auf HIGH gesetzt werden muss, als Einwortbefehl implementiert:

 

#if DROP_ENABLE   
      else if(cmd.substring(1).equals("drop") || cmd.substring(1).equals("DROP")){

        digitalWrite(DROP_PIN,HIGH); 
        
      }
#endif  

 

7. Watchdog

Damit es bei einem Ausfall von Sensoren oder der Funkverbindung nicht zu Zwischenfällen kommt wurde noch ein Watchdog implementiert. Diese ist in der Lage die Gondel unter gegeben Umständen entweder abzuschalten oder die Regelung zu umgehen und somit beispielsweise einen Sinkflug einzuleiten.

 

#if WATCHDOG_ENABLE

#define WD_TIME 5000  // [ms]

unsigned long wd_time = 0; // store the time;
byte wd_enable = 0; // ed main switch
byte wd_switch = 0; // must be set to 1 periodically, or else emergency mode will be activated
#if ULTS_ENABLE
float wd_ults_avg = 0.0f;
int wd_ults_avg_cnt = 1;

#define WD_ULTS_AVG_MAXCOUNT 20 // 4 secs
#define WD_ULTS_MAX 305
#define WD_ULTS_MIN 29

#define WD_EN 1
#define WD_REG 2
#define WD_BEHAV WD_REG

#endif

 

Hier besteht er er aus zwei Teilen, einer der Ständig aktiv ist und den Ultraschallsensor überwacht und einem Teil der bei Bedarf genutzt werden kann.

Der ständig aktive Teil bildet einen Mittelwert über eine festgelegte Anzahl von Messungen und leitet bei unter- oder überschreiten der Grenzwerte einen Sinkflug ein, indem der Schub der Höhenmotoren auf einen festen Wert gesetzt wird. Wahlweise kann auch der "Schalter" des anderen Teils umgelegt werden.

 

#if WATCHDOG_ENABLE

    wd_ults_avg += ((ults_h - wd_ults_avg)/((float)(wd_ults_avg_cnt+1)));
    wd_ults_avg_cnt++;
    if( wd_ults_avg_cnt == WD_ULTS_AVG_MAXCOUNT){
      //Serial.println(wd_ults_avg);
      if( (wd_ults_avg > WD_ULTS_MAX || wd_ults_avg < WD_ULTS_MIN) && main_enable){

#if WD_BEHAV == WD_EN        
        wd_enable = 1;
        wd_switch = 0;
#elif WD_BEHAV == WD_REG

        mot_alt_cont_auto = 0;
        reg_set_h = -70;

#endif
      }

      wd_ults_avg = 0;
      wd_ults_avg_cnt = 1;
    }
#endif

 

Der andere Teil erfordert  das periodische Zurücksetzen eines Schalters. Wird dieser nicht über den Befehl "bwdr" (watchdog reset) zurückgesetzt, so wird nach einer definierten Zeit die Goldel abgeschaltet.

 

#if WATCHDOG_ENABLE
  if(wd_enable && main_enable){
    if( (millis() - wd_time) >= WD_TIME){ // WD_TIME hier 5000ms

      if(wd_switch){

        wd_switch= 0;
      }
      else{

        main_enable = 0;

      }

      wd_time = millis();
    }

  }
#endif

 

8. Flussdiagramm

Folgendes Flussdiagramm zeigt die Zusammenhänge zwischen den einzelnen Module vereinfacht auf:

 

 

9. Anhang

Das angehängte Archiv beinhaltet den kompletten Quellcode des Arduino-Sketches und die der genutzten Libraries.

Alle von mir selbst geschriebenen Programmteile sind unter der MIT-Lizenz verfügbar. Alle Codes in verschiedenen Entwicklungsstufen befinden sich zudem in einem eigenen Github Repository "Blowfish"

Anhänge:
Diese Datei herunterladen (Blowfish_Software.zip)Blowfish_Software.zip[ ]%2013-%07-%17 %1:%Jul%+02:00