itla.itla12

   1from time import sleep
   2
   3from . import logger
   4from .itla import ITLABase
   5from .itla_errors import (
   6    CIEError,
   7    CPException,
   8    EREError,
   9    ExecutionError,
  10    NOPException,
  11    RVEError,
  12)
  13from .itla_status import (
  14    MCB,
  15    AlarmTrigger,
  16    FatalError,
  17    FatalTrigger,
  18    Resena,
  19    SQRTrigger,
  20    WarningError,
  21    Waveform,
  22)
  23
  24
  25class ITLA12(ITLABase):
  26    """
  27    A class that represents the ITLA12
  28    and exposes a user friendly API for controlling functionality.
  29
  30    Things to figure out
  31
  32    * What should be exposed to the user?
  33    * Can we abstract away the concept of registers and stuff
  34        in here so you don't have to deal with it.
  35
  36    There are some functions that could be implemented like set_fatalstatus.
  37    I think this is probably a bad idea even though it isnt write only.
  38
  39    Set Frequency is my platonic ideal for the higher level functions.
  40
  41    """
  42
  43    def __init__(
  44        self, serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1
  45    ):
  46        """Initializes the ITLA12 object.
  47
  48        :param serial_port: The serial port to connect to.
  49        Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
  50        :param baudrate: The baudrate for communication. I have primarily seen
  51        lasers using 9600 as the default and then higher for firmware upgrades.
  52        This may be laser specific.
  53        :param timeout: How long should we wait to receive a response from the laser
  54        :param register_files: Any additional register files you would like to include
  55        beyond the default MSA-01.2 defined registers. These must be in a yaml format as
  56        described in the project's README.
  57        :param sleep_time: time in seconds. Used in wait function
  58        """
  59        if register_files is None:
  60            register_files = []
  61
  62        register_files = ["registers_itla12.yaml", *register_files]
  63
  64        self.sleep_time = sleep_time
  65
  66        super().__init__(
  67            serial_port, baudrate, timeout=timeout, register_files=register_files
  68        )
  69
  70    def nop(self, data=None):
  71        """The No-Op operation.
  72
  73        This is a good test to see if your laser is communicating properly.
  74        It should read 0000 data or echo whatever data you send it if you send it something.
  75        The data you write to nop gets overwritten as soon as you read it back.
  76
  77        `nop()` also returns more informative errors following an ExecutionError.
  78        This is not called by default so you must do this explicitly if you want
  79        to see more informative error information.
  80
  81        See 9.4.1 NOP/Status (ResEna 0x00) [RW] in OIF-ITLA-MSA.
  82
  83        :param data: Data to write
  84
  85        :returns: None
  86        """
  87        # pretty sure the data does nothing
  88        if data is not None:
  89            response = self._nop(data)
  90        else:
  91            response = self._nop()
  92
  93        error_field = int(response.hex()[-1], 16)
  94        if bool(error_field):
  95            raise self._nop_errors[error_field]
  96
  97    def enable(self):
  98        """Enables laser optical output.
  99        There is a time delay after execution of this function to
 100        proper stable laser output.
 101
 102        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 103
 104        :returns: None
 105        """
 106        # I'm writing this out partially for transparency
 107        # Maybe unnecessary or non-optimal
 108        self._resena(Resena.SENA)
 109
 110    def disable(self):
 111        """disables the laser
 112
 113        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 114
 115        :returns: None
 116        """
 117        self._resena(0)
 118
 119    def hard_reset(self):
 120        """initiate module reset
 121
 122        The hardware reset is typically traffic interrupting since it will reset
 123        control loops as well. The host can poll the communication’s interface
 124        waiting for a response packet indicating that the interface is ready to
 125        communicate. Note that a response is returned to acknowledge the reset
 126        request before the reset is started
 127
 128        The impact to the optical signal is undefined. This bit is self clearing.
 129
 130        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 131
 132        :returns: None
 133        """
 134        self._resena(Resena.MR)
 135
 136    def soft_reset(self):
 137        """initiate soft reset
 138
 139        The soft reset resets the communication’s interface and is traffic
 140        non-interrupting. Extended address registers are reset.
 141
 142        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 143
 144        :returns: None
 145        """
 146        self._resena(Resena.SR)
 147
 148    def get_reset_enable(self):
 149        """return ResEna register.
 150
 151        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 152        """
 153        response = self._resena()
 154        return Resena(int.from_bytes(response, "big"))
 155
 156    def set_alarm_during_tuning(self):
 157        """Set alarm during tuning
 158        mcb(ADT)
 159
 160        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 161        """
 162        self._mcb(MCB.ADT)
 163
 164    def set_shutdown_on_fatal(self):
 165        """Set shutdown on fatal.
 166        mcb(SDF)
 167
 168        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 169        """
 170        self._mcb(MCB.SDF)
 171
 172    def get_configuration_behavior(self):
 173        """
 174        Return MCB register.
 175
 176        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 177        """
 178        response = self._mcb()
 179        return MCB(int.from_bytes(response, "big"))
 180
 181    def is_disabled(self):
 182        """
 183        Return if disabled.
 184
 185        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
 186
 187        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
 188        Consider overwriting this methods and monitoring FatalError.ALM.
 189        """
 190        fatal_error = self.get_error_fatal()
 191        fatal_trigger = self.get_fatal_trigger()
 192        resena = self.get_reset_enable()
 193        mcb = self.get_configuration_behavior()
 194
 195        sdf = MCB.SDF in mcb
 196        sena = Resena.SENA in resena
 197        dis = FatalError.DIS in fatal_error
 198
 199        return ((fatal_error & fatal_trigger) and sdf) or (not sena) or (not dis)
 200
 201    def is_enabled(self, *args):
 202        """
 203        Return if enabled.  See is_disabled.
 204        """
 205        return not self.is_disabled(*args)
 206
 207    def get_device_type(self):
 208        """returns a string containing the device type
 209
 210        DevTyp returns the module’s device type. For all tunable lasers covered by this MSA, the
 211        module will return the null terminated string “CW ITLA\0” (eight bytes including the
 212        terminating null character) indirectly through the AEA mechanism. The device type register
 213        is provided such that a host can distinguish between different types of tunable devices.type.
 214
 215        See 9.4.2 Device Type (DevTyp 0x01) [R]
 216        """
 217        response_bytes = self._devtyp()
 218
 219        return response_bytes.decode("utf-8")
 220
 221    def get_manufacturer(self):
 222        """returns a string containing the manufacturer's name.
 223
 224        MFGR returns the module’s manufacturers ID null terminated string indirectly through the
 225        AEA mechanism
 226
 227        See 9.4.3 Manufacturer (MFGR 0x02) [R]
 228        """
 229        response_bytes = self._mfgr()
 230
 231        return response_bytes.decode("utf-8")
 232
 233    def get_model(self):
 234        """returns the model as a string
 235
 236        Model returns the module’s model designation string indirectly through the AEA
 237        mechanism. The null terminated string containing the module’s model designation is
 238        placed into a field of not more than 80 bytes in size. The model string is defined by the
 239        manufacturer
 240
 241        See 9.4.4 Model (Model 0x03) [R]
 242        """
 243        response_bytes = self._model()
 244
 245        return response_bytes.decode("utf-8")
 246
 247    def get_serialnumber(self):
 248        """returns the serial number
 249
 250        SerNo returns the module’s serial number string indirectly through the AEA mechanism.
 251        The null terminated string containing the module’s serial number is placed into a field of not
 252        more than 80 bytes in size. The serial number string is defined by the manufacturer.
 253
 254        See 9.4.5 Serial Number (SerNo 0x04) [R]
 255        """
 256        response_bytes = self._serno()
 257
 258        return response_bytes.decode("utf-8")
 259
 260    def get_manufacturing_date(self):
 261        """returns the manufacturing date
 262
 263        MFGDate returns the manufacturing date string of the module indirectly through the AEA
 264        mechanism. The null terminated string containing the date string is contained in a field
 265        size of 12 bytes.
 266
 267        See 9.4.5 Manufaturing Date (MFGDate 0x05) [R]
 268        """
 269        response_bytes = self._mfgdate()
 270
 271        return response_bytes.decode("utf-8")
 272
 273    def get_firmware_release(self):
 274        """returns a manufacturer specific firmware release
 275
 276        Release returns the release string of the module indirectly through the AEA mechanism.
 277        The null terminated string containing the module release information is placed into a field of
 278        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
 279        hardware revisions to track. The release field also encodes the application space
 280        identifier.
 281
 282        See 9.4.7 Release (Release 0x06) [R]
 283        """
 284        response_bytes = self._release()
 285
 286        return response_bytes.decode("utf-8")
 287
 288    def get_backwardscompatibility(self):
 289        """returns a manufacturer specific firmware backwards compatibility
 290        as a null terminated string
 291
 292        RelBack returns the release backwards compatibility string of the module indirectly through
 293        the AEA mechanism. The null terminated string containing the earliest release string which
 294        is fully backwards compatible with the current module. The string is contained in a field of
 295        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
 296        hardware revisions to track as described in the Release (0x06) register
 297
 298        See 9.4.8 Release Backwards Compatibility (RelBack 0x07) [R]
 299        """
 300        response_bytes = self._relback()
 301
 302        return response_bytes.decode("utf-8")
 303
 304    def get_general_module_configuration(self):
 305        """returns the power on/reset module configuration
 306
 307        GenCfg defines the general module configuration for the generic tunable device. For the
 308        tunable laser, the register is used to save the power on/reset module configuration
 309        defaults.
 310
 311        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
 312        """
 313        raise NotImplementedError()
 314
 315    def set_general_module_configuration(self):
 316        """returns the power on/reset module configuration
 317
 318        GenCfg defines the general module configuration for the generic tunable device. For the
 319        tunable laser, the register is used to save the power on/reset module configuration
 320        defaults.
 321
 322        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
 323        """
 324        raise NotImplementedError()
 325
 326    def get_iocap(self):
 327        """
 328        The IOCap register returns or sets the I/O interface capabilities
 329
 330        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
 331        """
 332        raise NotImplementedError()
 333
 334    def set_iocap(self, data):
 335        """
 336        The IOCap register returns or sets the I/O interface capabilities
 337
 338        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
 339        """
 340        raise NotImplementedError()
 341
 342    def read_aea(self):
 343        """reads the AEA register data until an execution error is thrown.
 344
 345        See 9.4.11 Extended Addressing Mode Registers (0x09-0x0B, 0x0E-0x10) [RW]
 346        """
 347        aea_response = b""
 348        try:
 349            while True:
 350                aea_response += self._aea_ear()
 351
 352        except ExecutionError:
 353            try:
 354                self.nop()
 355
 356            except EREError:
 357                # If this throws an ERE error then we have completed reading AEA
 358                pass
 359
 360            except NOPException as nop_e:
 361                raise nop_e
 362
 363        return aea_response
 364
 365    def get_download_configuration(self):
 366        """
 367        The DLConfig register configures a host to module download of code or data for
 368        reconfiguration purposes or configures a module to host upload of code or data to the host.
 369        A file transfer may occur at several locations such as vendor factory, customer site (on the
 370        bench), customer system (circuit down), or potentially a customer system (live circuit)
 371
 372        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
 373        """
 374        raise NotImplementedError()
 375
 376    def set_download_configuration(self):
 377        """
 378        The DLConfig register configures a host to module download of code or data for
 379        reconfiguration purposes or configures a module to host upload of code or data to the host.
 380        A file transfer may occur at several locations such as vendor factory, customer site (on the
 381        bench), customer system (circuit down), or potentially a customer system (live circuit)
 382
 383        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
 384        """
 385        raise NotImplementedError()
 386
 387    def get_download_status(self):
 388        """
 389        DLStatus provides information about the status or viability of a code segment.
 390
 391        See 9.4.14 Download Status (DLStatus 0x15) [R]
 392        """
 393        raise NotImplementedError()
 394
 395    def wait(self):
 396        """Wait until operation is complete.
 397        It check if the operation is completed every self.sleep_time seconds.
 398        """
 399        while True:
 400            try:
 401                self.nop()
 402            except CPException:
 403                if self.sleep_time is not None:
 404                    sleep(self.sleep_time)
 405            else:
 406                break
 407
 408    def wait_until_enabled(self):
 409        """TODO: Add documentation."""
 410        while self.is_disabled():
 411            if self.sleep_time is not None:
 412                sleep(self.sleep_time)
 413
 414    def wait_until_disabled(self):
 415        """TODO: Add documentation."""
 416        while self.is_enabled():
 417            if self.sleep_time is not None:
 418                sleep(self.sleep_time)
 419
 420    def set_power(self, pwr_dBm):
 421        """Sets the power of the ITLA laser.
 422        Units of dBm.
 423
 424        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
 425
 426        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
 427        :returns: None
 428        """
 429        try:
 430            self._pwr(round(pwr_dBm * 100))
 431
 432        except ExecutionError:
 433            try:
 434                self.nop()
 435
 436            except RVEError as error:
 437                logger.error(
 438                    "The provided power %.2f dBm is outside of the range for this device. "
 439                    "The power is currently set to: %.2f dBm.",
 440                    pwr_dBm,
 441                    self.get_power_setting(),
 442                )
 443                raise error
 444
 445    def get_power_setting(self):
 446        """Gets current power setting set by set_power.
 447        Should be in dBm.
 448
 449        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
 450
 451        :returns:
 452        """
 453        # Gets power setting, not actual optical output power.
 454        response = self._pwr()
 455        return int.from_bytes(response, "big", signed=True) / 100
 456
 457    def get_power_output(self):
 458        """returns external optical power estimate
 459
 460        See 9.6.8 Optical Output Power (OOP 0x42) [R]
 461
 462        :returns: External optical power estimate in dBm
 463        """
 464        response = self._oop()
 465
 466        return int.from_bytes(response, "big", signed=True) / 100
 467
 468    def get_power_min(self):
 469        """returns minimum optical power output of the module.
 470        Units dBm.
 471
 472        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
 473
 474        :returns: minimum optical power
 475        """
 476        response = self._opsl()
 477        return int.from_bytes(response, "big", signed=True) / 100
 478
 479    def get_power_max(self):
 480        """returns maximum optical power output of the module.
 481        Units dBm.
 482
 483        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
 484
 485        :returns: maximum optical power
 486        """
 487        response = self._opsh()
 488        return int.from_bytes(response, "big", signed=True) / 100
 489
 490    def set_fcf(self, freq):
 491        """
 492        This sets the first channel frequency.
 493        It does not reset the channel so this frequency will only be equal
 494        to the output frequency if channel=1.
 495
 496        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 497
 498        :returns: None
 499        """
 500        # convert frequency to MHz
 501        freq = round(freq * 1e6)
 502
 503        freq_str = str(freq)
 504        fcf1 = int(freq_str[0:3])
 505        fcf2 = int(freq_str[3:7])
 506
 507        try:
 508            # is it better to split these into their own try/except blocks?
 509            self._fcf1(fcf1)
 510            self._fcf2(fcf2)
 511
 512        except ExecutionError:
 513            try:
 514                self.nop()
 515            except RVEError as error:
 516                logger.error(
 517                    "%.2f THz is out of bounds for this laser. "
 518                    "The frequency must be within the range %.2f - %.2f THz.",
 519                    fcf1,
 520                    self.get_frequency_min(),
 521                    self.get_frequency_max(),
 522                )
 523                raise error
 524
 525            except CIEError as error:
 526                logger.error(
 527                    "You cannot change the first channel frequency while the laser is enabled. "
 528                    "The current frequency is: %.2f THz",
 529                    self.get_frequency(),
 530                )
 531                raise error
 532
 533    def set_frequency(self, freq):
 534        """Sets the frequency of the laser in TeraHertz.
 535
 536        Has MHz resolution. Will round down.
 537
 538        This function sets the first channel frequency and then sets the
 539        channel to 1 so that the frequency output when the laser is enabled
 540        will be the frequency given to this function.
 541
 542        If you would like to only change the first channel frequency use
 543        the function `set_fcf`.
 544
 545        Disable the laser before calling this function.
 546
 547        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 548
 549        :param freq: The desired frequency setting in THz.
 550        :returns: None
 551        """
 552
 553        # This does a check so this only runs if fine tuning has been turned on.
 554        if self.get_fine_tuning() != 0:
 555            # Set the fine tuning off!
 556            while True:
 557                try:
 558                    self.set_fine_tuning(0)
 559                except ExecutionError:
 560                    sleep(self.sleep_time)
 561                except CPException:
 562                    self.wait()
 563                    break
 564                else:
 565                    break
 566
 567        try:
 568            self.set_fcf(freq)
 569        except CPException:
 570            self.wait()
 571
 572        try:
 573            self.set_channel(1)
 574        except CPException:
 575            self.wait()
 576
 577    def get_fcf(self):
 578        """returns first channel frequency
 579
 580        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 581
 582        :returns: First channel frequency in THz
 583        """
 584        response = self._fcf1()
 585        fcf1 = int.from_bytes(response, "big")
 586
 587        response = self._fcf2()
 588        fcf2 = int.from_bytes(response, "big")
 589
 590        return fcf1 + fcf2 * 1e-4
 591
 592    def get_frequency(self):
 593        """gets the current laser operating frequency
 594
 595        See 9.6.7 Laser Frequency (LF1, LF2 0x40 – 0x41) [R]
 596
 597        :returns: Current laser frequency in THz.
 598        """
 599        response = self._lf1()
 600        lf1 = int.from_bytes(response, "big")
 601
 602        response = self._lf2()
 603        lf2 = int.from_bytes(response, "big")
 604
 605        return lf1 + lf2 * 1e-4
 606
 607    def dither_enable(self, waveform: Waveform = Waveform.SIN):
 608        """enables dither
 609
 610        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 611
 612        :param waveform: specifies the dither waveform
 613        """
 614
 615        data = (waveform << 4) | 2
 616
 617        self._dithere(data)
 618
 619    def dither_disable(self):
 620        """disables digital dither
 621
 622        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 623        """
 624        # TODO: This should preserve the waveform setting if possible
 625        self._dithere(0)
 626
 627    def set_dither_rate(self, rate):
 628        """set dither rate in kHz
 629        utilizes DitherR register
 630
 631        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 632
 633        :param rate: an unsigned short integer specifying dither rate in kHz
 634        """
 635        self._ditherr(rate)
 636
 637    def get_dither_rate(self):
 638        """get dither rate
 639        utilizes DitherR register
 640
 641        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 642
 643        :returns: Dither rate in kHz
 644        """
 645        response = self._ditherr()
 646        return int.from_bytes(response, "big")
 647
 648    def set_dither_frequency(self, rate):
 649        """set dither modulation frequency
 650        utilizes DitherF register
 651
 652        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 653
 654        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
 655        deviation as Ghz*10
 656        """
 657        self._ditherf(rate)
 658
 659    def get_dither_frequency(self):
 660        """get dither modulation frequency
 661        utilizes DitherF register
 662
 663        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 664
 665        :returns: dither modulation frequency in GHz * 10
 666        """
 667        response = self._ditherf()
 668        return int.from_bytes(response, "big")
 669
 670    def set_dither_amplitude(self, amplitude):
 671        """set dither modulation amplitude
 672        utilizes DitherA register
 673
 674        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 675
 676        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
 677        deviation as 10*percentage of the optical power
 678
 679        :returns: None
 680        """
 681        self._dithera(amplitude)
 682
 683    def get_dither_amplitude(self):
 684        """get dither modulation amplitude
 685        utilizes DitherA register
 686
 687        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 688
 689        :returns: dither aplitude in 10ths of percents
 690        """
 691        response = self._dithera()
 692        return int.from_bytes(response, "big")
 693
 694    def get_temp(self):
 695        """Returns the current primary control temperature in deg C.
 696
 697        See 9.6.9 Current Temperature (CTemp 0x43) [R]
 698
 699        :returns: current primary control temperature in deg C
 700        """
 701        response = self._ctemp()
 702        temp_100 = int.from_bytes(response, "big")
 703
 704        return temp_100 / 100
 705
 706    def get_frequency_min(self):
 707        """returns minimum frequency supported by the module
 708
 709        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
 710
 711        :returns: minimum frequency in THz
 712        """
 713        response = self._lfl1()
 714        lfl1 = int.from_bytes(response, "big")
 715
 716        response = self._lfl2()
 717        lfl2 = int.from_bytes(response, "big")
 718
 719        return lfl1 + lfl2 * 1e-4
 720
 721    def get_frequency_max(self):
 722        """returns maximum frequency supported by the module
 723
 724        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
 725
 726        :returns: maximum frequency in THz
 727        """
 728        response = self._lfh1()
 729        fcf1 = int.from_bytes(response, "big")
 730
 731        response = self._lfh2()
 732        fcf2 = int.from_bytes(response, "big")
 733
 734        return fcf1 + fcf2 * 1e-4
 735
 736    def get_grid_min(self):
 737        """returns minimum grid spacing of the module
 738
 739        See 9.7.4 Laser’s Minimum Grid Spacing (LGrid 0x56) [R]
 740
 741        :returns: The minimum grid supported by the module in GHz
 742        """
 743        try:
 744            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
 745
 746        except ExecutionError:
 747            self.nop()
 748
 749        return freq_lgrid * 1e-1
 750
 751    def set_grid(self, grid_freq):
 752        """Set the grid spacing in GHz.
 753        MHz resolution.
 754
 755        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
 756
 757        :param grid_freq: the grid frequency spacing in GHz
 758        :returns:
 759
 760        """
 761        grid_freq = str(round(grid_freq * 1000))
 762        data = int(grid_freq[0:4])
 763
 764        self._grid(data)
 765
 766    def get_grid(self):
 767        """get the grid spacing in GHz
 768
 769        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
 770
 771        :returns: The grid spacing in GHz.
 772        """
 773        response = self._grid()
 774        grid_freq = int.from_bytes(response, "big", signed=True)
 775
 776        return grid_freq * 1e-1
 777
 778    def get_age(self):
 779        """Returns the percentage of aging of the laser.
 780        0% indicated a brand new laser
 781        100% indicates the laser should be replaces.
 782
 783        See 9.8.6 Laser Age (Age 0x61) [R]
 784
 785        :returns: percent aging of the laser
 786        """
 787        response = self._age()
 788        age = int.from_bytes(response, "big")
 789
 790        return age
 791
 792    def set_channel(self, channel):
 793        """Sets the laser's operating channel.
 794
 795        MSA-01.2 lasers support a 16 bit channel value.
 796
 797        This defines how many spaces along the grid
 798        the laser's frequency is displaced from the first channel frequency.
 799
 800        See 9.6.1 Channel (Channel 0x30) [RW]
 801
 802        :param channel:
 803        :returns: None
 804
 805        """
 806        # check type and stuff
 807        if not isinstance(channel, int):
 808            raise TypeError("Channel must be an integer")
 809        if channel < 0:
 810            raise ValueError("Channel must be positive.")
 811        if channel > 0xFFFF:
 812            raise ValueError("Channel must be a 16 bit integer (<=0xFFFF).")
 813
 814        # Split the channel choice into two options.
 815        channel_hex = f"{channel:08x}"
 816
 817        channell = int(channel_hex[4:], 16)
 818
 819        # Set the channel registers.
 820        self._channel(channell)
 821
 822    def get_channel(self):
 823        """gets the current channel setting
 824
 825        The channel defines how many spaces along the grid
 826        the laser's frequency is displaced from the first channel frequency.
 827
 828        See 9.6.1 Channel (Channel 0x30) [RW]
 829
 830        :returns: channel as an integer.
 831
 832        """
 833        # This concatenates the data bytestrings
 834        response = self._channel()
 835
 836        channel = int.from_bytes(response, "big")
 837
 838        return channel
 839
 840    def set_fine_tuning(self, ftf):
 841        """
 842        This function provides off grid tuning for the laser's frequency.
 843        The adjustment applies to all channels uniformly.
 844        This is typically used after the laser if locked and minor adjustments
 845        are required to the frequency.
 846
 847        The command may be pending in the event that the laser output is
 848        enabled. The pending bit is cleared once the fine tune frequency
 849        has been achieved.
 850
 851        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
 852
 853        **???** It seems like this can be done with the laser running.
 854
 855        :param ftf: The fine tune frequency adjustment in GHz
 856        """
 857
 858        ftf = round(ftf * 1e3)
 859
 860        # We will leave this bare. This way the user can set and handle
 861        # their own timing and check to make sure the laser has reached the
 862        # desired fine tuning frequency.
 863        # It WILL throw a "pending" error if the laser is on when setting.
 864        self._ftf(ftf)
 865
 866    def get_fine_tuning(self):
 867        """
 868        This function returns the setting for the
 869        off grid tuning for the laser's frequency.
 870
 871        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
 872
 873        :return ftf: The fine tune frequency adjustment in GHz
 874        """
 875
 876        response = self._ftf()
 877        ftf = int.from_bytes(response, "big", signed=True)
 878
 879        return ftf * 1e-3
 880
 881    def get_ftf_range(self):
 882        """return the maximum and minimum off grid tuning for the laser's frequency.
 883
 884        See 9.7.1 Fine Tune Frequency Range (FTF 0x4F) [R]
 885
 886        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
 887        """
 888        response = self._ftfr()
 889
 890        ftfr = int.from_bytes(response, "big")
 891
 892        return ftfr * 1e-3
 893
 894    def get_temps(self):
 895        """returns a list of technology specific temperatures
 896        units in deg C unless otherwise specified
 897
 898        **Technologies**
 899        Technology 1: [Diode Temp, Case Temp]
 900        Technology 2: [Diode Temp, Case Temp]
 901        (why are they the same?)
 902
 903        See 9.8.2 Module Temperatures (Temps 0x58) [R]
 904
 905        :returns: list of temperatures in deg C
 906        """
 907        # get response this should be a long byte string
 908        response = self._temps()
 909
 910        data = [
 911            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
 912            for i in range(0, len(response), 2)
 913        ]
 914
 915        return data
 916
 917    def get_currents(self):
 918        """returns a list of technology specific currents in mA.
 919
 920        **Technologies**
 921        Technology 1: [TEC, Diode]
 922        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
 923        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
 924
 925        See 9.8.1 Module Currents (Currents 0x57) [R]
 926
 927        :returns: list of currents in mA
 928        """
 929        # get response this should be a long byte string
 930        response = self._currents()
 931
 932        data = [
 933            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
 934            for i in range(0, len(response), 2)
 935        ]
 936
 937        return data
 938
 939    def get_last_response(self):
 940        """reads the last response sent from the laser
 941
 942        This function gets the most recent response sent from the laser.
 943        The response is parsed for errors the way any normal response
 944        would be. The variable `self._response` is set to the last response again.
 945
 946        See 9.4.12 Last Response (LstResp 0x13) [R]
 947
 948        """
 949        return self._lstresp()
 950
 951    def get_error_fatal(self, reset=False):
 952        """reads fatal error register statusf and checks each bit against the
 953        fatal error table to determine the fault
 954
 955        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
 956
 957        :param reset: resets/clears latching errors
 958
 959        """
 960        response = self._statusf()
 961        statusf = int.from_bytes(response, "big")
 962
 963        logger.debug("Current Status Fatal Error: %d", statusf)
 964
 965        if reset:
 966            data_reset = 0x00FF
 967            self._statusf(data_reset)
 968
 969        return FatalError(statusf)
 970
 971    def get_error_warning(self, reset=False):
 972        """reads warning error register statusw and checks each bit against the
 973        warning error table to determine the fault
 974
 975        If the laser is off then some of the latched warnings will be set on.
 976
 977        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
 978
 979        :param reset: resets/clears latching errors
 980        """
 981        response = self._statusw()
 982        statusw = int.from_bytes(response, "big")
 983
 984        logger.debug("Current Status Warning Error: %d", statusw)
 985
 986        if reset:
 987            data_reset = 0x00FF
 988            self._statusw(data_reset)
 989
 990        return WarningError(statusw)
 991
 992    def get_fatal_power_thresh(self):
 993        """reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
 994
 995        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
 996        """
 997        response = self._fpowth()
 998        pow_fatal = int.from_bytes(response, "big") / 100
 999        # correcting for proper order of magnitude
