COOKIES

This site may be using cookies to melk you with your own data. I, ben0bi, am not the owner of this web service and I also do not maintain their servers. But the EU and the owner of this service think, that the user (me) has the responsibility to inform the consumer (you), that this website uses cookies. Again: I, ben0bi, NEVER use cookies. I am not responsible for the setup of this web service. I just present some information here and do not intend to spy on you for whatever reason ever. But (also again), I do not host this website nor do I maintain any servers related to this website nor do I benefit from using the cookies maintained from this service. I hereby give the responsibility for using cookies on blogspot back to the owners of blogspot.
Posts mit dem Label DIY werden angezeigt. Alle Posts anzeigen
Posts mit dem Label DIY werden angezeigt. Alle Posts anzeigen

Mittwoch, 23. Oktober 2019

Das Buch der Helden und Abenteuer

Das Net(te)Buch der Helden und Abenteuer

Zutaten:

  • 1x Netbook im A4-Format. (Meins: Acer Spin 1)
  • 1x Buch oder Comic im A4-Format mit festem Einband, gebunden, innere Buchhöhe mindestens 8mm.
  • 1x Rolle Doppelklebeband, möglichst fest klebend.
  • 1x Cutter

Aufbau:

  • Man schneide die kleinen Gummiknubel unten am Netbook ab, oder reisse sie heraus, welche als Stand dienen.
  • Man schneide mit dem Cutter alle Seiten aus dem Buch heraus, und zwar so, dass die Bindung noch ganz bleibt wenn möglich.
  • Man gucke, wie das Netbook am besten in die Hülle passt.
  • Man beklebe die Ober- und Unterseite des Netbooks mit Doppelklebeband, und zwar in Flicken, damits nicht gleich alles abreisst, wenns was abreisst.
  • Und wickle dann die Hülle drum herum. Und presse sie an. Man habe nun weiterhin Spass mit dem Netbook, auch wenn man es nicht mehr ganz umdrehen kann (Spin ist ein Convertible).



PS: Für mich ist das ein guter Trick, damit ich nicht immer den Bildschirm aufmachen will und es trotzdem in Sichtweite haben kann. PPS: Ich habe das vier Freunden jeweils in die Hand gedrückt mit "Guck mal, das ist das beste Buch ever..." und die haben erst gemerkt, dass da ein Laptop drin ist, als sie es jeweils aufgemacht haben. Obwohl sie es auch halb von der Seite angeguckt haben und vorher mindestens 1 Minute in den Pfoten hatten...perfekte Tarnung. :)

Samstag, 19. Oktober 2019

DIY Emulator Teil 2.1.1: Refining EmuGraphicsAdapter

DIY Emulator Teil 2.1.1: Refining EmuGraphicsAdapter

in Unity

Nachdem ich in diesem Artikel gelernt habe, dass schon kleinste Änderungen am Code enorm mehr Speed herausholen können, werden nun erstmal ein paar Speed-Optimierungen gemacht.

(Ich mache dann (eventuell :) ) schon mal einen "richtigen" Emulator aber ich muss erst noch rausfinden, wie man einen emulierten (schon programmierten) Chip überhaupt einbaut und benutzt und davor hab ich schon ein bisschen Bammel weil ich wirklich absolut keine Ahnung davon habe. Bitte entschuldigt das andauernde Aufgeschiebe. Diese Tutorials sollen ja auch für mich sein, nicht nur für dich... ;) )

Darum wurde dort die MarkPixelToChangeByIndex-Funktion im EmuGraphicsAdapter (der Klasse aus diesem Artikel) hinzugefügt.

Um noch mehr Speed herauszuholen - denn millionenfache Funktionsaufrufe brauchen dann doch ein bisschen Zeit - werden wir nun noch eine Funktion generieren, die gleich eine gesamte Map (Display) mit der zugehörigen Palette direkt auf der Textur verrechnet, ohne dazu immer extra pro Pixel eine Funktion aufrufen zu müssen.*

[edit]: Wenn dein Emulator Display genau gleich gross ist, wie die Textur und auf 0,0 positioniert wird (also keine "hidden Pixel" hat), ist MarkPixelToChangeByIndex immer noch ein bisschen schneller als die folgende Funktion.*

 [edit 2]: Ich bin grüble darüber nach, ob und wie man das mit Shadern "richtig schnell" machen kann. Anders gesagt: Auch wenn hierbei kein Emulator rauskommt, hast du dann trotzdem eine richtig flotte 2D-Pixelzeichungs-Engine parat.

*Desweiteren wird diese Funktion auch das Display des Emulators "korrekt" auf der Textur platzieren:
Einige Systeme haben einen grösseren Display-Speicher als auf dem Bildschirm angezeigt. Eine oder mehrere Linien oder Zeilen sind ausserhalb des sichtbaren Bereiches. Diese "hidden pixel" werden vor allem bei Scrolling-Funktionen benutzt, damit es kein Geflacker gibt.

Wir könnten entweder die Texturgrösse anpassen, oder einfach diese Pixel auslassen. Ich habe mich erstmal für zweiteres entschieden.

Schlussendlich gibt es noch einen paletteModifier, so dass man nicht immer vorher die gesamte Map mit neuen Indexen aufbauen muss, sondern nur diesen Modifier verändert. (Wird durch den plasmaIndex im Placeholder_Emulator bestimmt.)

Also, hier die Funktion: MarkPalettedMapToChange1D in der EmuGraphicsAdapter-Klasse
Die Funktion hat ziemlich viele Parameter, Sorry dafür.

  /* Parameters:
     * map: 1-dimensional array with palette indexes.
     * mapwidth: width (max-x) of the map to make it 2-dimensional.
     * mapheight: height (max-y) of the map to make it 2-dimensional.
                  mapheight can be calculated in-function but that wastes processing power.
     * palette: 1-dimensional array with colors arranged in the right order.
     * paletteModifier: add this modifier to the palette index on the map to get the right color.
     *                  used to not create the whole map with each palette change.
     * posX: start-position of the map on the display. used for displays with "hidden pixels".
     * posY: start-position of the map on the display. used for displays with "hidden pixels".
     */
    // define the variables once instead of in each function call.
    private int tmp_texx, tmp_texy, tmp_texidx, tmp_emux, tmp_emuy, tmp_emuidx;
    private int tmp_palsize, tmp_palindex;
    private int tmp_spritewidth, tmp_spriteheight, tmp_mapsize;
    public void MarkPalettedMapToChange1D(int[] map, int mapwidth, int mapheight,
                                          Color[] palette, int paletteModifier = 0,
                                          int posX = 0, int posY = 0)
    {
        // get spritewidth and height.
        tmp_spritewidth = (int)m_drawable_sprite.rect.width;
        tmp_spriteheight = (int)m_drawable_sprite.rect.height;
        tmp_mapsize = map.Length;
        tmp_palsize = palette.Length;
        // go through x and y instead of only the index.
        // for better calculation of the emulator-display-position.
        // go through the DISPLAYED texture array:
        for(tmp_texy=0;tmp_texy < tmp_spriteheight;tmp_texy++)
        {
            tmp_emuy = tmp_texy - posY;
            // y not in bounds, break or continue.
            if (tmp_emuy < 0) // < 0 = continue
                continue;
            if(tmp_emuy >= mapheight) // > height = break
                break;
            for (tmp_texx = 0; tmp_texx < tmp_spritewidth; tmp_texx++)
            {
                // calculate position on the EMULATOR display:
                tmp_emux = tmp_texx - posX;
                // x is not in bounds, break or continue.
                if (tmp_emux < 0)
                    continue; // continue if smaller.
                if (tmp_emux >= mapwidth)
                    break; // break if bigger.

                // calculate the real index on the map.
                // because x and y are in boundsl, we do not need to check the index itself.
                tmp_emuidx = (int)((tmp_emuy * mapwidth) + tmp_emux);

                // get the palette index
                tmp_palindex = map[tmp_emuidx] + paletteModifier;
                // maybe we need to adjust the palette index to get it into bounds.
                while (tmp_palindex >= tmp_palsize)
                    tmp_palindex -= tmp_palsize;

                // finally set the new color.
                tmp_texidx = tmp_texy * tmp_spritewidth + tmp_texx;
                m_cur_colors[tmp_texidx] = palette[tmp_palindex];
            }
         }
    }

Alle tmp-Variablen werden im Scope der Klasse erstellt, damit sie nicht bei jedem Aufruf der Funktion neu erstellt werden müssen. Die Parameter der Funktion sind die folgenden:
  • map: 1-dimensionales Array mit Palette-Indexen.
  • mapwidth: Breite einer Zeile der map (x)
  • mapheight: Höhe der map (y) (Könnte man auch in der Funktion jedesmal neu berechnen, was jedoch unnötig Prozessorpower verbraucht.)
  • palette: 1-dimensionales Array mit den Farben (Color) in der richtigen Reihenfolge.
  • paletteModifier: Addiere diese Zahl zu jedem Index auf der map.
  • posX: Startposition der map auf der Textur in Pixeln (x).
  • posY: Startposition der map auf der Textur in Pixeln (y).
