itla.itla13

  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 ITLA13(ITLABase):
 26    """
 27    A class that represents the ITLA13
 28    and exposes a user friendly API for controlling functionality.
 29
 30    OIF-ITLA-MSA-01.3 is the standard implemented here.
 31    Certain functions such as set_frequency() dont fully implement this yet
 32    because I dont know how to do checks to see if the optional FCF3
 33    functions are implemented for a particular laser.
 34
 35    Things to figure out
 36
 37      * What should be exposed to the user?
 38      * Can we abstract away the concept of registers and stuff
 39        in here so you don't have to deal with it.
 40
 41    There are some functions that could be implemented like set_fatalstatus.
 42    I think this is probably a bad idea even though it isnt write only.
 43
 44    Set Frequency is my platonic ideal for the higher level functions.
 45
 46    """
 47
 48    def __init__(
 49        self, serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1
 50    ):
 51        """Initializes the ITLA12 object.
 52
 53        :param serial_port: The serial port to connect to.
 54        Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
 55        :param baudrate: The baudrate for communication. I have primarily seen
 56        lasers using 9600 as the default and then higher for firmware upgrades.
 57        This may be laser specific.
 58        :param timeout: How long should we wait to receive a response from the laser
 59        :param register_files: Any additional register files you would like to include
 60        beyond the default MSA-01.3 defined registers. These must be in a yaml format as
 61        described in the project's README.
 62        :param sleep_time: time in seconds. Use in wait function
 63        """
 64        if register_files is None:
 65            register_files = []
 66
 67        register_files = ["registers_itla12.yaml", *register_files]
 68
 69        self.sleep_time = sleep_time
 70
 71        super().__init__(
 72            serial_port, baudrate, timeout=timeout, register_files=register_files
 73        )
 74
 75    def nop(self, data=None):
 76        """The No-Op operation.
 77
 78        This is a good test to see if your laser is communicating properly.
 79        It should read 0000 data or echo whatever data you send it if you send it something.
 80        The data you write to nop gets overwritten as soon as you read it back.
 81
 82        `nop()` also returns more informative errors following an ExecutionError.
 83        This is not called by default so you must do this explicitly if you want
 84        to see more informative error information.
 85
 86        :param data: Data to write
 87
 88        """
 89        # pretty sure the data does nothing
 90        if data is not None:
 91            response = self._nop(data)
 92        else:
 93            response = self._nop()
 94
 95        error_field = int(response.hex()[-1], 16)
 96        if bool(error_field):
 97            raise self._nop_errors[error_field]
 98
 99    def enable(self):