1000        return pow_fatal
1001
1002    def get_warning_power_thresh(self):
1003        """reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
1004
1005        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
1006        """
1007        response = self._wpowth()
1008        pow_warn = int.from_bytes(response, "big") / 100
1009        # correcting for proper order of magnitude
1010        return pow_warn
1011
1012    def get_fatal_freq_thresh(self):
1013        """reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
1014
1015        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1016        """
1017        response = self._ffreqth()
1018        freq_fatal = int.from_bytes(response, "big") / 10
1019        return freq_fatal
1020
1021    def get_warning_freq_thresh(self):
1022        """reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
1023
1024        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1025        """
1026        response = self._wfreqth()
1027        freq_warn = int.from_bytes(response, "big") / 10
1028        return freq_warn
1029
1030    def get_fatal_therm_thresh(self):
1031        """reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
1032
1033        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1034        """
1035        response = self._fthermth()
1036        therm_fatal = int.from_bytes(response, "big") / 100
1037        # correcting for proper order of magnitude
1038        return therm_fatal
1039
1040    def get_warning_therm_thresh(self):
1041        """reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
1042
1043        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1044        """
1045        response = self._wthermth()
1046        therm_thresh = int.from_bytes(response, "big") / 100
1047        # correcting for proper order of magnitude
1048        return therm_thresh
1049
1050    def get_srq_trigger(self):
1051        """identifies corresponding bits in status registers StatusF, StatusW
1052
1053        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
1054
1055        See 9.5.5 SRQ* Triggers (SRQT 0x28) [RW]
1056        """
1057        response = self._srqt()
1058        status = int.from_bytes(response, "big")
1059
1060        logger.debug("SRQT Status: %d", status)
1061
1062        return SQRTrigger(status)
1063
1064    def get_fatal_trigger(self):
1065        """identifies which fatal condition was asserted in StatusF and StatusW registers
1066
1067        See 9.5.6 FATAL Triggers (FatalT 0x29) [RW]
1068        """
1069        response = self._fatalt()
1070        status = int.from_bytes(response, "big")
1071
1072        logger.debug("FatalT Status: %d", status)
1073
1074        return FatalTrigger(status)
1075
1076    def get_alm_trigger(self):
1077        """identifies why ALM was asserted in StatusF and StatusW registers
1078
1079        See 9.5.7 ALM Triggers (ALMT 0x2A) [RW]
1080        """
1081        response = self._almt()
1082        status = int.from_bytes(response, "big")
1083
1084        logger.debug("AlarmT Status: %d", status)
1085
1086        return AlarmTrigger(status)
class ITLA12(itla.itla.ITLABase):
  26class ITLA12(ITLABase):
  27    """
  28    A class that represents the ITLA12
  29    and exposes a user friendly API for controlling functionality.
  30
  31    Things to figure out
  32
  33    * What should be exposed to the user?
  34    * Can we abstract away the concept of registers and stuff
  35        in here so you don't have to deal with it.
  36
  37    There are some functions that could be implemented like set_fatalstatus.
  38    I think this is probably a bad idea even though it isnt write only.
  39
  40    Set Frequency is my platonic ideal for the higher level functions.
  41
  42    """
  43
  44    def __init__(
  45        self, serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1
  46    ):
  47        """Initializes the ITLA12 object.
  48
  49        :param serial_port: The serial port to connect to.
  50        Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
  51        :param baudrate: The baudrate for communication. I have primarily seen
  52        lasers using 9600 as the default and then higher for firmware upgrades.
  53        This may be laser specific.
  54        :param timeout: How long should we wait to receive a response from the laser
  55        :param register_files: Any additional register files you would like to include
  56        beyond the default MSA-01.2 defined registers. These must be in a yaml format as
  57        described in the project's README.
  58        :param sleep_time: time in seconds. Used in wait function
  59        """
  60        if register_files is None:
  61            register_files = []
  62
  63        register_files = ["registers_itla12.yaml", *register_files]
  64
  65        self.sleep_time = sleep_time
  66
  67        super().__init__(
  68            serial_port, baudrate, timeout=timeout, register_files=register_files
  69        )
  70
  71    def nop(self, data=None):
  72        """The No-Op operation.
  73
  74        This is a good test to see if your laser is communicating properly.
  75        It should read 0000 data or echo whatever data you send it if you send it something.
  76        The data you write to nop gets overwritten as soon as you read it back.
  77
  78        `nop()` also returns more informative errors following an ExecutionError.
  79        This is not called by default so you must do this explicitly if you want
  80        to see more informative error information.
  81
  82        See 9.4.1 NOP/Status (ResEna 0x00) [RW] in OIF-ITLA-MSA.
  83
  84        :param data: Data to write
  85
  86        :returns: None
  87        """
  88        # pretty sure the data does nothing
  89        if data is not None:
  90            response = self._nop(data)
  91        else:
  92            response = self._nop()
  93
  94        error_field = int(response.hex()[-1], 16)
  95        if bool(error_field):
  96            raise self._nop_errors[error_field]
  97
  98    def enable(self):
  99        """Enables laser optical output.
 100        There is a time delay after execution of this function to
 101        proper stable laser output.
 102
 103        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 104
 105        :returns: None
 106        """
 107        # I'm writing this out partially for transparency
 108        # Maybe unnecessary or non-optimal
 109        self._resena(Resena.SENA)
 110
 111    def disable(self):
 112        """disables the laser
 113
 114        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 115
 116        :returns: None
 117        """
 118        self._resena(0)
 119
 120    def hard_reset(self):
 121        """initiate module reset
 122
 123        The hardware reset is typically traffic interrupting since it will reset
 124        control loops as well. The host can poll the communication’s interface
 125        waiting for a response packet indicating that the interface is ready to
 126        communicate. Note that a response is returned to acknowledge the reset
 127        request before the reset is started
 128
 129        The impact to the optical signal is undefined. This bit is self clearing.
 130
 131        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 132
 133        :returns: None
 134        """
 135        self._resena(Resena.MR)
 136
 137    def soft_reset(self):
 138        """initiate soft reset
 139
 140        The soft reset resets the communication’s interface and is traffic
 141        non-interrupting. Extended address registers are reset.
 142
 143        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 144
 145        :returns: None
 146        """
 147        self._resena(Resena.SR)
 148
 149    def get_reset_enable(self):
 150        """return ResEna register.
 151
 152        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
 153        """
 154        response = self._resena()
 155        return Resena(int.from_bytes(response, "big"))
 156
 157    def set_alarm_during_tuning(self):
 158        """Set alarm during tuning
 159        mcb(ADT)
 160
 161        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 162        """
 163        self._mcb(MCB.ADT)
 164
 165    def set_shutdown_on_fatal(self):
 166        """Set shutdown on fatal.
 167        mcb(SDF)
 168
 169        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 170        """
 171        self._mcb(MCB.SDF)
 172
 173    def get_configuration_behavior(self):
 174        """
 175        Return MCB register.
 176
 177        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
 178        """
 179        response = self._mcb()
 180        return MCB(int.from_bytes(response, "big"))
 181
 182    def is_disabled(self):
 183        """
 184        Return if disabled.
 185
 186        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
 187
 188        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
 189        Consider overwriting this methods and monitoring FatalError.ALM.
 190        """
 191        fatal_error = self.get_error_fatal()
 192        fatal_trigger = self.get_fatal_trigger()
 193        resena = self.get_reset_enable()
 194        mcb = self.get_configuration_behavior()
 195
 196        sdf = MCB.SDF in mcb
 197        sena = Resena.SENA in resena
 198        dis = FatalError.DIS in fatal_error
 199
 200        return ((fatal_error & fatal_trigger) and sdf) or (not sena) or (not dis)
 201
 202    def is_enabled(self, *args):
 203        """
 204        Return if enabled.  See is_disabled.
 205        """
 206        return not self.is_disabled(*args)
 207
 208    def get_device_type(self):
 209        """returns a string containing the device type
 210
 211        DevTyp returns the module’s device type. For all tunable lasers covered by this MSA, the
 212        module will return the null terminated string “CW ITLA\0” (eight bytes including the
 213        terminating null character) indirectly through the AEA mechanism. The device type register
 214        is provided such that a host can distinguish between different types of tunable devices.type.
 215
 216        See 9.4.2 Device Type (DevTyp 0x01) [R]
 217        """
 218        response_bytes = self._devtyp()
 219
 220        return response_bytes.decode("utf-8")
 221
 222    def get_manufacturer(self):
 223        """returns a string containing the manufacturer's name.
 224
 225        MFGR returns the module’s manufacturers ID null terminated string indirectly through the
 226        AEA mechanism
 227
 228        See 9.4.3 Manufacturer (MFGR 0x02) [R]
 229        """
 230        response_bytes = self._mfgr()
 231
 232        return response_bytes.decode("utf-8")
 233
 234    def get_model(self):
 235        """returns the model as a string
 236
 237        Model returns the module’s model designation string indirectly through the AEA
 238        mechanism. The null terminated string containing the module’s model designation is
 239        placed into a field of not more than 80 bytes in size. The model string is defined by the
 240        manufacturer
 241
 242        See 9.4.4 Model (Model 0x03) [R]
 243        """
 244        response_bytes = self._model()
 245
 246        return response_bytes.decode("utf-8")
 247
 248    def get_serialnumber(self):
 249        """returns the serial number
 250
 251        SerNo returns the module’s serial number string indirectly through the AEA mechanism.
 252        The null terminated string containing the module’s serial number is placed into a field of not
 253        more than 80 bytes in size. The serial number string is defined by the manufacturer.
 254
 255        See 9.4.5 Serial Number (SerNo 0x04) [R]
 256        """
 257        response_bytes = self._serno()
 258
 259        return response_bytes.decode("utf-8")
 260
 261    def get_manufacturing_date(self):
 262        """returns the manufacturing date
 263
 264        MFGDate returns the manufacturing date string of the module indirectly through the AEA
 265        mechanism. The null terminated string containing the date string is contained in a field
 266        size of 12 bytes.
 267
 268        See 9.4.5 Manufaturing Date (MFGDate 0x05) [R]
 269        """
 270        response_bytes = self._mfgdate()
 271
 272        return response_bytes.decode("utf-8")
 273
 274    def get_firmware_release(self):
 275        """returns a manufacturer specific firmware release
 276
 277        Release returns the release string of the module indirectly through the AEA mechanism.
 278        The null terminated string containing the module release information is placed into a field of
 279        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
 280        hardware revisions to track. The release field also encodes the application space
 281        identifier.
 282
 283        See 9.4.7 Release (Release 0x06) [R]
 284        """
 285        response_bytes = self._release()
 286
 287        return response_bytes.decode("utf-8")
 288
 289    def get_backwardscompatibility(self):
 290        """returns a manufacturer specific firmware backwards compatibility
 291        as a null terminated string
 292
 293        RelBack returns the release backwards compatibility string of the module indirectly through
 294        the AEA mechanism. The null terminated string containing the earliest release string which
 295        is fully backwards compatible with the current module. The string is contained in a field of
 296        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
 297        hardware revisions to track as described in the Release (0x06) register
 298
 299        See 9.4.8 Release Backwards Compatibility (RelBack 0x07) [R]
 300        """
 301        response_bytes = self._relback()
 302
 303        return response_bytes.decode("utf-8")
 304
 305    def get_general_module_configuration(self):
 306        """returns the power on/reset module configuration
 307
 308        GenCfg defines the general module configuration for the generic tunable device. For the
 309        tunable laser, the register is used to save the power on/reset module configuration
 310        defaults.
 311
 312        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
 313        """
 314        raise NotImplementedError()
 315
 316    def set_general_module_configuration(self):
 317        """returns the power on/reset module configuration
 318
 319        GenCfg defines the general module configuration for the generic tunable device. For the
 320        tunable laser, the register is used to save the power on/reset module configuration
 321        defaults.
 322
 323        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
 324        """
 325        raise NotImplementedError()
 326
 327    def get_iocap(self):
 328        """
 329        The IOCap register returns or sets the I/O interface capabilities
 330
 331        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
 332        """
 333        raise NotImplementedError()
 334
 335    def set_iocap(self, data):
 336        """
 337        The IOCap register returns or sets the I/O interface capabilities
 338
 339        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
 340        """
 341        raise NotImplementedError()
 342
 343    def read_aea(self):
 344        """reads the AEA register data until an execution error is thrown.
 345
 346        See 9.4.11 Extended Addressing Mode Registers (0x09-0x0B, 0x0E-0x10) [RW]
 347        """
 348        aea_response = b""
 349        try:
 350            while True:
 351                aea_response += self._aea_ear()
 352
 353        except ExecutionError:
 354            try:
 355                self.nop()
 356
 357            except EREError:
 358                # If this throws an ERE error then we have completed reading AEA
 359                pass
 360
 361            except NOPException as nop_e:
 362                raise nop_e
 363
 364        return aea_response
 365
 366    def get_download_configuration(self):
 367        """
 368        The DLConfig register configures a host to module download of code or data for
 369        reconfiguration purposes or configures a module to host upload of code or data to the host.
 370        A file transfer may occur at several locations such as vendor factory, customer site (on the
 371        bench), customer system (circuit down), or potentially a customer system (live circuit)
 372
 373        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
 374        """
 375        raise NotImplementedError()
 376
 377    def set_download_configuration(self):
 378        """
 379        The DLConfig register configures a host to module download of code or data for
 380        reconfiguration purposes or configures a module to host upload of code or data to the host.
 381        A file transfer may occur at several locations such as vendor factory, customer site (on the
 382        bench), customer system (circuit down), or potentially a customer system (live circuit)
 383
 384        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
 385        """
 386        raise NotImplementedError()
 387
 388    def get_download_status(self):
 389        """
 390        DLStatus provides information about the status or viability of a code segment.
 391
 392        See 9.4.14 Download Status (DLStatus 0x15) [R]
 393        """
 394        raise NotImplementedError()
 395
 396    def wait(self):
 397        """Wait until operation is complete.
 398        It check if the operation is completed every self.sleep_time seconds.
 399        """
 400        while True:
 401            try:
 402                self.nop()
 403            except CPException:
 404                if self.sleep_time is not None:
 405                    sleep(self.sleep_time)
 406            else:
 407                break
 408
 409    def wait_until_enabled(self):
 410        """TODO: Add documentation."""
 411        while self.is_disabled():
 412            if self.sleep_time is not None:
 413                sleep(self.sleep_time)
 414
 415    def wait_until_disabled(self):
 416        """TODO: Add documentation."""
 417        while self.is_enabled():
 418            if self.sleep_time is not None:
 419                sleep(self.sleep_time)
 420
 421    def set_power(self, pwr_dBm):
 422        """Sets the power of the ITLA laser.
 423        Units of dBm.
 424
 425        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
 426
 427        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
 428        :returns: None
 429        """
 430        try:
 431            self._pwr(round(pwr_dBm * 100))
 432
 433        except ExecutionError:
 434            try:
 435                self.nop()
 436
 437            except RVEError as error:
 438                logger.error(
 439                    "The provided power %.2f dBm is outside of the range for this device. "
 440                    "The power is currently set to: %.2f dBm.",
 441                    pwr_dBm,
 442                    self.get_power_setting(),
 443                )
 444                raise error
 445
 446    def get_power_setting(self):
 447        """Gets current power setting set by set_power.
 448        Should be in dBm.
 449
 450        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
 451
 452        :returns:
 453        """
 454        # Gets power setting, not actual optical output power.
 455        response = self._pwr()
 456        return int.from_bytes(response, "big", signed=True) / 100
 457
 458    def get_power_output(self):
 459        """returns external optical power estimate
 460
 461        See 9.6.8 Optical Output Power (OOP 0x42) [R]
 462
 463        :returns: External optical power estimate in dBm
 464        """
 465        response = self._oop()
 466
 467        return int.from_bytes(response, "big", signed=True) / 100
 468
 469    def get_power_min(self):
 470        """returns minimum optical power output of the module.
 471        Units dBm.
 472
 473        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
 474
 475        :returns: minimum optical power
 476        """
 477        response = self._opsl()
 478        return int.from_bytes(response, "big", signed=True) / 100
 479
 480    def get_power_max(self):
 481        """returns maximum optical power output of the module.
 482        Units dBm.
 483
 484        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
 485
 486        :returns: maximum optical power
 487        """
 488        response = self._opsh()
 489        return int.from_bytes(response, "big", signed=True) / 100
 490
 491    def set_fcf(self, freq):
 492        """
 493        This sets the first channel frequency.
 494        It does not reset the channel so this frequency will only be equal
 495        to the output frequency if channel=1.
 496
 497        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 498
 499        :returns: None
 500        """
 501        # convert frequency to MHz
 502        freq = round(freq * 1e6)
 503
 504        freq_str = str(freq)
 505        fcf1 = int(freq_str[0:3])
 506        fcf2 = int(freq_str[3:7])
 507
 508        try:
 509            # is it better to split these into their own try/except blocks?
 510            self._fcf1(fcf1)
 511            self._fcf2(fcf2)
 512
 513        except ExecutionError:
 514            try:
 515                self.nop()
 516            except RVEError as error:
 517                logger.error(
 518                    "%.2f THz is out of bounds for this laser. "
 519                    "The frequency must be within the range %.2f - %.2f THz.",
 520                    fcf1,
 521                    self.get_frequency_min(),
 522                    self.get_frequency_max(),
 523                )
 524                raise error
 525
 526            except CIEError as error:
 527                logger.error(
 528                    "You cannot change the first channel frequency while the laser is enabled. "
 529                    "The current frequency is: %.2f THz",
 530                    self.get_frequency(),
 531                )
 532                raise error
 533
 534    def set_frequency(self, freq):
 535        """Sets the frequency of the laser in TeraHertz.
 536
 537        Has MHz resolution. Will round down.
 538
 539        This function sets the first channel frequency and then sets the
 540        channel to 1 so that the frequency output when the laser is enabled
 541        will be the frequency given to this function.
 542
 543        If you would like to only change the first channel frequency use
 544        the function `set_fcf`.
 545
 546        Disable the laser before calling this function.
 547
 548        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 549
 550        :param freq: The desired frequency setting in THz.
 551        :returns: None
 552        """
 553
 554        # This does a check so this only runs if fine tuning has been turned on.
 555        if self.get_fine_tuning() != 0:
 556            # Set the fine tuning off!
 557            while True:
 558                try:
 559                    self.set_fine_tuning(0)
 560                except ExecutionError:
 561                    sleep(self.sleep_time)
 562                except CPException:
 563                    self.wait()
 564                    break
 565                else:
 566                    break
 567
 568        try:
 569            self.set_fcf(freq)
 570        except CPException:
 571            self.wait()
 572
 573        try:
 574            self.set_channel(1)
 575        except CPException:
 576            self.wait()
 577
 578    def get_fcf(self):
 579        """returns first channel frequency
 580
 581        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
 582
 583        :returns: First channel frequency in THz
 584        """
 585        response = self._fcf1()
 586        fcf1 = int.from_bytes(response, "big")
 587
 588        response = self._fcf2()
 589        fcf2 = int.from_bytes(response, "big")
 590
 591        return fcf1 + fcf2 * 1e-4
 592
 593    def get_frequency(self):
 594        """gets the current laser operating frequency
 595
 596        See 9.6.7 Laser Frequency (LF1, LF2 0x40 – 0x41) [R]
 597
 598        :returns: Current laser frequency in THz.
 599        """
 600        response = self._lf1()
 601        lf1 = int.from_bytes(response, "big")
 602
 603        response = self._lf2()
 604        lf2 = int.from_bytes(response, "big")
 605
 606        return lf1 + lf2 * 1e-4
 607
 608    def dither_enable(self, waveform: Waveform = Waveform.SIN):
 609        """enables dither
 610
 611        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 612
 613        :param waveform: specifies the dither waveform
 614        """
 615
 616        data = (waveform << 4) | 2
 617
 618        self._dithere(data)
 619
 620    def dither_disable(self):
 621        """disables digital dither
 622
 623        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 624        """
 625        # TODO: This should preserve the waveform setting if possible
 626        self._dithere(0)
 627
 628    def set_dither_rate(self, rate):
 629        """set dither rate in kHz
 630        utilizes DitherR register
 631
 632        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 633
 634        :param rate: an unsigned short integer specifying dither rate in kHz
 635        """
 636        self._ditherr(rate)
 637
 638    def get_dither_rate(self):
 639        """get dither rate
 640        utilizes DitherR register
 641
 642        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 643
 644        :returns: Dither rate in kHz
 645        """
 646        response = self._ditherr()
 647        return int.from_bytes(response, "big")
 648
 649    def set_dither_frequency(self, rate):
 650        """set dither modulation frequency
 651        utilizes DitherF register
 652
 653        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 654
 655        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
 656        deviation as Ghz*10
 657        """
 658        self._ditherf(rate)
 659
 660    def get_dither_frequency(self):
 661        """get dither modulation frequency
 662        utilizes DitherF register
 663
 664        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 665
 666        :returns: dither modulation frequency in GHz * 10
 667        """
 668        response = self._ditherf()
 669        return int.from_bytes(response, "big")
 670
 671    def set_dither_amplitude(self, amplitude):
 672        """set dither modulation amplitude
 673        utilizes DitherA register
 674
 675        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 676
 677        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
 678        deviation as 10*percentage of the optical power
 679
 680        :returns: None
 681        """
 682        self._dithera(amplitude)
 683
 684    def get_dither_amplitude(self):
 685        """get dither modulation amplitude
 686        utilizes DitherA register
 687
 688        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
 689
 690        :returns: dither aplitude in 10ths of percents
 691        """
 692        response = self._dithera()
 693        return int.from_bytes(response, "big")
 694
 695    def get_temp(self):
 696        """Returns the current primary control temperature in deg C.
 697
 698        See 9.6.9 Current Temperature (CTemp 0x43) [R]
 699
 700        :returns: current primary control temperature in deg C
 701        """
 702        response = self._ctemp()
 703        temp_100 = int.from_bytes(response, "big")
 704
 705        return temp_100 / 100
 706
 707    def get_frequency_min(self):
 708        """returns minimum frequency supported by the module
 709
 710        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
 711
 712        :returns: minimum frequency in THz
 713        """
 714        response = self._lfl1()
 715        lfl1 = int.from_bytes(response, "big")
 716
 717        response = self._lfl2()
 718        lfl2 = int.from_bytes(response, "big")
 719
 720        return lfl1 + lfl2 * 1e-4
 721
 722    def get_frequency_max(self):
 723        """returns maximum frequency supported by the module
 724
 725        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
 726
 727        :returns: maximum frequency in THz
 728        """
 729        response = self._lfh1()
 730        fcf1 = int.from_bytes(response, "big")
 731
 732        response = self._lfh2()
 733        fcf2 = int.from_bytes(response, "big")
 734
 735        return fcf1 + fcf2 * 1e-4
 736
 737    def get_grid_min(self):
 738        """returns minimum grid spacing of the module
 739
 740        See 9.7.4 Laser’s Minimum Grid Spacing (LGrid 0x56) [R]
 741
 742        :returns: The minimum grid supported by the module in GHz
 743        """
 744        try:
 745            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
 746
 747        except ExecutionError:
 748            self.nop()
 749
 750        return freq_lgrid * 1e-1
 751
 752    def set_grid(self, grid_freq):
 753        """Set the grid spacing in GHz.
 754        MHz resolution.
 755
 756        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
 757
 758        :param grid_freq: the grid frequency spacing in GHz
 759        :returns:
 760
 761        """
 762        grid_freq = str(round(grid_freq * 1000))
 763        data = int(grid_freq[0:4])
 764
 765        self._grid(data)
 766
 767    def get_grid(self):
 768        """get the grid spacing in GHz
 769
 770        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
 771
 772        :returns: The grid spacing in GHz.
 773        """
 774        response = self._grid()
 775        grid_freq = int.from_bytes(response, "big", signed=True)
 776
 777        return grid_freq * 1e-1
 778
 779    def get_age(self):
 780        """Returns the percentage of aging of the laser.
 781        0% indicated a brand new laser
 782        100% indicates the laser should be replaces.
 783
 784        See 9.8.6 Laser Age (Age 0x61) [R]
 785
 786        :returns: percent aging of the laser
 787        """
 788        response = self._age()
 789        age = int.from_bytes(response, "big")
 790
 791        return age
 792
 793    def set_channel(self, channel):
 794        """Sets the laser's operating channel.
 795
 796        MSA-01.2 lasers support a 16 bit channel value.
 797
 798        This defines how many spaces along the grid
 799        the laser's frequency is displaced from the first channel frequency.
 800
 801        See 9.6.1 Channel (Channel 0x30) [RW]
 802
 803        :param channel:
 804        :returns: None
 805
 806        """
 807        # check type and stuff
 808        if not isinstance(channel, int):
 809            raise TypeError("Channel must be an integer")
 810        if channel < 0:
 811            raise ValueError("Channel must be positive.")
 812        if channel > 0xFFFF:
 813            raise ValueError("Channel must be a 16 bit integer (<=0xFFFF).")
 814
 815        # Split the channel choice into two options.
 816        channel_hex = f"{channel:08x}"
 817
 818        channell = int(channel_hex[4:], 16)
 819
 820        # Set the channel registers.
 821        self._channel(channell)
 822
 823    def get_channel(self):
 824        """gets the current channel setting
 825
 826        The channel defines how many spaces along the grid
 827        the laser's frequency is displaced from the first channel frequency.
 828
 829        See 9.6.1 Channel (Channel 0x30) [RW]
 830
 831        :returns: channel as an integer.
 832
 833        """
 834        # This concatenates the data bytestrings
 835        response = self._channel()
 836
 837        channel = int.from_bytes(response, "big")
 838
 839        return channel
 840
 841    def set_fine_tuning(self, ftf):
 842        """
 843        This function provides off grid tuning for the laser's frequency.
 844        The adjustment applies to all channels uniformly.
 845        This is typically used after the laser if locked and minor adjustments
 846        are required to the frequency.
 847
 848        The command may be pending in the event that the laser output is
 849        enabled. The pending bit is cleared once the fine tune frequency
 850        has been achieved.
 851
 852        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
 853
 854        **???** It seems like this can be done with the laser running.
 855
 856        :param ftf: The fine tune frequency adjustment in GHz
 857        """
 858
 859        ftf = round(ftf * 1e3)
 860
 861        # We will leave this bare. This way the user can set and handle
 862        # their own timing and check to make sure the laser has reached the
 863        # desired fine tuning frequency.
 864        # It WILL throw a "pending" error if the laser is on when setting.
 865        self._ftf(ftf)
 866
 867    def get_fine_tuning(self):
 868        """
 869        This function returns the setting for the
 870        off grid tuning for the laser's frequency.
 871
 872        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
 873
 874        :return ftf: The fine tune frequency adjustment in GHz
 875        """
 876
 877        response = self._ftf()
 878        ftf = int.from_bytes(response, "big", signed=True)
 879
 880        return ftf * 1e-3
 881
 882    def get_ftf_range(self):
 883        """return the maximum and minimum off grid tuning for the laser's frequency.
 884
 885        See 9.7.1 Fine Tune Frequency Range (FTF 0x4F) [R]
 886
 887        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
 888        """
 889        response = self._ftfr()
 890
 891        ftfr = int.from_bytes(response, "big")
 892
 893        return ftfr * 1e-3
 894
 895    def get_temps(self):
 896        """returns a list of technology specific temperatures
 897        units in deg C unless otherwise specified
 898
 899        **Technologies**
 900        Technology 1: [Diode Temp, Case Temp]
 901        Technology 2: [Diode Temp, Case Temp]
 902        (why are they the same?)
 903
 904        See 9.8.2 Module Temperatures (Temps 0x58) [R]
 905
 906        :returns: list of temperatures in deg C
 907        """
 908        # get response this should be a long byte string
 909        response = self._temps()
 910
 911        data = [
 912            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
 913            for i in range(0, len(response), 2)
 914        ]
 915
 916        return data
 917
 918    def get_currents(self):
 919        """returns a list of technology specific currents in mA.
 920
 921        **Technologies**
 922        Technology 1: [TEC, Diode]
 923        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
 924        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
 925
 926        See 9.8.1 Module Currents (Currents 0x57) [R]
 927
 928        :returns: list of currents in mA
 929        """
 930        # get response this should be a long byte string
 931        response = self._currents()
 932
 933        data = [
 934            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
 935            for i in range(0, len(response), 2)
 936        ]
 937
 938        return data
 939
 940    def get_last_response(self):
 941        """reads the last response sent from the laser
 942
 943        This function gets the most recent response sent from the laser.
 944        The response is parsed for errors the way any normal response
 945        would be. The variable `self._response` is set to the last response again.
 946
 947        See 9.4.12 Last Response (LstResp 0x13) [R]
 948
 949        """
 950        return self._lstresp()
 951
 952    def get_error_fatal(self, reset=False):
 953        """reads fatal error register statusf and checks each bit against the
 954        fatal error table to determine the fault
 955
 956        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
 957
 958        :param reset: resets/clears latching errors
 959
 960        """
 961        response = self._statusf()
 962        statusf = int.from_bytes(response, "big")
 963
 964        logger.debug("Current Status Fatal Error: %d", statusf)
 965
 966        if reset:
 967            data_reset = 0x00FF
 968            self._statusf(data_reset)
 969
 970        return FatalError(statusf)
 971
 972    def get_error_warning(self, reset=False):
 973        """reads warning error register statusw and checks each bit against the
 974        warning error table to determine the fault
 975
 976        If the laser is off then some of the latched warnings will be set on.
 977
 978        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
 979
 980        :param reset: resets/clears latching errors
 981        """
 982        response = self._statusw()
 983        statusw = int.from_bytes(response, "big")
 984
 985        logger.debug("Current Status Warning Error: %d", statusw)
 986
 987        if reset:
 988            data_reset = 0x00FF
 989            self._statusw(data_reset)
 990
 991        return WarningError(statusw)
 992
 993    def get_fatal_power_thresh(self):
 994        """reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
 995
 996        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
 997        """
 998        response = self._fpowth()
 999        pow_fatal = int.from_bytes(response, "big") / 100