Es wird, obwohl die Arrays eindimensional sind, trotzdem 2-dimensional durch das Textur-Array hindurch gegangen. Dabei wird jeweils die Position des Pixels auf der map berechnet, und ob diese Position überhaupt auf der map vorhanden ist. Dazu braucht es mapwidth und mapheight. Wenn x oder y auf der map kleiner als 0 ist, wird die Schleife mit continue sofort fortgesetzt, wenn die jeweilige Variable jedoch zu gross ist, wird die zugehörige Schleife sofort mit break gleich ganz verlassen. PS: Einige Checks habe ich wieder herausgenommen, da sie nur unnötig Prozessorzeit verbrauchten und nie "erfüllt" wurden. Dies hier ist die aktuellste Version.

Nun können wir noch die copyDisplay-Funktion im Placeholder_Emulator ändern:

    // copy the paletted display colors to the real color array.
    protected void copyDisplay()
    {
        // alt: for(q = 0; ...
        gfx.MarkPalettedMapToChange1D(m_display, disp_width, disp_height,
                                      m_palette, (int)m_plasmaIndex, 0, 0);
    }

Uuund....weiteres folgt bald. Eventuell.

Das Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Mittwoch, 16. Oktober 2019

DIY Emulator Teil 2.1: Der Placeholder-Emulator

DIY Emulator Teil 2.1: Der Placeholder-Emulator

in Unity


(Interlude - Dieser Artikel ist für den weiteren Verlauf der Tutorials nicht relevant.)

Du solltest das Framework des vorigen Artikels nun aufgesetzt haben...

Nachdem wir in den vorigen zwei Artikeln ein kleines 2D-Framework für Emulatoren in Unity3D aufgesetzt haben, soll dieses Framework nun auch einmal etwas anzeigen.

Wie schon beschrieben ist der Placeholder-Emulator kein wirklicher Emulator sondern hat nur die selbe Klasse als Basis. Der Placeholder-Emulator läuft im Hauptmenü, wenn kein anderer Emulator geladen ist. Dies dient der Vereinfachung: Im System ist immer ein Emulator am laufen und somit muss kein Extracode für den Fall geschrieben werden, wenn eben kein Emulator geladen ist. Desweiteren kann man hier verschiedenste Sachen austesten, ohne gross etwas kaputt zu machen.

In diesem Artikel werden wir einen Plasma-Effekt generieren.

Dazu wird eine Farbpalette erstellt.
Dann wird das Display des Emulators mit zufälligen Indexen dieser Palette gefüllt.
Danach wird das Display "refined", also alle Pixel werden vom Wert her an die benachbarten angeglichen. Das gibt schöne "Hügel und Täler".

Dieses Bild aus Paletten-Indexen dient nun als Ausgangsbasis.

In Jedem Frame werden die Paletten-Indexe des Displays als Farbe (Color) aus der Palette geholt und auf die Textur "gemalt". Dabei wird ein "plasmaIndex" dazu gerechnet, welcher das Plasma...plasmieren lässt.

Alles was man pro Frame ändern muss ist der plasmaIndex, und dann natürlich noch das Display in Colors umrechnen.

Wir nehmen den Placeholder-Emulator und erweitern diesen (mehrere Abschnitte):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Placeholder_Emulator : EmulatorBase
{
    protected int[] m_display; // pos = y * disp_width + x
    protected Color[] m_palette;
    protected int m_palSize = 0xFF; // palsize is one byte. :)

    // this index is added to the actual map palette index to "move" the colors.
    protected float m_plasmaIndex = 0.0f;

    // we need to slow down the things a little.
    protected int colorsPerSecond = 20;

    public Placeholder_Emulator(GameObject g) : base(g)
    {
        Debug.Log("EPlaceholder_Constructor");
        disp_width = 320;
        disp_height = 240;
        m_display = new int[disp_width * disp_height];

        createPalette();
        createPlasmaField();
    }

    public override void Update()
    {
        copyDisplay();

        m_plasmaIndex+=Time.deltaTime*colorsPerSecond;
        if ((int)m_plasmaIndex >= m_palSize)
            m_plasmaIndex = (float)m_palSize-m_plasmaIndex;
    }

Der Placeholder_Emulator ist von der vorher erstellten EmulatorBase abgeleitet.
  • m_display ist das der Textur entsprechende Array mit den Palette-Indexen für das Plasma-Bild.
  • m_palette ist das Array mit den Colors für die Indexe in m_display.
  • m_palSize ist die Grösse der Palette.
  • m_plasmaIndex ist der aktuelle Index, welcher auf jeden Index in m_display aufgerechnet wird.
  • colorsPerSecond zeigt an, wieviele Farbwechsel (Änderung von m_plasmaIndex) es pro Sekunde geben soll.
  • Im Konstruktor wird das GameObject (von EmuCreator) an den Basis-Konstruktor übergeben.
    Die Grösse des Displays wird auf 320x240 (PC-SCREEN 13) eingestellt und m_display wird initialisiert. Dann werden die Palette und das Plasmabild erstellt mit createPalette und createPlasmaField.
  • Schliesslich wird in jedem Update das Display per copyDisplay auf die Textur "gerendert". Dann wird der plasmaIndex um frameTime * colorsPerSecond erhöht.

createPalette()

Erst mal die createPalette-Funktion:

    // create a palette.
    protected void createPalette()
    {
        m_palette = new Color[m_palSize];

        float r = 0.0f;
        float g = 0.0f;
        float b = 0.0f;
        float eight = m_palSize / 8.0f;
        for(int i=0;i < m_palSize;i++)
        {
            // first red to yellow
            if (i<=eight)
            {
                r = 1.0f;
                g = 1.0f / eight * i;
                b = 0.0f;
            }

            // not to white because it's to bright.
            // then yellow to black
            if(i>eight && i<=eight*2)
            {
                r = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                g = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                b = 0.0f;
            }

            // then black to turkis
            if (i > eight*2 && i <= eight * 3)
            {
                r = 0.0f;
                g = 1.0f / eight * (i - (eight * 2));
                b = 1.0f/eight*(i-(eight*2));
            }

            // then turkis to green
            if (i > eight * 3 && i <= eight * 4)
            {
                r = 0.0f;
                g = 1.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 3)));  
            }

            // then green to black
            if (i > eight * 4 && i <= eight * 5)
            {
                r = 0.0f;
                g = 1.0f - (1.0f / eight * (i - (eight * 4))); // reverse
                b = 0.0f;
            }

            // then black to blue
            if (i > eight * 5 && i <= eight * 6)
            {
                r = 0.0f;
                g = 0.0f;
                b = 1.0f / eight * (i - (eight * 5));
            }

            // then blue to magenta
            if (i > eight * 6 && i <= eight * 7)
            {
                r = 1.0f / eight * (i - (eight * 6));
                g = 0.0f;
                b = 1.0f;
            }

            // then magenta to red
            if (i > eight * 7)
            {
                r = 1.0f;
                g = 0.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 7))); // reverse
            }

            m_palette[i] = new Color(r, g, b);
        }
    }


Diese Palette geht durch alle Farben ausser Weiss (das war zu hell, ich habe es durch Schwarz ersetzt. Rate, wo. ;) ). Darum braucht es acht Abschnitte. Die Variable eight ist ein Achtel der Gesamtpalette. So kann man in jedem Unterabschnitt mit eight und dem Index von 0.0f bis 1.0f oder umgekehrt gehen.

Zunehmende Farbe ist: 1.0f/eight * i
Abnehmende Farbe ist: 1.0f-(1.0f/eight * i)

Dazu muss man von i jeweils noch ein paar Achtel abziehen, damit i immer im Bereich von 0 bis eight ist.

Die Reihenfolge der Farben ist die folgende:  Rot, Gelb, Schwarz, Türkis, Grün, Schwarz, Blau, Magenta, Rot

createPlasmaField()

