ProView provides a nice I/O API. All devices are controlled through a pair of API calls for reading and writing data from signals. You cannot use channels directly, so every channel should be bound to signals for I/O. All write requests are made using io_write and read requests are made using io_read, where both methods have their context in the ProView application. All devices that should be controlled through any ProView application should use the Process flag set to User2 with value 8. The problem is how to deal with I/O calls centralized in two API calls rather than using device specific I/O calls. Your code should be consistent enough to support that I/O method.
We are currently using Hierarchical State Machines, and once a state requires channel usage, the state machine stops its executions, runs the I/O calls io_read and io_write and continues its execution on the next scan cycle. Device synchronization is using single boolean variables holding read/write states as semaphores and reviewed on each task request. The entire framework is designed as it is running a PLC program, so you have a scan → read → write cycle. All state machines are pausing its execution on channel requests because they must not use the communication channel concurrently and we cannot use a locking mechanism.
We have logical composite modules to hold the device structure, where a device uses the same physical levels as rack, card and channel, but with its logical level as rack, card and signal. This allows a better organization and binding of devices along the project structure. Modularity is important, so we are using device classes to hold each device structure. Channels remain untouched, and we are writing and reading data exclusively from signals, despite we know some calls to access channels directly.
The device organization is pretty clear. Since we are using ModBus, we organize signal groups according to register groups in the device for each register block that we want to read or write. Even more for coil registers, where the are almost organized in groups, thanks to the class BaseComponent, which allows that kind of abstraction, rather than using ModBus modules and cards directly.