1000        # correcting for proper order of magnitude
1001        return pow_fatal
1002
1003    def get_warning_power_thresh(self):
1004        """reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
1005
1006        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
1007        """
1008        response = self._wpowth()
1009        pow_warn = int.from_bytes(response, "big") / 100
1010        # correcting for proper order of magnitude
1011        return pow_warn
1012
1013    def get_fatal_freq_thresh(self):
1014        """reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
1015
1016        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1017        """
1018        response = self._ffreqth()
1019        freq_fatal = int.from_bytes(response, "big") / 10
1020        return freq_fatal
1021
1022    def get_warning_freq_thresh(self):
1023        """reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
1024
1025        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1026        """
1027        response = self._wfreqth()
1028        freq_warn = int.from_bytes(response, "big") / 10
1029        return freq_warn
1030
1031    def get_fatal_therm_thresh(self):
1032        """reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
1033
1034        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1035        """
1036        response = self._fthermth()
1037        therm_fatal = int.from_bytes(response, "big") / 100
1038        # correcting for proper order of magnitude
1039        return therm_fatal
1040
1041    def get_warning_therm_thresh(self):
1042        """reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
1043
1044        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1045        """
1046        response = self._wthermth()
1047        therm_thresh = int.from_bytes(response, "big") / 100
1048        # correcting for proper order of magnitude
1049        return therm_thresh
1050
1051    def get_srq_trigger(self):
1052        """identifies corresponding bits in status registers StatusF, StatusW
1053
1054        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
1055
1056        See 9.5.5 SRQ* Triggers (SRQT 0x28) [RW]
1057        """
1058        response = self._srqt()
1059        status = int.from_bytes(response, "big")
1060
1061        logger.debug("SRQT Status: %d", status)
1062
1063        return SQRTrigger(status)
1064
1065    def get_fatal_trigger(self):
1066        """identifies which fatal condition was asserted in StatusF and StatusW registers
1067
1068        See 9.5.6 FATAL Triggers (FatalT 0x29) [RW]
1069        """
1070        response = self._fatalt()
1071        status = int.from_bytes(response, "big")
1072
1073        logger.debug("FatalT Status: %d", status)
1074
1075        return FatalTrigger(status)
1076
1077    def get_alm_trigger(self):
1078        """identifies why ALM was asserted in StatusF and StatusW registers
1079
1080        See 9.5.7 ALM Triggers (ALMT 0x2A) [RW]
1081        """
1082        response = self._almt()
1083        status = int.from_bytes(response, "big")
1084
1085        logger.debug("AlarmT Status: %d", status)
1086
1087        return AlarmTrigger(status)

