Skip to content

Dokumentacja i opis projektu

Jakub Mendyk edited this page Jan 15, 2018 · 1 revision

Dokumentacja programu ‘Logicon’ do symulowania układów logicznych.

Spis treści:
Ogólne założenia
  uwagi
Biblioteki
Hierarchia
Wygląd GUI
Mechanika
  Ogólne
    App
    Engine
    FileController (wcześniej CircuitParser)
    Data
    Circuit
  Grafika
    Canvas
    GCircuit
    GBlock
    GPort
  Bramki
    Gate
    And, Or, Xor, Not ...
    Clock
    Delay
    Input
    Switch
Własne bloki

Ogólne założenia: TOP

Program reprezentuje graficznie układy logiczne złożone z prostych bramek logicznych, guzików, włączników i zegarów. Program umożliwia tworzenie zapisywanie i wczytywanie układów z plików w formacie CSV i JSON, dodawanie własnych bloków, interakcję z układem, symulowanie ciągłe z ustawionym przez użytkownika taktowaniem oraz symulowanie krok po kroku. Dodatkowo program może monitorować stan elementów i wypisywać je do plików typu '.log'. Wszystkie pliki należą do namespace Logicon.

uwagi: TOP

Program musi korzystać z pliku config, który bedzie miał zapisany wygląd podstawowych bramek w formacie, który da się przetłumaczyć na klasę typu Data. Domyslny układ współrzędnych jest taki, że x zwiększa się w prawo a y w dół. od lewego górnego rogu elementu chyba że napisane inaczej. port: Jeśli będę mówił gdzieś o portach, to chodzi o inputy i outputy, z albo bez rozróżnienia, to już wynika z kontekstu. Najczęściej chodzi o to, że trzeba wygenerować metody dla obu i sprawdzać czy chodzi nam o ten port etc. Czasami wartości zwracane przez funkcje zamiast obiektami będą wskaźnikami na obiekty. W takich wypadkach należy używać smart pointerów (weak_ptr, shared_ptr, uniqe_ptr) - operatory działają jak na zwykłych wskaźnikach ale są ciut "lepsze" ID: jeszcze nie sprecyzowaliśmy jak reprezentujemy ID, póki co stosuję liczby > 0 jako ID Point: dla uproszczenia będę pisał point, ale może być jako para x,y lub nawet jako osobne argumenty czyli f(Point) = f(x,y) Data: prawdopodobnie będzie można zaimplementować później, ale nie jest wykluczone, że będzie potrzebne do działania zegara - wszędzie gdzie jest pole typu Data musi być funkcja getData() żeby można było wykonywać operacje na danych []: nie oznacza konkretnie tablicy a jakikolwiek kontener - zalezy od miejsca - czasem może to być lista, czasem vector etc. tuple : to inaczej krotki - żeby nie używać par<par> etc. - krotki są fajne ;)

Biblioteki: TOP

  • OpenGL - renderowanie okna i baza dla ImGUI
  • glew, gl3w - dodatki do OpenGL ułatwiające pracę z grafiką
  • ImGui - główne GUI aplikacji
  • json - obsługa formatu JSON

Hierarchia: TOP

ogólne

  • App - główna klasa aplikacji;
  • Engine - silnik prowadzący symulację
  • FileController (wcześniej CircuitParser) - klasa zajmująca się interakcją aplikacja <-> pliki
  • Data - klasa zajmująca się wewnętrznymi danymi bramek, które można zmieniać
  • Circuit - klasa reprezentująca układ logiczny - reprezentacja grafu
  • types.h - header zawierający wszystkie aliasy typów w projektu

grafika

  • Canvas - plansza do graficznej edycji układu
  • MenuWidget - widget ImGui do zarządzania plikami i symulacją
  • BlocksWidget - widget ImGui do dodawania bloków z palety
  • FooterWidget - widget ImGui do wyświetlania informacji
  • ContextMenuWidget - widget ImGui odpalający menu kontekstowe dla elementu
  • GCircuit - graficzna reprezentacja układu
  • GBlock - graficzna reprezentacja bloków
  • GPoert

