Search the Community
Showing results for tags 'application design'.
I've built (and re-built) a medium-sized application over the past months, learning more about LabVIEW in the process. The design is simplistic and reaching its limits by now, so I've been re-thinking the design lately. I'm looking for feedback on my design choices before I start implementing... all criticism and tips are welcome! The goal: flexible software for an ever-changing university laboratory environment, to be used and extended by students/staff with limited LabVIEW skills. Decisions: Avoid LVOOP unless clearly called for OOP has it's benefits but it makes code more complicated to grasp for novices. Avoid asynchronous commands unless clearly called for Many hardware operations are synchronous; these should be carried out synchronously (with time-outs) to minimize complexity. No message queues, definitely no Command architecture. Of course, queues are still OK for inter-thread communication, e.g. a data acquisition thread in a DAQ module. Modular Model-View-Controller (MVC) paradigm This should facilitate our need for extendible and adaptable software Every module needs to be able to run independently A central GUI will usually be running, but it should be possible to start/operate any module separately during run-time. This simplifies debugging and allows for flexibility during measurements ('let's quickly check the signal by hooking up a scope') Every module must be able to 'require' and 'unrequire' other modules If a measurement module is loaded, any required equipment should be initialized unless it's already running. A module should gracefully shut down when there are no modules left requiring it This is vital to prevent equipement from melting during the night! What I've come up with (core ideas are underlined): MVC implementation Each module is a lvlib containing Typedefs, Public Functions, Private functions, and Panels. The Model is a typedef'd cluster containing Settings, Data, Events, and Resources (queues, refs, etc.) The Model cluster is stored as a Data Value Reference (DVR) in a Functional Global Variable (FGV). The "First Call?" functionality within the FGV is used to initialize the Model (read Settings from a .ini file, create User Events, spawn background threads, etc.) and create the DVR. I could be re-inventing the wheel here, but I'm pretty enthusiastic about this approach. Note that the Model could alternatively be stored in a Class instance holding the DVR. This is useful in applications where you expect multiple copies of the same module (maybe you have six DAQs attached) but it complicates application design as you need to specify which instance you'd like to access before each Controller function call. The Controller is a set of synchronous functions operating on the Model. Each function obtains the Model DVR from the FGV, and uses an in-place element structure (IPES) with DVR Read/Write terminals to access and modify the data. Note that this provides some protection against race conditions, and reduces memory usage. Inside this IPES could be a second IPES, with Cluster Unbundle/Bundle terminals, to increase readability and again reduce memory usage. All Controller functions that change the Model generate appropriate User Events when doing so. Views are panels that execute public Controller functions to interact with the Model. The View registers for (a subset of) the User Events emitted by the Controller to reflect changes in the Model. Modularity Each module has public Require and Unrequire functions. These maintain a list of dependent modules, and call a (private) Stop function if the list is empty. Right now, I'm simply using the Call Chain string to identify the calling module, but there should be a more appropriate solution for this (in light of OOP, where the Call Chain is no longer unique). A View Requires its own Controller, and Unrequires it on Panel Close. A controller can Require other modules, which it must Unrequire when it no longer needs them. Hardware interfaces, software components, and measurements are all modules. Practical Example User runs the View (panel) of a Measurement Module, which starts the related Controller. The measurement controller Requires some hardware (say a DAQ board and a camera) and software (say an Error Logger) and loads certain Views (panels) to them into SubPanels (e.g. it display the Error Log). The Required modules in turn Require other modules (the camera Requires an Image Processor) or the same modules (the camera also Requires the Error Logger) which are initialized if not already running. Uses carries out his measurement, calling public functions on his required controllers. All changes are immediately reflected in the open Views through event handling. User stops his measurement and closes the view. All required modules (the DAQ, Camera, and Error Logger) are Unrequired. In turn, these models Unrequire their respective Required modules; the Camera Unrequires the Image Processor, etc. All modules shut down gracefully when there are no modules left requiring them. Concerns Unexpected errors may break the require/unrequire chain, causing modules to run indefinitely. Similarly, if a users forgets to Unrequire a module, it will live on (possibly without a panel) indefinitely. A sort of application-wide emergency stop system (using events or notifiers) could be used to 'fix' this, but I'd like to refrain from using any central structures of possible. Controller lifetime. Say two modules Require the same third module. The third module will be 'owned' by the first caller, causing it to abort when the first caller leaves memory. This leaves the second caller module with a problem. I think this can be prevented by having each controller maintain a reference to itself, which it only releases upon graceful shutdown. Will this work, and will it be reliable? Stop code in a View Panel (notably Unrequiring its controller) might not run. Again, I think this can be circumvented by having the panel maintain a reference to itself, which is only released after calling graceful shutdown code in the Panel Close event case. Wow, that sure turned into a short novel. If you're still with me, I'd love to hear your thoughts!
I've recently built a medium-sized application that controls a bunch of devices in a university laboratory. From the start, I've been focussed on writing a modular, extensible program, as people tend to add or remove equipment and write new measurement routines accordingly. In my architecture, each device is represented by an Action Engine (i.e. Keithley Controller.vi). The UI is comprised of subpanels that contain a simple UI for a single device (i.e. Keithley Panel.vi). Due to the nature of AE's, any vi can send commands to any device; data changes are communicated to the panel through User Events. This all works really well. Creating new measurements is extremely simple, which is vital as most of my colleagues and students are LabVIEW novices. Adding a device is relatively simple too, as Action Engines are easily understood. And now, the downsides. Mainly, there is a lot of code duplication in this architecture. Many AE's look very similar, with actions like Set VISA Address popping up in every single device. Moreover, imagine having two identical Keithleys (a very real prospect)... one would have to duplicate each vi to create Keithley A and Keithley B, which seems silly indeed. Also, I don't particularly like the extensive use of Variants to limit the number of inputs on the AE. All this tells me it's time for LVOOP, but so far I haven't found a satisfactory design pattern for my use case. The problem is that classes in LabVIEW are by-data instead of by-reference. Somehow, I need to get class instances of my devices across to different parts of my application (including measurement plug-ins), without having them operate on different copies of the instance. Solutions to this problem seem to come in two flavours: communicate to each instance via a command queue (Actor Framework) or operate on references (DVR, Single Element Queue). I find both approaches unsatisfactory: the former buries the elegance of a class (properties and methods) under a pile of asynchronous mumbo-jumbo I don't need, while the latter can introduce deadlocks and forces my users (students and co-workers who will create measurement routines) to place all calls inside de-referencing blocks (be it an In-Place Element structure or a Checkin-Checkout procedure). Finally, I should mention that I'm aware of the existence of GOOP, which seems to enable by-reference OOP in LabVIEW, but I would like to stick to native solutions if at all possible. Is there a way of harnessing the power of OOP (inheritance, instancing) in LabVIEW, without moving to asynchronous designs or potential deadlocks? Where do I go from here? PS: my apologies if this turned into something of a rant, but shifting to the OOP paradigm in LabVIEW seems overly complicated compared to other languages.
I have some projects on the horizon that have to be more reliable than anything I have done in the past. And this new requirement has me thinking about how I deal with errors in LabVIEW more and more. Simultaneously I have been looking into the Actor Framework. The sample project in LabVIEW 2012 for the Evaporative Cooler does a great job of showing how many of the pieces of a full application fit together. However, as I have studied the example, I have noticed that in many places error handling has simply been ignored. An example of this is in the water level control... specifically, I take issue with the word "guarantee". Documentation states: "Water Level is a concrete implementation of a Level Controller. We will use it to guarantee that the water in our evaporative cooler’s reservoir never falls below a minimum safe level." What if the building loses water pressure? Or the piping fails? Or the valve doesn't actually respond? Or any of a million scenarios that result in the program being unable to really guarantee anything. I'd like to open a discussion of how the Evaporative Cooler example could improve on its error handling so that it was robust enough to safely deal with all the "what ifs" of the world. This question is covering everything from "what if writing to the value property of a UI indicator fails?" to "what if the building water supply fails and the water level doesn't respond when I command the valve?" And those seem like two great places to start! In the context of the Evaporative Cooler example project, I found three VIs whose error handling I would like to discuss: 1. Timed Loop Actor.lvlib:Timed Loop Actor.lvclass:Read Controller Name.vi 2. Live User Controller.lvclass:Update Current Temperature.vi 3. Level Controller.lvlib:Level Controller.lvclass:Update Core.vi In the case of #1, the VI does not have error in/out terminals, but some of the VI's it calls does. This is a situation I have encountered before: how do you add error handling to a very low level VI that does not (and for the sake of discussion cannot) have error terminals? In the case of #2, the VI writes to a property node and ignores the error terminals. In a big application there are soooo many property nodes! What are the implications of ignoring the errors? When is it safe to ignore them? This is probably very common, and has some overlap with #1... In general, in LabVIEW, when can you ignore error terminals? #3 is the logic VI that determines whether to open or close a valve based on water level. But it doesn't keep track of time and whether things are "working". And certainly if you put a human operator in place of the VI, the human would after some time decide that opening the valve hadn't helped the water level. Can the VI mimic this? For all of this error handling business, I think I am really asking four questions: A) What should the software do on its own to respond to the error? B) What should the software tell the operator (who might have never heard of LabVIEW or college) about the error? C) What should the software tell the operator's supervisor about the error? D) What should the software tell the software developer about the error? It is pretty easy to write an application when everything works the way it should! But when things might go haywire, how do you write application then?