A class that represents the ITLA12 and exposes a user friendly API for controlling functionality.

Things to figure out

  • What should be exposed to the user?
  • Can we abstract away the concept of registers and stuff in here so you don't have to deal with it.

There are some functions that could be implemented like set_fatalstatus. I think this is probably a bad idea even though it isnt write only.

Set Frequency is my platonic ideal for the higher level functions.

ITLA12( serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1)
44    def __init__(
45        self, serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1
46    ):
47        """Initializes the ITLA12 object.
48
49        :param serial_port: The serial port to connect to.
50        Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
51        :param baudrate: The baudrate for communication. I have primarily seen
52        lasers using 9600 as the default and then higher for firmware upgrades.
53        This may be laser specific.
54        :param timeout: How long should we wait to receive a response from the laser
55        :param register_files: Any additional register files you would like to include
56        beyond the default MSA-01.2 defined registers. These must be in a yaml format as
57        described in the project's README.
58        :param sleep_time: time in seconds. Used in wait function
59        """
60        if register_files is None:
61            register_files = []
62
63        register_files = ["registers_itla12.yaml", *register_files]
64
65        self.sleep_time = sleep_time
66
67        super().__init__(
68            serial_port, baudrate, timeout=timeout, register_files=register_files
69        )

Initializes the ITLA12 object.