bramki

  • Gate - klasa abstrakcyjna reprezentująca bramkę, po której dziedziczą wszystkie pozostałe
  • ^ And, Or, Not, Xor etc. - proste bramki z prostą logiką liczenia kolejnego stanu
  • ^ Clock - klasa zegara który włącza się i wyłącza cyklicznie w zależności od czasu
  • ^ Delay - puszcza sygnał z zadanym opóźnineniem; konfigurowalny jak Clock
  • ^ Input - reaguje na kliknięcie; zmienia stan wyjścia przy kliknięciu (taki toggle button)
  • ^ Switch - reaguje na kliknięcie; jeśli jest jest w stanie ON przekazuje wejście na wyjście (przełącznik)

Wygląd GUI TOP

MenuWidget: [nowy, otwórz, zapisz] [start, pauza, krok, restart] [pole liczbowe: tickrate] BlocksWidget: przewijana lista klocków reprezentujących bramki logiczne FooterWidget: stópka z tekstem informacyjnym w prawym dolnym rogu - nazwa układu, ilość jakich komponentów etc. Canvas: przewijana (może nieskończona) plansza 2D z siatką o odstępach 32x32 px na której wyświetlany jest model układu

Mechanika TOP

Ogólne TOP

App TOP

App tworzy okienko z OpenGLa, ma singletony(chyba) widgetów, planszy i silnika. Słóży jako kontener na właściwą aplikację i GUI. Do tego musi generować kolejne ID bloków za pomocą metody nextID().

// psudokod klasy App
class App{
    Clock clock;
    Engine engine;
    MenuWidget menu;
    BlocksWidget blockMenu;
    FooterWidget footer;
    Canvas canvas;
    GCircuit gCircuit;
    Time tickrate //  w milisekundach czas co ile ma odpalić się engine.calcLogic()
    STATE state  = {UNINITIALIZED, RUNNING, PAUSED, STEP_BY_STEP} // w zależności od stanu 
    bool init();
    void render_ui();
    bool close();
    void run(){
        init();
        while(){
            render_ui();
            // UI events
            if(tickrate /**/ && STATE){
                engine.calcLogic(gCircuit.circuit);
                // redraw GCircuit
            }
        }
        close();
    }
    ID nextID(); // zwraca następne dostępne ID
}

Engine TOP

Engine liczy stan układu (może przekazać mu Circuit do policzenia w argumencie calculateLogic(c)?). Silnik musi wspierać funkcję do zainicjalizowania układu od nowa oraz funkcję do policzenia następnego stanu. To w enginie siedzi algorytm do liczenia następnych stanów grafu reprezentowanego przez circuit. Trzeba też zadbać o poprawne liczenie i inicjalizowanie układu z cyklami (np. NOT łączący się z samym sobą). Bramki, których wejść nie da się bezpośrednio zainicjalizować mają mieć domyślnie na tych wejściach podane 0 (iinymi słowy, jeśli jest ciąg bramek, które w jakiś sposób się zapętlają i bramka, która jako jedno z wejść przyjmuje wartość zależną od obliczeń tej samej bramki, to to wejście ma być interpretowane jako 0 np. bramka AND o wejściach A i B która wypluwa wynik na C, ale z C sygnał przechodzi przez inne bramki i wraca do wejścia A dodatkowo będąc niejednoznaczny(nie jesteśmy w stanie stwierdzić czy zawsze jest prawdziwy czy fałszywy), to to wejście traktujemy jako 0)

class Engine{
    void restart(Circuit c); // inicjalizuje układ na stan początkowy (faktyczny stan układu w zerowym ticku)
    void calcLogic(Circuit c); // liczy następny stan układu - tutaj siedzi algorytm
}

FileController (wcześniej CircuitParser) TOP

FileController musi być w stanie zwrócić nowy obiekt typu GCircuit oraz Circuit z pliku w formacie CSV lub JSON i vice versa. Parser do CSV trzeba napisać samemu, natomiast do JSON-a można skorzystać z gotowej biblioteki. Trzeba też sprawdzać poprawność danych, żeby przypadkiem czegoś nie spieprzyć i pamiętać żeby przy odpalonych kilku instancjach programu nie zepsuć zapisywania do pliku.

Data TOP

