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>