Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
blocks:drivers:example [2018-03-06 06:56]
admin
blocks:drivers:example [2023-06-12 11:11] (current)
admin Clarified connect callback parameters
Line 3: Line 3:
 The WOCustomDrvr.ts file demonstrates how to write a driver for a device that provides a TCP "socket" based control protocol. The WOCustomDrvr.ts file demonstrates how to write a driver for a device that provides a TCP "socket" based control protocol.
  
-This example driver communicates with Dataton WATCHOUT production software, controlling some basic+This example driver communicates with [[https://www.dataton.com/products/watchout|Dataton WATCHOUT]] production software, controlling some basic
 functions. It's provided only as an example, since Blocks already has full functions. It's provided only as an example, since Blocks already has full
 support for controlling WATCHOUT built-in. Using WATCHOUT as an example device support for controlling WATCHOUT built-in. Using WATCHOUT as an example device
Line 10: Line 10:
   - Many of you are already familiar with WATCHOUT.   - Many of you are already familiar with WATCHOUT.
   - The production software UI clearly shows what's going on.   - The production software UI clearly shows what's going on.
-  - It's available as a free download, so anyone can use it to try out the driver+  - It's available as a [[https://www.dataton.com/downloads/watchout|free download]], so anyone can use it to try out the driver.
- +
-Open the driver using the Atom editor, and follow along in the code as you read this walk-through.+
  
 +Open the driver using a [[blocks:drivers:tools|code editor]], and follow along in the code as you read this walk-through.
 ===== Import Statements =====  ===== Import Statements ===== 
  
Line 24: Line 23:
 </code> </code>
  
-These other files provide definitions of system functions used to communicate with the device, such as the NetworkTCP interface imported from the "system/Network" file. You can navigate to referenced classes or files by command-clicking (Mac) or control-clicking (Windows) these items in the Atom editor. The referenced files contain the relevant declarations, and often also contain comments that further describe the referenced functions.+These other files provide definitions of system functions used to communicate with the device, such as the NetworkTCP interface imported from the "system/Network" file. You can navigate to referenced classes or files by command-clicking (Mac) or control-clicking (Windows) these items in the editor. The referenced files contain the relevant declarations, and often also contain comments that further describe the referenced functions.
  
-Not only does this provide relevant documentation. It also provides information for the TypeScript compiler as well as for the [[blocks:drivers:tools|Atom]] editor. The Atom editor uses this information to guide you when writing code, often providing hints as you type, showing available functions, parameters, etc. If you make a mistake, this is often highlighted in red by the Atom editor and/or flagged by the TypeScript compiler.+Not only does this provide relevant documentation. It also provides information for the TypeScript compiler as well as for the [[blocks:drivers:tools|code editor]]. The code editor uses this information to guide you when writing code, often providing hints as you type, showing available functions, parameters, etc. If you make a mistake, this is often highlighted in red by the code editor and/or flagged by the TypeScript compiler.
  
 =====  Driver Class Declaration =====  =====  Driver Class Declaration ===== 
Line 47: Line 46:
 <code> <code>
 private pendingQueries: Dictionary<Query> = {}; private pendingQueries: Dictionary<Query> = {};
-private mAsFeedback = false; // Set while processing feedback, to only fire events+private mAsFeedback = false;    // Set while processing feedback, to only fire events
  
-private mConnected = false; // Connected to WATCHOUT +private mConnected = false;    // Connected to WATCHOUT 
-private mPlaying = false; // Most recent state (obtained from WO initially)+private mPlaying = false;    // Most recent state (obtained from WO initially)
 private mStandBy = false; private mStandBy = false;
-private mLevel = 0; // Numeric state of Output+private mLevel = 0;        // Numeric state of Output
  
 private mLayerCond = 0; private mLayerCond = 0;
Line 68: Line 67:
 <code> <code>
 public constructor(private socket: NetworkTCP) { public constructor(private socket: NetworkTCP) {
 +    super(socket);
 +    ...
 </code> </code>
  
Line 73: Line 74:
  
 This parameter must be passed to the //Driver// base class using the //super// keyword, as shown. This parameter must be passed to the //Driver// base class using the //super// keyword, as shown.
- 
 ==== Event Subscriptions ==== ==== Event Subscriptions ====
  
Line 80: Line 80:
 <code> <code>
 socket.subscribe('connect', (sender, message)=> { socket.subscribe('connect', (sender, message)=> {
- this.connectStateChanged();+    this.connectStateChanged();
 }); });
-</code<+</code>
  
-Event subscriptions are similar to how //addEventListener// is used in web browsers to listen to DOM events. The 'connect' event indicates that the connection status of the socket has changed. All events are described in the Network.ts file. Jump directly to the relevant declaration by command/control-clicking a //subscribe// call. +Event subscriptions are similar to how //addEventListener// is used in web browsers to listen to DOM events. The 'connect' event indicates that the connection status of the socket has changed or failed (as indicated by the type field of the message parameter). All events are described in the Network.ts file, so always look there for full details. Jump directly to the relevant declaration by command/control-clicking a //subscribe// call. 
  
 When the subscribed-to event occurs, the function body following the //[[https://basarat.gitbooks.io/typescript/docs/arrow-functions.html|lambda]]// => operator will be invoked. Either do what needs to be done right here, if its only a line or two, or call a function defined elsewhere in the driver. When the subscribed-to event occurs, the function body following the //[[https://basarat.gitbooks.io/typescript/docs/arrow-functions.html|lambda]]// => operator will be invoked. Either do what needs to be done right here, if its only a line or two, or call a function defined elsewhere in the driver.
Line 105: Line 105:
 @Meta.property("Layer condition flags") @Meta.property("Layer condition flags")
 public set layerCond(cond: number) { public set layerCond(cond: number) {
- if (this.mLayerCond !== cond) { +    if (this.mLayerCond !== cond) { 
- this.mLayerCond = cond; +        this.mLayerCond = cond; 
- this.tell("enableLayerCond " + cond); +        this.tell("enableLayerCond " + cond); 
- }+    }
 } }
 public get layerCond(): number { public get layerCond(): number {
- return this.mLayerCond;+    return this.mLayerCond;
 } }
 </code> </code>
Line 132: Line 132:
 @Meta.max(1) @Meta.max(1)
 public set input(level: number) { public set input(level: number) {
- this.tell("setInput In1 " + level); +    this.tell("setInput In1 " + level); 
- this.mLevel = level;+    this.mLevel = level;
 } }
 public get input() { public get input() {
- return this.mLevel;+    return this.mLevel;
 } }
 </code> </code>
Line 149: Line 149:
 @Meta.callable("Play or stop any auxiliary timeline") @Meta.callable("Play or stop any auxiliary timeline")
 public playAuxTimeline( public playAuxTimeline(
- @Meta.parameter("Name of aux timeline to control") name: string, +    @Meta.parameter("Name of aux timeline to control") name: string, 
- @Meta.parameter("Whether to start the timeline") start: boolean+    @Meta.parameter("Whether to start the timeline") start: boolean
 ) { ) {
- this.tell((start ? "run " : "kill ") + name);+    this.tell((start ? "run " : "kill ") + name);
 } }
 </code> </code>
Line 182: Line 182:
  */  */
 private connectStateChanged() { private connectStateChanged() {
- this.connected = this.socket.connected; // Propagate state to clients +    this.connected = this.socket.connected; // Propagate state to clients 
- if (this.socket.connected) +    if (this.socket.connected) 
- this.getInitialStatus(); +        this.getInitialStatus(); 
- else +    else 
- this.discardAllQueries();+        this.discardAllQueries();
 } }
 </code> </code>
Line 204: Line 204:
   - Provoke the device to send the data you're interested in. In most cases, devices only send data in response to commands. This can either be an acknowledgement of the command (or an error message if the command failed for some reason), or a reply to a question posed by the command.   - Provoke the device to send the data you're interested in. In most cases, devices only send data in response to commands. This can either be an acknowledgement of the command (or an error message if the command failed for some reason), or a reply to a question posed by the command.
   - Parse out the part of the data coming from the device you're interested in.   - Parse out the part of the data coming from the device you're interested in.
-  - Act intelligently on the data. E.g., handle any errors, or update exposed properties according to an unexpected change in device state.+  - Act intelligently on the data. E.g., handle any errors, or update exposed properties according to change of some state in the device.
  
 For devices that communicate using a text based protocol, regular expressions often come in handy when parsing the data. For devices that communicate using a text based protocol, regular expressions often come in handy when parsing the data.
Line 212: Line 212:
 </code> </code>
  
-[[https://en.wikipedia.org/wiki/Regular_expression|Regular expressions]] are directly supported by the [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions|language]]. Just as strings are delimited by single or double quotes, regular expression are delimited by an opening and closing forward slash.+[[https://en.wikipedia.org/wiki/Regular_expression|Regular expressions]] are [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions| directly supported]] by the scripting language. Just as strings are delimited by single or double quotes, regular expression are delimited by forward slash.
  
-While the regular expression syntax is very powerful, it is also often hard to read and understand. When developing and testing a regular expression, you may therefore want to use an online service such as [[https://regex101.com|this one]].+While the regular expression syntax is quite powerful, it is also often hard to read and understand. When developing and testing a regular expression, you may therefore want to use an online service such as [[https://regex101.com|this one]]. Here you can enter sample data, representative of what's sent by the device, and work on the regular expression until it does what you want. Then copy the resulting expression and paste it into the driver.
  
-The //textReceived// function uses the above regular expression to parse the replies received from WATCHOUT, handing those off to associated queries.+The //textReceived// function uses the regular expression shown above to parse replies received from WATCHOUT, handing the results off to their associated queries for further processing
  
 <code> <code>
 private textReceived(text: string) { private textReceived(text: string) {
- const pieces = WOCustomDrvr.kReplyParser.exec(text); +    const pieces = WOCustomDrvr.kReplyParser.exec(text); 
- if (pieces && pieces.length > 3) { +    if (pieces && pieces.length > 3) { 
- const id = pieces[1]; +        const id = pieces[1]; 
- const what = pieces[2]; +        const what = pieces[2]; 
- const query = this.pendingQueries[id]; +        const query = this.pendingQueries[id]; 
- if (query) { +        if (query) { 
- delete this.pendingQueries[id]; // Now taken +            delete this.pendingQueries[id]; // Now taken 
- query.handleResult(what, pieces[3]); +            query.handleResult(what, pieces[3]); 
- } else // No corresponding query found +        } else // No corresponding query found 
- console.warn("Unexpected reply", text); +            console.warn("Unexpected reply", text); 
- } else +    } else 
- console.warn("Spurious data", text);+        console.warn("Spurious data", text);
 } }
 </code> </code>
  
 +The fact that WATCHOUT allows you to tag each question, and then returns that same tag in the associated reply makes it easy to match them up. That's not always the case. Also, WATCHOUT accepts numerous commands sent back to back, possibly intermixed with questions. Many devices have limits on how fast you can send commands. Sometimes you need to wait for a command to be acknowledged by some returned data before you can send further commands. In other cases, you may have to limit how fast you send commands, and the maximum command rate may not be well documented. 
 ===== Promises ===== ===== Promises =====
  
-Communicating with a device often means doing something at one point that results in some related action at a later point:+Communicating with a device often means sending a command at one point in time that results in a reply or other action later on. For example:
  
-  * Sending a question to the device that will be replied to by the device in due course. +  * Asking a question that will be replied to by the device in due course. 
-  * Initiating an action that after some pre-determined time will be followed up by some other action, such as sending a command that is expected to return an acknowledge ore reply within one second, and then doing something else if this reply hasn't arrived within this timeframe.+  * Giving a command that is expected to cause some result, and then doing something else if the result hasn't arrived in time.
  
-Since you can not (and **must not**) have your code sit tight and wait for the answer to come back, or for the time to elapse, you must instead use a [[https://scotch.io/tutorials/understanding-javascript-promises-pt-i-background-basics|Promise]] to deal with such situations.+Since you can not (and **must not**) have your code sit tight and wait for the answer to come back, or for the maximum time to elapse, you must instead use a [[https://scotch.io/tutorials/understanding-javascript-promises-pt-i-background-basics|Promise]] to deal with such situations.
  
 +<code>
 private ask(question: string): Promise<string> { private ask(question: string): Promise<string> {
- if (this.socket.connected) { +    if (this.socket.connected) { 
- const query = new Query(question); +        const query = new Query(question); 
- this.pendingQueries[query.id] = query; +        this.pendingQueries[query.id] = query; 
- this.socket.sendText(query.fullCmd); +        this.socket.sendText(query.fullCmd); 
- return query.promise; +        return query.promise; 
- } else +    } else 
- console.error("Can't ask. Not connected");+        console.error("Can't ask. Not connected");
 } }
 +</code>
  
-A //promise// is an object that tracks the future outcome of the action. When this future outcome becomes clear, the promise will call a function provided by you. Alternatively, if the action fails, it may call another function to deal with that situation.+A //promise// is an object that tracks the future outcome of the action. When this future outcome becomes clear, the promise will call a function provided by you. Alternatively, if the action fails, it calls another function to deal with that situation.
  
 The //getInitialStatus// function shows how a promise can be used. The //getInitialStatus// function shows how a promise can be used.
Line 261: Line 263:
 <code> <code>
 private getInitialStatus() { private getInitialStatus() {
- this.ask('getStatus').then(reply => { +    this.ask('getStatus').then(reply => { 
- this.mAsFeedback = true; // Calling setters for feedback only +        this.mAsFeedback = true; // Calling setters for feedback only 
- const pieces = reply.split(' '); +        const pieces = reply.split(' '); 
- if (pieces[4] === 'true') { // Show is active +        if (pieces[4] === 'true') {    // Show is active 
- // Go through setters to notify any listeners out there +            // Go through setters to notify any change listeners out there 
- this.playing = (pieces[7] === 'true'); +            this.playing = (pieces[7] === 'true'); 
- this.standBy = (pieces[9] === 'true'); +            this.standBy = (pieces[9] === 'true'); 
- +        
- this.mAsFeedback = false; +        this.mAsFeedback = false; 
- });+    });
 } }
 </code> </code>
  
-Here you can see how the //then// function is called to pick up the outcome of the question asked. Note that the body inside the function won't be invoked until the reply comes back.+Here you can see how the //then// function is called to pick up the outcome of the question asked. Note that the body inside the function won't be invoked until the reply to the //getStatus// question comes back from WATCHOUT.
  
  
 ===== Custom Classes ===== ===== Custom Classes =====
  
-For more complex functionality, you can define your own classes inside a driver, or in a library module (a separate TypeScript file store in the //script/lib// directory. Define the class inside the driver file if it's only for use by that driver. Store more general purposes classes in the //script/lib// directory, allowing them to be used from any driver.+For more complex functionality, you can define your own classes inside a driver, or in a library module (a separate TypeScript file store in the //script/lib// directory). Define the class inside the driver file if it's only used by that driver. Store more general purposes classes in ther own files in the //script/lib// directory, allowing them to be used from multiple drivers.
  
 <code> <code>
 class Query { class Query {
- ...+    ...
 </code> </code>
  
 Just like the driver itself, such classes define their own instance variables, constructor and functions. You instantiate objects of your custom class using the //new// keyword, as shown above in the example under //Promises//. Just like the driver itself, such classes define their own instance variables, constructor and functions. You instantiate objects of your custom class using the //new// keyword, as shown above in the example under //Promises//.