_images/alpaca128.png

Developer Roadmap

It’s suggested that you make an Alpaca driver for a single ASCOM device type and a single instance of that device, using the sample Rotator driver as a guide. Before doing this, though, it’s good to know how the moving parts fit together. This roadmap is written with that in mind.

Note

If you are more inclined to dive in and sink or swim, follow the steps in Creating Your First Driver then when you get mixed up, don’t know what’s what, you can come back here and take the time to come up to speed on the structure and usage of the moving parts.

Logging Support

The boilerplate logic includes an app-wide logging function. See Master Logger Support for details. Calls to logger.info(), logger.debug(), etc., write to a file alpaca.log. See Logging HOWTO (Python).

Logging Level

Normally, the logger will log at level INFO. This means info and higher messages will be logged, and debug ones will not. See Logging HOWTO (Python). To change the logging level, edit config.toml at log_level. Note that config.py converts the text level name (e.g. INFO) to the numeric value needed. So you can use INFO, DEBUG, WARNING, etc.

Rotation of Logs

Each time the app is started, the existing alpaca.log is renamed alpaca.log.1. Next ime that one will be renamed alpaca.log.2 and the alpaca.log will be renamed alpaca.log.1 with the newest log being alpaca.log. The logger will kee up to 10 generations, but you can change this in config.toml as num_keep_logs. Logs will be rotated if the size exceeeds 5MB, but you can change this in config.toml as max_size_mb.

Logging to the Console

The logger normally writes only to alpaca.log since a device driver will not normally run with an interactive console. However for debugging purposes, you may wish to have it log to the console (stdout) as well as the file. This is set in config.toml as log_to_stdout.

Alpaca Driver Request Flow - Responder Classes

Incoming HTTP/REST requests from the client are routed through the server by Falcon, and result in calls to the responder classes for the device API. For example, an app would send the following HTTP request to see if the rotator is moving:

/api/v1/rotator/0/ismoving?ClientID=xxx&ClientTransactionID=yyy

Here is the code to handle a simple request for the Rotator’s IsMoving property.

_images/ismoving.png

Preprocessor

@before - This is a decorator PreProcessRequest() which is applied to all responder classes. TIts job is to quality check the request. It rejects illegal values for Alpaca ClientID and ClientTransactionID. It also checks that the DeviceNumber in the request is a valid integer, and in range for the maximum device number (0 by default). If any of these tests fail, it raises an HTTPBadRequest [1] with a body containing a specific error message.

If your driver supports multiple instances of your device (e.g. multiple focusers) then the maxdev variable in your device responder code (e.g. focuser.py) will need to be one less than the number of instances you support.

Note

Raising an HTTPBadRequest [1] anywhere within a responder, including within the low-level device logic or a decorator, immediately abandons processing and and sends an HTTP 400 Bad Request response back to the client. It cannot be an Alpaca response (which would have a 200 OK status) because the request is not even a legal Alpaca request.

GET responder

Once the request is deemed Alpaca-legal by the pre-processor, the responder’s on_get() method is called if the API request is for a GET (get the value of a property). The first thing you see is a call into the rotator device to see if it is connected. If not, it’s an Alpaca NotConnectedException. Then it tries to read the position from the rotator device. If an exception is raised from within the device code, it’s caught with a generic except and results in an Alpaca DriverException. Otherwise, it uses a PropertyResponse object to construct the JSON for an Alpaca property response, including the retrieved position value. For example:

Alpaca property response
{
    "Value": true,              // It's moving
    "ClientTransactionID": 321,
    "ServerTransactionID": 1,   // Automatically bumped by PropertyResponse
    "ErrorNumber": 0,           // Success
    "ErrorMessage": "",
}

It sets the Response.text to the above Alpaca JSON, and returns to Falcon, which sends the response to the remote app the JSON as the HTTP body with a 200 OK status. That’s it!

PUT Responder