Parameters
  • serial_port: The serial port to connect to. Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
  • baudrate: The baudrate for communication. I have primarily seen lasers using 9600 as the default and then higher for firmware upgrades. This may be laser specific.
  • timeout: How long should we wait to receive a response from the laser
  • register_files: Any additional register files you would like to include beyond the default MSA-01.2 defined registers. These must be in a yaml format as described in the project's README.
  • sleep_time: time in seconds. Used in wait function
sleep_time
def nop(self, data=None):
71    def nop(self, data=None):
72        """The No-Op operation.
73
74        This is a good test to see if your laser is communicating properly.
75        It should read 0000 data or echo whatever data you send it if you send it something.
76        The data you write to nop gets overwritten as soon as you read it back.
77
78        `nop()` also returns more informative errors following an ExecutionError.
79        This is not called by default so you must do this explicitly if you want
80        to see more informative error information.
81
82        See 9.4.1 NOP/Status (ResEna 0x00) [RW] in OIF-ITLA-MSA.
83
84        :param data: Data to write
85
86        :returns: None
87        """
88        # pretty sure the data does nothing
89        if data is not None:
90            response = self._nop(data)
91        else:
92            response = self._nop()
93
94        error_field = int(response.hex()[-1], 16)
95        if bool(error_field):
96            raise self._nop_errors[error_field]

The No-Op operation.

This is a good test to see if your laser is communicating properly. It should read 0000 data or echo whatever data you send it if you send it something. The data you write to nop gets overwritten as soon as you read it back.

nop() also returns more informative errors following an ExecutionError. This is not called by default so you must do this explicitly if you want to see more informative error information.

See 9.4.1 NOP/Status (ResEna 0x00) [RW] in OIF-ITLA-MSA.

Parameters
  • data: Data to write

:returns: None

def enable(self):
 98    def enable(self):
 99        """Enables laser optical output.
100        There is a time delay after execution of this function to
101        proper stable laser output.
102
103        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
104
105        :returns: None
106        """
107        # I'm writing this out partially for transparency
108        # Maybe unnecessary or non-optimal
109        self._resena(Resena.SENA)

Enables laser optical output. There is a time delay after execution of this function to proper stable laser output.

See 9.6.3 Reset/Enable (ResEna 0x32) [RW]

:returns: None

def disable(self):
111    def disable(self):
112        """disables the laser
113
114        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
115
116        :returns: None
117        """
118        self._resena(0)

disables the laser

See 9.6.3 Reset/Enable (ResEna 0x32) [RW]