Nun die createPlasmaField-Funktion:
Erst wird das gesamte Display mit Zufallswerten von 0 bis zur Palettengrösse gefüllt.
Dann wird vier mal durch das Display durchgegangen. Die umliegenden und der aktuelle Pixel werden zusammengezählt und der Durchschnitt des Ergebnisses ausgerechnet. Der Dividor für den Durchschnitt wird jedesmal neu berechnet, da es am Rand weniger Pixel hat.

    protected void createPlasmaField()
    {
         // initialize the display array.
         int m_display = new int[disp_width * disp_height];
         // fill the display with random values.
         for(int i=0;i<m_display.Length;i++)
              m_display[i] = (int)Random.Range(0, m_palSize);

         // go several times through the whole display and
         // smoothen the pixel color(-indexes)
         // 4 steps are appropriate: less do it carvy, more flatten it out to one color.
         int dividor = 0;
 
         int newcol = 0; // color is a palette index, not a color.
         for(int steps=0;steps < 4;steps++)
         {
             // go through x and y instead of mapIndexes because...
             for(int y = 0;y < disp_height; y++)
             {
                for(int x = 0;x < disp_width; x++)
                {
                    dividor = 0;
                    // .. we need to get the right position here.
                    for(int yp= -1;yp<=1;yp++)
                    {
                       for(int xp= -1;xp<=1;xp++)
                       {
                          // get x and y of the pixel to add
                          int newx = x + xp;
                          int newy = y + yp;
                          // check if it is in bounds.
                          if(newx >= 0 && newx < disp_width &&
                             newy >= 0 && newy < disp_height)
                           {
                              dividor++;
                              int idx = newy*disp_height + newx;
                              newcol += m_display[idx];
                           }   
                       }
                    }
                    // divide by dividor.
                    newcol = (int)newcol / dividor;
                    // set new, "smooth" palette index.
                    m_display[y*disp_width +x]=newcol;
                }
             }
         }
    }

In dieser Funktion hat es ziemlich viele for-s, doch das macht nichts, da sie nur einmal aufgerufen wird. Bei Update allerdings musste ich mehrmals "drüber", da schon kleinste Multiplikationen (zB. index aus x,y ausrechnen pro Pixel) einen enormen Einfluss auf den Verarbeitungs-Speed haben (bei einem Array in der Grösse eines Displays). Im EmuGraphicsAdapter hat es dafür eine neue Funktion MarkPixelToChangeByIndex. Eventuell kommt später noch eine "CopyPaletteToTexture(map, palette)" in den EmuGraphicsAdapter, um nur einen Funktions-Call zu machen statt xTausend, doch bis jetzt reicht das.

Was genau gemacht wird, ist oben schon erklärt. Mit den innersten Schleifen werden die Pixel um x und y herum ausgelesen.

copyDisplay():

Schliesslich noch die copyDisplay-Funktion. Da das Display hier gleich gross ist wie die Textur, kann man direkt mit dem Index arbeiten.

    protected void copyDisplay()
    {
       // speed up the things a little with direct indexing.
       for(int i=0; i &lt m_display.Length; i++)
       {
          gfx.MarkPixelToChangeByIndex(i, m_palette[m_display[i]]);
       }
    }
} // end the class here.

Die vielen MarkPixelToChange-Aufrufe pro Frame sind nicht so der Bringer, das wird noch refined.

Desweiteren muss ja eigentlich nicht in jedem Frame das Bild neu gelöscht, aufgebaut und angezeigt werden, sondern nur wenn sich der plasmaIndex ändert (also alle 3 Frames, zur Zeit: 20 Änderungen auf 60 Frames pro Sekunde verteilt). Dazu müsste man switchBuffers aus dem EmuGraphicsAdapter-Update herausnehmen und im Emulator selbst aufrufen.

Ich lerne das Zeug auch alles gleich neu, da wird eventuell noch einiges geändert in älteren Artikeln.
[änderungen folgen] ;)

Jetzt musst du noch.....Play drücken. :)

Das Projekt findest du auch auf github, und zwar hier:
https://github.com/ben0bi/EmulatroniX
im Ordner "JUMPEE".

JUMPEE heisst "JavaScript & Unity Multi Purpose Emulator Environment"
naja, J müsste man rausnehmen, es ist ja jetzt C#. Aber dann tönts nicht mehr so gut....

Den Plasmaeffekt in JavaScript findest du zum angucken hier:
https://ben0bi.github.io/EmulatroniX

Dienstag, 15. Oktober 2019

DIY Emulator Teil 2.1: Plugin-Emulator (UNITY)

DIY Emulator Teil 2.1: Plugin-Emulator-Framework

in Unity

Nachdem wir in diesem Artikel ein grafisches Grundgerüst aufgesetzt haben, wird nun hier das Grundgerüst für die Emulatoren selbst aufgesetzt.

Die Applikation soll mehrere Emulatoren unterstützen können und mit "Plugins" aufgerüstet werden können. Um die Dinge zu vereinfachen, wird "immer" ein Emulator laufen, es gibt keinen spezifischen Hintergrund für die Applikation selbst. Jegliche Grafik bis auf das UI wird durch einen "Emulator" dargestellt. Das heisst, dass, während KEIN Emulator geladen ist, ein "Platzhalter-Emulator" läuft, welcher eine beliebige Grafikausgabe machen kann und auch nicht wirklich ein Emulator ist. Er hat nur die selbe Basisklasse wie ein Emulator.

Dazu gibt es mehrere Scripts:
  • Ein EmuCreator-Script, welches den Emulator auswählt, erstellt und initialisiert. Dieses wird an das GameObject mit dem EmuGraphicsAdapter (das Script aus dem vorigen Artikel) angehängt.
  • Eine Emulator-Basis von welcher alle Emulator-Klassen abgeleitet sind.
  • Eine abgeleitete Emulator-Klasse zu Testzwecken und später als Platzhalter-Emulator.
Erstellen wir also erstmal den EmuCreator und hängen dieses Script an das GameObject an. (In der Szene sollte nur ein GameObject vorhanden sein bis jetzt.)

EmuCreator.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(EmuGraphicsAdapter))]
public class EmuCreator : MonoBehaviour
{
    protected EmuGraphicsAdapter gfx;
    protected EmulatorBase m_emulator=null;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("EmuCreator started.");
        gfx = gameObject.GetComponent<EmuGraphicsAdapter>();

        // create an emulator just for testing.
        createEmulator(new Placeholder_Emulator(gameObject));
    }

    // Update is called once per frame
    void Update()
    {
        if (m_emulator!=null)
            m_emulator.Update();
    }

    // create an emulator and initialize it.
    void createEmulator(EmulatorBase emu)
    {
        m_emulator = emu;
        gfx.setEmuScreenSize(emu.getDisplayWidth(), emu.getDisplayHeight());
    }
}

  • gfx ist die Referenz auf die Graphik-Komponente, welche im vorigen Artikel erstellt wurde.
  • m_emulator ist die Instanz des aktuell laufenden Emulators.
Da dieses Script an das GameObjekt angehängt wird, werden Start und Update hier automatisch aufgerufen. Beim Emulator selbst gibt es keine Start-Funktion sondern einen Konstruktor und Update  muss von "hier" aus manuell ausgeführt werden.

Mit createEmulator(emulator) wird m_emulator gesetzt und dann die Bildschirmgrösse auf das Emulatordisplay angepasst.

Mehr braucht es hier gerade nicht. In Start wird der Platzhalter-Emulator gestartet, welcher im übernächsten Script beschrieben wird. Doch dafür brauchen wir erst die EmulatorBase-Klasse, welche als Basis für alle unsere Emulator-Plugins dienen wird.

EmulatorBase.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EmulatorBase
{
    protected GameObject gameObject;
    protected EmuGraphicsAdapter gfx;

    // Change those values and...go. ;)
    protected int disp_width = 20;
    protected int disp_height = 20;

    public int getDisplayWidth() { return disp_width; }
    public int getDisplayHeight() { return disp_height; }

    public EmulatorBase(GameObject g)
    {
        Debug.Log("EBASE Constructor");
        gameObject = g;
        gfx = g.GetComponent();
    }

    // Update is called once per frame
    public virtual void Update() { }
}

  • gfx ist nochmal/wieder die Referenz auf die Graphik-Komponente, welche im vorigen Artikel erstellt wurde.
  • gameObject ist das gameObject welches vom EmuCreator übergeben wird, also das gameObject, in welchem der EmuCreator als Komponente registriert ist.
  • disp_width und disp_height ist die native Grösse des Displays (in Pixeln) des jeweiligen Systems. Stelle diese Werte im Konstruktor ein.
Jede von dieser Basisklasse abgeleitete Klasse kann als Emulator in den EmuCreator "gestöpselt" werden. Update wird "normal" aufgerufen, also in jedem Frame. Update ist virtuell und muss in abgeleiteten Klassen mit override überschrieben werden. Im Konstruktor deines Emulators musst du nur noch disp_width und disp_height richtig einstellen und schon geht alles.

Dieser Platzhalter-"Emulator" setzt einfach einen Pixel. Später folgt noch ein schöner Grafikeffekt. ;)