Alpaca API method calls, those which do something, use the HTTP PUT method. Here is the responder code for MoveAbsolute:

_images/moveabsolute.png

The main thing to note here is that the parameter for the method comes in the HTTP body of the PUT as “form data”. The boilerplate function get_request_field() handles getting parameter text from the PUT body, including capitalization requirements, raising an HTTPBadRequest exception if anything goes wrong. The PUT responder uses the MethodResponse class to construct the JSON response. We’ll cover the more detailed exception handling in the next section.

Alpaca Exceptions

Continuing with the above sample, note how the Alpaca NotConnectedException is returned to the remote app. The PropertyResponse constructor gets the Falcon Request object as its first parameter. The second parameter, the Alpaca exception class NotConnectedException is used by PropertyResponse to get the Alpaca error number and an error message with which it constructs the Alpaca JSON Response.

Alpaca NotConnectedException response
{
    "ClientTransactionID": 321,
    "ServerTransactionID": 1,
    "ErrorNumber": 1031,        // 0x407
    "ErrorMessage": "The device is not connected.",
}

It sets the Response.text to the above Alpaca JSON, and returns to Falcon, which returns the JSON as the HTTP body with a 200 OK status. Note that any Alpaca request which gets to the responder always returns with an HTTP 200 OK status, even though the response might be an Alpaca exception like this. Also note that the Value field is missing. It is meaningless in an exception response where ErrorNumber is non-zero. The MethodResponse class takes care of this.

Tip

You should supply your own error message as an optional parameter to any of the Alpaca exception classes. You should try to help the client app and its user by providing specifics about the error, and even perhaps a suggestion on how to fix the problem.

Note

These Alpaca exception classes should not be confused with raised Python exceptions such as ZeroDivisionError and RunTimeError which you can raise for internal device failures. Be sure to review Handling Exceptions and the usage of the Alpaca exceptions in the Rotator sample.

Run-Time Errors - DriverException

The Alpaca DriverException is specified for use by the device for any error or failure not covered by the other more specific Alpaca exceptions. In the example above notice that the call into the device rot_dev.ismoving is guarded by a try/except. The exception is passed to the DriverException class which creates a detailed report. Let’s see how this works…

Important

It’s vital that any problem encountered by your device be telegraphed back to the app via one of the Alpaca exceptions. For most problems, this will be the DriverException.

The DriverException has unique enhancements. Look now. In the example above, note the construction of DriverException includes an error code, an automaticelly constructed responder class name, and the Python exception object. This allows DriverException to construct a detailed error message that includes the API endpoint name (the name of the responder class), the Python module and line number, and optionally a Python call stack traceback (the verbose_driver_exceptions config option).

Also, since DriverException can use any error codes from 0x500 through 0xFFF, you can supply an error code. These codes are for you to use and have no specified meaning within Alpaca.

Invocations of DriverException

Throughout the template/sample, the invocation of DriverException uses the caught Python runtime exception (as ex) to DriverException for error reporting including possible traceback (see next section). You will see this pattern used throughout the template/sample and it is self-documenting thanks to the templates already having the device and member names.

Alpaca DriverException response
except Exception as ex:
    resp.text = MethodResponse(req, # Put is actually like a method :-(
                    DriverException(0x500, '{Device.Member} failed', ex)).json
    return

Attention

This may surprise you, but if your device runs into trouble after successfully starting an operation, you must raise an exception when the client app later asks for the status of that operation. See Handling Exceptions and Asynchronous APIs.

So if your Rotator accepts a request to move to a new angle, and then gets jammed up or otherwise fails to successfully complete the move to the new angle, then IsMoving must raise a DriverException, preferably with a detailed error message like Rotator has failed, possible jam or cable wrap. If the completion property IsMoving returns False it means “no longer moving and it got there successfully.”

Ease of Raising Exceptions

