Changes to SAL script: schemas and dds

ts_scriptqueue 2.2 include two very important changes from 1.x:

  • SAL scripts must specify a schema, which will be used to validate the configuration and set default values (first introduced in version 2.1 but the 2.2 version is described here).
  • This version uses the OpenSplice dds library instead of SALPY libraries (first introduced in version 2.0).

All SAL scripts will need to be updated. I will do this for the scripts in ts_standardscripts. Changes include:

Schema

You must add a class method get_schema that returns a jsonschema description of the configuration of your script, as a dict. Please include a description of each field and a default value if a default makes sense. If your script needs no configuration then you can return None. (Note: ts_scriptqueue 2.1 implemented the schema as a property named schema instead, but in order to implement DM-18012 I have changed it to a classmethod in version 2.2). Here is an example:

@classmethod
def get_schema(cls):
   """Get the configuration schema as a jsonschema `dict`.
   """
    schema_yaml = """
        $schema: http://json-schema.org/draft-07/schema#
        $id: https://github.com/lsst-ts/ts_standardscripts/auxtel/SlewTelescopeIcrs.yaml
        title: SlewTelescopeIcrs v1
        description: Configuration for SlewTelescopeIcrs
        type: object
        properties:
          ra:
            description: ICRS right ascension (hour)
            type: number
            minimum: 0
            maximum: 24
          dec:
            description: ICRS declination (deg)
            type: number
            minimum: -90
            maximum: 90
          rot_pa:
            description: Desired instrument position angle, Eastwards from North (deg)
            type: number
            default: 0
          target_name:
            type: string
            default: ""
        required: [ra, dec, rot_pa, target_name]
        additionalProperties: false
    """
    return yaml.safe_load(schema_yaml)

Note that I created the schema as a yaml string and then use the yaml library to encode it as a Python dict. You can create the dict directly if you prefer. I like yaml because it is much more succinct and I find it easier to read.

Remotes

Your script must now create salobj.Remote instances after calling super().__init__, instead of before as used to be done. This is because the dds salobj Remote constructor now requires a domain, and that is made by super().__init__. Here is an example:

class SlewTelescopeIcrs(scriptqueue.BaseScript):
    """Slew the telescope to a specified ICRS position.
    ...
    """
    __test__ = False  # stop pytest from warning that this is not a test

    def __init__(self, index):
        super().__init__(index=index, descr="Slew the auxiliary telescope to an ICRS position")
        self.atptg = salobj.Remote(domain=self.domain, name="ATPtg")
        self.tracking_started = False

Unit Tests

As with all dds salobj code, unit tests must now carefully clean up the script (or domain, if there is no script) when done. Most of tests tests create a Harness object that constructs the script and a remote to talk to it. I recommend that you make your harness an asynchronous context manager by adding __aenter__(self) and __aexit__(self, *args) methods. For example:

index_gen = salobj.index_generator()

class Harness:

def __init__(self):
    self.index = next(index_gen)
    self.script = SlewTelescopeIcrs(index=self.index)
    self.atptg = salobj.Controller("ATPtg")

async def __aenter__(self):
    await self.script.start_task
    await self.atptg.start_task
    return self

async def __aexit__(self, *args):
    await self.script.close()
    await self.atptg.close()

Here is an example of using the harness in a test method:

def test_configure_with_defaults(self):
    async def doit():
        async with Harness() as harness:
            config_data = harness.script.cmd_configure.DataType()
            config_data.config = "ra: 5.1\ndec: 36.2"
            await harness.script.do_configure(config_data)
            self.assertEqual(harness.script.config.ra, 5.1)
            self.assertEqual(harness.script.config.dec, 36.2)
            self.assertEqual(harness.script.config.rot_pa, 0)
            self.assertEqual(harness.script.config.target_name, "")

    asyncio.get_event_loop().run_until_complete(doit())