:returns: None

def hard_reset(self):
120    def hard_reset(self):
121        """initiate module reset
122
123        The hardware reset is typically traffic interrupting since it will reset
124        control loops as well. The host can poll the communication’s interface
125        waiting for a response packet indicating that the interface is ready to
126        communicate. Note that a response is returned to acknowledge the reset
127        request before the reset is started
128
129        The impact to the optical signal is undefined. This bit is self clearing.
130
131        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
132
133        :returns: None
134        """
135        self._resena(Resena.MR)

initiate module reset

The hardware reset is typically traffic interrupting since it will reset control loops as well. The host can poll the communication’s interface waiting for a response packet indicating that the interface is ready to communicate. Note that a response is returned to acknowledge the reset request before the reset is started

The impact to the optical signal is undefined. This bit is self clearing.

See 9.6.3 Reset/Enable (ResEna 0x32) [RW]

:returns: None

def soft_reset(self):
137    def soft_reset(self):
138        """initiate soft reset
139
140        The soft reset resets the communication’s interface and is traffic
141        non-interrupting. Extended address registers are reset.
142
143        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
144
145        :returns: None
146        """
147        self._resena(Resena.SR)

initiate soft reset

The soft reset resets the communication’s interface and is traffic non-interrupting. Extended address registers are reset.

See 9.6.3 Reset/Enable (ResEna 0x32) [RW]

:returns: None

def get_reset_enable(self):
149    def get_reset_enable(self):
150        """return ResEna register.
151
152        See 9.6.3 Reset/Enable (ResEna 0x32) [RW]
153        """
154        response = self._resena()
155        return Resena(int.from_bytes(response, "big"))

return ResEna register.

See 9.6.3 Reset/Enable (ResEna 0x32) [RW]

def set_alarm_during_tuning(self):
157    def set_alarm_during_tuning(self):
158        """Set alarm during tuning
159        mcb(ADT)
160
161        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
162        """
163        self._mcb(MCB.ADT)

Set alarm during tuning mcb(ADT)

See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]

def set_shutdown_on_fatal(self):
165    def set_shutdown_on_fatal(self):
166        """Set shutdown on fatal.
167        mcb(SDF)
168
169        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
170        """
171        self._mcb(MCB.SDF)

Set shutdown on fatal. mcb(SDF)

See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]

def get_configuration_behavior(self):
173    def get_configuration_behavior(self):
174        """
175        Return MCB register.
176
177        See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]
178        """
179        response = self._mcb()
180        return MCB(int.from_bytes(response, "big"))

Return MCB register.

See 9.6.4 Module Configuration Behavior (MCB 0x33) [RW]

def is_disabled(self):
182    def is_disabled(self):
183        """
184        Return if disabled.
185
186        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
187
188        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
189        Consider overwriting this methods and monitoring FatalError.ALM.
190        """
191        fatal_error = self.get_error_fatal()
192        fatal_trigger = self.get_fatal_trigger()
193        resena = self.get_reset_enable()
194        mcb = self.get_configuration_behavior()
195
196        sdf = MCB.SDF in mcb
197        sena = Resena.SENA in resena
198        dis = FatalError.DIS in fatal_error
199
200        return ((fatal_error & fatal_trigger) and sdf) or (not sena) or (not dis)

Return if disabled.

See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.

For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it). Consider overwriting this methods and monitoring FatalError.ALM.

def is_enabled(self, *args):
202    def is_enabled(self, *args):
203        """
204        Return if enabled.  See is_disabled.
205        """
206        return not self.is_disabled(*args)

Return if enabled. See is_disabled.

def get_device_type(self):
208    def get_device_type(self):
209        """returns a string containing the device type
210
211        DevTyp returns the module’s device type. For all tunable lasers covered by this MSA, the
212        module will return the null terminated string “CW ITLA\0” (eight bytes including the
213        terminating null character) indirectly through the AEA mechanism. The device type register
214        is provided such that a host can distinguish between different types of tunable devices.type.
215
216        See 9.4.2 Device Type (DevTyp 0x01) [R]
217        """
218        response_bytes = self._devtyp()
219
220        return response_bytes.decode("utf-8")

returns a string containing the device type

DevTyp returns the module’s device type. For all tunable lasers covered by this MSA, the module will return the null terminated string “CW ITLA” (eight bytes including the terminating null character) indirectly through the AEA mechanism. The device type register is provided such that a host can distinguish between different types of tunable devices.type.

See 9.4.2 Device Type (DevTyp 0x01) [R]

def get_manufacturer(self):
222    def get_manufacturer(self):
223        """returns a string containing the manufacturer's name.
224
225        MFGR returns the module’s manufacturers ID null terminated string indirectly through the
226        AEA mechanism
227
228        See 9.4.3 Manufacturer (MFGR 0x02) [R]
229        """
230        response_bytes = self._mfgr()
231
232        return response_bytes.decode("utf-8")

returns a string containing the manufacturer's name.

MFGR returns the module’s manufacturers ID null terminated string indirectly through the AEA mechanism

See 9.4.3 Manufacturer (MFGR 0x02) [R]

def get_model(self):
234    def get_model(self):
235        """returns the model as a string
236
237        Model returns the module’s model designation string indirectly through the AEA
238        mechanism. The null terminated string containing the module’s model designation is
239        placed into a field of not more than 80 bytes in size. The model string is defined by the
240        manufacturer
241
242        See 9.4.4 Model (Model 0x03) [R]
243        """
244        response_bytes = self._model()
245
246        return response_bytes.decode("utf-8")

returns the model as a string

Model returns the module’s model designation string indirectly through the AEA mechanism. The null terminated string containing the module’s model designation is placed into a field of not more than 80 bytes in size. The model string is defined by the manufacturer

See 9.4.4 Model (Model 0x03) [R]

def get_serialnumber(self):
248    def get_serialnumber(self):
249        """returns the serial number
250
251        SerNo returns the module’s serial number string indirectly through the AEA mechanism.
252        The null terminated string containing the module’s serial number is placed into a field of not
253        more than 80 bytes in size. The serial number string is defined by the manufacturer.
254
255        See 9.4.5 Serial Number (SerNo 0x04) [R]
256        """
257        response_bytes = self._serno()
258
259        return response_bytes.decode("utf-8")

returns the serial number

SerNo returns the module’s serial number string indirectly through the AEA mechanism. The null terminated string containing the module’s serial number is placed into a field of not more than 80 bytes in size. The serial number string is defined by the manufacturer.

See 9.4.5 Serial Number (SerNo 0x04) [R]

def get_manufacturing_date(self):
261    def get_manufacturing_date(self):
262        """returns the manufacturing date
263
264        MFGDate returns the manufacturing date string of the module indirectly through the AEA
265        mechanism. The null terminated string containing the date string is contained in a field
266        size of 12 bytes.
267
268        See 9.4.5 Manufaturing Date (MFGDate 0x05) [R]
269        """
270        response_bytes = self._mfgdate()
271
272        return response_bytes.decode("utf-8")

returns the manufacturing date

MFGDate returns the manufacturing date string of the module indirectly through the AEA mechanism. The null terminated string containing the date string is contained in a field size of 12 bytes.

See 9.4.5 Manufaturing Date (MFGDate 0x05) [R]

def get_firmware_release(self):
274    def get_firmware_release(self):
275        """returns a manufacturer specific firmware release
276
277        Release returns the release string of the module indirectly through the AEA mechanism.
278        The null terminated string containing the module release information is placed into a field of
279        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
280        hardware revisions to track. The release field also encodes the application space
281        identifier.
282
283        See 9.4.7 Release (Release 0x06) [R]
284        """
285        response_bytes = self._release()
286
287        return response_bytes.decode("utf-8")

returns a manufacturer specific firmware release

Release returns the release string of the module indirectly through the AEA mechanism. The null terminated string containing the module release information is placed into a field of not more than 80 bytes in size. Note that a module may have one or more firmware and/or hardware revisions to track. The release field also encodes the application space identifier.

See 9.4.7 Release (Release 0x06) [R]

def get_backwardscompatibility(self):
289    def get_backwardscompatibility(self):
290        """returns a manufacturer specific firmware backwards compatibility
291        as a null terminated string
292
293        RelBack returns the release backwards compatibility string of the module indirectly through
294        the AEA mechanism. The null terminated string containing the earliest release string which
295        is fully backwards compatible with the current module. The string is contained in a field of
296        not more than 80 bytes in size. Note that a module may have one or more firmware and/or
297        hardware revisions to track as described in the Release (0x06) register
298
299        See 9.4.8 Release Backwards Compatibility (RelBack 0x07) [R]
300        """
301        response_bytes = self._relback()
302
303        return response_bytes.decode("utf-8")

returns a manufacturer specific firmware backwards compatibility as a null terminated string

RelBack returns the release backwards compatibility string of the module indirectly through the AEA mechanism. The null terminated string containing the earliest release string which is fully backwards compatible with the current module. The string is contained in a field of not more than 80 bytes in size. Note that a module may have one or more firmware and/or hardware revisions to track as described in the Release (0x06) register

See 9.4.8 Release Backwards Compatibility (RelBack 0x07) [R]

def get_general_module_configuration(self):
305    def get_general_module_configuration(self):
306        """returns the power on/reset module configuration
307
308        GenCfg defines the general module configuration for the generic tunable device. For the
309        tunable laser, the register is used to save the power on/reset module configuration
310        defaults.
311
312        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
313        """
314        raise NotImplementedError()

returns the power on/reset module configuration

GenCfg defines the general module configuration for the generic tunable device. For the tunable laser, the register is used to save the power on/reset module configuration defaults.

See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]

def set_general_module_configuration(self):
316    def set_general_module_configuration(self):
317        """returns the power on/reset module configuration
318
319        GenCfg defines the general module configuration for the generic tunable device. For the
320        tunable laser, the register is used to save the power on/reset module configuration
321        defaults.
322
323        See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]
324        """
325        raise NotImplementedError()

returns the power on/reset module configuration

GenCfg defines the general module configuration for the generic tunable device. For the tunable laser, the register is used to save the power on/reset module configuration defaults.

See 9.4.9 General Module Configuration (GenCfg 0x08) [RW]

def get_iocap(self):
327    def get_iocap(self):
328        """
329        The IOCap register returns or sets the I/O interface capabilities
330
331        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
332        """
333        raise NotImplementedError()

The IOCap register returns or sets the I/O interface capabilities

See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]

def set_iocap(self, data):
335    def set_iocap(self, data):
336        """
337        The IOCap register returns or sets the I/O interface capabilities
338
339        See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]
340        """
341        raise NotImplementedError()

The IOCap register returns or sets the I/O interface capabilities

See 9.4.10 IO Capabilities (IOCap 0x0D) [RW]

def read_aea(self):
343    def read_aea(self):
344        """reads the AEA register data until an execution error is thrown.
345
346        See 9.4.11 Extended Addressing Mode Registers (0x09-0x0B, 0x0E-0x10) [RW]
347        """
348        aea_response = b""
349        try:
350            while True:
351                aea_response += self._aea_ear()
352
353        except ExecutionError:
354            try:
355                self.nop()
356
357            except EREError:
358                # If this throws an ERE error then we have completed reading AEA
359                pass
360
361            except NOPException as nop_e:
362                raise nop_e
363
364        return aea_response

reads the AEA register data until an execution error is thrown.

See 9.4.11 Extended Addressing Mode Registers (0x09-0x0B, 0x0E-0x10) [RW]

def get_download_configuration(self):
366    def get_download_configuration(self):
367        """
368        The DLConfig register configures a host to module download of code or data for
369        reconfiguration purposes or configures a module to host upload of code or data to the host.
370        A file transfer may occur at several locations such as vendor factory, customer site (on the
371        bench), customer system (circuit down), or potentially a customer system (live circuit)
372
373        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
374        """
375        raise NotImplementedError()

The DLConfig register configures a host to module download of code or data for reconfiguration purposes or configures a module to host upload of code or data to the host. A file transfer may occur at several locations such as vendor factory, customer site (on the bench), customer system (circuit down), or potentially a customer system (live circuit)

See 9.4.13 Download Configuration (DLConfig 0x14) [RW]

def set_download_configuration(self):
377    def set_download_configuration(self):
378        """
379        The DLConfig register configures a host to module download of code or data for
380        reconfiguration purposes or configures a module to host upload of code or data to the host.
381        A file transfer may occur at several locations such as vendor factory, customer site (on the
382        bench), customer system (circuit down), or potentially a customer system (live circuit)
383
384        See 9.4.13 Download Configuration (DLConfig 0x14) [RW]
385        """
386        raise NotImplementedError()

The DLConfig register configures a host to module download of code or data for reconfiguration purposes or configures a module to host upload of code or data to the host. A file transfer may occur at several locations such as vendor factory, customer site (on the bench), customer system (circuit down), or potentially a customer system (live circuit)

See 9.4.13 Download Configuration (DLConfig 0x14) [RW]