100        """Enables laser optical output.
101        There is a time delay after execution of this function to
102        proper stable laser output.
103
104        :returns: None
105
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        """Tells the laser to stop lasing.
113
114        :returns: None
115
116        """
117        self._resena(0)
118
119    def hard_reset(self):
120        """TODO describe function
121
122        :returns:
123
124        """
125        self._resena(Resena.MR)
126
127    def soft_reset(self):
128        """TODO describe function
129
130        :returns:
131
132        """
133        self._resena(Resena.SR)
134
135    def get_reset_enable(self):
136        """
137        Return ResEna register.
138        """
139        response = self._resena()
140        return Resena(int.from_bytes(response, "big"))
141
142    def set_alarm_during_tuning(self):
143        """Set alarm during tuning
144        mcb(ADT)
145        """
146        self._mcb(MCB.ADT)
147
148    def set_shutdown_on_fatal(self):
149        """Set shutdown on fatal.
150        mcb(SDF)
151        """
152        self._mcb(MCB.SDF)
153
154    def get_configuration_behavior(self):
155        """
156        Return MCB register.
157        """
158        response = self._mcb()
159        return MCB(int.from_bytes(response, "big"))
160
161    def is_disabled(self):
162        """
163        Return if disabled.
164
165        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
166
167        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
168        Consider overwriting this methods and monitoring FatalError.ALM.
169        """
170        fatal_error = self.get_error_fatal()
171        fatal_trigger = self.get_fatal_trigger()
172        resena = self.get_reset_enable()
173        mcb = self.get_configuration_behavior()
174
175        sdf = MCB.SDF in mcb
176        sena = Resena.SENA in resena
177        dis = FatalError.DIS in fatal_error
178
179        return ((fatal_error & fatal_trigger) and sdf) or (not sena) or (not dis)
180
181    def is_enabled(self, *args):
182        """
183        Return if enabled.  See is_disabled.
184        """
185        return not self.is_disabled(*args)
186
187    def get_device_type(self):
188        """
189        returns a string containing the device type.
190        """
191        response_bytes = self._devtyp()
192
193        return response_bytes.decode("utf-8")
194
195    def get_manufacturer(self):
196        """
197        Return's a string containing the manufacturer's name.
198        """
199        response_bytes = self._mfgr()
200
201        return response_bytes.decode("utf-8")
202
203    def get_model(self):
204        """
205        return's the model as a string
206        """
207        response_bytes = self._model()
208
209        return response_bytes.decode("utf-8")
210
211    def get_serialnumber(self):
212        """
213        returns the serial number
214        """
215        response_bytes = self._serno()
216
217        return response_bytes.decode("utf-8")
218
219    def get_manufacturing_date(self):
220        """returns the manufacturing date"""
221        response_bytes = self._mfgdate()
222
223        return response_bytes.decode("utf-8")
224
225    def get_firmware_release(self):
226        """
227        returns a manufacturer specific firmware release
228        """
229        response_bytes = self._release()
230
231        return response_bytes.decode("utf-8")
232
233    def get_backwardscompatibility(self):
234        """
235        returns a manufacturer specific firmware backwards compatibility
236        as a null terminated string
237        """
238        response_bytes = self._relback()
239
240        return response_bytes.decode("utf-8")
241
242    def read_aea(self):
243        """
244        reads the AEA register data until an execution error is thrown.
245        """
246        aea_response = b""
247        try:
248            while True:
249                aea_response += self._aea_ear()
250
251        except ExecutionError:
252            try:
253                self.nop()
254
255            except EREError:
256                # If this throws an ERE error then we have completed reading AEA
257                pass
258
259            except NOPException as nop_e:
260                raise nop_e
261
262        return aea_response
263
264    def wait(self):
265        """Wait until operation is complete. It check if the operation is completed every self.sleep_time seconds."""
266        while True:
267            try:
268                self.nop()
269            except CPException:
270                if self.sleep_time is not None:
271                    sleep(self.sleep_time)
272            else:
273                break
274
275    def wait_until_enabled(self):
276        while self.is_disabled():
277            if self.sleep_time is not None:
278                sleep(self.sleep_time)
279
280    def wait_until_disabled(self):
281        while self.is_enabled():
282            if self.sleep_time is not None:
283                sleep(self.sleep_time)
284
285    def set_power(self, pwr_dBm):
286        """Sets the power of the ITLA laser. Units of dBm.
287
288        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
289        :returns: None
290
291        """
292        try:
293            self._pwr(round(pwr_dBm * 100))
294
295        except ExecutionError:
296            try:
297                self.nop()
298
299            except RVEError as error:
300                logger.error(
301                    "The provided power %.2f dBm is outside of the range for this device. "
302                    "The power is currently set to: %.2f dBm.",
303                    pwr_dBm,
304                    self.get_power_setting(),
305                )
306                raise error
307
308    def get_power_setting(self):
309        """Gets current power setting set by set_power. Should be in dBm.
310
311        :returns:
312
313        """
314        # Gets power setting, not actual optical output power.
315        response = self._pwr()
316        return int.from_bytes(response, "big", signed=True) / 100
317
318    def get_power_output(self):
319        """Gets the actual optical output power of the laser.
320        Only an approximation, apparently. Units dBm.
321
322        :returns:
323
324        """
325        response = self._oop()
326
327        return int.from_bytes(response, "big", signed=True) / 100
328
329    def get_power_min(self):
330        """Gets the minimum optical power output of the module. Units dBm.
331
332        :returns:
333
334        """
335        response = self._opsl()
336        return int.from_bytes(response, "big", signed=True) / 100
337
338    def get_power_max(self):
339        """Gets the maximum optical power output of the module. Units dBm.
340
341        :returns:
342
343        """
344        response = self._opsh()
345        return int.from_bytes(response, "big", signed=True) / 100
346
347    def set_fcf(self, freq):
348        """
349        This sets the first channel frequency.
350        It does not reset the channel so this frequency will only be equal
351        to the output frequency if channel=1.
352
353        """
354        # convert frequency to MHz
355        freq = round(freq * 1e6)
356
357        freq_str = str(freq)
358        fcf1 = int(freq_str[0:3])
359        fcf2 = int(freq_str[3:7])
360        fcf3 = int(freq_str[7:])
361
362        try:
363            # is it better to split these into their own try/except blocks?
364            self._fcf1(fcf1)
365            self._fcf2(fcf2)
366            self._fcf3(fcf3)
367
368        except ExecutionError:
369            try:
370                self.nop()
371            except RVEError as error:
372                logger.error(
373                    "%.2f THz is out of bounds for this laser. "
374                    "The frequency must be within the range %.2f - %.2f THz.",
375                    fcf1,
376                    self.get_frequency_min(),
377                    self.get_frequency_max(),
378                )
379                raise error
380
381            except CIEError as error:
382                logger.error(
383                    "You cannot change the first channel frequency while the laser is enabled. "
384                    "The current frequency is: %.2f THz",
385                    self.get_frequency(),
386                )
387                raise error
388
389    def set_frequency(self, freq):
390        """Sets the frequency of the laser in TeraHertz.
391
392        Has MHz resolution. Will round down.
393
394        This function sets the first channel frequency and then sets the
395        channel to 1 so that the frequency output when the laser is enabled
396        will be the frequency given to this function.
397
398        If you would like to only change the first channel frequency use
399        the function `set_fcf`.
400
401        Disable the laser before calling this function.
402
403        :param freq: The desired frequency setting in THz.
404        :returns: None
405        """
406        # This does a check so this only runs if fine tuning has been turned on.
407        if self.get_fine_tuning() != 0:
408            # Set the fine tuning off!
409            while True:
410                try:
411                    self.set_fine_tuning(0)
412                except ExecutionError:
413                    sleep(self.sleep_time)
414                except CPException:
415                    self.wait()
416                    break
417                else:
418                    break
419
420        try:
421            self.set_fcf(freq)
422        except CPException:
423            self.wait()
424
425        try:
426            self.set_channel(1)
427        except CPException:
428            self.wait()
429
430    def get_fcf(self):
431        """Get the currently set first channel frequency."""
432        response = self._fcf1()
433        fcf1 = int.from_bytes(response, "big")
434
435        response = self._fcf2()
436        fcf2 = int.from_bytes(response, "big")
437
438        response = self._fcf3()
439        fcf3 = int.from_bytes(response, "big")
440
441        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6
442
443    def get_frequency(self):
444        """gets the current laser operating frequency with channels
445        and everything accounted for.
446
447        :returns:
448
449        """
450        response = self._lf1()
451        lf1 = int.from_bytes(response, "big")
452
453        response = self._lf2()
454        lf2 = int.from_bytes(response, "big")
455
456        response = self._lf3()
457        lf3 = int.from_bytes(response, "big")
458
459        return lf1 + lf2 * 1e-4 + lf3 * 1e-6
460
461    def dither_enable(self, waveform: Waveform = Waveform.SIN):
462        """enables dither
463
464        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
465
466        :param waveform: specifies the dither waveform
467        """
468        data = (waveform << 4) | 2
469
470        self._dithere(data)
471
472    def dither_disable(self):
473        """disables digital dither"""
474        # TODO: This should preserve the waveform setting if possible
475        self._dithere(0)
476
477    def set_dither_rate(self, rate):
478        """
479        set dither rate in kHz, utilizes DitherR register
480        :param rate: an unsigned short integer specifying dither rate in kHz
481        """
482        self._ditherr(rate)
483
484    def get_dither_rate(self):
485        """
486        get dither rate, utilizes DitherR register
487        """
488        response = self._ditherr()
489        return int.from_bytes(response, "big")
490
491    def set_dither_frequency(self, rate):
492        """
493        set dither modulation frequency, utilizes DitherF register
494        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
495        deviation as Ghz*10
496        """
497        self._ditherf(rate)
498
499    def get_dither_frequency(self):
500        """
501        get dither modulation frequency, utilizes DitherF register
502        """
503        response = self._ditherf()
504        return int.from_bytes(response, "big")
505
506    def set_dither_amplitude(self, amplitude):
507        """
508        set dither modulation amplitude, utilizes DitherA register
509        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
510        deviation as 10*percentage of the optical power
511        """
512        self._dithera(amplitude)
513
514    def get_dither_amplitude(self):
515        """
516        get dither modulation amplitude, utilizes DitherA register
517        """
518        response = self._dithera()
519        return int.from_bytes(response, "big")
520
521    def get_temp(self):
522        """Returns the current primary control temperature in deg C.
523
524        :returns:
525
526        """
527        response = self._ctemp()
528        temp_100 = int.from_bytes(response, "big")
529
530        return temp_100 / 100
531
532    def get_frequency_min(self):
533        """command to read minimum frequency supported by the module
534
535        :returns:
536
537        """
538        response = self._lfl1()
539        lfl1 = int.from_bytes(response, "big")
540
541        response = self._lfl2()
542        lfl2 = int.from_bytes(response, "big")
543
544        response = self._lfl3()
545        lfl3 = int.from_bytes(response, "big")
546
547        return lfl1 + lfl2 * 1e-4 + lfl3 * 1e-6
548
549    def get_frequency_max(self):
550        """command to read maximum frequency supported by the module
551
552        :returns:
553
554        """
555        response = self._lfh1()
556        fcf1 = int.from_bytes(response, "big")
557
558        response = self._lfh2()
559        fcf2 = int.from_bytes(response, "big")
560
561        response = self._lfh3()
562        fcf3 = int.from_bytes(response, "big")
563
564        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6
565
566    def get_grid_min(self):
567        """command to read minimum grid supported by the module
568
569        :returns: The minimum grid supported by the module in GHz
570
571        """
572        try:
573            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
574            freq_lgrid2 = int.from_bytes(self._lgrid2(), "big", signed=False)
575
576        except ExecutionError:
577            self.nop()
578
579        return freq_lgrid * 1e-1 + freq_lgrid2 * 1e-3
580
581    def set_grid(self, grid_freq):
582        """Set the grid spacing in GHz.
583
584        MHz resolution.
585
586        :param grid_freq: the grid frequency spacing in GHz
587        :returns:
588
589        """
590        grid_freq = str(round(grid_freq * 1000))
591        data = int(grid_freq[0:4])
592        data_2 = int(grid_freq[4:])
593
594        self._grid(data)
595        self._grid2(data_2)
596
597    def get_grid(self):
598        """get the grid spacing in GHz
599
600        :returns: The grid spacing in GHz.
601
602        """
603        response = self._grid()
604        grid_freq = int.from_bytes(response, "big", signed=True)
605
606        response = self._grid2()
607        grid2_freq = int.from_bytes(response, "big", signed=True)
608
609        return grid_freq * 1e-1 + grid2_freq * 1e-3
610
611    def get_age(self):
612        """Returns the percentage of aging of the laser.
613        0% indicated a brand new laser
614        100% indicates the laser should be replaces.
615
616        :returns:
617
618        """
619        response = self._age()
620        age = int.from_bytes(response, "big")
621
622        return age
623
624    def set_channel(self, channel):
625        """Sets the laser's operating channel.
626
627        MSA-01.3 lasers support a 32 bit channel value.
628
629        This defines how many spaces along the grid
630        the laser's frequency is displaced from the first channel frequency.
631
632        :param channel:
633        :returns:
634
635        """
636        # check type and stuff
637        if not isinstance(channel, int):
638            raise TypeError("Channel must be an integer")
639        if channel < 0:
640            raise ValueError("Channel must be positive.")
641        if channel > 0xFFFFFFFF:
642            raise ValueError("Channel must be a 32 bit integer (<=0xFFFFFFFF).")
643
644        # Split the channel choice into two options.
645        channel_hex = f"{channel:08x}"
646
647        channell = int(channel_hex[4:], 16)
648        channelh = int(channel_hex[0:4], 16)
649
650        # Set the channel registers.
651        self._channel(channell)
652        self._channelh(channelh)
653
654    def get_channel(self):
655        """gets the current channel setting
656
657        The channel defines how many spaces along the grid
658        the laser's frequency is displaced from the first channel frequency.
659
660        :returns: channel as an integer.
661
662        """
663        # This concatenates the data bytestrings
664        response = self._channelh() + self._channel()
665
666        channel = int.from_bytes(response, "big")
667
668        return channel
669
670    def set_fine_tuning(self, ftf):
671        """
672        This function provides off grid tuning for the laser's frequency.
673        The adjustment applies to all channels uniformly.
674        This is typically used after the laser if locked and minor adjustments
675        are required to the frequency.
676
677        The command may be pending in the event that the laser output is
678        enabled. The pending bit is cleared once the fine tune frequency
679        has been achieved.
680
681        **???** It seems like this can be done with the laser running.
682
683        :param ftf: The fine tune frequency adjustment in GHz
684        """
685
686        ftf = round(ftf * 1e3)
687
688        # We will leave this bare. This way the user can set and handle
689        # their own timing and check to make sure the laser has reached the
690        # desired fine tuning frequency.
691        # It WILL throw a "pending" error if the laser is on when setting.
692        self._ftf(ftf)
693
694    def get_fine_tuning(self):
695        """
696        This function returns the setting for the
697        off grid tuning for the laser's frequency.
698
699        :return ftf: The fine tune frequency adjustment in GHz
700        """
701
702        response = self._ftf()
703        ftf = int.from_bytes(response, "big", signed=True)
704
705        return ftf * 1e-3
706
707    def get_ftf_range(self):
708        """
709        Return the maximum and minimum off grid tuning for the laser's frequency.
710
711        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
712        """
713        response = self._ftfr()
714
715        ftfr = int.from_bytes(response, "big")
716
717        return ftfr * 1e-3
718
719    def get_temps(self):
720        """
721        Returns a list of currents in degrees Celcius.
722        In the following order:
723
724        Technology 1: [Diode Temp, Case Temp]
725        """
726        # get response this should be a long byte string
727        response = self._temps()
728
729        data = [
730            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
731            for i in range(0, len(response), 2)
732        ]
733
734        return data
735
736    def get_currents(self):
737        """
738        Returns a list of currents in mA.
739        In the following order:
740
741        Technology 1: [TEC, Diode]
742        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
743        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
744        """
745        # get response this should be a long byte string
746        response = self._currents()
747
748        data = [
749            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
750            for i in range(0, len(response), 2)
751        ]
752
753        return data
754
755    def get_last_response(self):
756        """This function gets the most recent response sent from the laser.
757        The response is parsed for errors and stuff the way any normal response
758        would be. The variable `self._response` is set to the last response again.
759
760        I dont know why you would need to do this.
761        """
762        return self._lstresp()
763
764    def get_error_fatal(self, reset=False):
765        """
766        reads fatal error register statusf and checks each bit against the
767        fatal error table to determine the fault
768
769        :param reset: resets/clears latching errors
770
771        """
772        response = self._statusf()
773        statusf = int.from_bytes(response, "big")
774
775        logger.debug("Current Status Fatal Error: %d", statusf)
776
777        if reset:
778            data_reset = 0x00FF
779            self._statusf(data_reset)
780
781        return FatalError(statusf)
782
783    def get_error_warning(self, reset=False):
784        """
785        reads warning error register statusw and checks each bit against the
786        warning error table to determine the fault
787
788        If the laser is off then some of the latched warnings will be set on.
789
790        :param reset: resets/clears latching errors
791        """
792        response = self._statusw()
793        statusw = int.from_bytes(response, "big")
794
795        logger.debug("Current Status Warning Error: %d", statusw)
796
797        if reset:
798            data_reset = 0x00FF
799            self._statusw(data_reset)
800
801        return WarningError(statusw)
802
803    def get_fatal_power_thresh(self):
804        """
805        reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
806
807        """
808        response = self._fpowth()
809        pow_fatal = int.from_bytes(response, "big") / 100
810        # correcting for proper order of magnitude
811        return pow_fatal
812
813    def get_warning_power_thresh(self):
814        """
815        reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
816
817        """
818        response = self._wpowth()
819        pow_warn = int.from_bytes(response, "big") / 100
820        # correcting for proper order of magnitude
821        return pow_warn
822
823    def get_fatal_freq_thresh(self):
824        """
825        reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
826
827        """
828        response = self._ffreqth()
829        freq_fatal = int.from_bytes(response, "big") / 10
830        # correcting for proper order of magnitude
831        response2 = self._ffreqth2()
832        freq_fatal2 = int.from_bytes(response2, "big") / 100
833        # get frequency deviation in MHz and add to GHz value
834        return freq_fatal + freq_fatal2
835
836    def get_warning_freq_thresh(self):
837        """
838        reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
839
840        """
841        response = self._wfreqth()
842        freq_warn = int.from_bytes(response, "big") / 10
843        # correcting for proper order of magnitude
844        response2 = self._wfreqth2()
845        freq_warn2 = int.from_bytes(response2, "big") / 100
846        # get frequency deviation in MHz and add to GHz value
847        return freq_warn + freq_warn2
848
849    def get_fatal_therm_thresh(self):
850        """
851        reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
852
853        """
854        response = self._fthermth()
855        therm_fatal = int.from_bytes(response, "big") / 100
856        # correcting for proper order of magnitude
857        return therm_fatal
858
859    def get_warning_therm_thresh(self):
860        """
861        reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
862        """
863        response = self._wthermth()
864        therm_thresh = int.from_bytes(response, "big") / 100
865        # correcting for proper order of magnitude
866        return therm_thresh
867
868    def get_srq_trigger(self):
869        """
870        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
871
872        """
873        response = self._srqt()
874        status = int.from_bytes(response, "big")
875
876        logger.debug("SRQT Status: %d", status)
877
878        return SQRTrigger(status)
879
880    def get_fatal_trigger(self):
881        """
882        Utilizes FatalT register to identify which fatal conditon was asserted in StatusF and StatusW registers
883
884        """
885        response = self._fatalt()
886        status = int.from_bytes(response, "big")
887
888        logger.debug("FatalT Status: %d", status)
889
890        return FatalTrigger(status)
891
892    def get_alm_trigger(self):
893        """
894        Utilizes ALMT register to identify why ALM was asserted in StatusF and StatusW registers
895
896        """
897        response = self._almt()
898        status = int.from_bytes(response, "big")
899
900        logger.debug("AlarmT Status: %d", status)
901
902        return AlarmTrigger(status)
class ITLA13(itla.itla.ITLABase):
 26class ITLA13(ITLABase):
 27    """
 28    A class that represents the ITLA13
 29    and exposes a user friendly API for controlling functionality.
 30
 31    OIF-ITLA-MSA-01.3 is the standard implemented here.
 32    Certain functions such as set_frequency() dont fully implement this yet
 33    because I dont know how to do checks to see if the optional FCF3
 34    functions are implemented for a particular laser.
 35
 36    Things to figure out
 37
 38      * What should be exposed to the user?
 39      * Can we abstract away the concept of registers and stuff
 40        in here so you don't have to deal with it.
 41
 42    There are some functions that could be implemented like set_fatalstatus.
 43    I think this is probably a bad idea even though it isnt write only.
 44
 45    Set Frequency is my platonic ideal for the higher level functions.
 46
 47    """
 48
 49    def __init__(
 50        self, serial_port, baudrate, timeout=0.5, register_files=None, sleep_time=0.1
 51    ):
 52        """Initializes the ITLA12 object.
 53
 54        :param serial_port: The serial port to connect to.
 55        Can either be linux/mac type such as '/dev/ttyUSBX' or windows type 'comX'
 56        :param baudrate: The baudrate for communication. I have primarily seen
 57        lasers using 9600 as the default and then higher for firmware upgrades.
 58        This may be laser specific.
 59        :param timeout: How long should we wait to receive a response from the laser
 60        :param register_files: Any additional register files you would like to include
 61        beyond the default MSA-01.3 defined registers. These must be in a yaml format as
 62        described in the project's README.
 63        :param sleep_time: time in seconds. Use in wait function
 64        """
 65        if register_files is None:
 66            register_files = []
 67
 68        register_files = ["registers_itla12.yaml", *register_files]
 69
 70        self.sleep_time = sleep_time
 71
 72        super().__init__(
 73            serial_port, baudrate, timeout=timeout, register_files=register_files
 74        )
 75
 76    def nop(self, data=None):
 77        """The No-Op operation.
 78
 79        This is a good test to see if your laser is communicating properly.
 80        It should read 0000 data or echo whatever data you send it if you send it something.
 81        The data you write to nop gets overwritten as soon as you read it back.
 82
 83        `nop()` also returns more informative errors following an ExecutionError.
 84        This is not called by default so you must do this explicitly if you want
 85        to see more informative error information.
 86
 87        :param data: Data to write
 88
 89        """
 90        # pretty sure the data does nothing
 91        if data is not None:
 92            response = self._nop(data)
 93        else:
 94            response = self._nop()
 95
 96        error_field = int(response.hex()[-1], 16)
 97        if bool(error_field):
 98            raise self._nop_errors[error_field]
 99
100    def enable(self):
101        """Enables laser optical output.
102        There is a time delay after execution of this function to
103        proper stable laser output.
104
105        :returns: None
106
107        """
108        # I'm writing this out partially for transparency
109        # Maybe unnecessary or non-optimal
110        self._resena(Resena.SENA)
111
112    def disable(self):
113        """Tells the laser to stop lasing.
114
115        :returns: None
116
117        """
118        self._resena(0)
119
120    def hard_reset(self):
121        """TODO describe function
122
123        :returns:
124
125        """
126        self._resena(Resena.MR)
127
128    def soft_reset(self):
129        """TODO describe function
130
131        :returns:
132
133        """
134        self._resena(Resena.SR)
135
136    def get_reset_enable(self):
137        """
138        Return ResEna register.
139        """
140        response = self._resena()
141        return Resena(int.from_bytes(response, "big"))
142
143    def set_alarm_during_tuning(self):
144        """Set alarm during tuning
145        mcb(ADT)
146        """
147        self._mcb(MCB.ADT)
148
149    def set_shutdown_on_fatal(self):
150        """Set shutdown on fatal.
151        mcb(SDF)
152        """
153        self._mcb(MCB.SDF)
154
155    def get_configuration_behavior(self):
156        """
157        Return MCB register.
158        """
159        response = self._mcb()
160        return MCB(int.from_bytes(response, "big"))
161
162    def is_disabled(self):
163        """
164        Return if disabled.
165
166        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
167
168        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
169        Consider overwriting this methods and monitoring FatalError.ALM.
170        """
171        fatal_error = self.get_error_fatal()
172        fatal_trigger = self.get_fatal_trigger()
173        resena = self.get_reset_enable()
174        mcb = self.get_configuration_behavior()
175
176        sdf = MCB.SDF in mcb
177        sena = Resena.SENA in resena
178        dis = FatalError.DIS in fatal_error
179
180        return ((fatal_error & fatal_trigger) and sdf) or (not sena) or (not dis)
181
182    def is_enabled(self, *args):
183        """
184        Return if enabled.  See is_disabled.
185        """
186        return not self.is_disabled(*args)
187
188    def get_device_type(self):
189        """
190        returns a string containing the device type.
191        """
192        response_bytes = self._devtyp()
193
194        return response_bytes.decode("utf-8")
195
196    def get_manufacturer(self):
197        """
198        Return's a string containing the manufacturer's name.
199        """
200        response_bytes = self._mfgr()
201
202        return response_bytes.decode("utf-8")
203
204    def get_model(self):
205        """
206        return's the model as a string
207        """
208        response_bytes = self._model()
209
210        return response_bytes.decode("utf-8")
211
212    def get_serialnumber(self):
213        """
214        returns the serial number
215        """
216        response_bytes = self._serno()
217
218        return response_bytes.decode("utf-8")
219
220    def get_manufacturing_date(self):
221        """returns the manufacturing date"""
222        response_bytes = self._mfgdate()
223
224        return response_bytes.decode("utf-8")
225
226    def get_firmware_release(self):
227        """
228        returns a manufacturer specific firmware release
229        """
230        response_bytes = self._release()
231
232        return response_bytes.decode("utf-8")
233
234    def get_backwardscompatibility(self):
235        """
236        returns a manufacturer specific firmware backwards compatibility
237        as a null terminated string
238        """
239        response_bytes = self._relback()
240
241        return response_bytes.decode("utf-8")
242
243    def read_aea(self):
244        """
245        reads the AEA register data until an execution error is thrown.
246        """
247        aea_response = b""
248        try:
249            while True:
250                aea_response += self._aea_ear()
251
252        except ExecutionError:
253            try:
254                self.nop()
255
256            except EREError:
257                # If this throws an ERE error then we have completed reading AEA
258                pass
259
260            except NOPException as nop_e:
261                raise nop_e
262
263        return aea_response
264
265    def wait(self):
266        """Wait until operation is complete. It check if the operation is completed every self.sleep_time seconds."""
267        while True:
268            try:
269                self.nop()
270            except CPException:
271                if self.sleep_time is not None:
272                    sleep(self.sleep_time)
273            else:
274                break
275
276    def wait_until_enabled(self):
277        while self.is_disabled():
278            if self.sleep_time is not None:
279                sleep(self.sleep_time)
280
281    def wait_until_disabled(self):
282        while self.is_enabled():
283            if self.sleep_time is not None:
284                sleep(self.sleep_time)
285
286    def set_power(self, pwr_dBm):
287        """Sets the power of the ITLA laser. Units of dBm.
288
289        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
290        :returns: None
291
292        """
293        try:
294            self._pwr(round(pwr_dBm * 100))
295
296        except ExecutionError:
297            try:
298                self.nop()
299
300            except RVEError as error:
301                logger.error(
302                    "The provided power %.2f dBm is outside of the range for this device. "
303                    "The power is currently set to: %.2f dBm.",
304                    pwr_dBm,
305                    self.get_power_setting(),
306                )
307                raise error
308
309    def get_power_setting(self):
310        """Gets current power setting set by set_power. Should be in dBm.
311
312        :returns:
313
314        """
315        # Gets power setting, not actual optical output power.
316        response = self._pwr()
317        return int.from_bytes(response, "big", signed=True) / 100
318
319    def get_power_output(self):
320        """Gets the actual optical output power of the laser.
321        Only an approximation, apparently. Units dBm.
322
323        :returns:
324
325        """
326        response = self._oop()
327
328        return int.from_bytes(response, "big", signed=True) / 100
329
330    def get_power_min(self):
331        """Gets the minimum optical power output of the module. Units dBm.
332
333        :returns:
334
335        """
336        response = self._opsl()
337        return int.from_bytes(response, "big", signed=True) / 100
338
339    def get_power_max(self):
340        """Gets the maximum optical power output of the module. Units dBm.
341
342        :returns:
343
344        """
345        response = self._opsh()
346        return int.from_bytes(response, "big", signed=True) / 100
347
348    def set_fcf(self, freq):
349        """
350        This sets the first channel frequency.
351        It does not reset the channel so this frequency will only be equal
352        to the output frequency if channel=1.
353
354        """
355        # convert frequency to MHz
356        freq = round(freq * 1e6)
357
358        freq_str = str(freq)
359        fcf1 = int(freq_str[0:3])
360        fcf2 = int(freq_str[3:7])
361        fcf3 = int(freq_str[7:])
362
363        try:
364            # is it better to split these into their own try/except blocks?
365            self._fcf1(fcf1)
366            self._fcf2(fcf2)
367            self._fcf3(fcf3)
368
369        except ExecutionError:
370            try:
371                self.nop()
372            except RVEError as error:
373                logger.error(
374                    "%.2f THz is out of bounds for this laser. "
375                    "The frequency must be within the range %.2f - %.2f THz.",
376                    fcf1,
377                    self.get_frequency_min(),
378                    self.get_frequency_max(),
379                )
380                raise error
381
382            except CIEError as error:
383                logger.error(
384                    "You cannot change the first channel frequency while the laser is enabled. "
385                    "The current frequency is: %.2f THz",
386                    self.get_frequency(),
387                )
388                raise error
389
390    def set_frequency(self, freq):
391        """Sets the frequency of the laser in TeraHertz.
392
393        Has MHz resolution. Will round down.
394
395        This function sets the first channel frequency and then sets the
396        channel to 1 so that the frequency output when the laser is enabled
397        will be the frequency given to this function.
398
399        If you would like to only change the first channel frequency use
400        the function `set_fcf`.
401
402        Disable the laser before calling this function.
403
404        :param freq: The desired frequency setting in THz.
405        :returns: None
406        """
407        # This does a check so this only runs if fine tuning has been turned on.
408        if self.get_fine_tuning() != 0:
409            # Set the fine tuning off!
410            while True:
411                try:
412                    self.set_fine_tuning(0)
413                except ExecutionError:
414                    sleep(self.sleep_time)
415                except CPException:
416                    self.wait()
417                    break
418                else:
419                    break
420
421        try:
422            self.set_fcf(freq)
423        except CPException:
424            self.wait()
425
426        try:
427            self.set_channel(1)
428        except CPException:
429            self.wait()
430
431    def get_fcf(self):
432        """Get the currently set first channel frequency."""
433        response = self._fcf1()
434        fcf1 = int.from_bytes(response, "big")
435
436        response = self._fcf2()
437        fcf2 = int.from_bytes(response, "big")
438
439        response = self._fcf3()
440        fcf3 = int.from_bytes(response, "big")
441
442        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6
443
444    def get_frequency(self):
445        """gets the current laser operating frequency with channels
446        and everything accounted for.
447
448        :returns:
449
450        """
451        response = self._lf1()
452        lf1 = int.from_bytes(response, "big")
453
454        response = self._lf2()
455        lf2 = int.from_bytes(response, "big")
456
457        response = self._lf3()
458        lf3 = int.from_bytes(response, "big")
459
460        return lf1 + lf2 * 1e-4 + lf3 * 1e-6
461
462    def dither_enable(self, waveform: Waveform = Waveform.SIN):
463        """enables dither
464
465        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
466
467        :param waveform: specifies the dither waveform
468        """
469        data = (waveform << 4) | 2
470
471        self._dithere(data)
472
473    def dither_disable(self):
474        """disables digital dither"""
475        # TODO: This should preserve the waveform setting if possible
476        self._dithere(0)
477
478    def set_dither_rate(self, rate):
479        """
480        set dither rate in kHz, utilizes DitherR register
481        :param rate: an unsigned short integer specifying dither rate in kHz
482        """
483        self._ditherr(rate)
484
485    def get_dither_rate(self):
486        """
487        get dither rate, utilizes DitherR register
488        """
489        response = self._ditherr()
490        return int.from_bytes(response, "big")
491
492    def set_dither_frequency(self, rate):
493        """
494        set dither modulation frequency, utilizes DitherF register
495        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
496        deviation as Ghz*10
497        """
498        self._ditherf(rate)
499
500    def get_dither_frequency(self):
501        """
502        get dither modulation frequency, utilizes DitherF register
503        """
504        response = self._ditherf()
505        return int.from_bytes(response, "big")
506
507    def set_dither_amplitude(self, amplitude):
508        """
509        set dither modulation amplitude, utilizes DitherA register
510        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
511        deviation as 10*percentage of the optical power
512        """
513        self._dithera(amplitude)
514
515    def get_dither_amplitude(self):
516        """
517        get dither modulation amplitude, utilizes DitherA register
518        """
519        response = self._dithera()
520        return int.from_bytes(response, "big")
521
522    def get_temp(self):
523        """Returns the current primary control temperature in deg C.
524
525        :returns:
526
527        """
528        response = self._ctemp()
529        temp_100 = int.from_bytes(response, "big")
530
531        return temp_100 / 100
532
533    def get_frequency_min(self):
534        """command to read minimum frequency supported by the module
535
536        :returns:
537
538        """
539        response = self._lfl1()
540        lfl1 = int.from_bytes(response, "big")
541
542        response = self._lfl2()
543        lfl2 = int.from_bytes(response, "big")
544
545        response = self._lfl3()
546        lfl3 = int.from_bytes(response, "big")
547
548        return lfl1 + lfl2 * 1e-4 + lfl3 * 1e-6
549
550    def get_frequency_max(self):
551        """command to read maximum frequency supported by the module
552
553        :returns:
554
555        """
556        response = self._lfh1()
557        fcf1 = int.from_bytes(response, "big")
558
559        response = self._lfh2()
560        fcf2 = int.from_bytes(response, "big")
561
562        response = self._lfh3()
563        fcf3 = int.from_bytes(response, "big")
564
565        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6
566
567    def get_grid_min(self):
568        """command to read minimum grid supported by the module
569
570        :returns: The minimum grid supported by the module in GHz
571
572        """
573        try:
574            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
575            freq_lgrid2 = int.from_bytes(self._lgrid2(), "big", signed=False)
576
577        except ExecutionError:
578            self.nop()
579
580        return freq_lgrid * 1e-1 + freq_lgrid2 * 1e-3
581
582    def set_grid(self, grid_freq):
583        """Set the grid spacing in GHz.
584
585        MHz resolution.
586
587        :param grid_freq: the grid frequency spacing in GHz
588        :returns:
589
590        """
591        grid_freq = str(round(grid_freq * 1000))
592        data = int(grid_freq[0:4])
593        data_2 = int(grid_freq[4:])
594
595        self._grid(data)
596        self._grid2(data_2)
597
598    def get_grid(self):
599        """get the grid spacing in GHz
600
601        :returns: The grid spacing in GHz.
602
603        """
604        response = self._grid()
605        grid_freq = int.from_bytes(response, "big", signed=True)
606
607        response = self._grid2()
608        grid2_freq = int.from_bytes(response, "big", signed=True)
609
610        return grid_freq * 1e-1 + grid2_freq * 1e-3
611
612    def get_age(self):
613        """Returns the percentage of aging of the laser.
614        0% indicated a brand new laser
615        100% indicates the laser should be replaces.
616
617        :returns:
618
619        """
620        response = self._age()
621        age = int.from_bytes(response, "big")
622
623        return age
624
625    def set_channel(self, channel):
626        """Sets the laser's operating channel.
627
628        MSA-01.3 lasers support a 32 bit channel value.
629
630        This defines how many spaces along the grid
631        the laser's frequency is displaced from the first channel frequency.
632
633        :param channel:
634        :returns:
635
636        """
637        # check type and stuff
638        if not isinstance(channel, int):
639            raise TypeError("Channel must be an integer")
640        if channel < 0:
641            raise ValueError("Channel must be positive.")
642        if channel > 0xFFFFFFFF:
643            raise ValueError("Channel must be a 32 bit integer (<=0xFFFFFFFF).")
644
645        # Split the channel choice into two options.
646        channel_hex = f"{channel:08x}"
647
648        channell = int(channel_hex[4:], 16)
649        channelh = int(channel_hex[0:4], 16)
650
651        # Set the channel registers.
652        self._channel(channell)
653        self._channelh(channelh)
654
655    def get_channel(self):
656        """gets the current channel setting
657
658        The channel defines how many spaces along the grid
659        the laser's frequency is displaced from the first channel frequency.
660
661        :returns: channel as an integer.
662
663        """
664        # This concatenates the data bytestrings
665        response = self._channelh() + self._channel()
666
667        channel = int.from_bytes(response, "big")
668
669        return channel
670
671    def set_fine_tuning(self, ftf):
672        """
673        This function provides off grid tuning for the laser's frequency.
674        The adjustment applies to all channels uniformly.
675        This is typically used after the laser if locked and minor adjustments
676        are required to the frequency.
677
678        The command may be pending in the event that the laser output is
679        enabled. The pending bit is cleared once the fine tune frequency
680        has been achieved.
681
682        **???** It seems like this can be done with the laser running.
683
684        :param ftf: The fine tune frequency adjustment in GHz
685        """
686
687        ftf = round(ftf * 1e3)
688
689        # We will leave this bare. This way the user can set and handle
690        # their own timing and check to make sure the laser has reached the
691        # desired fine tuning frequency.
692        # It WILL throw a "pending" error if the laser is on when setting.
693        self._ftf(ftf)
694
695    def get_fine_tuning(self):
696        """
697        This function returns the setting for the
698        off grid tuning for the laser's frequency.
699
700        :return ftf: The fine tune frequency adjustment in GHz
701        """
702
703        response = self._ftf()
704        ftf = int.from_bytes(response, "big", signed=True)
705
706        return ftf * 1e-3
707
708    def get_ftf_range(self):
709        """
710        Return the maximum and minimum off grid tuning for the laser's frequency.
711
712        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
713        """
714        response = self._ftfr()
715
716        ftfr = int.from_bytes(response, "big")
717
718        return ftfr * 1e-3
719
720    def get_temps(self):
721        """
722        Returns a list of currents in degrees Celcius.
723        In the following order:
724
725        Technology 1: [Diode Temp, Case Temp]
726        """
727        # get response this should be a long byte string
728        response = self._temps()
729
730        data = [
731            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
732            for i in range(0, len(response), 2)
733        ]
734
735        return data
736
737    def get_currents(self):
738        """
739        Returns a list of currents in mA.
740        In the following order:
741
742        Technology 1: [TEC, Diode]
743        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
744        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
745        """
746        # get response this should be a long byte string
747        response = self._currents()
748
749        data = [
750            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
751            for i in range(0, len(response), 2)
752        ]
753
754        return data
755
756    def get_last_response(self):
757        """This function gets the most recent response sent from the laser.
758        The response is parsed for errors and stuff the way any normal response
759        would be. The variable `self._response` is set to the last response again.
760
761        I dont know why you would need to do this.
762        """
763        return self._lstresp()
764
765    def get_error_fatal(self, reset=False):
766        """
767        reads fatal error register statusf and checks each bit against the
768        fatal error table to determine the fault
769
770        :param reset: resets/clears latching errors
771
772        """
773        response = self._statusf()
774        statusf = int.from_bytes(response, "big")
775
776        logger.debug("Current Status Fatal Error: %d", statusf)
777
778        if reset:
779            data_reset = 0x00FF
780            self._statusf(data_reset)
781
782        return FatalError(statusf)
783
784    def get_error_warning(self, reset=False):
785        """
786        reads warning error register statusw and checks each bit against the
787        warning error table to determine the fault
788
789        If the laser is off then some of the latched warnings will be set on.
790
791        :param reset: resets/clears latching errors
792        """
793        response = self._statusw()
794        statusw = int.from_bytes(response, "big")
795
796        logger.debug("Current Status Warning Error: %d", statusw)
797
798        if reset:
799            data_reset = 0x00FF
800            self._statusw(data_reset)
801
802        return WarningError(statusw)
803
804    def get_fatal_power_thresh(self):
805        """
806        reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
807
808        """
809        response = self._fpowth()
810        pow_fatal = int.from_bytes(response, "big") / 100
811        # correcting for proper order of magnitude
812        return pow_fatal
813
814    def get_warning_power_thresh(self):
815        """
816        reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
817
818        """
819        response = self._wpowth()
820        pow_warn = int.from_bytes(response, "big") / 100
821        # correcting for proper order of magnitude
822        return pow_warn
823
824    def get_fatal_freq_thresh(self):
825        """
826        reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
827
828        """
829        response = self._ffreqth()
830        freq_fatal = int.from_bytes(response, "big") / 10
831        # correcting for proper order of magnitude
832        response2 = self._ffreqth2()
833        freq_fatal2 = int.from_bytes(response2, "big") / 100
834        # get frequency deviation in MHz and add to GHz value
835        return freq_fatal + freq_fatal2
836
837    def get_warning_freq_thresh(self):
838        """
839        reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
840
841        """
842        response = self._wfreqth()
843        freq_warn = int.from_bytes(response, "big") / 10
844        # correcting for proper order of magnitude
845        response2 = self._wfreqth2()
846        freq_warn2 = int.from_bytes(response2, "big") / 100
847        # get frequency deviation in MHz and add to GHz value
848        return freq_warn + freq_warn2
849
850    def get_fatal_therm_thresh(self):
851        """
852        reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
853
854        """
855        response = self._fthermth()
856        therm_fatal = int.from_bytes(response, "big") / 100
857        # correcting for proper order of magnitude
858        return therm_fatal
859
860    def get_warning_therm_thresh(self):
861        """
862        reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
863        """
864        response = self._wthermth()
865        therm_thresh = int.from_bytes(response, "big") / 100
866        # correcting for proper order of magnitude
867        return therm_thresh
868
869    def get_srq_trigger(self):
870        """
871        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
872
873        """
874        response = self._srqt()
875        status = int.from_bytes(response, "big")
876
877        logger.debug("SRQT Status: %d", status)
878
879        return SQRTrigger(status)
880
881    def get_fatal_trigger(self):
882        """
883        Utilizes FatalT register to identify which fatal conditon was asserted in StatusF and StatusW registers
884
885        """
886        response = self._fatalt()
887        status = int.from_bytes(response, "big")
888
889        logger.debug("FatalT Status: %d", status)
890
891        return FatalTrigger(status)
892
893    def get_alm_trigger(self):
894        """
895        Utilizes ALMT register to identify why ALM was asserted in StatusF and StatusW registers
896
897        """
898        response = self._almt()
899        status = int.from_bytes(response, "big")
900
901        logger.debug("AlarmT Status: %d", status)
902
903        return AlarmTrigger(status)

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

OIF-ITLA-MSA-01.3 is the standard implemented here. Certain functions such as set_frequency() dont fully implement this yet because I dont know how to do checks to see if the optional FCF3 functions are implemented for a particular laser.

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.

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

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.3 defined registers. These must be in a yaml format as described in the project's README.
  • sleep_time: time in seconds. Use in wait function
sleep_time
def nop(self, data=None):
76    def nop(self, data=None):
77        """The No-Op operation.
78
79        This is a good test to see if your laser is communicating properly.
80        It should read 0000 data or echo whatever data you send it if you send it something.
81        The data you write to nop gets overwritten as soon as you read it back.
82
83        `nop()` also returns more informative errors following an ExecutionError.
84        This is not called by default so you must do this explicitly if you want
85        to see more informative error information.
86
87        :param data: Data to write
88
89        """
90        # pretty sure the data does nothing
91        if data is not None:
92            response = self._nop(data)
93        else:
94            response = self._nop()
95
96        error_field = int(response.hex()[-1], 16)
97        if bool(error_field):
98            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.

Parameters
  • data: Data to write
def enable(self):
100    def enable(self):
101        """Enables laser optical output.
102        There is a time delay after execution of this function to
103        proper stable laser output.
104
105        :returns: None
106
107        """
108        # I'm writing this out partially for transparency
109        # Maybe unnecessary or non-optimal
110        self._resena(Resena.SENA)

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

:returns: None

def disable(self):
112    def disable(self):
113        """Tells the laser to stop lasing.
114
115        :returns: None
116
117        """
118        self._resena(0)

Tells the laser to stop lasing.

:returns: None

def hard_reset(self):
120    def hard_reset(self):
121        """TODO describe function
122
123        :returns:
124
125        """
126        self._resena(Resena.MR)

TODO describe function

:returns:

def soft_reset(self):
128    def soft_reset(self):
129        """TODO describe function
130
131        :returns:
132
133        """
134        self._resena(Resena.SR)

TODO describe function

:returns:

def get_reset_enable(self):
136    def get_reset_enable(self):
137        """
138        Return ResEna register.
139        """
140        response = self._resena()
141        return Resena(int.from_bytes(response, "big"))

Return ResEna register.

def set_alarm_during_tuning(self):
143    def set_alarm_during_tuning(self):
144        """Set alarm during tuning
145        mcb(ADT)
146        """
147        self._mcb(MCB.ADT)

Set alarm during tuning mcb(ADT)

def set_shutdown_on_fatal(self):
149    def set_shutdown_on_fatal(self):
150        """Set shutdown on fatal.
151        mcb(SDF)
152        """
153        self._mcb(MCB.SDF)

Set shutdown on fatal. mcb(SDF)

def get_configuration_behavior(self):
155    def get_configuration_behavior(self):
156        """
157        Return MCB register.
158        """
159        response = self._mcb()
160        return MCB(int.from_bytes(response, "big"))

Return MCB register.

def is_disabled(self):
162    def is_disabled(self):
163        """
164        Return if disabled.
165
166        See 9.6.3 Reset/Enable (ResEna 0x32) [RW] in OIF-ITLA-MSI.
167
168        For some lasers, FatalError.DIS is not triggered (even if TriggerT allows it).
169        Consider overwriting this methods and monitoring FatalError.ALM.
170        """
171        fatal_error = self.get_error_fatal()
172        fatal_trigger = self.get_fatal_trigger()
173        resena = self.get_reset_enable()
174        mcb = self.get_configuration_behavior()
175
176        sdf = MCB.SDF in mcb
177        sena = Resena.SENA in resena
178        dis = FatalError.DIS in fatal_error
179
180        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):
182    def is_enabled(self, *args):
183        """
184        Return if enabled.  See is_disabled.
185        """
186        return not self.is_disabled(*args)

Return if enabled. See is_disabled.

def get_device_type(self):
188    def get_device_type(self):
189        """
190        returns a string containing the device type.
191        """
192        response_bytes = self._devtyp()
193
194        return response_bytes.decode("utf-8")

returns a string containing the device type.

def get_manufacturer(self):
196    def get_manufacturer(self):
197        """
198        Return's a string containing the manufacturer's name.
199        """
200        response_bytes = self._mfgr()
201
202        return response_bytes.decode("utf-8")

Return's a string containing the manufacturer's name.

def get_model(self):
204    def get_model(self):
205        """
206        return's the model as a string
207        """
208        response_bytes = self._model()
209
210        return response_bytes.decode("utf-8")

return's the model as a string

def get_serialnumber(self):
212    def get_serialnumber(self):
213        """
214        returns the serial number
215        """
216        response_bytes = self._serno()
217
218        return response_bytes.decode("utf-8")

returns the serial number

def get_manufacturing_date(self):
220    def get_manufacturing_date(self):
221        """returns the manufacturing date"""
222        response_bytes = self._mfgdate()
223
224        return response_bytes.decode("utf-8")

returns the manufacturing date

def get_firmware_release(self):
226    def get_firmware_release(self):
227        """
228        returns a manufacturer specific firmware release
229        """
230        response_bytes = self._release()
231
232        return response_bytes.decode("utf-8")

returns a manufacturer specific firmware release

def get_backwardscompatibility(self):
234    def get_backwardscompatibility(self):
235        """
236        returns a manufacturer specific firmware backwards compatibility
237        as a null terminated string
238        """
239        response_bytes = self._relback()
240
241        return response_bytes.decode("utf-8")

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

def read_aea(self):
243    def read_aea(self):
244        """
245        reads the AEA register data until an execution error is thrown.
246        """
247        aea_response = b""
248        try:
249            while True:
250                aea_response += self._aea_ear()
251
252        except ExecutionError:
253            try:
254                self.nop()
255
256            except EREError:
257                # If this throws an ERE error then we have completed reading AEA
258                pass
259
260            except NOPException as nop_e:
261                raise nop_e
262
263        return aea_response

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

def wait(self):
265    def wait(self):
266        """Wait until operation is complete. It check if the operation is completed every self.sleep_time seconds."""
267        while True:
268            try:
269                self.nop()
270            except CPException:
271                if self.sleep_time is not None:
272                    sleep(self.sleep_time)
273            else:
274                break

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

def wait_until_enabled(self):
276    def wait_until_enabled(self):
277        while self.is_disabled():
278            if self.sleep_time is not None:
279                sleep(self.sleep_time)
def wait_until_disabled(self):
281    def wait_until_disabled(self):
282        while self.is_enabled():
283            if self.sleep_time is not None:
284                sleep(self.sleep_time)
def set_power(self, pwr_dBm):
286    def set_power(self, pwr_dBm):
287        """Sets the power of the ITLA laser. Units of dBm.
288
289        :param pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX.
290        :returns: None
291
292        """
293        try:
294            self._pwr(round(pwr_dBm * 100))
295
296        except ExecutionError:
297            try:
298                self.nop()
299
300            except RVEError as error:
301                logger.error(
302                    "The provided power %.2f dBm is outside of the range for this device. "
303                    "The power is currently set to: %.2f dBm.",
304                    pwr_dBm,
305                    self.get_power_setting(),
306                )
307                raise error

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

Parameters
  • pwr_dBm: The power setting for the laser in dBm. Has precision XX.XX. :returns: None
def get_power_setting(self):
309    def get_power_setting(self):
310        """Gets current power setting set by set_power. Should be in dBm.
311
312        :returns:
313
314        """
315        # Gets power setting, not actual optical output power.
316        response = self._pwr()
317        return int.from_bytes(response, "big", signed=True) / 100

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

:returns:

def get_power_output(self):
319    def get_power_output(self):
320        """Gets the actual optical output power of the laser.
321        Only an approximation, apparently. Units dBm.
322
323        :returns:
324
325        """
326        response = self._oop()
327
328        return int.from_bytes(response, "big", signed=True) / 100

Gets the actual optical output power of the laser. Only an approximation, apparently. Units dBm.

:returns:

def get_power_min(self):
330    def get_power_min(self):
331        """Gets the minimum optical power output of the module. Units dBm.
332
333        :returns:
334
335        """
336        response = self._opsl()
337        return int.from_bytes(response, "big", signed=True) / 100

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

:returns:

def get_power_max(self):
339    def get_power_max(self):
340        """Gets the maximum optical power output of the module. Units dBm.
341
342        :returns:
343
344        """
345        response = self._opsh()
346        return int.from_bytes(response, "big", signed=True) / 100

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

:returns:

def set_fcf(self, freq):
348    def set_fcf(self, freq):
349        """
350        This sets the first channel frequency.
351        It does not reset the channel so this frequency will only be equal
352        to the output frequency if channel=1.
353
354        """
355        # convert frequency to MHz
356        freq = round(freq * 1e6)
357
358        freq_str = str(freq)
359        fcf1 = int(freq_str[0:3])
360        fcf2 = int(freq_str[3:7])
361        fcf3 = int(freq_str[7:])
362
363        try:
364            # is it better to split these into their own try/except blocks?
365            self._fcf1(fcf1)
366            self._fcf2(fcf2)
367            self._fcf3(fcf3)
368
369        except ExecutionError:
370            try:
371                self.nop()
372            except RVEError as error:
373                logger.error(
374                    "%.2f THz is out of bounds for this laser. "
375                    "The frequency must be within the range %.2f - %.2f THz.",
376                    fcf1,
377                    self.get_frequency_min(),
378                    self.get_frequency_max(),
379                )
380                raise error
381
382            except CIEError as error:
383                logger.error(
384                    "You cannot change the first channel frequency while the laser is enabled. "
385                    "The current frequency is: %.2f THz",
386                    self.get_frequency(),
387                )
388                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.

def set_frequency(self, freq):
390    def set_frequency(self, freq):
391        """Sets the frequency of the laser in TeraHertz.
392
393        Has MHz resolution. Will round down.
394
395        This function sets the first channel frequency and then sets the
396        channel to 1 so that the frequency output when the laser is enabled
397        will be the frequency given to this function.
398
399        If you would like to only change the first channel frequency use
400        the function `set_fcf`.
401
402        Disable the laser before calling this function.
403
404        :param freq: The desired frequency setting in THz.
405        :returns: None
406        """
407        # This does a check so this only runs if fine tuning has been turned on.
408        if self.get_fine_tuning() != 0:
409            # Set the fine tuning off!
410            while True:
411                try:
412                    self.set_fine_tuning(0)
413                except ExecutionError:
414                    sleep(self.sleep_time)
415                except CPException:
416                    self.wait()
417                    break
418                else:
419                    break
420
421        try:
422            self.set_fcf(freq)
423        except CPException:
424            self.wait()
425
426        try:
427            self.set_channel(1)
428        except CPException:
429            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.

Parameters
  • freq: The desired frequency setting in THz. :returns: None
def get_fcf(self):
431    def get_fcf(self):
432        """Get the currently set first channel frequency."""
433        response = self._fcf1()
434        fcf1 = int.from_bytes(response, "big")
435
436        response = self._fcf2()
437        fcf2 = int.from_bytes(response, "big")
438
439        response = self._fcf3()
440        fcf3 = int.from_bytes(response, "big")
441
442        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6

Get the currently set first channel frequency.

def get_frequency(self):
444    def get_frequency(self):
445        """gets the current laser operating frequency with channels
446        and everything accounted for.
447
448        :returns:
449
450        """
451        response = self._lf1()
452        lf1 = int.from_bytes(response, "big")
453
454        response = self._lf2()
455        lf2 = int.from_bytes(response, "big")
456
457        response = self._lf3()
458        lf3 = int.from_bytes(response, "big")
459
460        return lf1 + lf2 * 1e-4 + lf3 * 1e-6

gets the current laser operating frequency with channels and everything accounted for.

:returns:

def dither_enable(self, waveform: itla.itla_status.Waveform = <Waveform.SIN: 0>):
462    def dither_enable(self, waveform: Waveform = Waveform.SIN):
463        """enables dither
464
465        See 9.8.3 Digital Dither (Dither(E,R,A,F) 0x59-0x5C) [RW] [Optional]
466
467        :param waveform: specifies the dither waveform
468        """
469        data = (waveform << 4) | 2
470
471        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):
473    def dither_disable(self):
474        """disables digital dither"""
475        # TODO: This should preserve the waveform setting if possible
476        self._dithere(0)

disables digital dither

def set_dither_rate(self, rate):
478    def set_dither_rate(self, rate):
479        """
480        set dither rate in kHz, utilizes DitherR register
481        :param rate: an unsigned short integer specifying dither rate in kHz
482        """
483        self._ditherr(rate)

set dither rate in kHz, utilizes DitherR register

Parameters
  • rate: an unsigned short integer specifying dither rate in kHz
def get_dither_rate(self):
485    def get_dither_rate(self):
486        """
487        get dither rate, utilizes DitherR register
488        """
489        response = self._ditherr()
490        return int.from_bytes(response, "big")

get dither rate, utilizes DitherR register

def set_dither_frequency(self, rate):
492    def set_dither_frequency(self, rate):
493        """
494        set dither modulation frequency, utilizes DitherF register
495        :param rate: an unsigned short integer encoded as the FM peak-to-peak frequency
496        deviation as Ghz*10
497        """
498        self._ditherf(rate)

set dither modulation frequency, utilizes DitherF register

Parameters
  • rate: an unsigned short integer encoded as the FM peak-to-peak frequency deviation as Ghz*10
def get_dither_frequency(self):
500    def get_dither_frequency(self):
501        """
502        get dither modulation frequency, utilizes DitherF register
503        """
504        response = self._ditherf()
505        return int.from_bytes(response, "big")

get dither modulation frequency, utilizes DitherF register

def set_dither_amplitude(self, amplitude):
507    def set_dither_amplitude(self, amplitude):
508        """
509        set dither modulation amplitude, utilizes DitherA register
510        :param amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude
511        deviation as 10*percentage of the optical power
512        """
513        self._dithera(amplitude)

set dither modulation amplitude, utilizes DitherA register

Parameters
  • amplitude: an unsigned short integer encoded as the AM peak-to-peak amplitude deviation as 10*percentage of the optical power
def get_dither_amplitude(self):
515    def get_dither_amplitude(self):
516        """
517        get dither modulation amplitude, utilizes DitherA register
518        """
519        response = self._dithera()
520        return int.from_bytes(response, "big")

get dither modulation amplitude, utilizes DitherA register

def get_temp(self):
522    def get_temp(self):
523        """Returns the current primary control temperature in deg C.
524
525        :returns:
526
527        """
528        response = self._ctemp()
529        temp_100 = int.from_bytes(response, "big")
530
531        return temp_100 / 100

Returns the current primary control temperature in deg C.

:returns:

def get_frequency_min(self):
533    def get_frequency_min(self):
534        """command to read minimum frequency supported by the module
535
536        :returns:
537
538        """
539        response = self._lfl1()
540        lfl1 = int.from_bytes(response, "big")
541
542        response = self._lfl2()
543        lfl2 = int.from_bytes(response, "big")
544
545        response = self._lfl3()
546        lfl3 = int.from_bytes(response, "big")
547
548        return lfl1 + lfl2 * 1e-4 + lfl3 * 1e-6

command to read minimum frequency supported by the module

:returns:

def get_frequency_max(self):
550    def get_frequency_max(self):
551        """command to read maximum frequency supported by the module
552
553        :returns:
554
555        """
556        response = self._lfh1()
557        fcf1 = int.from_bytes(response, "big")
558
559        response = self._lfh2()
560        fcf2 = int.from_bytes(response, "big")
561
562        response = self._lfh3()
563        fcf3 = int.from_bytes(response, "big")
564
565        return fcf1 + fcf2 * 1e-4 + fcf3 * 1e-6

command to read maximum frequency supported by the module

:returns:

def get_grid_min(self):
567    def get_grid_min(self):
568        """command to read minimum grid supported by the module
569
570        :returns: The minimum grid supported by the module in GHz
571
572        """
573        try:
574            freq_lgrid = int.from_bytes(self._lgrid(), "big", signed=False)
575            freq_lgrid2 = int.from_bytes(self._lgrid2(), "big", signed=False)
576
577        except ExecutionError:
578            self.nop()
579
580        return freq_lgrid * 1e-1 + freq_lgrid2 * 1e-3

command to read minimum grid supported by the module

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

def set_grid(self, grid_freq):
582    def set_grid(self, grid_freq):
583        """Set the grid spacing in GHz.
584
585        MHz resolution.
586
587        :param grid_freq: the grid frequency spacing in GHz
588        :returns:
589
590        """
591        grid_freq = str(round(grid_freq * 1000))
592        data = int(grid_freq[0:4])
593        data_2 = int(grid_freq[4:])
594
595        self._grid(data)
596        self._grid2(data_2)

Set the grid spacing in GHz.

MHz resolution.

Parameters
  • grid_freq: the grid frequency spacing in GHz :returns:
def get_grid(self):
598    def get_grid(self):
599        """get the grid spacing in GHz
600
601        :returns: The grid spacing in GHz.
602
603        """
604        response = self._grid()
605        grid_freq = int.from_bytes(response, "big", signed=True)
606
607        response = self._grid2()
608        grid2_freq = int.from_bytes(response, "big", signed=True)
609
610        return grid_freq * 1e-1 + grid2_freq * 1e-3

get the grid spacing in GHz

:returns: The grid spacing in GHz.

def get_age(self):
612    def get_age(self):
613        """Returns the percentage of aging of the laser.
614        0% indicated a brand new laser
615        100% indicates the laser should be replaces.
616
617        :returns:
618
619        """
620        response = self._age()
621        age = int.from_bytes(response, "big")
622
623        return age

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

:returns:

def set_channel(self, channel):
625    def set_channel(self, channel):
626        """Sets the laser's operating channel.
627
628        MSA-01.3 lasers support a 32 bit channel value.
629
630        This defines how many spaces along the grid
631        the laser's frequency is displaced from the first channel frequency.
632
633        :param channel:
634        :returns:
635
636        """
637        # check type and stuff
638        if not isinstance(channel, int):
639            raise TypeError("Channel must be an integer")
640        if channel < 0:
641            raise ValueError("Channel must be positive.")
642        if channel > 0xFFFFFFFF:
643            raise ValueError("Channel must be a 32 bit integer (<=0xFFFFFFFF).")
644
645        # Split the channel choice into two options.
646        channel_hex = f"{channel:08x}"
647
648        channell = int(channel_hex[4:], 16)
649        channelh = int(channel_hex[0:4], 16)
650
651        # Set the channel registers.
652        self._channel(channell)
653        self._channelh(channelh)

Sets the laser's operating channel.

MSA-01.3 lasers support a 32 bit channel value.

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

Parameters
  • channel: :returns:
def get_channel(self):
655    def get_channel(self):
656        """gets the current channel setting
657
658        The channel defines how many spaces along the grid
659        the laser's frequency is displaced from the first channel frequency.
660
661        :returns: channel as an integer.
662
663        """
664        # This concatenates the data bytestrings
665        response = self._channelh() + self._channel()
666
667        channel = int.from_bytes(response, "big")
668
669        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.

:returns: channel as an integer.

def set_fine_tuning(self, ftf):
671    def set_fine_tuning(self, ftf):
672        """
673        This function provides off grid tuning for the laser's frequency.
674        The adjustment applies to all channels uniformly.
675        This is typically used after the laser if locked and minor adjustments
676        are required to the frequency.
677
678        The command may be pending in the event that the laser output is
679        enabled. The pending bit is cleared once the fine tune frequency
680        has been achieved.
681
682        **???** It seems like this can be done with the laser running.
683
684        :param ftf: The fine tune frequency adjustment in GHz
685        """
686
687        ftf = round(ftf * 1e3)
688
689        # We will leave this bare. This way the user can set and handle
690        # their own timing and check to make sure the laser has reached the
691        # desired fine tuning frequency.
692        # It WILL throw a "pending" error if the laser is on when setting.
693        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.

??? 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):
695    def get_fine_tuning(self):
696        """
697        This function returns the setting for the
698        off grid tuning for the laser's frequency.
699
700        :return ftf: The fine tune frequency adjustment in GHz
701        """
702
703        response = self._ftf()
704        ftf = int.from_bytes(response, "big", signed=True)
705
706        return ftf * 1e-3

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

Returns

The fine tune frequency adjustment in GHz

def get_ftf_range(self):
708    def get_ftf_range(self):
709        """
710        Return the maximum and minimum off grid tuning for the laser's frequency.
711
712        :return ftfr: The fine tune frequency range [-ftfr, + ftfr] in GHz
713        """
714        response = self._ftfr()
715
716        ftfr = int.from_bytes(response, "big")
717
718        return ftfr * 1e-3

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

Returns

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

def get_temps(self):
720    def get_temps(self):
721        """
722        Returns a list of currents in degrees Celcius.
723        In the following order:
724
725        Technology 1: [Diode Temp, Case Temp]
726        """
727        # get response this should be a long byte string
728        response = self._temps()
729
730        data = [
731            int.from_bytes(response[i : i + 2], "big", signed=True) / 100
732            for i in range(0, len(response), 2)
733        ]
734
735        return data

Returns a list of currents in degrees Celcius. In the following order:

Technology 1: [Diode Temp, Case Temp]

def get_currents(self):
737    def get_currents(self):
738        """
739        Returns a list of currents in mA.
740        In the following order:
741
742        Technology 1: [TEC, Diode]
743        Technology 2: [TED, Diode 1, Diode 2, Diode 3, Diode 4, SOA]
744        Technology 3: [TEC, Diode 1, tbd, tbd, tbd, ...]
745        """
746        # get response this should be a long byte string
747        response = self._currents()
748
749        data = [
750            int.from_bytes(response[i : i + 2], "big", signed=True) / 10
751            for i in range(0, len(response), 2)
752        ]
753
754        return data

Returns a list of currents in mA. In the following order:

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

def get_last_response(self):
756    def get_last_response(self):
757        """This function gets the most recent response sent from the laser.
758        The response is parsed for errors and stuff the way any normal response
759        would be. The variable `self._response` is set to the last response again.
760
761        I dont know why you would need to do this.
762        """
763        return self._lstresp()

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

I dont know why you would need to do this.

def get_error_fatal(self, reset=False):
765    def get_error_fatal(self, reset=False):
766        """
767        reads fatal error register statusf and checks each bit against the
768        fatal error table to determine the fault
769
770        :param reset: resets/clears latching errors
771
772        """
773        response = self._statusf()
774        statusf = int.from_bytes(response, "big")
775
776        logger.debug("Current Status Fatal Error: %d", statusf)
777
778        if reset:
779            data_reset = 0x00FF
780            self._statusf(data_reset)
781
782        return FatalError(statusf)

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

Parameters
  • reset: resets/clears latching errors
def get_error_warning(self, reset=False):
784    def get_error_warning(self, reset=False):
785        """
786        reads warning error register statusw and checks each bit against the
787        warning error table to determine the fault
788
789        If the laser is off then some of the latched warnings will be set on.
790
791        :param reset: resets/clears latching errors
792        """
793        response = self._statusw()
794        statusw = int.from_bytes(response, "big")
795
796        logger.debug("Current Status Warning Error: %d", statusw)
797
798        if reset:
799            data_reset = 0x00FF
800            self._statusw(data_reset)
801
802        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.

Parameters
  • reset: resets/clears latching errors
def get_fatal_power_thresh(self):
804    def get_fatal_power_thresh(self):
805        """
806        reads maximum plus/minus power deviation in dB for which the fatal alarm is asserted
807
808        """
809        response = self._fpowth()
810        pow_fatal = int.from_bytes(response, "big") / 100
811        # correcting for proper order of magnitude
812        return pow_fatal

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

def get_warning_power_thresh(self):
814    def get_warning_power_thresh(self):
815        """
816        reads maximum plus/minus power deviation in dB for which the warning alarm is asserted
817
818        """
819        response = self._wpowth()
820        pow_warn = int.from_bytes(response, "big") / 100
821        # correcting for proper order of magnitude
822        return pow_warn

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

def get_fatal_freq_thresh(self):
824    def get_fatal_freq_thresh(self):
825        """
826        reads maximum plus/minus frequency deviation in GHz for which the fatal alarm is asserted
827
828        """
829        response = self._ffreqth()
830        freq_fatal = int.from_bytes(response, "big") / 10
831        # correcting for proper order of magnitude
832        response2 = self._ffreqth2()
833        freq_fatal2 = int.from_bytes(response2, "big") / 100
834        # get frequency deviation in MHz and add to GHz value
835        return freq_fatal + freq_fatal2

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

def get_warning_freq_thresh(self):
837    def get_warning_freq_thresh(self):
838        """
839        reads maximum plus/minus frequency deviation in GHz for which the warning alarm is asserted
840
841        """
842        response = self._wfreqth()
843        freq_warn = int.from_bytes(response, "big") / 10
844        # correcting for proper order of magnitude
845        response2 = self._wfreqth2()
846        freq_warn2 = int.from_bytes(response2, "big") / 100
847        # get frequency deviation in MHz and add to GHz value
848        return freq_warn + freq_warn2

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

def get_fatal_therm_thresh(self):
850    def get_fatal_therm_thresh(self):
851        """
852        reads maximum plus/minus thermal deviation in degree celcius for which the fatal alarm is asserted
853
854        """
855        response = self._fthermth()
856        therm_fatal = int.from_bytes(response, "big") / 100
857        # correcting for proper order of magnitude
858        return therm_fatal

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

def get_warning_therm_thresh(self):
860    def get_warning_therm_thresh(self):
861        """
862        reads maximum plus/minus thermal deviation in degree celcius for which the warning alarm is asserted
863        """
864        response = self._wthermth()
865        therm_thresh = int.from_bytes(response, "big") / 100
866        # correcting for proper order of magnitude
867        return therm_thresh

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

def get_srq_trigger(self):
869    def get_srq_trigger(self):
870        """
871        Utilizes SRQT register to identify why SRQ was asserted in StatusF and StatusW registers
872
873        """
874        response = self._srqt()
875        status = int.from_bytes(response, "big")
876
877        logger.debug("SRQT Status: %d", status)
878
879        return SQRTrigger(status)

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

def get_fatal_trigger(self):
881    def get_fatal_trigger(self):
882        """
883        Utilizes FatalT register to identify which fatal conditon was asserted in StatusF and StatusW registers
884
885        """
886        response = self._fatalt()
887        status = int.from_bytes(response, "big")
888
889        logger.debug("FatalT Status: %d", status)
890
891        return FatalTrigger(status)

Utilizes FatalT register to identify which fatal conditon was asserted in StatusF and StatusW registers

def get_alm_trigger(self):
893    def get_alm_trigger(self):
894        """
895        Utilizes ALMT register to identify why ALM was asserted in StatusF and StatusW registers
896
897        """
898        response = self._almt()
899        status = int.from_bytes(response, "big")
900
901        logger.debug("AlarmT Status: %d", status)
902
903        return AlarmTrigger(status)

Utilizes ALMT register to identify why ALM was asserted in StatusF and StatusW registers