Placeholder_Emulator.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Placeholder_Emulator : EmulatorBase
{
    public Placeholder_Emulator(GameObject g) : base(g)
    {
        Debug.Log("EPlaceholder_Constructor");
        disp_height = 50;
        disp_width = 50;
    }

    public override void Update()
    {
        gfx.MarkPixelToChange(19, 19, Color.white);
    }
}

Somit können nun verschiedenste Systeme über dieselbe Schnittstelle mit Unity "kommunizieren".

Ich hoffe, das hilft bei deiner Gesundung. :)

Das komplette Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Weiter gehts mit ein bisschen Auflockerung: Ein Grafik-Effekt (ohne Shader - wenn mir das jemand in Shadern machen könnte, also das Display, das wär was.)

Freitag, 11. Oktober 2019

DIY Emulator Teil 2: Setup Basisgerüst (UNITY)

DIY: Emulator Teil 2: Setup Basisgerüst

in Unity

Früher hatte ich erwähnt, dass ich den Emulator am liebsten mit Unity3D machen würde, jedoch RTT (Render-To-Texture) damals nicht möglich war mit der frei verfügbaren Edition.

Nun, jetzt gibt es eine Möglichkeit.

Nachdem ich dieses Tutorial gefunden hatte,
habe ich erst mal das neueste Unity installiert: 2018.4.11f
und das Tutorial ausgetestet.

(Hier gehts zum vorigen Artikel: Teil 1: Research (deutsch)
Dabei ging ich noch von Javascript aus, dazu hat es auch ein paar Artikel.
JS war jedoch nur eine Notlösung, siehe oben...)

Nun gut, das läuft..

...machen wir was draus.

Wie schon beschrieben bin ich ein grafischer Mensch also wird erst mal ein Basis-Grafikgerüst aufgesetzt.

Mit dem Setup unten wird am Ende ein Bild auf dem Bildschirm dargestellt, auf welchem der Emulator sein Display zeichnet. Das Bild hat die Pixelgrösse des Emulator-Systems und wird auf Fullscreen hochskaliert. Wenn das keepAspectRatio-Flag gesetzt ist, wird das Bildseitenverhältnis des Original-Displays beibehalten. Dabei gibt es eventuell schwarze Balken an den Seiten oder oben und unten.

Der Emulator kann mit setEmuScreenSize die Displaygrösse anpassen und mit MarkPixelToChange die Farbe eines Pixels ändern.

Das Bild wird gebuffert, das heisst, im Hintergrund wird das Bild gezeichnet während im Vordergrund der "alte" Buffer angezeigt wird.

Man kann mehr als zwei Buffer-Images bestimmen oder auch nur eines: Dann wird direkt auf die sichtbare Textur gezeichnet.

Los gehts...

Lösche alles bis auf die Main-Kamera.

* Erstelle ein leeres GameObjekt auf Position 0,0,0
* Erstelle ein neues Script und hänge es an das GameObjekt an.

Das ist der Code, er wird unten erklärt:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EmuGraphicsAdapter : MonoBehaviour
{
    // each display has the same sprite size.
    protected Vector2 sprite_size;
    protected Vector2 local_sprite_size;

    // The actual emulator display game object which is visible on the screen.
    protected GameObject m_display_image;
    protected SpriteRenderer m_display_renderer;

    // at least 2 sprites for the buffering process.
    public int Buffer_Size = 2;
    protected Sprite[] m_buffer_sprites;
    protected int m_actualBuffer = 0;

    // keep the emulator aspect ratio?
    public bool keepAspectRatio = false;

#if (UNITY_EDITOR)
    private bool m_oldKeepAspectRatio = false;
#endif

    protected Texture2D m_drawable_texture;
    protected Sprite m_drawable_sprite;
    Color32 Reset_Colour = new Color32(0,0,69,0xFF);
    Color32[] m_cur_colors;

    // Start is called before the first frame update
    void Awake()
    {
        if (Buffer_Size < 1)
            Buffer_Size = 1;

        Debug.Log("EmuGraphicsAdapter started.");

        // create the display image game object.
        m_display_image = new GameObject("DISPLAY");
        m_display_image.AddComponent<SpriteRenderer>();
        // get the renderer to not search for it in each frame.
        m_display_renderer = m_display_image.GetComponent<SpriteRenderer>();

        // set some settings on the renderer.
        m_display_renderer.flipY = true;
        m_display_renderer.receiveShadows = false;
        m_display_renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;

        setEmuScreenSize(50, 50);
    }

    // call this function after the screen has resized or the sprite has changed.
    protected void resized()
    {
        // resize.
        var sprr = m_display_image.GetComponent<SpriteRenderer>();

        // The real image size in pixels for later.
        sprite_size = sprr.sprite.rect.size;
        // and in local space units.
        local_sprite_size = sprite_size / sprr.sprite.pixelsPerUnit;

        // get the screen height & width in world space units
        float worldSpriteWidth = sprr.sprite.bounds.size.x;
        float worldSpriteHeight = sprr.sprite.bounds.size.y;

        float worldScreenHeight = Camera.main.orthographicSize * 2.0f;
        float worldScreenWidth = (worldScreenHeight / Screen.height) * Screen.width;

        // initialize new scale to the current scale
        Vector3 newScale = m_display_image.transform.localScale;

        // resize the local scale to the world scale.
        newScale.x = worldScreenWidth / worldSpriteWidth;
        newScale.y = worldScreenHeight / worldSpriteHeight;

        // maybe keep the aspect ratio
        if (keepAspectRatio)
        {
           // get the aspect ratio of the sprite image.
           float aspectRatio = worldSpriteWidth / worldSpriteHeight;
           Debug.Log("Aspect Ratio: " + aspectRatio);
           float newy = newScale.x/aspectRatio;
           float newx = newScale.x;
           float wrldy = newy * local_sprite_size.y;
           // new height is bigger than screen height, switch aspect ratio calculation.
           if (wrldy > worldScreenHeight)
           {
                newy = newScale.y;
                newx = newScale.y * aspectRatio;
           }
           newScale.x = newx;
           newScale.y = newy;
        }
        // set the new local scale on display gameobject.
        m_display_image.transform.localScale = newScale;

        Debug.Log("Emulator display size (Image Pixels): x" + sprite_size.x + " y" + sprite_size.y+" (P->LS) x"+local_sprite_size.x+" y"+local_sprite_size.y);
        Debug.Log("Emulator display size (LS): x" + (newScale.x*local_sprite_size.x)+" y"+(newScale.y*local_sprite_size.y));
        Debug.Log("Real display size (WS): x" + worldScreenWidth + " y" + worldScreenHeight);
    }

    // switch the buffer images.
    protected void switchBuffers()
    {
        // apply all pixel changes to the draw texture.
        ApplyMarkedPixelChanges();

        // set the old stuff to the displayed image.
        m_display_renderer.sprite = m_buffer_sprites[m_actualBuffer];

        // set next buffer image.
        m_actualBuffer++;
        if (m_actualBuffer >= Buffer_Size)
            m_actualBuffer = 0;

        // set the new drawables.
        m_drawable_sprite = m_buffer_sprites[m_actualBuffer];
        m_drawable_texture = m_drawable_sprite.texture;

        m_cur_colors = m_drawable_texture.GetPixels32();

        clearDrawArray();
    }

    // create new textures for a new display size.
    public void setEmuScreenSize(int width, int height)
    {
        m_buffer_sprites = new Sprite[Buffer_Size];

        // create the buffer sprites.
        for (int i = 0; i < Buffer_Size; i++)
        {
            Debug.Log("Creating buffer image #" + i);
            Texture2D tex = new Texture2D(width, height);
            tex.filterMode = FilterMode.Point;
            //tex.Apply(false);
            Sprite spr = Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
            m_buffer_sprites[i] = spr;
        }

        m_display_renderer.sprite = m_buffer_sprites[0];

        // resize the displays.
        resized();

        // initialize the buffers, set the draw image etc.
        switchBuffers();
    }

    // set or unset the keep aspect ratio flag by code.
    void setKeepAspectRatio(bool setflag)
    {
        keepAspectRatio = setflag;
        resized();
    }

    // Update is called once per frame
    void Update()
    {
#if (UNITY_EDITOR)
        // maybe the keep aspect ratio flag has changed.
        // this will only happen in editor when you click the flag box.
        // else, setKeepAspectRatio(bool setflag) should be used.
        if(keepAspectRatio!=m_oldKeepAspectRatio)
        {
            m_oldKeepAspectRatio = keepAspectRatio;
            resized();
        }
#endif
      // Pixel Test
      //  MarkPixelToChange(10, 10, Color.green);
      //  MarkPixelToChange(11, 11, Color.yellow);
      //  MarkPixelToChange(15, 15, Color.yellow);
 
       switchBuffers();        
    }

    // DRAW FUNCTIONS
    public void MarkPixelToChange(int x, int y, Color color)
    {
        // Need to transform x and y coordinates to flat coordinates of array
        int array_pos = y * (int)m_drawable_sprite.rect.width + x;

        // Check if this is a valid position
        if (array_pos > m_cur_colors.Length || array_pos < 0)
            return;

        m_cur_colors[array_pos] = color;
    }
 
    public void MarkPixelToChangeByIndex(int index, Color color)
    {
        if(index>=0 && index<m_cur_colors.Length)
             m_cur_colors[index]=color; 
    } 
 
    // apply all pixel changes.
    public void ApplyMarkedPixelChanges()
    {
        if (!m_drawable_texture)
            return;

        m_drawable_texture.SetPixels32(m_cur_colors);
        m_drawable_texture.Apply();
    }

    // clear the drawing image.
    public void clearDrawArray()
    {
        for(int i=0;i < m_cur_colors.Length; i++)
        {
            m_cur_colors[i]=Reset_Colour;
        }
    }
}

