Primer16

Zagon dveh različnih kontrolnih algoritmov

Uporabniški vmesnik bomo dopolnili z možnostjo zagona različnih kontrolnih algoritmov na strani klienta. Dodali bomo gumba za zagon in zaustavitev P in PID kontrolnega algoritma. Tako želimo primerjati različne algoritme in njihovo učinkovitost.

Na strani klienta bomo dodali izpis statičnega sporočila v <div> s pomočjo funkcije innerHTML:

document.getElementById("divZaStatičniIzpis").innerHTML = "Status: " + sporočilo;

Izhodišče za primer16 je primer15.

V .js datoteki bomo dodali funkcijo pošljiStatičnoSporočiloPrekoVtičnika , ki bo omogočila izpis statičnih sporočil v <div> na strani klienta (iz prejšnjega primera imamo deklarirano funkcijo pošljiVrednostPrekoVtičnika):

var pošljiVrednostPrekoVtičnika = function(){}; // spr. za pošiljanje sporočil
var pošljiStatičnoSporočiloPrekoVtičnika = function() {}; // funkcija za pošiljanje statičnega sporočila

V nadaljevanju primer15 spremenimo v primer16

fs.readFile(__dirname + "/primer16.html"

Izpišemo dodatno sporočilo v statični <div>:

socket.emit("pošljiStatičnoSporočiloPrekoVtičnika", "Strežnik povezan, plošča pripravljena.")

Funkcija startKontrolniAlgoritem ima tokrat argument štKontrolnegaAlg, ki sicer lahko vsebuje tudi json strukturo; v našem primeru za faktorje, ki jih upoštevamo v algoritmih. Spremenljivka štKontrolnegaAlg pove, katerega od algoritmov želimo zagnati:

        socket.on("startKontrolniAlgoritem", function(štKontrolnegaAlg){
           startKontrolniAlgoritem(štKontrolnegaAlg); 
        });

Znotraj vtičnika ("socket") dodamo dve novi funkciji (tokrat z argumentom "value"):

    pošljiVrednostPrekoVtičnika = function (value) {
        io.sockets.emit("sporočiloKlientu", value);
    } 
    
    pošljiStatičnoSporočiloPrekoVtičnika = function (value) {
        io.sockets.emit("statičnoSporočiloKlientu", value);
    }

Funkcija kontrolniAlgoritem ima prav tako v argumentu spremenljivko "parametri"; glede na spremenljivko štKontrolnegaAlg poženemo ustrezni algoritem:

function kontrolniAlgoritem (parametri) {
    if (parametri.štKontrolnegaAlg == 1) {
        pwm = parametri.faktor*(želenaVrednost-dejanskaVrednost);
        if (pwm > 255) {pwm = 255}; // omejimo vrednost pwm na 255
        if (pwm < -255) {pwm = -255}; // omejimo vrednost pwm na -255
        if (pwm > 0) {board.digitalWrite(2,0)}; // določimo smer če je > 0
        if (pwm < 0) {board.digitalWrite(2,1)}; // določimo smer če je < 0
        board.analogWrite(3, Math.abs(pwm)); // zapišemo abs vrednost na pin 3
        if (dejanskaVrednost < 150 || dejanskaVrednost > 910) {
            stopKontrolniAlgoritem();
        }
    }
    if (parametri.štKontrolnegaAlg == 2) {
        err = želenaVrednost - dejanskaVrednost; // odstopanje ali error
        errVsota += err; // vsota napak (kot integral)
        dErr = err - zadnjiErr; // razlika odstopanj
        pwm = parametri.Kp1*err + parametri.Ki1*errVsota + parametri.Kd1*dErr; // izraz za PID kontroler (iz enačbe)
        zadnjiErr = err; // shranimo vrednost za naslednji cikel za oceno odvoda
    
        if (pwm > 255) {pwm = 255}; // omejimo vrednost pwm na 255
        if (pwm < -255) {pwm = -255}; // omejimo vrednost pwm na -255
        if (pwm > 0) {board.digitalWrite(2,0)}; // določimo smer če je > 0
        if (pwm < 0) {board.digitalWrite(2,1)}; // določimo smer če je < 0
        board.analogWrite(3, Math.abs(pwm)); // zapišemo abs vrednost na pin 3
    
        if (dejanskaVrednost < 200 || dejanskaVrednost > 850) {
            stopKontrolniAlgoritem();
        }
    }    

}

Vrstico s pwm v zgornjem zapisu kode spremenimo na naslednji način:

pwm = parametri.Kp1*err + parametri.Ki1*errVsota + parametri.Kd1*dErr; // izraz za PID kontroler (iz enačbe)


Funkcija startKontrolniAlgoritem ima prav tako parametre v argumentu:

function startKontrolniAlgoritem (parametri) {
    if (kontrolniAlgoritemVključen == 0) {
        kontrolniAlgoritemVključen = 1;
        intervalCtrl = setInterval(function(){kontrolniAlgoritem(parametri);}, 30); // kličemo alg. na 30ms
        console.log("Vključen kontrolni algoritem št. " + parametri.štKontrolnegaAlg)
        pošljiStatičnoSporočiloPrekoVtičnika("Kontrolni algoritem št. " + parametri.štKontrolnegaAlg + " zagnan | " + json2txt(parametri));
    }
}

V funkciji setInterval dodamo log in pošiljanje statičnega sporočila:

intervalCtrl = setInterval(function(){kontrolniAlgoritem(parametri);}, 30); // kličemo alg. na 30ms
console.log("Vključen kontrolni algoritem št. " + parametri.štKontrolnegaAlg)
pošljiStatičnoSporočiloPrekoVtičnika("Kontrolni algoritem št. " + parametri.štKontrolnegaAlg + " zagnan | " + json2txt(parametri));

V funkciji stopKontrolniAlgoritem dodamo izpis obvestila, ki se prenese na stran klienta:

 console.log("Kontrolni algoritem zaustavljen.");
pošljiStatičnoSporočiloPrekoVtičnika("Kontrolni algoritem zaustavljen.")


Dodamo funkcijo za izpis vsebine json strukture v berljivi obliki:


function json2txt(obj) // funkcija za izpis json imen in vrednosti
{
  var txt = '';
  var recurse = function(_obj) {
    if ('object' != typeof(_obj)) {
      txt += ' = ' + _obj + '\n';
    }
    else {
      for (var key in _obj) {
        if (_obj.hasOwnProperty(key)) {
          txt += '.' + key;
          recurse(_obj[key]);
        } 
      }
    }
  };
  recurse(obj);
  return txt;
}


V .html datoteki moramo dodati vnosna polja ter ustrezne funkcije:

faktor: <input id="faktor" value="0.5" size="5" />
<button id="startKontrolniAlgoritem1" onClick="startKontrolniAlgoritem1();">Start Ctrl Alg</button>
<button id="stopKontrolniAlgoritem" onClick="stopKontrolniAlgoritem();">Stop Ctrl Alg</button>
<p></p>
Kp: <input id="Kp1" value="0.5" size = "5" />
Ki: <input id="Ki1" value="0.0055" size = "5" />
Kd: <input id="Kd1" value="0.25" size = "5" />
<button id="startKontrolniAlgoritem2" onClick="startKontrolniAlgoritem2();">Start Ctrl Alg2</button>
<button id="stopKontrolniAlgoritem" onClick="stopKontrolniAlgoritem();">Stop Kontrolni Alg</button>

Deklariramo funkcijo, ki bo poslušala na vtičniku:

socket.on("statičnoSporočiloKlientu", function(sporočilo){
   document.getElementById("divZaStatičniIzpis").innerHTML = "Status: " + sporočilo;
});

Funkcija mora imeti na koncu dodano št. 1 oz. št. 2(startKontrolniAlgoritem1 in startKontrolniAlgoritem2):

function startKontrolniAlgoritem1() {
    stopKontrolniAlgoritem(); // za vsak primer algoritem najprej zaustavimo
    faktor = document.getElementById("faktor").value; // beremo vrednost faktorja iz vnosnega polja
    socket.emit("startKontrolniAlgoritem", {"štKontrolnegaAlg": 1, "faktor": faktor}); /// beremo vrednost iz vnosnega polja
}
function startKontrolniAlgoritem2() {
    stopKontrolniAlgoritem(); // za vsak primer algoritem najprej zaustavimo
    Kp1 = document.getElementById("Kp1").value; // beremo vrednost iz vnosnega polja
    Ki1 = document.getElementById("Ki1").value; // beremo vrednost iz vnosnega polja
    Kd1 = document.getElementById("Kd1").value; // beremo vrednost iz vnosnega polja
    socket.emit("startKontrolniAlgoritem", {"štKontrolnegaAlg": 2, "Kp1": Kp1, "Ki1": Ki1, "Kd1": Kd1}); // pošljemo json z vrednostmi na strežnik
}

Uporabniški vmesnik je prikazan na naslednji sliki. Izbiramo lahko med P in PID algoritmom in podamo ustrezne parametre:

primer16.js

var http = require("http").createServer(handler); // ob zahtevi req -> handler
var firmata = require("firmata");
var fs = require("fs"); // knjižnjica za delo z datotekami (File System fs)
var io = require("socket.io").listen(http); // knjiž. za komunik. prek socket-a 

console.log("Priklop Arduina");

var board = new firmata.Board("/dev/ttyACM0", function(){
    console.log("Aktiviramo analogni pin 0");
    board.pinMode(0, board.MODES.ANALOG);
    console.log("Aktiviramo analogni pin 1");
    board.pinMode(1, board.MODES.ANALOG);
    console.log("Aktiviramo pin 2");
    board.pinMode(2, board.MODES.OUTPUT); // pin za smer na H-mostu
    console.log("Aktiviramo pin 3");
    board.pinMode(3, board.MODES.PWM); // Pulse Width Modulation - hitrost
    console.log("Aktiviramo pin 8");
    board.pinMode(8, board.MODES.INPUT); // pin za HW gumb
});

function handler(req, res) {
    fs.readFile(__dirname + "/primer16.html",
    function(err, data) {
        if (err) {
            res.writeHead(500, {"Content-Type": "text/plain"});
            return res.end("Napaka pri nalaganju html strani!");
        }
        res.writeHead(200);
        res.end(data);
    });
}

http.listen(8080); // strežnik bo poslušal na vratih 8080

var želenaVrednost = 0; // želeno vrednost postavimo na 0
var dejanskaVrednost = 0; // dejansko vrednost postavimo na 0
var faktor =0.4; // faktor, ki določa hitrost doseganja želenega stanja
var pwm = 0;

// Spremenljivke PID algoritma
var Kp = 0.8; // proporcionalni faktor
var Ki = 0.008; // integralni faktor
var Kd = 0.15; // diferencialni faktor

var err = 0; // error
var errVsota = 0; // vsota napak
var dErr = 0; // diferenca napak
var zadnjiErr = 0; // da obdržimo vrednost prejšnje napake



var kontrolniAlgoritemVključen = 0; // spremenljivka, ki določa ali je ctrl. alg. vključen
var intervalCtrl; // spremenljivka za setInterval v globalnem prostoru

console.log("Zagon sistema"); // izpis sporočila o zagonu

var pošljiVrednostPrekoVtičnika = function(){}; // spr. za pošiljanje sporočil
var pošljiStatičnoSporočiloPrekoVtičnika = function() {}; // funkcija za pošiljanje statičnega sporočila

board.on("ready", function(){
    console.log("Plošča pripravljena");

    board.analogRead(0, function(value){
        želenaVrednost = value; // neprekinjeno branje pina A0
    });
    board.analogRead(1, function(value){
        dejanskaVrednost = value; // neprekinjeno branje pina A1
    });
    
    //startKontrolniAlgoritem(); // poženemo kontrolni algoritem
    
    io.sockets.on("connection", function(socket){
        
        socket.emit("pošljiStatičnoSporočiloPrekoVtičnika", "Strežnik povezan, plošča pripravljena.")

        setInterval(pošljiVrednosti, 40, socket); // na 40ms pošlj. vred.
        
        socket.on("startKontrolniAlgoritem", function(štKontrolnegaAlg){
           startKontrolniAlgoritem(štKontrolnegaAlg); 
        });
        
        socket.on("stopKontrolniAlgoritem", function(){
           stopKontrolniAlgoritem(); 
        });   
        
    pošljiVrednostPrekoVtičnika = function (value) {
        io.sockets.emit("sporočiloKlientu", value);
    } 
    
    pošljiStatičnoSporočiloPrekoVtičnika = function (value) {
        io.sockets.emit("statičnoSporočiloKlientu", value);
    }
    
    
        
    }); // konec socket.on("connection")
 
}); // konec board.on("ready")

function kontrolniAlgoritem (parametri) {
    if (parametri.štKontrolnegaAlg == 1) {
        pwm = parametri.faktor*(želenaVrednost-dejanskaVrednost);
        if (pwm > 255) {pwm = 255}; // omejimo vrednost pwm na 255
        if (pwm < -255) {pwm = -255}; // omejimo vrednost pwm na -255
        if (pwm > 0) {board.digitalWrite(2,0)}; // določimo smer če je > 0
        if (pwm < 0) {board.digitalWrite(2,1)}; // določimo smer če je < 0
        board.analogWrite(3, Math.abs(pwm)); // zapišemo abs vrednost na pin 3
        if (dejanskaVrednost < 150 || dejanskaVrednost > 910) {
            stopKontrolniAlgoritem();
        }
    }
    if (parametri.štKontrolnegaAlg == 2) {
        err = želenaVrednost - dejanskaVrednost; // odstopanje ali error
        errVsota += err; // vsota napak (kot integral)
        dErr = err - zadnjiErr; // razlika odstopanj
        pwm = parametri.Kp1*err + parametri.Ki1*errVsota + parametri.Kd1*dErr; // izraz za PID kontroler (iz enačbe)
        zadnjiErr = err; // shranimo vrednost za naslednji cikel za oceno odvoda
    
        if (pwm > 255) {pwm = 255}; // omejimo vrednost pwm na 255
        if (pwm < -255) {pwm = -255}; // omejimo vrednost pwm na -255
        if (pwm > 0) {board.digitalWrite(2,0)}; // določimo smer če je > 0
        if (pwm < 0) {board.digitalWrite(2,1)}; // določimo smer če je < 0
        board.analogWrite(3, Math.abs(pwm)); // zapišemo abs vrednost na pin 3
    
        if (dejanskaVrednost < 200 || dejanskaVrednost > 850) {
            stopKontrolniAlgoritem();
        }
    }    

}

function startKontrolniAlgoritem (parametri) {
    if (kontrolniAlgoritemVključen == 0) {
        kontrolniAlgoritemVključen = 1;
        intervalCtrl = setInterval(function(){kontrolniAlgoritem(parametri);}, 30); // kličemo alg. na 30ms
        console.log("Vključen kontrolni algoritem št. " + parametri.štKontrolnegaAlg)
        pošljiStatičnoSporočiloPrekoVtičnika("Kontrolni algoritem št. " + parametri.štKontrolnegaAlg + " zagnan | " + json2txt(parametri));
    }
}

function stopKontrolniAlgoritem () {
    clearInterval(intervalCtrl); // brišemo interval klica kontrolnega algoritma
    board.analogWrite(3, 0);
    kontrolniAlgoritemVključen = 0;
    console.log("Kontrolni algoritem zaustavljen.");
    pošljiStatičnoSporočiloPrekoVtičnika("Kontrolni algoritem zaustavljen.");
};

function pošljiVrednosti(socket) {
    socket.emit("klientBeriVrednosti",
    {
        "želenaVrednost": želenaVrednost,
        "dejanskaVrednost": dejanskaVrednost,
        "pwm": pwm
    });
};

function json2txt(obj) // funkcija za izpis json imen in vrednosti
{
  var txt = '';
  var recurse = function(_obj) {
    if ('object' != typeof(_obj)) {
      txt += ' = ' + _obj + '\n';
    }
    else {
      for (var key in _obj) {
        if (_obj.hasOwnProperty(key)) {
          txt += '.' + key;
          recurse(_obj[key]);
        } 
      }
    }
  };
  recurse(obj);
  return txt;
}

primer16.html

<!DOCTYPE html>
<meta charset = utf8>
<html>
<head>
    <title>Kontrolni sistem</title>
</head>

<body onload="load()">
Kontrolni sistem P / PID
<br>
<canvas id="cv" width = "200" height = "100" style="border: 1px dashed #00c3c3;"></canvas>
<p></p>
faktor: <input id="faktor" value="0.5" size="5" />
<button id="startKontrolniAlgoritem1" onClick="startKontrolniAlgoritem1();">Start Ctrl Alg</button>
<button id="stopKontrolniAlgoritem" onClick="stopKontrolniAlgoritem();">Stop Ctrl Alg</button>
<p></p>
Kp: <input id="Kp1" value="0.5" size = "5" />
Ki: <input id="Ki1" value="0.0055" size = "5" />
Kd: <input id="Kd1" value="0.25" size = "5" />
<button id="startKontrolniAlgoritem2" onClick="startKontrolniAlgoritem2();">Start Ctrl Alg2</button>
<button id="stopKontrolniAlgoritem" onClick="stopKontrolniAlgoritem();">Stop Kontrolni Alg</button>
<p></p>
<div id="divZaStatičniIzpis">Status: ×</div>
<p></p>
<div id="divZaIzpis"></div>

<script type="text/javascript" src="/socket.io/socket.io.js"></script>

<script type="text/javascript">

var x1 = new Array(); // polje za spremenljivko x1
var y1 = new Array(); // polje za spremenljivko y1
var x2 = new Array(); // polje za spremenljivko x2
var y2 = new Array(); // polje za spremenljivko y2
var canvas;
var ctx;
var potVrednost1; // vrednost prvega potenciometra
var potVrednost2; // vrednost drugega potenciometra

var divZaIzpis = document.getElementById("divZaIzpis"); // var za div el.
var štVrsticPredPomikom = 10;
var števecIzpisanihVrstic = 0;

var socket = io.connect("192.168.1.136:8080"); // povezava na strež.

function load() {
    canvas = document.getElementById("cv");
    ctx = canvas.getContext("2d");

    // izrišemo prvo časovno vrsto za želene vrednosti
    ctx.lineWidth = "1";
    ctx.strokeStyle = "#ff0000";

    for (var i = 0; i<200; i++) {
        x1[i] = i; // x1 gre od 0, 1, ... 199
        y1[i] = 0; // za y podamo vrednost 0
    }

    ctx.beginPath(); // za začetek izrisa črte

    for (var i=0; i<200; i++) {
        ctx.lineTo(x1[i],y1[i]);
    }

    ctx.stroke();
    
    // izrišemo drugo časovno vrsto za dejanske vrednosti
    ctx.lineWidth = "1";
    ctx.strokeStyle = "#00ff00";

    for (var i = 0; i<200; i++) {
        x2[i] = i; // x2 gre od 0, 1, ... 199
        y2[i] = 0; // za y podamo vrednost 0
    }

    ctx.beginPath(); // za začetek izrisa črte

    for (var i=0; i<200; i++) {
        ctx.lineTo(x2[i],y2[i]);
    }

    ctx.stroke();    
    
}

socket.on("klientBeriVrednosti", function(value){
    
    potVrednost1 = value.želenaVrednost; // žel. vr. iz strežnika
    potVrednost2 = value.dejanskaVrednost; // dej. vr. iz strežnika
    
    // Graf izris **********************************************************
    ctx.clearRect(0, 0, canvas.width, canvas.height); // brišemo platno
    
    // izris prve črte
    ctx.strokeStyle = "#ff0000";
    ctx.beginPath(); // za začetek izrisa črte
    y1.splice(0, 1); // na mestu 0 v polju y1 brišemo eno vrednost
    y1[199] = potVrednost1; // novo vrednst pa dodamo na koncu polja
    
    for (var i=0; i<200; i++) {
        ctx.lineTo(x1[i], (100 - (y1[i] / 1023) * 100)); // izris vrednosti
    }
    
    ctx.stroke(); // za izris črte
    
    // izris druge črte
    ctx.strokeStyle = "#00ff00";
    ctx.beginPath(); // za začetek izrisa črte
    y2.splice(0, 1); // na mestu 0 v polju y2 brišemo eno vrednost
    y2[199] = potVrednost2; // novo vrednst pa dodamo na koncu polja
    
    for (var i=0; i<200; i++) {
        ctx.lineTo(x2[i], (100 - (y2[i] / 1023) * 100)); // izris vrednosti
    }
    
    ctx.stroke(); // za izris črte
    
    // Graf izris **********************************************************
    
        // izris legende
    
    ctx.strokeStyle = "#ff0000";
    ctx.font = "11px Arial";
    ctx.fillText("Želena", 70, 10);
    ctx.beginPath();
    ctx.lineTo(50,6);
    ctx.lineTo(65,6);
    ctx.stroke();
    
    ctx.strokeStyle = "#00ff00";
    ctx.font = "11px Arial";
    ctx.fillText("Dejanska", 140, 10);
    ctx.beginPath();
    ctx.lineTo(120,6);
    ctx.lineTo(135,6);
    ctx.stroke();
    
    log(value.želenaVrednost + "|" + value.dejanskaVrednost + "|" + (value.želenaVrednost-value.dejanskaVrednost) + "|" + (value.pwm).toFixed(0));
    
});

socket.on("statičnoSporočiloKlientu", function(sporočilo){
   document.getElementById("divZaStatičniIzpis").innerHTML = "Status: " + sporočilo;
});

function startKontrolniAlgoritem1() {
    stopKontrolniAlgoritem(); // za vsak primer algoritem najprej zaustavimo
    faktor = document.getElementById("faktor").value; // beremo vrednost faktorja iz vnosnega polja
    socket.emit("startKontrolniAlgoritem", {"štKontrolnegaAlg": 1, "faktor": faktor}); /// beremo vrednost iz vnosnega polja
}
function startKontrolniAlgoritem2() {
    stopKontrolniAlgoritem(); // za vsak primer algoritem najprej zaustavimo
    Kp1 = document.getElementById("Kp1").value; // beremo vrednost iz vnosnega polja
    Ki1 = document.getElementById("Ki1").value; // beremo vrednost iz vnosnega polja
    Kd1 = document.getElementById("Kd1").value; // beremo vrednost iz vnosnega polja
    socket.emit("startKontrolniAlgoritem", {"štKontrolnegaAlg": 2, "Kp1": Kp1, "Ki1": Ki1, "Kd1": Kd1}); // pošljemo json z vrednostmi na strežnik
}

function stopKontrolniAlgoritem() {
    socket.emit("stopKontrolniAlgoritem");
}

function log(sporočilo) {
    var node=document.createElement("tr"); // ustvarimo spremenljivko kot vrstico v tabeli "table row" (tr)
    var textnode=document.createTextNode(števecIzpisanihVrstic + " | " + sporočilo); // ustvarimo element z besedilom in dodamo števec
    node.appendChild(textnode); // dodamo besedilo k "node", t.j. k "table row"
    divZaIzpis.insertBefore(node, divZaIzpis.childNodes[0]); // vstavimo v spremenljivko "node"
    if (števecIzpisanihVrstic > štVrsticPredPomikom-1) { // če je vrstic več, kot je določeno -> pričnemo s pomikom ("scroll")
        divZaIzpis.removeChild(divZaIzpis.childNodes[štVrsticPredPomikom]); // odstranimo najstarejši izpis
    }
    števecIzpisanihVrstic++; // povečamo števec izpisov
}

</script>


</body>

</html>