def get_download_status(self):
388    def get_download_status(self):
389        """
390        DLStatus provides information about the status or viability of a code segment.
391
392        See 9.4.14 Download Status (DLStatus 0x15) [R]
393        """
394        raise NotImplementedError()

DLStatus provides information about the status or viability of a code segment.

See 9.4.14 Download Status (DLStatus 0x15) [R]

def wait(self):
396    def wait(self):
397        """Wait until operation is complete.
398        It check if the operation is completed every self.sleep_time seconds.
399        """
400        while True:
401            try:
402                self.nop()
403            except CPException:
404                if self.sleep_time is not None:
405                    sleep(self.sleep_time)
406            else:
407                break

Wait until operation is complete. It check if the operation is completed every self.sleep_time seconds.

def wait_until_enabled(self):
409    def wait_until_enabled(self):
410        """TODO: Add documentation."""
411        while self.is_disabled():
412            if self.sleep_time is not None:
413                sleep(self.sleep_time)

TODO: Add documentation.

def wait_until_disabled(self):
415    def wait_until_disabled(self):
416        """TODO: Add documentation."""
417        while self.is_enabled():
418            if self.sleep_time is not None:
419                sleep(self.sleep_time)

TODO: Add documentation.

def set_power(self, pwr_dBm):
421    def set_power(self, pwr_dBm):
422        """Sets the power of the ITLA laser.
423        Units of dBm.
424
425        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
426
427        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
428        :returns: None
429        """
430        try:
431            self._pwr(round(pwr_dBm * 100))
432
433        except ExecutionError:
434            try:
435                self.nop()
436
437            except RVEError as error:
438                logger.error(
439                    "The provided power %.2f dBm is outside of the range for this device. "
440                    "The power is currently set to: %.2f dBm.",
441                    pwr_dBm,
442                    self.get_power_setting(),
443                )
444                raise error

Sets the power of the ITLA laser. Units of dBm.

See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]

Parameters
  • pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX. :returns: None
def get_power_setting(self):
446    def get_power_setting(self):
447        """Gets current power setting set by set_power.
448        Should be in dBm.
449
450        See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]
451
452        :returns:
453        """
454        # Gets power setting, not actual optical output power.
455        response = self._pwr()
456        return int.from_bytes(response, "big", signed=True) / 100

Gets current power setting set by set_power. Should be in dBm.

See 9.6.2 Optical Power Set Point (PWR 0x31) [RW]

:returns:

def get_power_output(self):
458    def get_power_output(self):
459        """returns external optical power estimate
460
461        See 9.6.8 Optical Output Power (OOP 0x42) [R]
462
463        :returns: External optical power estimate in dBm
464        """
465        response = self._oop()
466
467        return int.from_bytes(response, "big", signed=True) / 100

returns external optical power estimate

See 9.6.8 Optical Output Power (OOP 0x42) [R]

:returns: External optical power estimate in dBm

def get_power_min(self):
469    def get_power_min(self):
470        """returns minimum optical power output of the module.
471        Units dBm.
472
473        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
474
475        :returns: minimum optical power
476        """
477        response = self._opsl()
478        return int.from_bytes(response, "big", signed=True) / 100

returns minimum optical power output of the module. Units dBm.

See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]

:returns: minimum optical power

def get_power_max(self):
480    def get_power_max(self):
481        """returns maximum optical power output of the module.
482        Units dBm.
483
484        See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]
485
486        :returns: maximum optical power
487        """
488        response = self._opsh()
489        return int.from_bytes(response, "big", signed=True) / 100

returns maximum optical power output of the module. Units dBm.

See 9.7.2 Optical Power Min/Max Set Points (OPSL, OPSH 0x50 – 0x51) [R]

:returns: maximum optical power

def set_fcf(self, freq):
491    def set_fcf(self, freq):
492        """
493        This sets the first channel frequency.
494        It does not reset the channel so this frequency will only be equal
495        to the output frequency if channel=1.
496
497        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
498
499        :returns: None
500        """
501        # convert frequency to MHz
502        freq = round(freq * 1e6)
503
504        freq_str = str(freq)
505        fcf1 = int(freq_str[0:3])
506        fcf2 = int(freq_str[3:7])
507
508        try:
509            # is it better to split these into their own try/except blocks?
510            self._fcf1(fcf1)
511            self._fcf2(fcf2)
512
513        except ExecutionError:
514            try:
515                self.nop()
516            except RVEError as error:
517                logger.error(
518                    "%.2f THz is out of bounds for this laser. "
519                    "The frequency must be within the range %.2f - %.2f THz.",
520                    fcf1,
521                    self.get_frequency_min(),
522                    self.get_frequency_max(),
523                )
524                raise error
525
526            except CIEError as error:
527                logger.error(
528                    "You cannot change the first channel frequency while the laser is enabled. "
529                    "The current frequency is: %.2f THz",
530                    self.get_frequency(),
531                )
532                raise error

This sets the first channel frequency. It does not reset the channel so this frequency will only be equal to the output frequency if channel=1.

See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]

:returns: None

def set_frequency(self, freq):
534    def set_frequency(self, freq):
535        """Sets the frequency of the laser in TeraHertz.
536
537        Has MHz resolution. Will round down.
538
539        This function sets the first channel frequency and then sets the
540        channel to 1 so that the frequency output when the laser is enabled
541        will be the frequency given to this function.
542
543        If you would like to only change the first channel frequency use
544        the function `set_fcf`.
545
546        Disable the laser before calling this function.
547
548        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
549
550        :param freq: The desired frequency setting in THz.
551        :returns: None
552        """
553
554        # This does a check so this only runs if fine tuning has been turned on.
555        if self.get_fine_tuning() != 0:
556            # Set the fine tuning off!
557            while True:
558                try:
559                    self.set_fine_tuning(0)
560                except ExecutionError:
561                    sleep(self.sleep_time)
562                except CPException:
563                    self.wait()
564                    break
565                else:
566                    break
567
568        try:
569            self.set_fcf(freq)
570        except CPException:
571            self.wait()
572
573        try:
574            self.set_channel(1)
575        except CPException:
576            self.wait()

Sets the frequency of the laser in TeraHertz.

Has MHz resolution. Will round down.

This function sets the first channel frequency and then sets the channel to 1 so that the frequency output when the laser is enabled will be the frequency given to this function.

If you would like to only change the first channel frequency use the function set_fcf.

Disable the laser before calling this function.

See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]

Parameters
  • freq: The desired frequency setting in THz. :returns: None
def get_fcf(self):
578    def get_fcf(self):
579        """returns first channel frequency
580
581        See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]
582
583        :returns: First channel frequency in THz
584        """
585        response = self._fcf1()
586        fcf1 = int.from_bytes(response, "big")
587
588        response = self._fcf2()
589        fcf2 = int.from_bytes(response, "big")
590
591        return fcf1 + fcf2 * 1e-4

returns first channel frequency

See 9.6.6 First Channel’s Frequency (FCF1, FCF2 0x35 – 0x36) [RW]

:returns: First channel frequency in THz

def get_frequency(self):
593    def get_frequency(self):
594        """gets the current laser operating frequency
595
596        See 9.6.7 Laser Frequency (LF1, LF2 0x40 – 0x41) [R]
597
598        :returns: Current laser frequency in THz.
599        """
600        response = self._lf1()
601        lf1 = int.from_bytes(response, "big")
602
603        response = self._lf2()
604        lf2 = int.from_bytes(response, "big")
605
606        return lf1 + lf2 * 1e-4

gets the current laser operating frequency

See 9.6.7 Laser Frequency (LF1, LF2 0x40 – 0x41) [R]

:returns: Current laser frequency in THz.

def dither_enable(self, waveform: itla.itla_status.Waveform = <Waveform.SIN: 0>):
608    def dither_enable(self, waveform: Waveform = Waveform.SIN):
609        """enables dither
610
611        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
612
613        :param waveform: specifies the dither waveform
614        """
615
616        data = (waveform << 4) | 2
617
618        self._dithere(data)

enables dither

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

Parameters
  • waveform: specifies the dither waveform
def dither_disable(self):
620    def dither_disable(self):
621        """disables digital dither
622
623        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
624        """
625        # TODO: This should preserve the waveform setting if possible
626        self._dithere(0)

disables digital dither

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

def set_dither_rate(self, rate):
628    def set_dither_rate(self, rate):
629        """set dither rate in kHz
630        utilizes DitherR register
631
632        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
633
634        :param rate: an unsigned short integer specifying dither rate in kHz
635        """
636        self._ditherr(rate)

set dither rate in kHz utilizes DitherR register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

Parameters
  • rate: an unsigned short integer specifying dither rate in kHz
def get_dither_rate(self):
638    def get_dither_rate(self):
639        """get dither rate
640        utilizes DitherR register
641
642        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
643
644        :returns: Dither rate in kHz
645        """
646        response = self._ditherr()
647        return int.from_bytes(response, "big")

get dither rate utilizes DitherR register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

:returns: Dither rate in kHz

def set_dither_frequency(self, rate):
649    def set_dither_frequency(self, rate):
650        """set dither modulation frequency
651        utilizes DitherF register
652
653        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
654
655        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
656        deviation as Ghz*10
657        """
658        self._ditherf(rate)

set dither modulation frequency utilizes DitherF register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

Parameters
  • rate: an unsigned short integer encoded as the FM peak-to-peak frequency deviation as Ghz*10
def get_dither_frequency(self):
660    def get_dither_frequency(self):
661        """get dither modulation frequency
662        utilizes DitherF register
663
664        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
665
666        :returns: dither modulation frequency in GHz * 10
667        """
668        response = self._ditherf()
669        return int.from_bytes(response, "big")

get dither modulation frequency utilizes DitherF register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

:returns: dither modulation frequency in GHz * 10

def set_dither_amplitude(self, amplitude):
671    def set_dither_amplitude(self, amplitude):
672        """set dither modulation amplitude
673        utilizes DitherA register
674
675        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
676
677        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
678        deviation as 10*percentage of the optical power
679
680        :returns: None
681        """
682        self._dithera(amplitude)

set dither modulation amplitude utilizes DitherA register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

Parameters
  • amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude deviation as 10*percentage of the optical power

:returns: None

def get_dither_amplitude(self):
684    def get_dither_amplitude(self):
685        """get dither modulation amplitude
686        utilizes DitherA register
687
688        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
689
690        :returns: dither aplitude in 10ths of percents
691        """
692        response = self._dithera()
693        return int.from_bytes(response, "big")

get dither modulation amplitude utilizes DitherA register

See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]

:returns: dither aplitude in 10ths of percents

def get_temp(self):
695    def get_temp(self):
696        """Returns the current primary control temperature in deg C.
697
698        See 9.6.9 Current Temperature (CTemp 0x43) [R]
699
700        :returns: current primary control temperature in deg C
701        """
702        response = self._ctemp()
703        temp_100 = int.from_bytes(response, "big")
704
705        return temp_100 / 100

Returns the current primary control temperature in deg C.

See 9.6.9 Current Temperature (CTemp 0x43) [R]

:returns: current primary control temperature in deg C

def get_frequency_min(self):
707    def get_frequency_min(self):
708        """returns minimum frequency supported by the module
709
710        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
711
712        :returns: minimum frequency in THz
713        """
714        response = self._lfl1()
715        lfl1 = int.from_bytes(response, "big")
716
717        response = self._lfl2()
718        lfl2 = int.from_bytes(response, "big")
719
720        return lfl1 + lfl2 * 1e-4

returns minimum frequency supported by the module

See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]

:returns: minimum frequency in THz

def get_frequency_max(self):
722    def get_frequency_max(self):
723        """returns maximum frequency supported by the module
724
725        See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]
726
727        :returns: maximum frequency in THz
728        """
729        response = self._lfh1()
730        fcf1 = int.from_bytes(response, "big")
731
732        response = self._lfh2()
733        fcf2 = int.from_bytes(response, "big")
734
735        return fcf1 + fcf2 * 1e-4

returns maximum frequency supported by the module

See 9.7.3 Laser’s First/Last Frequency (LFL1/2, LFH1/2 0x52-0x55) [R]

:returns: maximum frequency in THz

def get_grid_min(self):
737    def get_grid_min(self):
738        """returns minimum grid spacing of the module
739
740        See 9.7.4 Laser’s Minimum Grid Spacing (LGrid 0x56) [R]
741
742        :returns: The minimum grid supported by the module in GHz
743        """
744        try:
745            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
746
747        except ExecutionError:
748            self.nop()
749
750        return freq_lgrid * 1e-1

returns minimum grid spacing of the module

See 9.7.4 Laser’s Minimum Grid Spacing (LGrid 0x56) [R]

:returns: The minimum grid supported by the module in GHz

def set_grid(self, grid_freq):
752    def set_grid(self, grid_freq):
753        """Set the grid spacing in GHz.
754        MHz resolution.
755
756        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
757
758        :param grid_freq: the grid frequency spacing in GHz
759        :returns:
760
761        """
762        grid_freq = str(round(grid_freq * 1000))
763        data = int(grid_freq[0:4])
764
765        self._grid(data)

Set the grid spacing in GHz. MHz resolution.