Die Member-Variablen: 

  • sprite_size : Die Grösse des Emulator-Displays in Pixeln.
  • local_sprite_size: Die Grösse des Emulator-Displays in Units.
  • m_display_image: Das GameObjekt, in welchem der sichtbare Content dargestellt wird.
  • m_display_renderer: Die "SpriteRenderer"-Komponente von m_display_image. Damit muss man nicht immer wieder GetComponent aufrufen.
  • Buffer_Size: Wie viele Bilder werden gebuffert? Doublebuffer = 2, Triplebuffer = 3, etc.
  • m_buffer_sprites: Array mit der Anzahl an Buffer-Sprites welche in Buffer_Size angegeben ist.
  • m_actualBuffer: Der Index des aktuellen Buffer-Sprites in m_buffer_sprites.
  • keepAspectRatio: Das Bild wird auf den gesamten Bildschirm skaliert, wenn dieses Flag auf False ist. Ansonsten wird das Bildverhältnis (x/y) beibehalten und es gibt eventuell schwarze Balken am Rand. Dafür werden die Pixel korrekt dargestellt.
  • m_drawable_texture: Die aktuelle Textur, auf welcher gezeichnet wird.
  • m_drawable_sprite: Das zugehörige Sprite zu m_drawable_texture.
  • Reset_Colour: Die Hintergrundfarbe des Displays.
  • m_cur_colors: Array, auf welchem die Farben geändert werden können. Wird nach jedem Frame neu erstellt für den nächsten Buffer. Dieses Array wird später mit texture.setPixels(...) und texture.Apply() auf die Textur geladen. Wenn man das für jede Pixel-Änderung einzeln machen würde, wäre es wohl sehr langsam.

Die Funktionen:

  • Awake(): Diese Funktion wird VOR Start() aufgerufen und dient der Initialisierung "dieses" Objekts. Hier wird ein neues GameObjekt namens DISPLAY erstellt, welches die sichtbare Grafik darstellt. Dazu wird eine "SpriteRenderer"-Komponente an das "DISPLAY" angehängt und einige Werte eingestellt. Dann wird mit setEmuScreenSize(w, h) das Display initialisiert.
  • resized(): Wenn die Bildschirm- oder Displaygrösse geändert wird, sollte diese Funktion aufgerufen werden. Sie skaliert das Display auf die richtige Grösse.
  • switchBuffers(): Ruft ApplyMarkedPixelChanges() auf, setzt dann den neuen Buffer zum drauf zeichnen und zeigt den vorherigen (fertig gezeichneten) Buffer an.
  • setEmuScreenSize(width, height): Setze die Grösse des Emulator-Bildschirms in Pixeln. Danach wird resized() und switchBuffers() einmal aufgerufen.
  • setKeepAspectRatio(bool): Wenn dieses Flag gesetzt ist, wird das originale Seitenverhältnis beibehalten, ansonsten wird alles (x und y) auf den Bildschirm hochskaliert.
  • Update() : Update wird pro Frame ein mal aufgerufen. Home of switchBuffers() :)
  • MarkPixelToChange(x,y,color): "SetPixel" mit anderem Namen weil ApplyMarkedPixelChanges() aufgerufen werden muss, damit die Änderungen erkannt werden.
  • MarkPixelToChangeByIndex(index, color): Da das Emulator-Display normalerweise gleich gross ist wie das anzuzeigende Bild, kann man hier direkt mit dem Index arbeiten. Das verschnellert den Code ein bisschen.
  • ApplyMarkedPixelChanges(): Überträgt das mit MarkPixelToChange() modifizierte Color-Array auf die aktuelle Buffer-Textur.
  • clearDrawArray(): Setzt alle Pixel des Displays auf die angegebene Hintergrundfarbe (Reset_Colour).
Die Variablen zwischen #if(UNITY_EDITOR) und #endif werden im Spiel nicht gebraucht: Damit wird gecheckt, ob der Kasten im Editor angeklickt wurde.

Als nächstes wird ein Framework für die Emulatoren selbst erstellt:
DIY Emulator Teil 2.1: Plugin-Emulator (UNITY)

Das komplette Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Ich hoffe, das gefällt. :) 

Donnerstag, 20. Dezember 2018

Mein eigener Gameboy

Also ich halte mich dabei an den Gameboy Zero, ausser folgendem:

+ Ich werde wohl die Hülle selbst drucken/bauen.
+ Der Gameboy soll auch Musik spielen und verschiedene Hacking-Tools haben. :)

Das ist ein "Work-in-Progress" Artikel, also...

ACHTUNG: JEGLICHE Elektronik, welche ich bestellt und in Lego eingebaut habe, ist kaputt gegangen. (Die selbstgebaute Platine nicht.) Was soll mir das sagen? Scheiss auf Scheiss-Plastik, denk ich mal. Ich bau sonst alles in Holz ein, da ist mir noch nie was kaputt gegangen.....Also: Rechne mit heftigen Verlusten, wenn du Lego verwendest. :)

Dies ist die aktuellste Version mit 2" Screen. Hier fehlt nur noch eine Halterung für Raspi und Akku sowie die Kabel zu den Buttons. (Das Screenkabel ist gebrochen, darum gibts dann noch eine ganz neue Version später mit 3.5"-Screen.)
2" Version mit Bauanleitung weiter unten.

Erstmal habe ich mir folgendes bestellt:

Einen Raspi Zero W
Bestell dir am besten gleich das Minimal-Kit, denn mir fehlt nun zum Beispiel der MikroUSB-zu-USB-Adapter. Den HDMI-Adapter hab ich vom vorigen Kit (ohne WLAN).

Hier mit angelöteten Pins, TV-Kabel und SD-Karte.

Einen 2" NTSC/PAL-Screen (den soll man angeblich ganz einfach anhängen können)
(Ich wollte eigentlich einen 2.5"er aber den gabs nicht. 3.5" sind dann schon zu gross für einen originalen Gameboy.) (Neu: Lieber zu gross als zu klein, also bestellt mal schön den 3.5er, der 2er ist nämlich unleserlich klein.)

Etwa so unleserlich ist es auch in echt. :)
Und eine MicroSD Karte brauchts natürlich auch noch. Da mach ich keine Werbung für, weil man nur noch minimum 16GB bekommt. Ich will aber weniger, um dann ein "kleines" Image zu ziehen...egal. :)

Abgewinkelte Pins und Doppelpins (siehe erstes Foto)

Strom hab ich noch zum Testen. Da kommt dann später natürlich ein Akku dran.

Desweiteren noch eine/zwei Platinen für die Buttons und so kleine Pushbuttons zum draufstecken.
Davon mindestens sechs: Dpad = 4 und A+B (noch OHNE Start und Select)

Software, Computer-Hardware

Dann hab ich mir die Recalbox für den Zero gezogen und mit Etcher auf die microSD gebrannt.

(Vorher hab ich raspbian drauf gemacht, aber recalbox geht gleich out-of-the-box)

Das Ganze am (normalen) Bildschirm angesteckt, läuft.
Recalbox installiert sich selbst beim ersten Mal.

Den Bildschirm habe ich mit einem zweiten USB-Netzteil angeschlossen...leuchtet = läuft. :)

Lustigerweise hat die ADAFRUIT-Box, mit welcher der Bildschirm geliefert wurde, genau etwa die Masse, die mein Gameboy haben sollte. Ich werde also diese vorerst mal nehmen, bis ich sie dreiDdrucken kann. (Neu: Lego, siehe unten)


Das Controllerboard vom Bildschirm habe ich mit Klettverschluss(-Klebeband) an die hintere Seite vom Bildschirm angemacht. Es muss sowieso unter dem Screen sein, um Platz für die (Gameboy-)Buttons zu haben. (Nein, die Buttons sind nun seitlich angebracht. Trotzdem ist es so viel weniger fitzelig zum Basteln. :) )

