-
• #2
I've got it working, but it's hideous. There's gotta be a better way to do this...
Interfaces with an EasyVR voice recognition module.
/* Copyright (C) 2015 Spence Konde. See the file LICENSE for copying permission. */ /* This module interfaces with EasyVR voice recognition onCom is called with two arguments, the option group, and the command returned. onTimeout is called with one argument, the option group. */ exports.connect = function(serial,onCm,onTo,onEr) { return new EasyVR(serial,onCm,onTo,onEr); }; function EasyVR(ser,onCm,onTo,onEr) { this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.onErr=onEr; this.ser.on('data',this.onWatch); this.vrstate=-1; this.stsr='o'; this.rcvv=""; } EasyVR.prototype.argchar=function(val) { if (val<-1 || val > 31) {throw "Bad arg";} return String.fromCharCode(0x41+val); }; EasyVR.prototype.chararg=function(chr) { return chr.charCodeAt(0)-0x41; }; EasyVR.prototype.onWatch=function(data) { //this=evr; console.log("serial data watch: "+data); var rcv=data.charCodeAt(0); console.log("serial data: "+rcv); if (rcv>0x60) { console.log("status"); var temp=evr.sts_idx[data]; //console.log("serial lookup: "+temp); if (temp[0]) { evr.stsr=data; evr.ser.print(' '); } else { eval(temp[1]); } } else { console.log("data"); evr.rcvv+=data; if (evr.rcvv.length>=evr.sts_idx[evr.stsr][0]){ console.log("running callback "+evr.sts_idx[evr.stsr][1]); eval(evr.sts_idx[evr.stsr][1]); evr.rcvv=""; evr.stsr='o'; } else { console.log("need more data"); evr.ser.print(' '); } } }; EasyVR.prototype.sts_idx={ "o":[0,"console.log('STS_SUCCESS');"], "t":[0,"console.log('STS_TIMEOUT');"], "v":[0,"console.log('STS_INVALID');"], "i":[0,"console.log('STS_INTERR');"], "e":[2,"console.log('STS_ERROR '+evr.rcvv);evr.onErr(evr.rcvv);"], "r":[1,"console.log('STS_RESULT');"] }; EasyVR.prototype.onResult=function(r) { console.log("onresult"); console.log(r); var rt = this.onCommand(this.vrstate,r); if (rt.type!==undefined) { this.stop(); this.setRecognize(rt.type,rt.timeout); } }; EasyVR.prototype.setRecognize=function(type,to) { this.sts_idx.o[1]="evr.startRec("+type+","+to+");"; this.timeout(to); if (to) {setTimeout("eval(evr.sts_idx.t[1])",to*1000+1000);} }; EasyVR.prototype.startRec=function(type,timeout){ this.sts_idx.o[1]=""; this.sts_idx.r[1]="evr.onResult(evr.chararg(evr.rcvv));"; this.sts_idx.t[1]="evr.sts_idx.r[1]='';evr.sts_idx.t[1]='';evr.onTimeout(evr.vrstate);"; this.sendCmd('d',type); this.vrstate=type; }; EasyVR.prototype.sendCmd=function(cmd,arg) { //lastCmd=[cmd,arg]; this.ser.print(cmd); console.log("Sending command: "+cmd); if (arg!==undefined){console.log("With arg: "+this.argchar(arg));this.ser.print(this.argchar(arg));} }; EasyVR.prototype.stop=function(){ this.sts_idx={ "o":[0,"console.log('STS_SUCCESS');"], "t":[0,"console.log('STS_TIMEOUT');"], "v":[0,"console.log('STS_INVALID');"], "i":[0,"console.log('STS_INTERR');"], "e":[2,"console.log('STS_ERROR '+evr.rcvv);evr.onErr(evr.rcvv);"], "r":[1,"console.log('STS_RESULT');"] }; this.sendCmd('b'); }; EasyVR.prototype.timeout=function(arg) { this.sendCmd('o',arg); };
Of course it depends on the object being called evr.
var ocm=function(menu,option) { console.log("menu:"+menu+" option: "+option); if (menu==1&&option==2) { return {type:2,timeout:10}; } else { if (menu==2) { if (option==0) { digitalWrite([LED1,LED2,LED3],2); } else if(option==1) { digitalWrite([LED1,LED2,LED3],4); } else { digitalWrite([LED1,LED2,LED3],1); } } return {type:1,timeout:0}; } }; var otm=function(){ this.setRecognize(1,0); }; Serial4.setup(9600,{tx:C10,rx:C11}); var evr=require("easyvr").connect(Serial4,ocm,otm,otm);
-
• #3
I think this is actually exactly the same issue as you'd have with
setTimeout
-this
isn't set to the right thing.To solve it you'd define a variable in your function's scope and then use that:
function EasyVR(ser,onCm,onTo) { this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; var evr = this; // <------------------------------ this.ser.on('data',function(){evr.onWatch(); }); // <-------------- this.vrstate=-1; this.stsr='o'; this.rcvv=""; } ; EasyVR.prototype.onWatch=function(data) { console.log(this); };
Possibly
this
should actually be set to the serial object the event was on though - I'd need to figure out what NodeJS does. Even if that was the case, just callingthis.ser.on('data',this.onWatch);
wouldn't do what you'd want.There's some stuff on StackOverflow about it, and a neat solution would be to use
bind
- although I'm afraid that's not implemented yet :( -
• #4
DrAzzy, sorry to respond late on your first post... was out all day on water whale watching...
Your initial approach is just fine... the problem with JS is that
this
is super-late-binding: when it comes to execution,this
is the context at execution time... with a simple cheat you can get it running without having to violate encapsulation nor venture into globals (...as you got it working - no offense what so ever to be taken here of).function EasyVR(ser,onCm,onTo) { var _this = this; this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.ser.on('data',_this.onWatch); this.vrstate=-1; this.stsr='o'; this.rcvv=""; } ;
As you notice, you make a copy
_this
of the correctthis
reference to this in line 2 and use it in line 6. This resolves the issue of the runtime event, where serial receives data and has to invoke a the particular - in creation - object instance's method.onWatch()
is called.This is sufficient if you do not need to call
this
in.onWatch()
, because only the proper (prototype) method is passed as callback for theserial.on()
.In case you need to reference
this
in.onWatch()
- as you do with the simple example you use with console.log(this
) - you need to do more, because with passing the right method you did not pass the right context yet... after all, JavaScript IS a functional language - call subroutine - and not really an object language with comprehensive encapsulation of behavior AND state - send message with parm obj refs to object (also referenced to as invocation of a method of an object with parm obj refs).Nice 'concidence' to me after just looking up Lambda_calculus on, section of Lambda calculus and programming languages - Anonymous functions: it describes almost to a tee the language teachings and sequence and thinking about algorithms and data structures I was exposed to : Pascal (S) - 'brain child' of ALGOL 60 - Smalltalk - Javascript. In same section under 'more recent' C++11 is mentioned. Java is still missing... (explains 'needs Edit' comment). All other languages I got used to use were like noise on the road side.
function EasyVR(ser,onCm,onTo) { var _this = this; this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.ser.on('data',function(data){ _this.onWatch(data); }); this.vrstate=-1; this.stsr='o'; this.rcvv=""; } ;
This will fix this. ...nice word play (at least in the eyes - and ears - of a non native English speaker...).
On the Web you find
that
for_this
- but I like to stick with the name as close to what it means... and declared it to myself almost as a reserved word, like this.What frameworks - as mentioned in many forum entries mention - are doing with the bind is essentially what is done in line 6: make sure the function is called in the context of the object, so it is more a method invocation of an object then just a function call. It is binding the function to the object context. Unfortunately, most of all the good things that come with the frameworks are just too much bulky for resource frugal Espruino... just like V8 was to be useful. And even more unfortunate, that more an more Web entries under Javascript give solutions for problems not in Javascript but in frameworks, and Javascript core knowledge shrinks to being lost.
Java and 'real object oriented language' proponents will call this still hideous. But liking almost all languages, I appreciate the freedom I get with Javascript for a very small price... over (m)any other languages. Javascript got the Lambda calculus right in its initial release - when still called Lifescript and not even called Javascript yet, where it took Java 8 major releases to get there. Agreed: you could do Javascript-similar things with Java too - much more safely - but also much much much more hideously... Way more 'hideousness' than what is needed to make Javascript to have what Java has in regard of inheritance and polymorphism / class hierarchy).
-
• #5
I've just added
Function.prototype.bind
, so you should now (it the latest builds) be able to do:function EasyVR(ser,onCm,onTo) { this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.ser.on('data',evr.onWatch.bind(this)); // <-------------- this.vrstate=-1; this.stsr='o'; this.rcvv=""; } ; EasyVR.prototype.onWatch=function(data) { console.log(this); };
I'm not sure which is better - the
bind
will actually be slightly faster though. -
• #6
Nice solution the 'bind()'... the use of global 'evr' still makes making EasyVR a class a joke though...
-
• #7
With the newly implemented bind and the just to implement
Object.getPrototypeOf()
- as many Javascript engines implement - including IE9+'s - the_this
orthat
can be eliminated - including the more serious issue of having a global reference to evr (which locks the application down to a given variable name, like a reserved word...).this.ser.on('data',Object.getPrototypeOf(this).onWatch.bind(this));
@Gordon, if there are a few bytes left to be 'wasted' on Object.getPrototypeOf(object), that would be cool too.
@Gordon - sorry, again - from your knowledge of the internals of the bind() implementation, what is the memory footprint compared to creating an anonymous function instead? MSDN describes bind() as creating a function with same body on the bound object...
-
• #8
Thinking through what an implementation of getPrototypeOf() or getClass() (='the function object') could entail, it may not be easily done... Shims or fillins do that by creating an extra instance variable and assign the constructor function - or class (function). Since it is in the constructor itself, it is nothing wrong to 'globally' reference it in there. Taking the same approach for referencing the constructor function - self in itself - and use the (now) available .bind(), simplifies above approach to this:
function EasyVR(...) { ... this.ser.on('data',EasyVR.prototype.onWatch.bind(this)); ... }
@DrAzzy, renaming onWatch (vs. onTimeout) to onData would go in synch with serial.onData()... ;-)
-
• #9
Thanks, Gordon and allObjects.
I'll update tonight and try out bind(). That will make the code a lot more readable - can I get rid of the global reference entirely with:
function EasyVR(ser,onCm,onTo) { this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.ser.on('data',this.onWatch.bind(this)); // <-------------- this.vrstate=-1; this.stsr='o'; this.rcvv=""; } ; EasyVR.prototype.onWatch=function(data) { console.log(this); };
I'd really like to get rid of it, since it does defeat the purpose of the class.
yeah, onData() makes a lot more sense than onWatch(). The variable naming there is very dubious overall, and needs to be cleaned up.
-
• #10
If It - your final solution to get rid of all globals, including the 'class' - ... If It Were A Snake, It Would Have Bit Me.
Just don't get why it did't. - :(
-
• #11
Lucky thing this isn't python then ;-)
-
• #12
/* This module interfaces with EasyVR voice recognition onCom is called with two arguments, the option group, and the command returned. onTimeout is called with one argument, the option group. */ exports.connect = function(serial,onCm,onTo,onEr) { return new EasyVR(serial,onCm,onTo,onEr); }; function EasyVR(ser,onCm,onTo,onEr) { this.ser = ser; this.onCommand=onCm; this.onTimeout=onTo; this.onErr=onEr; this.ser.on('data',this.onData.bind(this)); this.stop(); this.vrstate=-1; this.stsr='o'; this.rcvv=""; this.tout=0; } EasyVR.prototype.argchar=function(val) { if (val<-1 || val > 31) {throw "Bad arg";} return String.fromCharCode(0x41+val); }; EasyVR.prototype.chararg=function(chr) { return chr.charCodeAt(0)-0x41; }; EasyVR.prototype.onData=function(data) { //this=evr; console.log("serial data watch: "+data); var rcv=data.charCodeAt(0); console.log("serial data: "+rcv); if (rcv>0x60) { console.log("status"); var temp=this.sts_idx[data]; //console.log("serial lookup: "+temp); if (temp[0]) { this.stsr=data; this.ser.print(' '); } else { eval(temp[1]); } } else { console.log("data"); this.rcvv+=data; if (this.rcvv.length>=this.sts_idx[this.stsr][0]){ console.log("running callback "+this.sts_idx[this.stsr][1]); eval(this.sts_idx[this.stsr][1]); this.rcvv=""; this.stsr='o'; } else { console.log("need more data"); this.ser.print(' '); } } }; EasyVR.prototype.sts_idx={ "o":[0,"console.log('STS_SUCCESS');"], "t":[0,"console.log('STS_TIMEOUT');"], "v":[0,"console.log('STS_INVALID');"], "i":[0,"console.log('STS_INTERR');"], "e":[2,"console.log('STS_ERROR '+evr.rcvv);evr.onErr(evr.rcvv);"], "s":[1,"console.log('STS_SIMILAR '+evr.rcvv);evr.onErr(evr.rcvv);"], "r":[1,"console.log('STS_RESULT');"] }; EasyVR.prototype.onResult=function(r) { console.log("onresult"); console.log(r); var rt = this.onCommand(this.vrstate,r); if (rt.type!==undefined) { this.stop(); this.setRecognize(rt.type,rt.timeout); } }; EasyVR.prototype.setRecognize=function(type,to) { if (this.tout) { clearTimeout(this.tout); this.tout=0; } this.sts_idx.o[1]="evr.startRec("+type+","+to+");"; this.timeout(to); if (to) {this.tout=setTimeout("eval(evr.sts_idx.t[1])",to*1000+1000);} }; EasyVR.prototype.startRec=function(type,timeout){ this.sts_idx.o[1]=""; this.sts_idx.r[1]="evr.onResult(evr.chararg(evr.rcvv));"; this.sts_idx.t[1]="evr.sts_idx.r[1]='';evr.sts_idx.t[1]='';evr.onTimeout(evr.vrstate);"; this.sendCmd('d',type); this.vrstate=type; }; EasyVR.prototype.sendCmd=function(cmd,arg) { //lastCmd=[cmd,arg]; this.ser.print(cmd); console.log("Sending command: "+cmd); if (arg!==undefined){console.log("With arg: "+this.argchar(arg));this.ser.print(this.argchar(arg));} }; EasyVR.prototype.stop=function(){ this.sts_idx={ "o":[0,"console.log('STS_SUCCESS');"], "t":[0,"console.log('STS_TIMEOUT');"], "v":[0,"console.log('STS_INVALID');"], "i":[0,"console.log('STS_INTERR');"], "e":[2,"console.log('STS_ERROR '+evr.rcvv);evr.onErr(evr.rcvv);"], "s":[1,"console.log('STS_SIMILAR '+evr.rcvv);evr.onErr(evr.rcvv);"], "r":[1,"console.log('STS_RESULT');"] }; this.sendCmd('b'); }; EasyVR.prototype.timeout=function(arg) { this.sendCmd('o',arg); }; EasyVR.prototype.setStrict=function(arg) { this.sendCmd('v',E.clip(arg,1,5)); };
var ocm=function(menu,option) { console.log("menu:"+menu+" option: "+option); if (menu==0) { if (option==0) { console.log("LIGHTS ON"); //do lights on calls } else if (option==1) { console.log("LIGHTS OFF"); //do lights off calls } else if (option==2) { console.log("SWITCH :"); digitalWrite(LED1,1); return {type:2,timeout:15}; } else if (option==3) { console.log("DESK :"); digitalWrite(LED1,1); return {type:3,timeout:15}; } else if (option==4) { console.log("NIXIE :"); digitalWrite(LED1,1); return {type:4,timeout:15}; } } else { if (menu==2) { // toggle a fargo or RF controlled device console.log("toggle device "+option); } else if (menu==3) { // control desk lamp console.log("desk lamp "+option); } else if (menu==4) { // control nixie clock console.log("control nixie clock "+option); } digitalWrite(LED1,0); return {type:1,timeout:0}; } }; var otm=function(){ digitalWrite(LED1,0); this.setRecognize(1,0); }; Serial4.setup(9600,{tx:C10,rx:C11}); var evr=require("easyvr").connect(Serial4,ocm,otm,otm);
Still needs to be cleaned up (and of course, code to do those actions written) , the code is really awkward right now... But it does seem to work, and I will be able to talk to my room, and it will be awesome.
Still needs the global, for the evals, due to issue #513.
-
• #13
Hopefully it's not a big deal to fix the eval... However can you not just define functions rather than strings? You can call them with
fn.call(this)
, and maybe even some arguments...bind() implementation, what is the memory footprint compared to creating an anonymous function instead?
It's not too bad. One memory unit for the function itself, one per parameter, plus one for
this
, one for code, and maybe others if the function was defined inside another function - but everything else is shared (including the code itself).It's going to be a bit better than an anonymous function - check it out with
trace()
:) -
• #14
@Gordon, thanks for the comparison of .bind() vs anonymous function in app.
@DrAzzy, eval() has the smell of a Trojan Horse... Any option to avoid it is useful... Since you do not 'grow' - increase the functionality - of the code, think of your 'pre-defined source code expressions' as prebuilt functions... held in an arrays and object properties (just as you already hold the expressions now).
You can then even think of a plug-in mechanism: pairs of voice recognizable commands and execution functions... a cool thing... (and I guess, will in the end also having this thing talking back to you... audibly...)
Since a while I have on my desk laying around some external audio player modules that I wanted to connect, but I'm bound into something else right now. = I know that Espruino can play wave forms and do that too, but I want offload that part.
How do I make
this
work here?This seems to be set to... an object containing everything. How do I get it to be the object that the function is a property of?
Secondly - with same code, try doing:
evr.onWatch=function(){console.log("roar!!!");} //where evr is an instance of that object
then provoke another serial data event. The callback isn't updated. How can I make it call the function I pointed it at, instead of remembering what said function happened to be when I initially set up the callback? Is this behavior intended?
Thanks