fermi-ad / acsys-python

Python module to access the Fermilab Control System
MIT License
8 stars 4 forks source link

Spitballing new API ideas #18

Open kjhazelwood opened 3 years ago

kjhazelwood commented 3 years ago

Quick thought on potential API

Connection = acsys.get_connection()

#Background asynchronous job using best node and handler methods
def main():
     with (DPM(connection, node=None)) as dpm:
          dpm.add_entry(id, drf_request, handler_method=handler_method, handle_args=handler_args)
          ....
          dpm.start()
          dpm.wait(timeout=None) #optional synchronous wait until initialized
          dpm.cancel(timeout=None) #optional, asynchronous, kills dpm entry jobs after completion if not periodic or after timeout if periodic
          dpm.stop()
          dpm.restart() #optional, restarts same job if not periodic, can not be called if cancel was called

#One off synchronous job using particular dpm node
def button_pushed():
     with (DPM(connection, node=node_name)) as dpm:
          dpm.add_entry(id, drf_request, handler_method=None, handler_args=None) #No handler method specified so this is a synchronous request, very useful for button pushes as you may want the dpm job to complete before anything else occurs
          dpm.start()
          dpm.wait()

          for i, item in enumerate(dpm.get()):
                #Do something with data

          dpm.stop()

def handler_method(handler_args[0]..., **items):
     #Do something with key value pairs items

Inside dpm start() the code could group like handler_methods and attach it to a Thread. Maintain threads in a pool so that the number of threads is manageable and that dead time between thread executions allow threads to pop.

rneswold commented 3 years ago

Seems like most of what you want is available now.

When you create a DPMContext, it's doing a service discovery and opening a list to the found DPM. At this point, there's a connection but nothing is being sent in either direction. Your script's initialization could do the set-up:

async def main(con):
    with DPMContext(con) as dpm_periodic:

        await dpm_periodic.add_entry(...)
        await dpm_periodic.add_entry(...)

        with DPMContext(con) as dpm_button:
            await dpm_button.add_entry(...)

            # Collection doesn't start on either DPM connection
            # until .start() is called.

            await main_loop(dpm_periodic, dpm_button)

You can build your button's handler to use the pre-made context so you're not doing service discoveries each time the button is pressed:

def mk_button_handler(dpm):
    async def button_pushed():
        await dpm.start()
        async for ii in dpm.replies():
            # Do something with button-related data. Exit
            # this loop when done with data.
        await dpm.stop()

    return button_pushed

If your script raises an exception up through the with-statements, you'll lose your connection with DPM. But if you handle everything in main_loop(), then you DPM contexts stay valid and you can simply start and stop acquisition as you see fit. main_loop() might look like this:

async def main_loop(dpm_p, dpm_b):
    # register button handler with GUI.

    register_button_handler(mk_button_handler(dpm_b))

    # Start-up DPM data collection for the main loop. Presumably
    # this is data you want to collect constantly during execution.

    await dpm_p.start()
    async for ii in dpm_p.replies():
        # handle periodic data
rneswold commented 3 years ago

The current API closely follows the underlying DPM protocol. I was hoping for an API that was a little more exotic. For instance:

# Get devices. If a device doesn't exist, an exception will be raised.

d_temp = Device('M:OUTTMP')
d_humid = Device('G:HUMID')

# Create a collection rate context

a = Acquisition('e,8f')

# Now enjoy correlated data

async for (ts, v_temp, v_humid) in a.start(d_temp, d_humid):
    print(f'timestamp: {ts}, temperature: {v_temp}, humidity: {v_humid}')