(Tipp: Anstatt dran rumzureissen, um den Klettverschluss zu trennen, kann man vorsichtig mit einem Messer mitten durch "schneiden".)

Den Bildschirm einrichten:

Der Bildschirm braucht seinen eigenen Strom!

Mit dem Kabel auf dem ersten Foto verbinden. Die TV-Pins sind angeschrieben auf dem Zero.

Mit F4 und dann Alt+F2 in den Terminalscreen.
Login: root, recalrootbox
https://github.com/recalbox/recalbox-os/wiki/Connect-your-recalbox-to-a-CRT-with-composite-%28EN%29

mount -o remount,rw /boot

nano /boot/config.txt

hier eintragen:

sdtv_mode = 0 bis 3, siehe Link
hdmi_ignore_hotplug=1

Ctrl-O, Ctrl-X

nano recalbox.conf

Suche mit Ctrl-W: global.videomode

global.videomode=default

Ctrl-O, Ctrl-X

Die Hülle, die Buttons

Erst wollte ich eine "normale" Hülle aus Plastik, Metall oder Karton bauen. Doch dann hab ich mir gedacht, warum nicht aus LEGO?

Also habe ich mir zuerst eine Hülle für den Screen gebastelt. Dieser selbst ist viel zu klein, aber es geht jedenfalls schon mal. Und darunter konnte ich noch eine Aussparung für Akku und weiteres einbauen.

Die Teile stammen hauptsächlich von diesen zwei Lego-Sets:
LEGO 42064: Ocean Explorer
LEGO 42065: RC-Racer

Ich möchte das modular bauen, darum hab ich mich dann an die Buttons gemacht: Man kann einfach ein paar lange Technic-Stäbe aneinanderlegen und dann mit Kreuzstäben die Buttons so anordnen, wie man will. Da mein Screenhüllendesign eher an den Gameboy Advance erinnert, baute ich mir zwei kleinere Pads nach diesem Schema für an die Seite ran.

Verworfene erste Version. Pad 1 links wurde hier (dafür) gelötet...*

Dann habe ich bei einer Platine, welche ich glaubs damals auf Conrad bestellt habe, die Buttons, welche ich glaubs auch damals dort bestellt hatte, angeordnet. Die Platine habe ich halbiert, für jede Seite eine Hälfte.

Nach ein bisschen Gefitzel und Rumgebastel passen nicht nur die Buttons, sondern auch die Platinen "perfekt" in die Hülle und werden von unten angeklemmt. Ein bisschen anpassen und die Buttons funktionieren besser als gedacht.

Nun gucken, wo man auf der Platine am besten die Leitungen hinlegen könnte, um die Buttons dann mit Steckverbindern an den Raspi zu machen....ich benutze hier gekrümmte Stecker.

Der Raspi hat eigene Pull-up/Pull-down Widerstände, die brauchts also nicht.
Hierbei konnte ich drei Buttons seriell in einer Reihe an GND anschliessen und musste nur den letzten noch separat mit GND verbinden. GND bzw. 5v ist der Pin ganz links, die anderen gehen an die GPIO-Eingänge.

Schliesslich hat mir die Grösse von der Bildschirmhülle nicht gefallen, deshalb habe ich das Bildschirm-Modul noch mal neu gebaut. Darum sieht man auf den Fotos auch etwas anderes als in der Bauanleitung.

Vorläufig endgültige Version für 2" Screens.
*..und die Padhülle musste an Pad 1 passen.

(Nur) Das Bildschirmmodul von unten gesehen.

Das neue Modul hält den Bildschirm auch gleich an der Stelle, das ging beim alten noch nicht. Da hat er nur pro forma reingepasst.

Hierbei darauf achten, dass der Bildschirm selbst über den Kreuzstäben, das Controllerboard jedoch unter den Kreuzstäben ist.

ACHTUNG: Der Aufbau geht, aber das Bildschirmkabel ist gebrochen.

FALSCH: Kabel über dem Kreuzstab

Mach ja nicht den Fehler, das Bildschirmkabel über den "einen" Kreuzstab zu legen "zur Stabilisierung". Alles schön lose machen und dann einfach überdecken.


RICHTIG: Bildschirmkabel unter/hinter dem Kreuzstab.
Hier sichtbar die untere Seite, einfach wie oben überdecken.


...darum musst du dir unten selbst einen Batterie- /Raspi-Kasten ausdenken.
Ich bestelle mir gleich einen 3.5" Bildschirm...

Die Pads mussten natürlich auch angepasst werden. Da ich für das eine Pad schon eine Platine gebaut hatte (siehe oben), wollte ich sie von der Form her beibehalten.

Die Anleitung gibts hier:
[TODO: Link zu Anleitung Pads und Centerteil]

Du kannst die Anleitung oben benutzen, wenn du den Adafruit 910 2" Screen bestellst.

Sie ist aber ziemlich verworren. Das hat der LDD (Lego Digital Designer) so gemacht, nicht ich...Darum bei den Pads: Erst die roten an die weissen Teile, dann die grünen und dann die Buttons so anordnen wie du willst. ;)

Die Pads sind jeweils seitenverkehrt genau gleich aufgebaut. Obwohl es unstabil aussieht, sitzen die Dinger "bombenfest".

Ich bin selber überrascht von der kuuuhlen Bauweise. :)

Uuuund das Kabel vom Bildschirm ist gebrochen....DA MUSS MAN EXTREMST AUFPASSEN.

Uund der Raspi ist auch am Arrrrrsch. Hier wird ganz sicher nicht "politisch korrekt" nur "A..." geschrieben, da könnt ihr aber Gift drauf nehmen.

Darum ist dieses Projekt vorerst mal eingestellt, vielleicht lade ich dann noch mal die Bauanleitung hoch. Ich hab ehrlich gesagt grad keine Lust mehr.

Weiteres folgt...

Montag, 26. November 2018

Fernsofagestell

Ein Multifunktionsmöbel. Ich wollte den Fernseher nicht immer sehen, wenn er aus ist. Darum kann man ihn nun im Gestell herunter lassen, so dass er vom Palett/Sofa verdeckt wird.

Neueste Version 2020: Ich habe das Palett rausgeschmissen, das war hässlich und zu breit und umständlich zum Bewegen (das Gestell stand im Palett drin, damit alles ein bisschen besser hält).

Nun habe ich einfach noch drei Bretter/Böden des selben Regalsystems vor die ganze Sache geschraubt. Dazwischen würde noch ein Plexiglas mit Spiegelfolie passen. Foto kommt, wenn ich eins hab. :) Hier die Fotos der Endversion, unten die Fotos des Originalaufbaus.


Und nun gehts los, also...erstmal die Originalfotos:




Teile

2 bis 3 Europallet: Neu: Schraube einfach 3 Böden von einem der Regale an die Vorderseite. Davor kannst du ein normales Sofa oder etwas anderes stellen...
1 Sofakissen in Palettgrösse: Neu: siehe oben.
2 LANDI Regale aus Holz für je c.a. 20.-, hier kann man evt. auch diese aus dem Hornbach nehmen.
1 Handwinde vom Hornbach und 3 M10 Schrauben und Muttern dazu (nicht dabei).
2mm Stahlseil
1 Scharnier in der Breite eines Brettchens der Regalbretter.
Holzschrauben (siehe Regale, solche. Und noch welche für das Scharnier)
ein paar Rollen und anderes Stahlseilbefestigungszeug (Todo)
1 (LG XYZ) Fernseher mit der Breite 75cm oder so, glaubs 82cm Diagonale..muss einfach reinpassen.
1 Backblech (hab ich gebraucht) für die Aufhängung am Fernseher.

Aufbau

--> Beim Backblech ein paar Löcher machen, damit es an die Löcher des Fernsehers passt, und anschrauben. Dabei darauf achten, dass die Anschlüsse frei bleiben.
     --> Zwei Karabiner dran machen.

Hier habe ich einfach in ein Blatt Papier die Löcher rein gedrückt und dieses dann auf das Backblech geklebt. Somit hatte ich die Löcher auf jeden Fall an der richtigen Stelle.



--> Ein Regal wird aufgebaut, und zwar mit dem obersten, zweitobersten und zweituntersten Boden
  Der Fernseher wird dann an die vorderen Beine gelehnt.


--> Der übrige Boden dieses Regals wird auseinander genommen.
--> Dann hat man vier lange Brettchen übrig (und zwei Kurze):

   Eines davon kommt seitlich unter den zweitobersten Boden an das vordere Bein. Ein Teil eines zweiten auch noch, damit die ganze Höhe ausgefüllt ist bis zum unteren Boden.



ACHTUNG: Das ist V1, mach die hintere Leiste NICHT dran, stattdessen oben noch ein Teil an die Seite!