See 9.6.5 Grid Spacing (Grid 0x34) [RW]

Parameters
  • grid_freq: the grid frequency spacing in GHz :returns:
def get_grid(self):
767    def get_grid(self):
768        """get the grid spacing in GHz
769
770        See 9.6.5 Grid Spacing (Grid 0x34) [RW]
771
772        :returns: The grid spacing in GHz.
773        """
774        response = self._grid()
775        grid_freq = int.from_bytes(response, "big", signed=True)
776
777        return grid_freq * 1e-1

get the grid spacing in GHz

See 9.6.5 Grid Spacing (Grid 0x34) [RW]

:returns: The grid spacing in GHz.

def get_age(self):
779    def get_age(self):
780        """Returns the percentage of aging of the laser.
781        0% indicated a brand new laser
782        100% indicates the laser should be replaces.
783
784        See 9.8.6 Laser Age (Age 0x61) [R]
785
786        :returns: percent aging of the laser
787        """
788        response = self._age()
789        age = int.from_bytes(response, "big")
790
791        return age

Returns the percentage of aging of the laser. 0% indicated a brand new laser 100% indicates the laser should be replaces.

See 9.8.6 Laser Age (Age 0x61) [R]

:returns: percent aging of the laser

def set_channel(self, channel):
793    def set_channel(self, channel):
794        """Sets the laser's operating channel.
795
796        MSA-01.2 lasers support a 16 bit channel value.
797
798        This defines how many spaces along the grid
799        the laser's frequency is displaced from the first channel frequency.
800
801        See 9.6.1 Channel (Channel 0x30) [RW]
802
803        :param channel:
804        :returns: None
805
806        """
807        # check type and stuff
808        if not isinstance(channel, int):
809            raise TypeError("Channel must be an integer")
810        if channel < 0:
811            raise ValueError("Channel must be positive.")
812        if channel > 0xFFFF:
813            raise ValueError("Channel must be a 16 bit integer (<=0xFFFF).")
814
815        # Split the channel choice into two options.
816        channel_hex = f"{channel:08x}"
817
818        channell = int(channel_hex[4:], 16)
819
820        # Set the channel registers.
821        self._channel(channell)

Sets the laser's operating channel.

MSA-01.2 lasers support a 16 bit channel value.

This defines how many spaces along the grid the laser's frequency is displaced from the first channel frequency.

See 9.6.1 Channel (Channel 0x30) [RW]

Parameters
  • channel: :returns: None
def get_channel(self):
823    def get_channel(self):
824        """gets the current channel setting
825
826        The channel defines how many spaces along the grid
827        the laser's frequency is displaced from the first channel frequency.
828
829        See 9.6.1 Channel (Channel 0x30) [RW]
830
831        :returns: channel as an integer.
832
833        """
834        # This concatenates the data bytestrings
835        response = self._channel()
836
837        channel = int.from_bytes(response, "big")
838
839        return channel

gets the current channel setting

The channel defines how many spaces along the grid the laser's frequency is displaced from the first channel frequency.

See 9.6.1 Channel (Channel 0x30) [RW]

:returns: channel as an integer.

def set_fine_tuning(self, ftf):
841    def set_fine_tuning(self, ftf):
842        """
843        This function provides off grid tuning for the laser's frequency.
844        The adjustment applies to all channels uniformly.
845        This is typically used after the laser if locked and minor adjustments
846        are required to the frequency.
847
848        The command may be pending in the event that the laser output is
849        enabled. The pending bit is cleared once the fine tune frequency
850        has been achieved.
851
852        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
853
854        **???** It seems like this can be done with the laser running.
855
856        :param ftf: The fine tune frequency adjustment in GHz
857        """
858
859        ftf = round(ftf * 1e3)
860
861        # We will leave this bare. This way the user can set and handle
862        # their own timing and check to make sure the laser has reached the
863        # desired fine tuning frequency.
864        # It WILL throw a "pending" error if the laser is on when setting.
865        self._ftf(ftf)

This function provides off grid tuning for the laser's frequency. The adjustment applies to all channels uniformly. This is typically used after the laser if locked and minor adjustments are required to the frequency.

The command may be pending in the event that the laser output is enabled. The pending bit is cleared once the fine tune frequency has been achieved.

See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]

??? It seems like this can be done with the laser running.

Parameters
  • ftf: The fine tune frequency adjustment in GHz
def get_fine_tuning(self):
867    def get_fine_tuning(self):
868        """
869        This function returns the setting for the
870        off grid tuning for the laser's frequency.
871
872        See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]
873
874        :return ftf: The fine tune frequency adjustment in GHz
875        """
876
877        response = self._ftf()
878        ftf = int.from_bytes(response, "big", signed=True)
879
880        return ftf * 1e-3

This function returns the setting for the off grid tuning for the laser's frequency.

See 9.8.7 Fine Tune Frequency (FTF 0x62) [RW]

Returns

The fine tune frequency adjustment in GHz

def get_ftf_range(self):
882    def get_ftf_range(self):
883        """return the maximum and minimum off grid tuning for the laser's frequency.
884
885        See 9.7.1 Fine Tune Frequency Range (FTF 0x4F) [R]
886
887        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
888        """
889        response = self._ftfr()
890
891        ftfr = int.from_bytes(response, "big")
892
893        return ftfr * 1e-3

return the maximum and minimum off grid tuning for the laser's frequency.

See 9.7.1 Fine Tune Frequency Range (FTF 0x4F) [R]

Returns

The fine tune frequency range [-ftfr, + ftfr] in GHz

def get_temps(self):
895    def get_temps(self):
896        """returns a list of technology specific temperatures
897        units in deg C unless otherwise specified
898
899        **Technologies**
900        Technology 1: [Diode Temp, Case Temp]
901        Technology 2: [Diode Temp, Case Temp]
902        (why are they the same?)
903
904        See 9.8.2 Module Temperatures (Temps 0x58) [R]
905
906        :returns: list of temperatures in deg C
907        """
908        # get response this should be a long byte string
909        response = self._temps()
910
911        data = [
912            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
913            for i in range(0, len(response), 2)
914        ]
915
916        return data

returns a list of technology specific temperatures units in deg C unless otherwise specified

Technologies Technology 1: [Diode Temp, Case Temp] Technology 2: [Diode Temp, Case Temp] (why are they the same?)

See 9.8.2 Module Temperatures (Temps 0x58) [R]

:returns: list of temperatures in deg C

def get_currents(self):
918    def get_currents(self):
919        """returns a list of technology specific currents in mA.
920
921        **Technologies**
922        Technology 1: [TEC, Diode]
923        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
924        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
925
926        See 9.8.1 Module Currents (Currents 0x57) [R]
927
928        :returns: list of currents in mA
929        """
930        # get response this should be a long byte string
931        response = self._currents()
932
933        data = [
934            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
935            for i in range(0, len(response), 2)
936        ]
937
938        return data

returns a list of technology specific currents in mA.

Technologies Technology 1: [TEC, Diode] Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA] Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]

See 9.8.1 Module Currents (Currents 0x57) [R]

:returns: list of currents in mA

def get_last_response(self):
940    def get_last_response(self):
941        """reads the last response sent from the laser
942
943        This function gets the most recent response sent from the laser.
944        The response is parsed for errors the way any normal response
945        would be. The variable `self._response` is set to the last response again.
946
947        See 9.4.12 Last Response (LstResp 0x13) [R]
948
949        """
950        return self._lstresp()

reads the last response sent from the laser

This function gets the most recent response sent from the laser. The response is parsed for errors the way any normal response would be. The variable self._response is set to the last response again.

See 9.4.12 Last Response (LstResp 0x13) [R]

def get_error_fatal(self, reset=False):
952    def get_error_fatal(self, reset=False):
953        """reads fatal error register statusf and checks each bit against the
954        fatal error table to determine the fault
955
956        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
957
958        :param reset: resets/clears latching errors
959
960        """
961        response = self._statusf()
962        statusf = int.from_bytes(response, "big")
963
964        logger.debug("Current Status Fatal Error: %d", statusf)
965
966        if reset:
967            data_reset = 0x00FF
968            self._statusf(data_reset)
969
970        return FatalError(statusf)

reads fatal error register statusf and checks each bit against the fatal error table to determine the fault

See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]

Parameters
  • reset: resets/clears latching errors
def get_error_warning(self, reset=False):
972    def get_error_warning(self, reset=False):
973        """reads warning error register statusw and checks each bit against the
974        warning error table to determine the fault
975
976        If the laser is off then some of the latched warnings will be set on.
977
978        See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]
979
980        :param reset: resets/clears latching errors
981        """
982        response = self._statusw()
983        statusw = int.from_bytes(response, "big")
984
985        logger.debug("Current Status Warning Error: %d", statusw)
986
987        if reset:
988            data_reset = 0x00FF
989            self._statusw(data_reset)
990
991        return WarningError(statusw)

reads warning error register statusw and checks each bit against the warning error table to determine the fault

If the laser is off then some of the latched warnings will be set on.

See 9.5.1 StatusF, StatusW (0x20, 0x21) [RW]

Parameters
  • reset: resets/clears latching errors
def get_fatal_power_thresh(self):
 993    def get_fatal_power_thresh(self):
 994        """reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
 995
 996        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
 997        """
 998        response = self._fpowth()
 999        pow_fatal = int.from_bytes(response, "big") / 100
1000        # correcting for proper order of magnitude
1001        return pow_fatal

reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted

See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]

def get_warning_power_thresh(self):
1003    def get_warning_power_thresh(self):
1004        """reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
1005
1006        See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]
1007        """
1008        response = self._wpowth()
1009        pow_warn = int.from_bytes(response, "big") / 100
1010        # correcting for proper order of magnitude
1011        return pow_warn

reads maximum plus/minus power deviation in dB for which the warning alarm is asserted

See 9.5.2 Power Threshold (FPowTh, WPowTh 0x22, 0x23) [RW]

def get_fatal_freq_thresh(self):
1013    def get_fatal_freq_thresh(self):
1014        """reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
1015
1016        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1017        """
1018        response = self._ffreqth()
1019        freq_fatal = int.from_bytes(response, "big") / 10
1020        return freq_fatal

reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted

See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]

def get_warning_freq_thresh(self):
1022    def get_warning_freq_thresh(self):
1023        """reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
1024
1025        See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]
1026        """
1027        response = self._wfreqth()
1028        freq_warn = int.from_bytes(response, "big") / 10
1029        return freq_warn

reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted

See 9.5.3 Frequency Threshold (FFreqTh, WFreqTh 0x24, 0x25) [RW]

def get_fatal_therm_thresh(self):
1031    def get_fatal_therm_thresh(self):
1032        """reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
1033
1034        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1035        """
1036        response = self._fthermth()
1037        therm_fatal = int.from_bytes(response, "big") / 100
1038        # correcting for proper order of magnitude
1039        return therm_fatal

reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted

See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]

def get_warning_therm_thresh(self):
1041    def get_warning_therm_thresh(self):
1042        """reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
1043
1044        See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]
1045        """
1046        response = self._wthermth()
1047        therm_thresh = int.from_bytes(response, "big") / 100
1048        # correcting for proper order of magnitude
1049        return therm_thresh

reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted

See 9.5.4 Thermal Threshold (FThermTh, WThermTh 0x26, 0x27) [RW]

def get_srq_trigger(self):
1051    def get_srq_trigger(self):
1052        """identifies corresponding bits in status registers StatusF, StatusW
1053
1054        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
1055
1056        See 9.5.5 SRQ* Triggers (SRQT 0x28) [RW]
1057        """
1058        response = self._srqt()
1059        status = int.from_bytes(response, "big")
1060
1061        logger.debug("SRQT Status: %d", status)
1062
1063        return SQRTrigger(status)

identifies corresponding bits in status registers StatusF, StatusW

Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers

See 9.5.5 SRQ* Triggers (SRQT 0x28) [RW]

def get_fatal_trigger(self):
1065    def get_fatal_trigger(self):
1066        """identifies which fatal condition was asserted in StatusF and StatusW registers
1067
1068        See 9.5.6 FATAL Triggers (FatalT 0x29) [RW]
1069        """
1070        response = self._fatalt()
1071        status = int.from_bytes(response, "big")
1072
1073        logger.debug("FatalT Status: %d", status)
1074
1075        return FatalTrigger(status)

identifies which fatal condition was asserted in StatusF and StatusW registers

See 9.5.6 FATAL Triggers (FatalT 0x29) [RW]

def get_alm_trigger(self):
1077    def get_alm_trigger(self):
1078        """identifies why ALM was asserted in StatusF and StatusW registers
1079
1080        See 9.5.7 ALM Triggers (ALMT 0x2A) [RW]
1081        """
1082        response = self._almt()
1083        status = int.from_bytes(response, "big")
1084
1085        logger.debug("AlarmT Status: %d", status)
1086
1087        return AlarmTrigger(status)

identifies why ALM was asserted in StatusF and StatusW registers

See 9.5.7 ALM Triggers (ALMT 0x2A) [RW]