Data ma być klasą reprezentującą dane - inaczej jest to zbiór zmiennych o różnych typach które mają swoje odpowiednie wartości. Dane mogłyby być np. w formacie JSON i odczytywanie i zapisywanie wartości odbywało by się przez znajdowanie danych pól i zmienianiu im wartości. Możemy całe Data zrobić jako opakowanie dla klasy od JSONa i dopisać jedynie klasy, które produkują odpowiednie klasy na podstawie plików JSON. Powinna mieć możliwość ustawiania i pobierania danych - coś typu get/setData("klucz", "opcja@wartość") Na pewno potrzebne są w Data - pola do pamiętania: obrazków, labeli, opisu, tablic napisów, wartości liczbowych i nazw zmiennych.

{
    // ...

    "gates": [
        {
            "name": "and",
            "description": "and gate",
            "icon": "graphics/icons/and.png",
            "inputs": [
                {
                    "label": "A",
                    "side":"EAST",
                    "coord": [0, 0]
                }
            ],
            "extras": {
                "Hz": 0.5
            }
        },
        // pozostałe bramki
    ]
}

Bardzo ułatwiłaby pracę z opcjami dla układów, bo sprawdzalibyśmy jedyni czy bramka.data jest puste i jeśli nie jest, to odpalali okienko z możliwością zmiany tych danych

Circuit TOP

Circuit reprezentuje układ bramek - jest to nasza reprezentacja "grafu", którego wierzchołkami są poszczególne bramki. Musi zawierać swój unikalny identyfikator ID albo CircuitID listę wszystkich bramek, które wchodzą w jego skład, pole data przechowujące wszystkie potrzebne dane, metody connect(ID1, output, ID2, input). disconnect(ID1, input, ID2, output), add(Gate), remove(ID) do zmieniania struktury grafu, find(ID)(wcześniejsze getBlockByID(ID)), getGates() do zwracania bramek które wchodzą w jego skład.

class Circuit{
    int ID;
    Gate gates[];
    Data data;
    
    void connect(ID1, output, ID2, input);
    void disconnect(ID1, output, ID2, input);
    void add(Gate g); // dodaje bramkę do grafu, łączyć trzeba samemu
    void remove(ID); // usuwa z grafu bramkę i usuwa wszystkie połączenia
    Gate find(ID); // zwraca bramkę o danym ID
    Gate[] getGates(); // zwraca całą listę bramek
}

Grafika TOP

Canvas TOP

Widget z Imgui w którym wyświetlamy nasz układ. Musi renderować pomocniczą siatkę, mieć scrolbary i ewentualnie jakieś inne bajery do zmieniania widoku. Domyślny układ współrzędnych, tyle że koordynaty (0,0) powinny być wyświetlane na środku. Canvas wyświetla tylko jeden GCircuit (w pszyszłości można zmienić).

GCircuit TOP

GCircuit (singleton?) jest graficzną reprezentacją układu na planszy - ma określone wymiary, referencję do Circuit oraz listę GBlocków, do tego zajmuje się wyłapywaniem eventów związanych z wstawianiem bloczka i usuwa blok o podanym ID zarówno z GCircuita jak i z Circuita. Jest podzielony na macierz z kwadracików o wymiarach siatki czyli 32x32 px (czy jakoś tak żeby pokrywało się z siatką). Dodatkowo musi umieć zwracać ID GBlocka w danym punkcie. Chyba może mieć w sobie funkcje do łączenia i rozłączania dwóch portów (zamiast umieszczać je w GInput i GOutput).