In this case, even deep within your device code, raise any Python exception (e.g. RuntimeError) with your detailed message. The boiler plate exception handling shown above and used in all of the responder classes will turn this into a useful Alpaca DriverException.

Note

The app must always check IsMoving to make sure that the move request completed successfully.

Example of DriverException with Normal and Verbose Exceptions

To see the exception handling in action, look at the MoveAbsolute() method in the simulated rotator logic rotatordevice.py where it checks to see if it’s being asked to move while it’s already moving:

if self._is_moving:
    self._lock.release()
    raise RuntimeError('Cannot start a move while the rotator is moving')

Now start up the rotator sample and then use a tool like curl or the Thunder Client for VS Code to send Alpaca HTTP requests to set Connected to True then MoveAbsolute(123) which will take some time. Now, while it is moving, make another request to MoveAbsolute(). This will trigger the above logic to raise an internal Python RuntimeError. The result will be your driver returning something like the following DriverException (with a 200 OK HTTP status).

Alpaca Normal DriverException Response
{
    "ServerTransactionID": 3,
    "ClientTransactionID": 321,
    "ErrorNumber": 1280,
    "ErrorMessage": "DriverException: Rotator.MoveAbsolute failed
            RuntimeError: Cannot start a move while the rotator is moving"
}

The DriverException class produces the first line, indicating which Alpaca method or property failed, followed by the Python exception name (RuntimeError) and the text you gave Python for this exception.

Note

All of this is provided by the “boilerplate” logic in the sample/template. All you need to do is raise an exception in your Python code that gets called from any of the Alpaca API responder classes.

Verbose Exception Reporting

If your driver’s configuration in ./config.toml has verbose_driver_exceptions = true then you’ll get a traceback as well

Alpaca Verbose DriverException Response
{
    "ServerTransactionID": 3,
    "ClientTransactionID": 321,
    "ErrorNumber": 1280,
    "ErrorMessage": "DriverException: MoveAbsolute failed
            Traceback (most recent call last):
            File \"device/rotator.py\", line 528, in on_put
                rot_dev.MoveAbsolute(newpos)    # async
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                    File \"device/rotatordevice.py\", line 357, in MoveAbsolute
                        raise RuntimeError('Cannot start a move while the rotator is moving')
                        RuntimeError: Cannot start a move while the rotator is moving"
}

Since the low-level call and the Alpaca endpoint names are the same and also the line numbers in the two modules are similar, this may be confusing. What this traceback says is that the Python exception RunTimeError is raised at line 357 in the rotatordevice.py module (in its MoveAbsolute) method, and that was called at line 528 in the Alpaca API responder class’ Rotator.MoveAbsolute() on_put() handler. Note the first part of the ErrorMessage automatically prints the Alpaca exception type DriverException as well at the name of the Alpaca API EndPoint MoveAbsolute. Also note that the error message passed To the Python RunTimeError exception appears in the Alpaca DriverException error message.

Note

Observe that the Rotator continues to function normally. The initial MoveAbsolute will complete normally, at which time IsMoving will transition from True To False. The failed second MoveAbsolute() will fail without compromising the device’s operation.

Unhandled Exceptions

What happens if there is an unhandled exception somewhere? If it’s triggered during handling of an Alpaca request, it needs to result in an HTTP 500 Server Error response. This template/sample handes this as well. See app.falcon_uncaught_exception_handler(), which calls app.custom_excepthook() to make sure the exception info is logged, then it sends the 500 Server Error. The simplicity of this logic is possibly lost in all of the docstring info.

Last but not least, if an unhandled exception occure outside the context of a Falcon API responder, it ends up in the “last-chance exception handler” app.custom_excepthook(). Here, a Control-C is allowed to kill the application. Otherwise the unhanded exception is logged and dismissed. If there is any possibility that the Python code can still run, it will. If the exception leads to a cascade of other exceptions, the Python will eventually die. This handler is installed during app startup app.main().

Footnotes