Das dritte davon kommt auf der anderen Seite auf dieselbe Weise hin. ABER: Du musst ein Stück abschneiden und mit dem Scharnier wieder dran machen, damit du später den Fernseher rein- und rausnehmen kannst.  Dieses ist nur zum ein bisschen den Fernseher verschieben, um ihn schräg stellen zu können. Hier ist die hintere Leiste relativ egal, da oben eh nur ein Boden hinkommt. Da kannst du selbst entscheiden, was du nehmen willst (siehe unten).

Das ganze verschrauben...

Beim Teil mit dem Scharnier ein Loch von der Seite reinmachen und einen Nagel reinstecken oder so als Blockierung.

Das zweite Regal wird partiell auseinander genommen:
Ein Bein muss an das eine Brett ohne Scharnier, so dass man (wieder) kürzere Böden hinten rein machen kann. Dazu muss ein Stück oben und unten abgeschnitten werden, damit es auf den untersten Boden des ersten Regals gestellt werden kann und bis zum zweitobersten Boden kommt, und mit dem vorderen Bein und den Brettchen dazwischen eine Seitenlaufleiste für den Fernseher bildet.



Dieses Bein noch nicht verschrauben: Erst kommen die Böden an dieses Teil, dann das ganze Gestell in das erste Regal.

Zwei Beine des neuen Regals werden gleich neben den Anschlüssen des Fernsehers an das erste Regal gemacht. Das eine ist dann zwischen den ersten zwei langen Brettern der Böden von vorne gesehen und stützt den Fernseher (noch weiter) von innen. Das andere ist hinten aussen auf derselben Breite.

Nur mal angestellt...

Nun werden bei drei Böden des neuen Regals jeweils eines der langen Bretter weggeschnitten, so dass die Böden weniger breit sind und hinter den Fernseher passen.

Dann werden zwei dieser Böden noch seitlich gekürzt, und das kürzere (Haltungs-)Brettchen wieder an das Ende geschraubt, auf Breite der zwei neuen Beine, so dass die Anschlüsse des Fernsehers nicht an die Regale rankommen.

Der oberste Boden muss nicht gekürzt werden, da die Anschlüsse darunter sind, doch man kann durchaus noch ein drittes kurzes Brett ranmachen an der Position der zwei neuen Beine.



Erst das gekürzte Bein an der RICHTIGEN Stelle anmachen, dann das Ganze in das erste Regal einbauen.
Siehst du das fehlende Bein...nicht?
Das letzte Bein kann man ganz am Schluss dran machen...
Fernseher reinstellen....uuuund....

Aufhängung

Erst mal wird bei der Handwinde das fette Stahlseil heraus genommen. Dieses ist viel zu dick. Das kannst du ins Auto legen und zum Abschleppen benutzen. :) (Nein!)

Das 2mm Stahlseil wird erst am Fernseher an einen Karabiner angemacht. (Hier schon komplett)



Dann wird es gerade nach oben gezogen, dort über eine Rolle, wieder gerade herunter, über eine Rolle am Karabiner auf die andere Seite.





Die obere Rolle habe ich einfach mit einer Stahlschraube auf das oberste Brett "gelegt".

(Das Bild ist von einer anderen, schlechteren Konfiguration)


Nun wieder über eine Rolle am (anderen) Karabiner hoch und dann direkt an die Winde angemacht.




Die Winde kann einfach auf das zweitoberste Brett gelegt werden. Sie muss nicht mal angeschraubt werden zum Testen. Mit der einen Hand muss man einfach die Bremse (so ein Knubel) bedienen UND gleichzeitig noch die Winde halten und ausrichten. Das Kurbeln selber gibt fast keinen Widerstand. Die Kurbel müsste sogar noch gekürzt werden, damit es weniger anstrengend ist.

Nun drehen wir den Fernseher mal hoch und stellen die Bremse. Unter dem Fernseher habe ich noch zwei Löcher gemacht und Stahlstifte rein gesteckt. Im hinteren Brett habe ich das Loch nicht ganz durch gemacht, so dass es den Stift hält und blockiert.

Die Stahlstifte habe ich vom Schlitten eines alten DVD-Gerätes. Die sind nur 3mm dick, recht lang und sehr stabil.






Also: Stifte raus, hochkurbeln, Stifte rein, Kurbel entspannen. Umgekehrt: Stifte raus, runterkurbeln, (Stifte rein) :)

 Vordergrund-Deko :)

Neu:

Siehe oben, schraube einfach drei Böden/Bretter desselben Regalsystems vorne dran. Darauf achten, dass das Loch vom Fernseh-Reinstell-Brettchen nicht blockiert wird. (Da hatte ich Glück, hui.) Das sieht dann besser aus und ist auch einfacher zum Verschieben und macht die ganze Sache auch ein bisschen stabiler.

Zwischen Fernseher und Bretter (also "hinten") kannst du auch ein Plexiglass-Brett mit Fenster-Spiegelfolie reinschieben. Wenn der Fernseher nicht läuft, dann spiegelt das einfach, ansonsten sieht man den Fernseher hinten durch leuchten. Leider hab ich gerade kein Plexi überig.

Alt:

Nun kann man bei einem Europalett zwei der hinteren Bretter wegnehmen und die zwei mittleren Klötze dazu auch. Das komplette Regal kann nun zwischen die untersten Palettbretter gestellt werden. Der Ferseher sollte nun hinter dem Palett sein und nur die Regale sichtbar...wenn der Fernseher unten ist.

Die anderen Paletts davor legen und das Sofakissen drauf.

Comics rein. (Leider passen nichtmal die LTBs von der Höhe her rein.)

Viel Spass, mir hats jedenfalls Spass gemacht.

;) Ohne Software :)

(: Wayne intressierts? :)

Montag, 27. November 2017

Netzkaffeekasse


Es gibt ja diese Sparschweine mit eingebautem Münzzähler. Mein Ziel war es, so ein Ding im Internet verfügbar zu haben. Und es soll Fotos machen von den edlen Spendern und räuberischen Dieben. :)

Dieser Artikel hat drei grössere Kapitel:
1. Die Hardware.
2. Installation der Software
3. Funktionsweise der Software.
Übersichtlicherweise sollte man erst den RasPi vorbereiten (2.) bevor alles zusammengebaut wird.
Zur Info. Also wenn dich nur das Proggen interessiert, gleich nach zu unterst scrollen. ;)

Was braucht man:

1x Sparschwein mit elektronischem Münzzähler
1x Raspberry Pi v3 (mit WLAN) mit Raspian oder so + Strom.
1x Flachband-Kabel für den Pi (Flachstecker zu Female)
1x RasPi Kamera mit Kabel
1x Platine (oder Karton ;) ) und ein paar Pins und Widerstände (hier: 600 Ohm) zum drauf löten.
1x schatzige Truhe, wenn möglich Schatztruhe. :)
ein paar Kabel und Lüsterklemmen.

Lötkram und Dremel wären auch von Vorteil.

Zur Konfiguration brauchst du natürlich noch einen Bildschirm und eine Tastatur.

Die Software von mir findet sich auf diesem Repository:
https://github.com/ben0bi/Netzkaffeekasse

Die Installation wird weiter unten erklärt.

Die Hardware

Bitte richte dir dann erst den RasPi ein, bevor du alles zusammen baust. ;)
(Siehe unten.)

Ich habe mir also mehrere Sparschweine mit elektronischem Münzzähler im Interdiscount gekauft, um dies zu realisieren. (Du brauchst nur eines.) Also etwas in der Art hier, mit Schweizer-Zählwerk:

Dieses habe ich komplett auseinander genommen. Die Elektronik-Platine kannst du wegschmeissen, die brauchen wir nicht. Wir brauchen nur den Zähler selbst. Dabei kam raus, dass der "Zähler" nur eine kleine Platte mit Kontakten ist, auf welcher eine kleine Stange bewegt wird um den Kontakt zu geben. Die Stange wird von der Münze zur Seite gedrückt. Diese Platine wurde über ein Flachbandkabel mit der Elektronik-Platine verbunden. Also einfach das Flachband von der Elektronik-Platine ab- und an die eigene dranlöten. Total einfach, analog und ohne Probleme zu "verbasteln".

Den Münzschlitz oben habe ich aus dem Plastikdeckel herausgedremelt und in die Kiste eingebaut. Das Zählwerk selbst kann man nun einfach darunter anschrauben.

Ich habe das Flachband zu den Kontakten von der Elektronik weg- und an meine eigene Platine gelötet. Dort kommen die Kontakte (von der einen Seite) als Stift heraus und werden noch mit einem Widerstand jeweils zu GND geleitet. Die andere Seite wird an 3.3v gelegt und braucht kein GND. Bei mir hat die Strom-Seite drei (3) Kontakte auf dem Flachband. Desweiteren kommen noch vier weitere Kontakte dazu (jeweils zwei zusammen), welche den Türöffnungsmechanismus überwachen. Auch diese brauchen wir als Stift, und auch diese müssen jeweils noch mit einen Widerstand mit GND verbunden werden. Mehr braucht es auf "unserer" Platine jedoch nicht.