GCircuit{
    int width, height; // ile kwadratów 32x32 zajmuje
    Circuit circuit; // referencja do circuita który reprezentuje
    tuple<GBlock, Point> blocks[]; // lista tupli graficznych bloków z ich pozycjami
    
    void insert(x, y, Block); // wkleja blok w (x,y)
    void remove(ID); // usuwa blok o danym ID
    void move(ID, Point destination) // zmienia pozycję bloku o ID na `destination`
    void connenct(...) // łączy port `a` z `b` - wywołuje circuit.connect()
    void disconnect(...) // rozłącza wszystkie połączenia z portem - wywołuje curcuit.disconnect()
    bool isOccupied(ID, Point a, Point d); // sprawdza czy w prostokącie wyznaczonym przez rogi `a` i `d` 
                                           // istnieje element inny niż ten o danym ID
    ID getIdAt(x, y); // zwraca ID bloku po kliknięciu w (x,y)
    GBlock getGBlockByID(ID); // zwraca GBlock o ID - potrzebne przy szukaniu klikniętych portów
    <ID, port> getElementAt(Point); // zwraca parę ID, port - jeśli któryś element nie został najechany,
                                                        // wartość ma być NULL
    private? void update(); // ściąga stan z c i aktualizuje każdy element (prywatne i wywołane lokalnie w render()?)
    void render(); // rysuje układ - rysuje bloki w ich współrzędnych a następnie kable jako krzywe Beziera
}

GBlock TOP

