03-21-2013 03:28 PM - edited 03-21-2013 03:43 PM
Hello all.
I've designed a framework for managing communication among asynchronous loops (did we really need another one?!). I thought I'd post it up here for critique. I have a small example program that uses the framework, which I'll explain at the bottom of this post. It was last saved in LV2012.
Some background -
I started working on this before I learned about the Actor Framework, however I decided to continue using it because it offered me some flexibility that I felt was absent and/or difficult to do with AF.
This framework came out of a test suite I am building for testing multiple devices simultaneously. My #1 criteron was that I wanted to be able to store test sequences as plain text files. While LabVIEW is a great platform, I feel it is much easier to create, understand, and modify a test sequence that is stored as text, instead of graphical blocks.
The framework at heart is built around a queued message handler and state machine, however those details are encapsulated in the Message and Service classes. The result is a framework in which the order of execution of cases can be separated from the programming logic. The framework doesn't actually need to be controlled using text commands, and is fully functional using graphical programming blocks for sending and receiving data. The design of all the components however facilitates the use of what I'm calling Service Script.
Design details -
I'll just give a brief description, and if anyone is interested I can explain more.
A Service is a reentrant, asynchronous call-and-forget loop. They're started and stopped using Start.vi and Stop.vi. Communication among services is performed by sending and receiving instances of the Message class. A message can be sent asynchronously using SendAsynchronousMessage.vi, or synchronously using SendSynchronousMessage.vi. A message must contain a destination and a state. It may also contain data as an associative array (key-value pairs). Messages can have multiple destinations that are executed serially. A service is implemented by overriding Exec.vi and adding cases to the case structure. The names of services correspond to destinations and the names of the cases in the case structure correspond to the states in a message.
Service script -
Lexer.vi and ParserMessageComposer turn text commands into Messages. The syntax is as follows:
destination1:state1(key1="value1",key2="value2"...) > destination2:state1(key1=value1) > ...
Values are parsed into their data type, i.e. "value" is a string, true/t/false/f are booleans, 5 is an int32, 5.2 is a double.
The > designates additional hops the message will take. It also allows piping data. The return value from the previous method will be stored in the value named _response. For example:
database:put(number=5)
database:pull(key="number") > math:add(number1=3, number2=_response)
Where "pull" is a method built into all services that gives read access to the variable store. Other built-in methods are "put", which gives write access, and "wait", which pauses all queued operations for the given time in seconds. While waiting, a service will still process incoming messages.
Pitfalls -
Because there is no type checking here, you wont discover problems until run time. Because lots of things are reentrant, you'll have a hard time tracing execution unless you turn off reentrant execution. Errors can be hard to catch/decipher, especially if the messaging system breaks down. I'm still working on improving error handling.
I've included a small example with the logging service that shows how one would start, stop, and interact with a service. It also shows how error logging is performed. In a subpanel I've included a console utility that allows input of service script in a console-like environment.