Nun wird das ganze per Flachband an ein Raspberry Pi (RasPi) Version 3 angeschlossen. Hierbei bitte darauf achten, dass 3.3v und GND richtig belegt sind. Die anderen (GPIO-Inputs) kann man dann in der Software anpassen.

Zusammengesetzt sieht die Elektronik dann etwa so aus:

Ich bin gerade zu faul, das noch mal auseinander zu nehmen. Da werden wirklich nur die Leitungen an das RasPi weiter gegeben, mehr ist da nicht ausser den Widerständen für GND jeweils.

Die grünen und roten Kabel kommen vom Türkontrollmechanismus. Dieser besteht einfach aus Kontakten, welche Strom führen, wenn die Tür zu ist.

Ein RasPi V3 brauchen wir, damit wir über WLAN Zugriff auf das (im Inneren der Kasse versteckte) Computerchen zu bekommen, ohne dass jemand einfach das Kabel durchschneiden könnte. Später wird eventuell noch eine Notfallbatterie eingebaut, damit man auch nicht einfach den Strom ausstecken kann.

Schliesslich habe ich den Münzzähler angeschraubt:

...und die Platine daneben angeklebt. Den Raspi habe ich mit einer Schraube befestigt:

Hier musste ich darauf achten, dass der Kamera-Port möglichst nah am "Türschlitz" ist. Schliesslich konnte ich einfach das Kamerakabel dazwischen durch schieben:


Zu der Kamera gab es noch eine Halterung. Diese kann man mit einem Cinch-Stecker verbinden.
Dazu habe ich aus einer beliebigen Stereoanlage alles ausgebaut, bis ich die Cinch-Stecker herausdremeln konnte:

Cinch-Stecker unten rechts...

...einfach so richtig brutal heraus gedremelt.
Achtung, das hat ziemlich gestunken und könnte giftig sein. Ich war im Keller und musste ein bisschen raus danach...

Hier solltest du einfach darauf achten, dass du das Ding auch irgendwie befestigen kannst. Bei mir hat es zwei Löcher für die Schrauben.

Schliesslich habe ich das Loch in die Truhe gedremelt. Es ist ein ganz klein bisschen breiter als gewünscht, damit das Stromkabel (und evt. HDMI) noch durch passt. Die Kabel muss man vor der Installation durchschieben, danach geht es nicht mehr.


Nachdem die Kamera befestigt wurde, ist die Hardware soweit fertig.



Die Software

Auf dem Pi brauchen wir nun folgendes:

0. Python (Version 3) und die RPi.GPIO und picamera-Bibliotheken.
Für RPi.GPIO gibt es folgende Befehle:
sudo apt-get install python{,3}-pip
sudo pip3 install RPi.GPIO


und für picamera:
sudo apt-get install python3-picamera

(Ich hoffe das ist richtig, ansonsten steht es noch im Sourcecode.)

1. Den Apache Webserver mit PHP5
sudo apt-get install apache2 php5

1.1 Das Web-Verzeichnis sollte /var/www/html sein oder in der Software unten
umgestellt werden.

2. Git brauchen wir, um die Software herunter zu laden:
sudo apt-get install git

3.  Screen brauchen wir, damit das Ganze im Hintergrund laufen kann:
sudo apt-get install screen

4. In /home/pi/Documents laden wir die Sofware herunter:
git clone https://github.com/ben0bi/Netzkaffeekasse.git

5. Nun wird die Software installiert:
cd /home/pi/Documents/Netzkaffeekasse
sudo ./install.sh

Damit werden eigentlich nur ein paar Dateien an die richtige Stelle kopiert.

Falls du Probleme mit den Berechtigungen der Dateien/Ordner hast, kannst du folgendes tun:
sudo chmod 777 dateiname gibt dateiname alle Rechte.
sudo chmod +x dateiname macht dateiname ausführbar.

6. Schliesslich stellen wir ein, dass das Ganze beim booten gestartet wird:
sudo nano /etc/rc.local

Und dort drin dann:

cd /home/pi/Documents/Netzkaffeekasse
sudo ./startbackground.sh


Diese Datei nun mit Ctrl-O speichern und mit Ctrl-X wieder in die Konsole zurück gehen. In der Software (RasPython/kassenkontroller.py) solltest du nun noch überprüfen, ob alle Pins richtig gelegt wurden und eventuell die Nummern anpassen bei den fX, rX und kontrollerX-Variablen.

frGND ist nicht GND sondern der Kontakt welcher im Ruhezustand gegeben wird (also wenn keine Münze im Schlitz ist)!

7. Nun müsstest du noch mit der Raspi-Config das WLAN einstellen.
sudo raspi-config
..und dort dann im Menü herumwuseln.

Mit ifconfig bekommst du die IP-Adresse deines RasPi heraus. Bitte stelle diese fest ein, denn mit dynamischer IP müsstest du jedesmal im Router nachgucken, da ja schliesslich am Ende kein Bildschirm mehr direkt am RasPi hängt*.

Nun sollte es laufen. Über die IP-Adresse sollte man nun eine Webseite sehen, welche das Total, Log und neueste Foto ausgibt.

*Autoboot mit Bildschirm


Um den Leuten auch ein bisschen Zucker zu geben, habe ich auch einen Bildschirm angehängt.

Damit der RasPi in den Desktop bootet, muss man in der raspi-config die Bootoption einstellen auf:
Boot into desktop with autologin.

Wenn du RetroPie benutzt, musst du das über retropie-setup machen, da er sonst zwar in den Desktop bootet, danach aber gleich die Emulationstation startet.

Hier findest du Informationen dazu, wie man Chromium direkt starten kann. Für die Netzkaffeekasse muss er natürlich auf localhost zeigen:

Damit der Bildschirm nicht nach einer Weile aus geht, habe ich xscreensaver installiert und dort dann alles ausgeschaltet:
sudo apt-get install xscreensaver

Damit sollte nun wirklich alles wie auf dem Foto oben funktionieren.

Funktionsweise der Software

Mit install.sh werden die start.sh, startbackground.sh und stop.sh ins Root-Verzeichnis (der Software) kopiert sowie die Ordner ALERT_IMAGES und EINWURF_IMAGES erstellt. Die Dateien von RasPython/html werden nach /var/www/html kopiert und dort noch der Ordner IMAGES erstellt.

stop.sh ist nur für den Zugriff auf den Screen, welcher mit startbackground.sh erstellt wird, konzipiert.
Beenden kann man die Software jeweils mit Ctrl-C.

Der Hauptbestandteil ist natürlich das Python-Script RasPython/kassenkontroller.py. Dieses macht ein Foto bei jedem Einwurf einer Münze sowie wenn man den Deckel öffnet. Dazu werden auch noch verschiedene *.nkk-Dateien angelegt.

TOTAL.nkk: Hier ist das aktuelle Total der Münzen in der Truhe angegeben.
LOG.nkk: Hier wird fein säuberlich jede Aktion hineingeschrieben.
ACTUALTEXT.nkk: Das ist der Text, der beim aktuellen Bild auf der Webseite angezeigt wird.
ACTUALTIMESTAMP.nkk: Dieser Zeitstempel wird benutzt, um das Bild auf der Webseite vom Server neu zu laden (anstatt vom Cache).

Jedes Bild wird entweder im Ordner EINWURF_IMAGES oder im Ordner ALERT_IMAGES abgelegt. Das jeweils neueste Bild wird auch noch in das /var/www/html/IMAGES-Verzeichnis kopiert als "latest.jpg" und dort von der Webseite geladen.

Das Script ist auf Schweizer Münzen ausgelegt, das heisst, die 50-Rappen Münze ist kleiner als die 10-Rappen Münze. Deshalb wird diese als 0.07 angegeben und später (weiter oben) wieder in 0.5 umgewandelt. Hier gilt nämlich, dass jeweils die grössere Nummer genommen wird wenn mehrere Kontakte gegeben werden.

Hier ist als Beispiel der Ablauf der Kontakte auf der Platine, wenn man ein 20 Rappen Stück einwirft:

GND -> 0.05 -> 0.07 -> 0.1 -> 0.2 -> 0.1 -> 0.07 -> 0.05 -> GND: Ah, es ist ein 20Rappen Stück.

Bei 0.07 wäre es ein 50Rappen Stück. Alle anderen Werte kann man direkt übernehmen.

Ich hoffe dieses Projekt hat dir gefallen und danke für die Aufmerksamkeit.