Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
blocks:drivers:example [2018-03-05 21:07] admin created |
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 " | The WOCustomDrvr.ts file demonstrates how to write a driver for a device that provides a TCP " | ||
- | This example driver communicates with Dataton WATCHOUT production software, controlling some basic | + | This example driver communicates with [[https:// |
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:// |
- | + | ||
- | 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: | ||
===== Import Statements ===== | ===== Import Statements ===== | ||
- | At the very top of the file, there are import statements, referencing other files and their content. These other files provide definitions of system functions used to communicate with the device, such as the NetworkTCP interface imported from the " | + | At the very top of the file, there are import statements, referencing other files and their content. |
+ | |||
+ | < | ||
+ | import {NetworkTCP} from " | ||
+ | import {Driver} from " | ||
+ | import * as Meta from " | ||
+ | </ | ||
+ | |||
+ | These other files provide definitions of system functions used to communicate with the device, such as the NetworkTCP interface imported from the " | ||
- | Not only does this provide relevant documentation. It also provides information for the TypeScript compiler as well as for the [[blocks: | + | Not only does this provide relevant documentation. It also provides information for the TypeScript compiler as well as for the [[blocks: |
===== Driver Class Declaration ===== | ===== Driver Class Declaration ===== | ||
Line 35: | Line 42: | ||
===== Instance Variables ===== | ===== Instance Variables ===== | ||
- | At the top of the class, you typically define any variables used by the driver to keep track of its state. Variables are typed; either explicitly, as the pendingQueries which is of type Dictionary< | + | At the top of the class, you typically define any variables used by the driver to keep track of its state. |
+ | |||
+ | < | ||
+ | private pendingQueries: | ||
+ | private mAsFeedback = false; | ||
+ | |||
+ | private mConnected = false; | ||
+ | private mPlaying = false; | ||
+ | private mStandBy = false; | ||
+ | private mLevel = 0; // Numeric state of Output | ||
+ | |||
+ | private mLayerCond = 0; | ||
+ | </ | ||
+ | |||
+ | |||
+ | Variables are typed; either explicitly, as the pendingQueries which is of type Dictionary< | ||
A driver may be used by multiple devices (for instance, controlling three projectors, all of the same brand and model). In this case, each driver instance will get its own private set of instance variables. | A driver may be used by multiple devices (for instance, controlling three projectors, all of the same brand and model). In this case, each driver instance will get its own private set of instance variables. | ||
Line 41: | Line 63: | ||
===== Constructor ===== | ===== Constructor ===== | ||
+ | The driver must have a constructor function. | ||
- | The driver must have a constructor | + | < |
+ | public | ||
+ | super(socket); | ||
+ | ... | ||
+ | </ | ||
- | This parameter | + | The driver constructor has a single |
+ | This parameter must be passed to the //Driver// base class using the //super// keyword, as shown. | ||
==== Event Subscriptions ==== | ==== Event Subscriptions ==== | ||
- | The constructor is a good place for one-time initializations, | + | The constructor is a good place for one-time initializations, |
- | When the subscribed-to event occurs, the function body following the // | + | < |
+ | socket.subscribe(' | ||
+ | this.connectStateChanged(); | ||
+ | }); | ||
+ | </code> | ||
+ | Event subscriptions are similar to how // | ||
- | ==== Property accessors | + | When the subscribed-to event occurs, the function body following the // |
+ | |||
+ | |||
+ | ===== Properties ===== | ||
A key concept of a driver is to encapsulate the intricate details of a device-specific communications protocol, and instead expose the device' | A key concept of a driver is to encapsulate the intricate details of a device-specific communications protocol, and instead expose the device' | ||
Line 60: | Line 96: | ||
* Current input selection | * Current input selection | ||
- | The //power// property of a projector, just like the // | + | The //power// property of a projector, just like the // |
+ | |||
+ | ==== Property Accessors ==== | ||
+ | |||
+ | A property is internally represented by one or two accessor functions. These use the keywords | ||
+ | |||
+ | < | ||
+ | @Meta.property(" | ||
+ | public set layerCond(cond: | ||
+ | if (this.mLayerCond !== cond) { | ||
+ | this.mLayerCond = cond; | ||
+ | this.tell(" | ||
+ | } | ||
+ | } | ||
+ | public get layerCond(): | ||
+ | return this.mLayerCond; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The example defines a property named // | ||
Some properties may not be controllable, | Some properties may not be controllable, | ||
Line 72: | Line 127: | ||
For a numeric property, you can also use //min// and //max// decorators, as shown for the //input// property in the example driver, specifying the minimum and maximum numeric value the property may take. | For a numeric property, you can also use //min// and //max// decorators, as shown for the //input// property in the example driver, specifying the minimum and maximum numeric value the property may take. | ||
+ | < | ||
+ | @Meta.property(" | ||
+ | @Meta.min(0) | ||
+ | @Meta.max(1) | ||
+ | public set input(level: | ||
+ | this.tell(" | ||
+ | this.mLevel = level; | ||
+ | } | ||
+ | public get input() { | ||
+ | return this.mLevel; | ||
+ | } | ||
+ | </ | ||
+ | ===== Callable Functions ===== | ||
+ | |||
+ | Whenever possible, use // | ||
+ | |||
+ | However, some features of a device or protocol may not allow themselves to be exposed as a simple property value. Such features can instead be exposed as a callable function. | ||
+ | |||
+ | < | ||
+ | @Meta.callable(" | ||
+ | public playAuxTimeline( | ||
+ | @Meta.parameter(" | ||
+ | @Meta.parameter(" | ||
+ | ) { | ||
+ | this.tell((start ? "run " : "kill ") + name); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | This example exposes the ability to start and stop an arbitrary timeline by name. While it would be possible to expose a property for each timeline to control this behavior, that would require you to: | ||
+ | |||
+ | * Know the name of each timeline ahead of time, as you write the driver. | ||
+ | * Provide separate get/set functions for the //running// state of each timeline. | ||
+ | |||
+ | In some cases this is neither possible nor desirable. Then you can instead provide a function, taking the desired number of // | ||
+ | |||
+ | - **name**, a string specifying the name of the timeline. | ||
+ | - **start**, a boolean specifying whether to start (true) or stop (false) the timeline. | ||
+ | |||
+ | A function can take any number of parameters of different types. A function marked as // callable// using the decorator shown above will be exposed for use from tasks. The description you provide for the // | ||
+ | |||
+ | :!: While a function can be called from a task and scripts, it can not be directly linked to a button or slider, or used as a task trigger or condition. | ||
+ | |||
+ | |||
+ | ===== Internal Functions ===== | ||
+ | |||
+ | You may want to define functions that are only used inside the driver, as a way of making the code more modular or self documenting, | ||
+ | |||
+ | < | ||
+ | /** | ||
+ | | ||
+ | | ||
+ | | ||
+ | */ | ||
+ | private connectStateChanged() { | ||
+ | this.connected = this.socket.connected; | ||
+ | if (this.socket.connected) | ||
+ | this.getInitialStatus(); | ||
+ | else | ||
+ | this.discardAllQueries(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The // | ||
+ | |||
+ | ===== Parsing Data ===== | ||
+ | |||
+ | A device driver typically does three basic things. | ||
+ | |||
+ | * Exposing desired functionality of the device through // | ||
+ | * Sending commands to the device to control it according to how those properties are set or functions are called, | ||
+ | * Receiving data coming back from the device, indicating its current state, error conditions, or similar. | ||
+ | |||
+ | This last point is often the hardest to get right, and often involves the following steps: | ||
+ | |||
+ | - 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. | ||
+ | - Act intelligently on the data. E.g., handle any errors, or update exposed properties according to a 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. | ||
+ | |||
+ | < | ||
+ | private static kReplyParser = / | ||
+ | </ | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | 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:// | ||
+ | |||
+ | The // | ||
+ | |||
+ | < | ||
+ | private textReceived(text: | ||
+ | const pieces = WOCustomDrvr.kReplyParser.exec(text); | ||
+ | if (pieces && pieces.length > 3) { | ||
+ | const id = pieces[1]; | ||
+ | const what = pieces[2]; | ||
+ | const query = this.pendingQueries[id]; | ||
+ | if (query) { | ||
+ | delete this.pendingQueries[id]; | ||
+ | query.handleResult(what, | ||
+ | } else // No corresponding query found | ||
+ | console.warn(" | ||
+ | } else | ||
+ | console.warn(" | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 ===== | ||
+ | |||
+ | 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: | ||
+ | |||
+ | * Asking a question that will be replied to by the device in due course. | ||
+ | * 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 maximum time to elapse, you must instead use a [[https:// | ||
+ | |||
+ | < | ||
+ | private ask(question: | ||
+ | if (this.socket.connected) { | ||
+ | const query = new Query(question); | ||
+ | this.pendingQueries[query.id] = query; | ||
+ | this.socket.sendText(query.fullCmd); | ||
+ | return query.promise; | ||
+ | } else | ||
+ | console.error(" | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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, | ||
+ | |||
+ | The // | ||
+ | |||
+ | < | ||
+ | private getInitialStatus() { | ||
+ | this.ask(' | ||
+ | this.mAsFeedback = true; // Calling setters for feedback only | ||
+ | const pieces = reply.split(' | ||
+ | if (pieces[4] === ' | ||
+ | // Go through setters to notify any change listeners out there | ||
+ | this.playing = (pieces[7] === ' | ||
+ | this.standBy = (pieces[9] === ' | ||
+ | } | ||
+ | this.mAsFeedback = false; | ||
+ | }); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 // | ||
+ | |||
+ | |||
+ | ===== Custom Classes ===== | ||
+ | |||
+ | For more complex functionality, | ||
+ | |||
+ | < | ||
+ | class Query { | ||
+ | ... | ||
+ | </ | ||
- | + | 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 // |