GBlock powinien być zaimplementowany jako ImGui::ImageButton. Służy reprezentacji graficznej bloku w GCircuit. Ma pola określające ID(albo referencję do Block?), wymiary bloku, listę gInputs[], gOutputs[], img oraz data (Potem będzie można usunąć wszystkie informacje poza ID i indeksem z GInputów i GOutputów i pamiętać wszystkie te informacje w polu data).

  • Jesli usuwamy blok za pomocą menu kontekstowego, to musi wspierać akcje PPM do wyświetlania menu z opcjami (Edit|Delete) (oczywiście kiedy edit jest nie dozwolone to tego nie wyswietla)
  • kliknięcie LPM do zmiany stanu guzika (triggeruje block.clickAction())
  • przeciągnięcia do poruszenia guzika(można też hamsko usuwać element za pomocą skrótów klawiszowych.

Musi mieć referencję do GCircuita, żeby móc korzystać z metod move(destination) przy przeniesieniu, i remove(this.ID) przy usuwaniu. Dodatkowo musi mieć funkcje getPortAt(Point p)zwracającą indeks portu w danym punkcie (zwraca np. -1 jak to nie jest dobry port albo w tym miejscu nie istnieje, albo jest poza blokiem itd.). Można potem renderować na czerwono podczas przesuwania jeśli nie da się przenieść w dane miejsce albo na zielono jeśli można.

class GBlock{
    GCircuit parent; // do funkcji move, getIdAd(), isOccupied() i dla inputów
    int ID;
    int width, height;
    Point position; // position in GCircuit
    GInput gInputs[]; // one same w swoim obrębie wykrywają łączenie kablami
    GOutput gOutputs[]; // może się okazać, że wystarczy jedna klasa GPort zamiast GIn i GOut
    Image img; 
    Data data; // dane typu label etc. (może potem?)

    void clickAction(){
        if(LPM) parent.circuit.getBlockById(ID).clickAction(); // triggeruje clickAction bramki
        if(PPM); // usuwanie albo menu kontekstowe albo opcje
        if(DRAGGED) 
            if(parent.isOccupied(this)) render(RED); // koloruje na czerwono jeśli nie można przesunąć
            else parent.move(this)
    }
    int getPortAt(Point p); // zwraca indeks portu w tej współrzędnej, relatywnie do środka układu współrzędnych canvas albo jakoś (po metodzie do input i output)
    private? void update(); // ściąga info z bloku (prywatne i wywołane lokalnie w render()?)
    void render(); // renderuje blok i wywołuje dla każdego portu render()
}

GPort TOP

GPort reprezentuje wejście i wyjście do/z bloku. W związku z tym ma referencję na blok do którego należy, pamięta swoją pozycję w tablicy gInputs[] lu b gOutputs[] w zależności od typu portu, swoje współrzędne względem lewego górnego rogu, stronę, od której powinien wchodzić kabel SIDE = {EAST, WEST, SOUTH, NORTH} oraz dodatkowe pole Data zawierające dodatkowe informacje. Powinien być zaimplementowany jako ImGui::ImageButton i wykrywać w swoim zakresie akcje takie jak kliknięcia i przeciągnięcia.

  • Przy naciśnięciu PPM rozłącza wszystkie połączenia portu. Dla inputa taki kabel jest jeden więc wystarczy kliknąć. Dla outputów może istnieć wiele kabli wychodzących, wtedy należy usunąć wszytskie kable. W przyszłości można otwierać menu kontekstowe z listą podpiętych bloków i portów, żeby wybrać które połączenie usunąć.
  • Przeciąganie output->input dodaje nowy kabel do outputa i usuwa każdy obecnie podpięty do inputa kabel.
  • Przeciąganie input->output usuwa każdy kabel podpięty do inputa i dodaje nowy kabel do outputa.

Ilość GInputów i Inputów dla bloku o danym ID jest taka sama, więc mapowanie który input jest który pomiędzy GBlock i Block odbywa się jedynie na zasadzie porównania indeksów w odpowiednich tablicach (czyli gInputs[7] w Gblock odpowiada inputs[7] w Block). Powinny wywoływać na GCircuit getGBlockAt(x,y) żeby dostać ID bloku z którym mają się połączyć

class GPort{ // wspólna klasa dla GInput i GOutput
    GBlock parent; // referencja na GBlock do którego należy
    int index; // index w tablicy parent.inputs[]
    enum SIDE {EAST, WEST, SOUTH, NORTH} side; // określa gdzie ma przychodzić kabel
    Point position; // pozycja wejścia relatywnie do lewego górnego rogu bloku
    Data data; // dodatkowe data jak label, description etc.
    final bool isInput = true; // domyślnie jest wejściem    

   GPort(bool isInput, ...) // konstruktor ustawia typ wejścia, może mieć dodatkowe parametry
        this.isInput =  isInput;
    
    void portButtonClicked(){ // po kliknięciu lub przytrzymaniu portu ma się dziać to
        if(PPM) parent.parent.disconnect(this) // rozłącza wszystkie porty z this
        if(LPM DRAGGED ...) { // musi sprawdzać czy łączone są IN-OUT a nie np. IN-IN
            if(target.isInput()) GCircuit.disconnect(this, target) // rozłącza kable z inputa targetu
            if(this.isInput()) GCircuit.disconncet(this, target) // rozłącza kable z tego input
            parent.connect(this, second); // łączy z tym gdzie puszczono - `second`
        }
    }

   bool isInput(); // jeśli port jest wejściem, zwraca true
}

Bramki TOP

Gate TOP

Gate jest abstrakcyjną klasą bramki, po której dziedziczą specjalistyczne bramki. Musi mieć referencję na Circuit do którego należy, pole ID identyfikujące bramkę, kontenery zawierające wejścia i wyjścia, gdzie wejścia są jako tuple <bool, ID, port_index>, a wyjścia jako tuple <bool, <ID,port_index>[] > (tablica par booli i tablic połączonych portów) i abstrakcyjną funkcję update() do liczenia outputów na podstawie swoich wejść, którą każda dziedzicząca klasa ma implementować.

  • Do każdego wejścia może wchodzić tylko jeden kabel, dlatego każdy element tablicy inputs jest jednoznacznie wyznaczony przez swój stan i element z którym się łączy.
  • Z jednego wyjścia może wychodzić wiele kabli, dlatego wyjścia reprezentowane są jako tablica par stanów i list wszystkich bloków i portów z którymi się łączy się dane wyjście

Trzeba umieć aktualizować dane na portach (stany i połączenia), dlatego potrzebne są funkcje get/setPortState(index, ...), get/setInputConnection(index, ...), add/removeOutputConnection(ID, port), getOutputConnections(ID, port). Dodatkowo powinna potrafić usunąć wszystkie aktualne połączenia metodą resetConnections(). Musi implementować funkcję clickAction() - domyślnie pustą, ale nie abstrakcyjną (dopiero klasy pochodne jak chcą, to ją przeciążają i implementują jej ciało). Będzie ona wywoływana jeśli nastąpi kliknięcie na blok (o ile nie było to kliknięcie w port). Dodatkowo może mieć potem dodane pole data które przechowuje pewne dane (dla zegara częstotliwość i fazę etc.).

class Gate{
    Circuit parent; // wskaźnik na swój kontener
    int ID; // indywidualne dla każdego bloku
    Tuple<bool, int, int> inputs[]; // <stan, ID, port_index>
    Tuple<bool, Tuple<int, int>[]> outputs[]; <stan, <ID,port_index>[] >
    Data data; // dodatkowe info dla bramki
    
    Gate(ID) // konstruktor
        this.ID = ID;
 
    bool getInput/OutputState(index) return inputs[index].tupleGet(0); // zwraca stan portu
    void setInput/OutputState(index,bool state) inputs[index].tupleSet(0, index); // zmienia stan portu
    
    Tuple<int, int> getInputConnection(index); // zwraca połączenie pod inputs[index]
    void setInputConnection(index, int, int); // ... ustawia tuplowi połączenie
    
    void addOutputConnection(index, ID, p_ix); // dodaje połączenie portu index z blokiem ID w porcie p_ix
    void removeOutputConnection(ID, p_ix); // wyszukuje połączenie w outputs i usuwa je
    Connection getOutputConnections(p_ix); // zwraca tablicę par <ID, port> z którymi jest połączony blok w p_ix

    resetConnections(); // usuwa wszystkie połączenia wchodzące i wychodzące na wszystkich portach

    toString(); // zwraca tekstową reprezentację bramki
}

And, Or, Xor, Not, Nand, Nor, Xnor TOP

Wszystkie bramki podstawowe mają praktycznie taką samą strukturę - przy konstruktorze tworzy im się tablicę wejść i wyjść o odpowiednich rozmiarach, ich metoda clickAction() pozostaje nieprzeciążona, a update() zwyczajnie aktualizuje stan wyjścia według swojej logiki. Wszystkie bramki są intuicyjne

class And{
    And() // konstruktor
        super(App.nextID());
    
    update()
        setOutputState(0, getInputState(0) && getInputState(1)); // ustawia output C na (A & B)
        
    // clickAction() nie przeciążamy, więc wywołuje domyślnie puste clickAction z `Gate`
}

Clock TOP

Clock(0 in, 1 out) jest rozszerzeniem zwykłej bramki o funkcję stanu w czasie. Ma mieć pola onPeriod: przez ile ticków zegar jest włączony offPeriod: przez ile ticków zegar jest wyłączony phase: przesunięcie fazowe - jakbyśmy narysowali wykres sygnału w czasie to dodatnie przesuwa go w prawo Prosto można zaimplementować mając wewnątrz jakiś licznik, który zlicza "cyknięcia" zegara

Delay TOP

Delay(1 in, 1 out) ma działać jako opóźniacz sygnału który wysyła sygnał z pewnym zdefiniowanym opóźnieniem. Powiniem mieć pole delay ≥ 0 oznaczające po ilu tickach dany sygnał zotanie przesłany - jeśli jest zerem, sygnał leci bez opóźnienia (jak zwykły kabel) Trzeba zaimplementować tak, żeby dokładnie odtwarzał sygnał wejściowy na wyjściu po odpowiedniej ilości ticków (czyli jeśli mamy od chwili t0 kolejno sygnały 1,0,1 i opóźnienie 5 to w chwili t5=1 t6=0, t7=1).

Input TOP

Input(0 in, 1 out) ma działać jak źródło prądu którego stan zmieniamy przy kliknięciu na bramkę. Może mieć flagę ON oznaczającą stan i zmienianą w metodzi clickAction(). Jeśli jest włączony metoda update() ma ustawiać wyjście na 1, w przeciwnym wypadku 0 (no logiczne raczej).

Switch TOP

Switch(1 input, 1 output) ma działać analogicznie do inputa, tylko że przekazuje stan wejścia na wyjście jeśli jest włączony i 0 jak jest wyłaczony. Ma działać jak kontakt :D

Własne bloki TOP

Jak już dojdziemy do tego etapu, że cała reszta działa (pozdro xD) można zaimplementować dodawania własnych bloków. Wtedy customowe bloki będą dodawane jako kafelek 6 x max(inputs,outputs) z nazwą na środku i wejściami jedno pod drugim z lewej i wyjściami jedno pod drugim z prawej. innymi słowy trzeba będzie zmienić trochę rzeczy, bo blok będzie musiał być wyświetlany jako pojedynczy, ale zachowywać się wewnątrz jako zwykła część układu - czyli pewnie trzeba będzie dla każdego GInput i GOutput przechowywać nie referencję na ojca ale ID ojca etc. etc.