.. image:: alpaca128.png
:height: 92px
:width: 128px
:align: right
==============================
Introduction to Alpaca Drivers
==============================
Writing a successful device driver requires, first and foremost, a clear
understanding of the role of a driver, and by extension, the responsibilities of
the driver and the developer. There are subtleties and easily missed aspects. We
can't overstress the importance of starting a driver project with a clear view
of the landscape. These documents will give you a good overview of driver
development.
1. |ascsite| All things ASCOM and Alpaca
2. |devhelp| See the Design Principles sections: General Principles,
Asynchronous APIs, and Exceptions.
3. |apiref| General specifications for Alpaca network protocol and API
4. |ascspecs|
Alpaca Device and Driver Architecture
-------------------------------------
An "Alpaca device" consists of a **server** which can host one or more
**drivers** for *multiple* ASCOM devices of *multiple* types. For example, a
single Alpaca device could provide the HTTP/REST communications for two ASCOM
cameras and an ASCOM mount, all through the same IP address and port.
The device's internal **server** is an HTTP (web) server that apps talk to using
the Alpaca HTTP/REST protocol, and which dispatches endpoint requests as calls
to the drivers for each device type and instance.
An ASCOM **device driver** consists of a set of *responders* for each Alpaca
REST endpoint (represented by a unique URI), including the ones common to all
ASCOM devices like ``Description`` and ``DriverVersion``, as well as the ones
specific to the ASCOM device type like ``Rotator.MoveMechanical()``. Incoming
REST requests are *routed* to their respective responders. The responder is
responsible for performing the action or accessing the data represented by the
endpoint.
.. important::
Each member of the ASCOM interface for a device is mapped
one-for-one to an Alpaca REST endpoint and thence to a responder
for that endpoint. This one-for-one mapping makes interoperation
between ASCOM Alpaca and classic ASCOM/COM possible.
Each endpoint URI contains both the device type (its device name) and also a
device number (the particular instance of that device type). The server is
responsible for calling the responder for the device type and instance.
Typically this is done via a routing table.
The Alpaca device's server is additionally responsible for responding to Alpaca
``Discovery`` multicasts coming from clients. This is a really simple mechanism.
It sends back a simple JSON response ``{AlpacaPort: *n*}``. Together with this
port number, and the server's IP address in the HTTP response packet, the client
now knows now to talk to the Alpaca server.
Once a client has found the Alpaca device, it can talk to its internal server to
determine the types of ASCOM devices served, and the number of instances of each
ASCOM device that are available (and some other metadata). This is done through
three ``Management`` endpoints. These are typically used by client apps to
select a specific device served [#]_
Finally, device settings and configuration are optionally provided with a set of
HTML web pages via the ``setup`` endpoint. Alternatively, for lightweight
applications (like this sample) an alternative is to use a simple text config
file.
.. hint::
For details see |apiref|
Sample Driver Organization
--------------------------
In this sample the HTTP server function is provided by the lightweight Python
|wsgiref| combined with the |falcweb|. These two components together provide the
REST API engine and endpoint URI-to-responder routing. The HTTP server and the
Falcon back-end application are created on the main thread at app startup and
run "forever".
The responders for each Alpaca device API are kept in separate modules, one for
the endpoints common to all device types, and the other for the device-specific
endpoints. In this sample, these are ``common.py`` and ``rotator.py``.
.. note:: Your main development effort will focus on the device-specific
responder classes. Metadata elsewhere can be tailored quickly.
Routing of incoming requests to the responder classes is done with a simple
function ``app.init_routes()`` which inspects each .py module containing
responder classes, finds each class, constructs the endpoint URI template and
then enters the URI and responder class into the Falcon routing table. Thus you
do not need to manually create routes for responders. See |falcweb|.
Alpaca discovery is provided by a simple engine running in a separate thread. It
is started at app startup, and runs "forever". You should not need to edit this.
Management API is provided in a separate module and routing for the management
endpoits is set up at app startup.
Logging is provided by the standard Python logger engine, with customizations
for the logging format including ISO-8601/UTC time stamps and logging to a file
(and optionally stdout). In addition, the HTTP server's logging output, normally
coming at the *end* of a request, is replaced with an HTTP request log at the
*beginning* of the request so that it is in context with logged messages that
may appear during processing of requests. The HTTP server is allowed to write
the post-request log line for non-200 (OK) HTTP responses.
Finally the ``setup`` endpoint simply displays a static web page. Configuration
for this lightweight sample uses a config file in |toml|. For details
see :doc:`/config`. Of course you can
provide your own web pages, or get really fancy and use |falcjinja|.
.. _async-intro:
Asynchronous Operations
-----------------------
All time-consuming device operations, such as slewing a mount, are implemented
in Alpaca as **asynchronous operations**. While you may be familiar with async
programming with an async/await type feature, the Alpaca base model is one of
explicit endpoints acting as *initiators* and *completion properties*.
.. attention::
* Your device and the responders in the driver must return promptly to every call.
* This may surprise you, but if your device runs into trouble after
successfully starting an operation, you *should* raise an exception when
the client app later asks for the status of that operation. See |async|.
.. _excep-intro:
Handling Exceptions
-------------------
It's vital that your driver implement the *prime directive* for distributed
systems:
.. epigraph::
*Do it right or raise an Exception*
-- ASCOM Initiative
For a detailed description of this vital principle as it applies to ASCOM and
Alpaca, read through |excep|. It will only take a few minutes. We've tried to
make this as TL:DR-proof as we could.
Alpaca Exceptions
~~~~~~~~~~~~~~~~~
The JSON responses to all Alpaca requests include ``ErrorNumber`` and
``ErrorMessage`` members. If ``ErrorNumber`` is 0 then the client considers the
request to have been a success (the ``ErrorMessage`` is ignored). Otherwise, a
non-zero ``ErrorNumber`` in the JSON response tells the client that an Alpaca
exception was raised (see :doc:`exceptions`). |apiref| (Sec. 2.8) describes
these Alpaca exceptions. Each one has a specific error number. The accompanying
error message defaults to a generic descriptive message but you can override the
message with something more detailed and helpful (recommended) when you
instantiate the Apaca Exception class.
.. 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. See the next section.
Python Exceptions
~~~~~~~~~~~~~~~~~
Within your driver, your code may raise Python Exceptions. So how do you
communicate a Python exception through your Alpaca API responder and back to the
client? The |apiref| specifies that the Alpaca
:py:class:`~exceptions.DriverException` should be used for all problems within
the device and driver code. In this sample, the
:py:class:`~exceptions.DriverException` class is unique in that it accepts a
Python exception object as would come from the `ex` of `except Exception as ex:`.
See :py:class:`~exceptions.DriverException` and :ref:`roadmap-exceptions`.
.. tip::
The built-in exception handling in this template/sample is detailed in the
:doc:`/roadmap`, specifically :ref:`roadmap-exceptions`.
Making this sample into your driver
-----------------------------------
When using this sample to make your own Alpaca device driver, follow this
general set of steps.
.. important::
The |ascspecs| are the final word in interface definition, data types,
exceptions, and behavior. Experiment with the |omnisim| OpenAPI interface
to see how each endpoint is supposed to work.
1. Familiarize yourself with |falcweb| specifically how incoming REST requests
are routed to *responders* with the Request and Response objects.
2. Run this sample, using the |conformu| tool to generate traffic to all of the
Rotator endpoints. Walk through the app startup in the :doc:`app` with the
debugger. See how the API endpoint URIs are registered to the responder
classes in the :py:func:`~app.init_routes` function. Walk through a GET
request, then a PUT request. See how the Alpaca JSON responses are created by
the :py:class:`~shr.PropertyResponse` and :py:class:`~shr.MethodResponse`
classes. Look how the simulated rotator machine is started and runs in a
separate class. Observe how locks are used to prevent conflicts in accesses
between threads. In short, become very familiar with how this simulated
device works.
3. Using :doc:`/rotator` as a guide, and one of the :doc:`/templates` provided
create a module containing responder classes for each Alpaca endpoint of
*your* device. Using the one for your device will be a big time saver!! Of
course, if you're making a Rotator driver you can use :doc:`/rotator` as a
starting point.
4. Look in :doc:`shr` for the :py:class:`~shr.DeviceMetadata` static class. Edit
the fields for your device. Generate your own unique **ID** using the
|guidgen|.
5. Adjust the user configuration file (config.toml) for the Title, IP/Port etc.
Use this to store your device's settings as well. See :doc:`/config`.
6. Develop the low-level code to control your device. Try to design it so that
it provides variables and functions that can be used by the Alpaca methods
and properties. Obviously this is going to be the major portion of your work,
followed by the time required to create the module containing the Alpaca
endpoint responder classes (step 2 above).
7. Wire up the device control code to the endpoint responder classes.
8. Test and fix until your device passes the full |conformu| tool's test.
9. Use the Alpaca Protocol Tester in ConformU to check your driver at the Alpaca
protocol level (as opposed to the operational tests provided by the
Conformance checker.)
.. |ascsite| raw:: html
ASCOM Initiative web site
.. |ascspecs| raw:: html
Master Generic ASCOM Device Interface Specifications
.. |devhelp| raw:: html
Alpaca Developers Info
.. |async| raw:: html
Asynchronous APIs
.. |excep| raw:: html
Exceptions in ASCOM
.. |guidgen| raw:: html
Online GUID / UUID Generator
.. |conformu| raw:: html
Conform Universal
.. |apiref| raw:: html
Alpaca API Reference (PDF)
.. |supforum| raw:: html
ASCOM Driver and Application Development Support Forum
.. |omnisim| raw:: html
Alpaca Omni Simulator
.. |falcweb| raw:: html
The Falcon Web Framework
.. |wsgiref| raw:: html
wsgiref.simple_server
.. |toml| raw:: html
Tom's Obvious Minimal Language
.. |falcjinja| raw:: html
Falcon support for Jinja-2
.. [#] The Windows ASCOM Chooser uses discovery and the management
endpoints to provide the user with the devices to select from.