Пример #1
0
def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument(
        "--stages",
        type=str,
        nargs="*",
        default=["firmware", "ramp", "adc", "afe"],
        help="Select which setup stages to run.",
    )

    args = parser.parse_args()

    if "firmware" in args.stages:
        program_firmware()

    if "erase_nvm" in args.stages:
        erase_nvm()

    if "ramp" in args.stages:
        run_ramp_calibration()

    if "adc" in args.stages:
        run_adc_calibration()

    if "afe" in args.stages:
        run_afe_calibration()

    log.section("Soft-resetting")
    gem = gemini.Gemini()
    gem.soft_reset()
    log.success("Finished")
Пример #2
0
def run(adc_resolution, invert, adc_channel, save):
    if invert:
        high_expected = 0
        low_expected = adc_resolution - 1
    else:
        high_expected = adc_resolution - 1
        low_expected = 0

    gem = gemini.Gemini()

    gem.enter_calibration_mode()

    # Knob calibration is done with error correction enabled.
    gem.enable_adc_error_correction()

    input(f"Set knob for channel {adc_channel} all the way CCW and press enter.")

    samples = []
    for s in range(512):
        samples.append(gem.read_adc(adc_channel))

    low_measured = statistics.mean(samples)

    log.info(f"> Measured {low_measured}, expected {low_expected}")

    input(f"Set knob for channel {adc_channel} all the way CW and press enter.")

    samples = []
    for s in range(512):
        samples.append(gem.read_adc(adc_channel))

    high_measured = statistics.mean(samples)

    log.info(f"> Measured {high_measured}, expected {high_expected}")

    gain_error = (high_expected - low_expected) / (high_measured - low_measured)
    offset_error = (low_measured * gain_error) - low_expected

    log.success(f"Knob gain error: {gain_error:.3f}, offset error: {offset_error:.1f}")

    local_copy = pathlib.Path("calibrations") / f"{gem.serial_number}.knob.json"
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        json.dump({"gain_error": gain_error, "offset_error": offset_error}, fh)

    log.success(f"Saved local copy to {local_copy}")

    if save:
        settings = gem.read_settings()
        settings.knob_gain_corr = gain_error
        settings.knob_offset_corr = offset_error

        gem.save_settings(settings)
        log.success("Saved to NVM.")
    else:
        log.warning("Dry run, not saved to NVM.")

    log.success("Done!")
    gem.close()
Пример #3
0
def erase_nvm():
    log.section("Erasing NVM")
    gem = gemini.Gemini()
    gem.erase_lut()
    log.success("Erased ramp look-up-table.")
    gem.reset_settings()
    log.success("Erased user settings.")
    gem.close()
def run(calibration_points, sample_count, adc_range, adc_resolution, invert, adc_channel, save):
    voltages = [
        n / calibration_points * adc_range
        for n in range(calibration_points + 1)
    ]

    expected_codes = [
        int(voltages[n] / adc_range * (adc_resolution - 1))
        for n in range(calibration_points + 1)
    ]

    if invert:
        expected_codes = [adc_resolution - 1 - code for code in expected_codes]

    measured_codes = []

    gem = gemini.Gemini()
    sol_ = sol.Sol()

    gem.enter_calibration_mode()
    gem.disable_adc_error_correction()

    sol_.send_voltage(0)

    for n in range(calibration_points + 1):
        voltage = n / calibration_points * adc_range
        print(f"Measuring {voltage:.3f}, expecting {expected_codes[n]}.")
        sol_.send_voltage(voltage)
        time.sleep(0.2)

        samples = []
        for s in range(sample_count):
            samples.append(gem.read_adc(adc_channel))
        
        result = statistics.mean(samples)
        
        print(f"Got {result:.1f}, diff {result - expected_codes[n]:.1f}")
        measured_codes.append(result)

    gain_error = adc_errors.calculate_avg_gain_error(expected_codes, measured_codes)
    offset_error = adc_errors.calculate_avg_offset_error(expected_codes, measured_codes, gain_error)
    print(f"Measured: Gain: {gain_error:.3f}, Offset: {offset_error:.1f}")

    corrected = adc_errors.apply_correction(measured_codes, gain_error, offset_error)
    corrected_gain_error = adc_errors.calculate_avg_gain_error(expected_codes, corrected)
    corrected_offset_error = adc_errors.calculate_avg_offset_error(expected_codes, corrected, corrected_gain_error)
    print(f"Corrected: Gain: {corrected_gain_error:.3f}, Offset: {corrected_offset_error:.1f}")

    if save:
        gem.set_adc_gain_error(gain_error)
        gem.set_adc_offset_error(int(offset_error))
        print("Saved to NVM.")
    else:
        print("Dry run, not saved to NVM.")
    
    gem.enable_adc_error_correction()
def run(adc_resolution, invert, adc_channel, save):
    if invert:
        high_expected = 0
        low_expected = adc_resolution - 1
    else:
        high_expected = adc_resolution - 1
        low_expected = 0

    gem = gemini.Gemini()

    gem.enter_calibration_mode()

    # Knob calibration is done with error correction enabled.
    gem.enable_adc_error_correction()

    input(
        f"Set knob for channel {adc_channel} all the way CCW and press enter.")

    samples = []
    for s in range(128):
        samples.append(gem.read_adc(adc_channel))

    low_measured = statistics.mean(samples)

    print(f"> Measured {low_measured}, expected {low_expected}")

    input(
        f"Set knob for channel {adc_channel} all the way CW and press enter.")

    samples = []
    for s in range(128):
        samples.append(gem.read_adc(adc_channel))

    high_measured = statistics.mean(samples)

    print(f"> Measured {high_measured}, expected {high_expected}")

    gain_error = (high_expected - low_expected) / (high_measured -
                                                   low_measured)
    offset_error = (low_measured * gain_error) - low_expected

    print(
        f"Knob gain error: {gain_error:.3f}, offset error: {offset_error:.1f}")

    if save:
        gain_f16 = int(gain_error * 0x10000)
        offset_f16 = int(offset_error * 0x10000)

        settings = gem.read_settings()
        settings.knob_gain_corr = gain_f16
        settings.knob_offset_corr = offset_f16

        gem.save_settings(settings)
        print("Saved to NVM.")
    else:
        print("Dry run, not saved to NVM.")
def run(save):
    # Oscilloscope setup.
    resource_manager = visa.ResourceManager("@ivi")
    scope = oscilloscope.Oscilloscope(resource_manager)
    scope.reset()

    scope.set_vertical_division("c1", 2)
    scope.set_vertical_cursor("c1", 0, 3.3)
    scope.set_trigger_level("c1", 1)

    # Gemini setup
    gem = gemini.Gemini()
    gem.enter_calibration_mode()

    # Calibrate both oscillators
    print("--------- Calibrating Castor ---------")
    input("Connect to RAMP A and press enter to start.")
    castor_calibration = _calibrate_oscillator(gem, scope, 0)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    print(f"Lowest voltage: {lowest_voltage:.2f}, Highest: {highest_voltage:.2f}")

    print("--------- Calibrating Pollux ---------")
    input("Connect to RAMP B and press enter to start.")
    pollux_calibration = _calibrate_oscillator(gem, scope, 1)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    print(f"Lowest voltage: {lowest_voltage:.2f}, Highest: {highest_voltage:.2f}")

    if save:
        print("--------- Saving calibration table ---------")

        for o, table in enumerate([castor_calibration, pollux_calibration]):
            for n, dac_code in enumerate(table.values()):
                print(f"> Set oscillator {o} entry {n} to {dac_code}.")
                gem.write_lut_entry(n, o, dac_code)
        
        print("Writing LUT to NVM")
        gem.write_lut()

        checksum = 0
        for dac_code in castor_calibration.values():
            checksum ^= dac_code

        print(f"Calibration table written, checksum: {checksum:04x}")

    else:
        print("WARNING: Dry run enabled, calibration table not saved.")
    
    gem.close()
    print("Done")
Пример #7
0
def main(stats=False):
    gem = gemini.Gemini()
    _check_firmware_version(gem)

    settings = gem.read_settings()

    log.info(settings)

    gem.enable_monitor()

    output = tui.Updateable(clear_all=False)

    seen_states = make_seen_states()

    with output:
        while True:
            update = gem.monitor()

            track_states(update, seen_states)
            draw(update, seen_states, stats=stats)

            output.update()
def run(
    num_calibration_points,
    sample_count,
    strategy,
    save,
):
    if strategy == "adc":
        strategy = DirectADCStrategy()
    elif strategy == "afe":
        strategy = ThroughAFEStrategy()
    else:
        raise ValueError(f"Unknonw strategy {strategy}.")

    # Create the list of calibration points and expected codes.
    voltages = [
        n / num_calibration_points * strategy.range_
        for n in range(num_calibration_points + 1)
    ]

    expected_codes = [
        int(voltages[n] / strategy.range_ * (strategy.resolution - 1))
        for n in range(num_calibration_points + 1)
    ]

    if strategy.invert:
        expected_codes = [
            strategy.resolution - 1 - code for code in expected_codes
        ]

    calibration_points = dict(zip(voltages, expected_codes))

    gem = gemini.Gemini()
    sol_ = sol.Sol()

    sol._setup()
    sol_.send_voltage(0, channel=strategy.sol_channel)

    gem.enter_calibration_mode()

    strategy.setup(gem)

    measured = _measure_range(gem, sol_, strategy, sample_count,
                              calibration_points)

    gain_error = adc_errors.calculate_avg_gain_error(expected_codes,
                                                     list(measured.values()))
    offset_error = adc_errors.calculate_avg_offset_error(
        expected_codes, list(measured.values()), gain_error)

    log.success(f"Measured gain={gain_error:.3f}, offset={offset_error:.1f}")

    strategy.finish(gem)

    local_copy = pathlib.Path("calibrations") / strategy.file_name(gem)
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        json.dump({"gain_error": gain_error, "offset_error": offset_error}, fh)

    log.info(f"Saved local copy to {local_copy}")

    if save:
        strategy.save(gem, gain_error, offset_error)
        log.success("Saved to NVM.")
    else:
        log.warning("Dry run, not saving to NVM.")
        return

    # Test out the new calibration

    log.info("Taking measurements with new calibration...")

    gem.enable_adc_error_correction()

    measured = _measure_range(gem, sol_, strategy, sample_count,
                              calibration_points)

    gain_error = adc_errors.calculate_avg_gain_error(expected_codes,
                                                     list(measured.values()))
    offset_error = adc_errors.calculate_avg_offset_error(
        expected_codes, list(measured.values()), gain_error)
    log.info(f"Remeasured gain={gain_error:.3f}, offset={offset_error:.1f}")

    log.success("Done")
    gem.close()
Пример #9
0
def run(save):
    # Oscilloscope setup.
    log.info("Configuring oscilloscope...")
    resource_manager = visa.ResourceManager("@ivi")
    scope = oscilloscope.Oscilloscope(resource_manager)
    scope.reset()

    scope.enable_bandwidth_limit()
    scope.set_time_division("10ms")
    scope.set_vertical_division("c1", "800mV")
    scope.set_vertical_division("c2", "800mV")
    scope.set_vertical_offset("c1", -1.65)
    scope.set_vertical_offset("c2", -1.65)

    # Gemini setup
    log.info("Connecting to Gemini...")
    gem = gemini.Gemini()
    gem.enter_calibration_mode()

    # Calibrate both oscillators
    log.section("Calibrating Castor...", depth=2)

    input(
        "Connect PROBE ONE to RAMP A\nConnect PROBE TWO to RAMP B\n> press enter to start."
    )
    castor_calibration = _calibrate_oscillator(gem, scope, 0)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    log.success(
        f"\nCalibrated:\n- Lowest: {lowest_voltage:.2f}v\n- Highest: {highest_voltage:.2f}v\n"
    )

    log.section("Calibrating Pollux...", depth=2)

    pollux_calibration = _calibrate_oscillator(gem, scope, 1)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    log.success(
        f"\nCalibrated:\n- Lowest: {lowest_voltage:.2f}v\n- Highest: {highest_voltage:.2f}v\n"
    )

    log.section("Saving calibration table...", depth=2)

    local_copy = pathlib.Path(
        "calibrations") / f"{gem.serial_number}.ramp.json"
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        data = {
            "castor": castor_calibration,
            "pollux": pollux_calibration,
        }
        json.dump(data, fh)

    log.success(f"Saved local copy to {local_copy}")

    if save:
        output = tui.Updateable()
        bar = tui.Bar()

        log.info("Sending LUT values to device...")

        for o, table in enumerate([castor_calibration, pollux_calibration]):
            for n, dac_code in enumerate(table.values()):
                with output:
                    progress = n / (len(table.values()) - 1)
                    bar.draw(
                        output,
                        tui.Segment(
                            progress,
                            color=tui.gradient(start_color, end_color,
                                               progress),
                        ),
                    )
                    log.debug(f"Set oscillator {o} entry {n} to {dac_code}.")

                    gem.write_lut_entry(n, o, dac_code)

        log.info("Committing LUT to NVM...")
        gem.write_lut()

        checksum = 0
        for dac_code in castor_calibration.values():
            checksum ^= dac_code

        log.success(f"Calibration table written, checksum: {checksum:04x}")

    else:
        log.warning("Dry run enabled, calibration table not saved to device.")

    gem.close()

    print("")
    log.success("Done!")
def main():
    start = time.monotonic()

    gem = gemini.Gemini()
    gem.enable_monitor()

    column_size = max(tui.width() / 10, 12)
    columns = tui.Columns(*[f">{column_size}"] * 4)

    output = tui.Updateable()

    with output:
        while True:
            update = gem.monitor()
            columns.draw(output, "", tui.underline, "Castor", "Pollux")

            columns.draw(
                output,
                "CV In │",
                color_range_cv(update.castor_pitch_cv, 0.0, 6.0),
                f"{update.castor_pitch_cv:.3f}v",
                color_range_cv(update.pollux_pitch_cv, 0.0, 6.0),
                f"{update.pollux_pitch_cv:.3f}v",
            )

            columns.draw(
                output,
                "CV Knob │",
                color_range_bipolar(update.castor_pitch_knob, -1.0, 1.0),
                f"{update.castor_pitch_knob:+.3f}v",
                color_range_bipolar(update.pollux_pitch_knob, -1.0, 1.0),
                f"{update.pollux_pitch_knob:+.3f}v",
            )

            columns.draw(
                output,
                "PW In │",
                color_range(update.castor_pulse_width_cv, 0, 4095),
                f"{update.castor_pulse_width_cv / 4095 * 100:.0f}%",
                color_range(update.pollux_pulse_width_cv, 0, 4095),
                f"{update.pollux_pulse_width_cv / 4095 * 100:.0f}%",
            )

            columns.draw(
                output,
                tui.underline,
                "PW Knob │",
                color_range(update.castor_pulse_width_knob, 0, 4095),
                f"{update.castor_pulse_width_knob / 4095 * 100:.0f}%",
                color_range(update.pollux_pulse_width_knob, 0, 4095),
                f"{update.pollux_pulse_width_knob / 4095 * 100:.0f}%",
            )

            columns.draw(
                output,
                "LFO │",
                color_range(update.lfo_intensity, 0, 1.0),
                f"{update.lfo_intensity * 100:.0f}%",
            )
            columns.draw(
                output,
                "Button │",
                color_range_bipolar(int(update.button_state), 0, 1.0),
                update.button_state,
            )
            columns.draw(
                output,
                "Loop time │",
                f"{update.loop_time}",
            )
            columns.draw(
                output,
                "LED time │",
                f"{update.animation_time}",
            )
            columns.draw(
                output,
                "ADC time │",
                f"{update.sample_time}",
            )
            columns.draw(
                output,
                "Runtime │",
                f"{time.monotonic() - start:.0f}",
            )

            output.update()
Пример #11
0
import statistics
import time

import IPython
import pyvisa
import wintertools.oscilloscope

from libgemini import fallback_calibration, gemini, oscillators, reference_calibration

_visa_resources_mgr = pyvisa.ResourceManager("@ivi")
gem = gemini.Gemini()


def set_oscillators_to_note(note, calibration=reference_calibration):
    freq = oscillators.midi_note_to_frequency(note)
    period = oscillators.frequency_to_timer_period(freq)

    charge_code_castor = oscillators.calibrated_charge_code_for_period(
        period, calibration.castor)
    charge_code_pollux = oscillators.calibrated_charge_code_for_period(
        period, calibration.pollux)

    print(
        f"Note: {note}, Freq: {freq}, Charge codes: {charge_code_castor}, {charge_code_pollux}"
    )

    gem.set_period(0, period)
    gem.set_dac(0, charge_code_castor, 0)
    time.sleep(0.1)  # Needed so the DAC has time to update EEPROM
    gem.set_period(1, period)
    gem.set_dac(2, charge_code_pollux, 0)
def run(
    calibration_points,
    sample_count,
    adc_range,
    adc_resolution,
    invert,
    adc_channel,
    save,
):
    voltages = [
        n / calibration_points * adc_range for n in range(calibration_points + 1)
    ]

    expected_codes = [
        int(voltages[n] / adc_range * (adc_resolution - 1))
        for n in range(calibration_points + 1)
    ]

    if invert:
        expected_codes = [adc_resolution - 1 - code for code in expected_codes]

    measured_codes = []

    gem = gemini.Gemini()
    sol_ = sol.Sol()

    gem.enter_calibration_mode()
    gem.disable_adc_error_correction()

    sol_.send_voltage(0)

    for n in range(calibration_points + 1):
        expected = expected_codes[n]
        voltage = n / calibration_points * adc_range
        log.info(f"Measuring {voltage:.3f}, expecting {expected}.")
        sol_.send_voltage(voltage)
        time.sleep(0.1)

        samples = []
        for s in range(sample_count):
            samples.append(gem.read_adc(adc_channel))

        result = statistics.mean(samples)

        diff = result - expected_codes[n]

        if abs(diff) > 100:
            log.error(
                "ADC reading too far out of range. Expected {expected}, measured: {result:.1f}, diff: {diff:.1f}"
            )

        log.info(f"Measured {result:.1f}, diff {diff:.1f}")
        measured_codes.append(result)

    gain_error = adc_errors.calculate_avg_gain_error(expected_codes, measured_codes)
    offset_error = adc_errors.calculate_avg_offset_error(
        expected_codes, measured_codes, gain_error
    )
    log.info(f"Measured: Gain: {gain_error:.3f}, Offset: {offset_error:.1f}")

    corrected = adc_errors.apply_correction(measured_codes, gain_error, offset_error)
    corrected_gain_error = adc_errors.calculate_avg_gain_error(
        expected_codes, corrected
    )
    corrected_offset_error = adc_errors.calculate_avg_offset_error(
        expected_codes, corrected, corrected_gain_error
    )
    log.success(
        f"Expected after correction: Gain: {corrected_gain_error:.3f}, Offset: {corrected_offset_error:.1f}"
    )

    local_copy = pathlib.Path("calibrations") / f"{gem.serial_number}.adc.json"
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        json.dump({"gain_error": gain_error, "offset_error": offset_error}, fh)

    log.info(f"Saved local copy to {local_copy}")

    if save:
        gem.set_adc_gain_error(gain_error)
        gem.set_adc_offset_error(int(offset_error))
        log.success("Saved to NVM.")
    else:
        log.warning("Dry run, not saved to NVM.")

    gem.enable_adc_error_correction()

    # Test out the new calibrated ADC

    log.info("Taking measurements with new calibration...")

    measured_codes = []
    for n in range(calibration_points + 1):
        voltage = n / calibration_points * adc_range
        log.debug(f"Measuring {voltage:.3f}, expecting {expected_codes[n]}.")
        sol_.send_voltage(voltage)
        time.sleep(0.1)

        samples = []
        for s in range(sample_count):
            samples.append(gem.read_adc(adc_channel))

        result = statistics.mean(samples)

        log.info(f"Measured {result:.1f}, diff {result - expected_codes[n]:.1f}")

        if abs(diff) > 50:
            log.error(
                "ADC reading too far out of range. Expected {expected}, measured: {result:.1f}, diff: {diff:.1f}"
            )

        measured_codes.append(result)

    gain_error = adc_errors.calculate_avg_gain_error(expected_codes, measured_codes)
    offset_error = adc_errors.calculate_avg_offset_error(
        expected_codes, measured_codes, gain_error
    )
    log.success(
        f"Measured, corrected: Gain: {gain_error:.3f}, Offset: {offset_error:.1f}"
    )

    log.success("Done")
    gem.close()
def run(save):
    # Gemini setup
    log.info("Connecting to Gemini...")
    gem = gemini.Gemini()
    gem.enter_calibration_mode()

    initial_period, initial_dac_code = next(iter(period_to_dac_code.items()))
    time.sleep(0.1)
    gem.set_period(0, initial_period)
    gem.set_dac(0, initial_dac_code, 0)
    time.sleep(0.1)
    gem.set_period(1, initial_period)
    gem.set_dac(2, initial_dac_code, 0)

    # Oscilloscope setup.
    log.info("Configuring oscilloscope...")
    resource_manager = visa.ResourceManager("@ivi")
    scope = oscilloscope.Oscilloscope(resource_manager)

    scope.reset()
    scope.enable_bandwidth_limit()
    scope.set_intensity(50, 100)

    # Enable both channels initially so that it's clear if the programming
    # board isn't connecting to the POGO pins.
    scope.set_time_division("10ms")
    scope.set_vertical_cursor("c1", 0, 3.3)
    scope.set_vertical_cursor("c2", 0, 3.3)
    scope.enable_channel("c1")
    scope.enable_channel("c2")
    scope.set_vertical_division("c1", "800mV")
    scope.set_vertical_division("c2", "800mV")
    scope.set_vertical_offset("c1", -1.65)
    scope.set_vertical_offset("c2", -1.65)

    input(
        "Connect PROBE ONE to RAMP A\nConnect PROBE TWO to RAMP B\n> press enter to start."
    )

    # Calibrate both oscillators
    log.section("Calibrating Castor...", depth=2)
    castor_calibration = _calibrate_oscillator(gem, scope, 0)

    lowest_voltage = oscillators.charge_code_to_volts(min(castor_calibration.values()))
    highest_voltage = oscillators.charge_code_to_volts(max(castor_calibration.values()))
    log.success(
        f"\nCalibrated:\n- Lowest: {lowest_voltage:.2f}v\n- Highest: {highest_voltage:.2f}v\n"
    )

    log.section("Calibrating Pollux...", depth=2)
    pollux_calibration = _calibrate_oscillator(gem, scope, 1)

    lowest_voltage = oscillators.charge_code_to_volts(min(pollux_calibration.values()))
    highest_voltage = oscillators.charge_code_to_volts(max(pollux_calibration.values()))
    log.success(
        f"\nCalibrated:\n- Lowest: {lowest_voltage:.2f}v\n- Highest: {highest_voltage:.2f}v\n"
    )

    log.section("Saving calibration table...", depth=2)

    local_copy = pathlib.Path("calibrations") / f"{gem.serial_number}.ramp.json"
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        data = {
            "castor": castor_calibration,
            "pollux": pollux_calibration,
        }
        json.dump(data, fh)

    log.success(f"Saved local copy to {local_copy}")

    if save:
        output = tui.Updateable()
        bar = tui.Bar()

        log.info("Sending LUT values to device...")

        with output:
            for n, timer_period in enumerate(castor_calibration.keys()):
                progress = n / (len(castor_calibration.values()) - 1)
                bar.draw(
                    tui.Segment(
                        progress,
                        color=tui.gradient(start_color, end_color, progress),
                    ),
                )
                output.update()

                castor_code = castor_calibration[timer_period]
                pollux_code = pollux_calibration[timer_period]

                gem.write_lut_entry(n, timer_period, castor_code, pollux_code)

                log.debug(
                    f"Set LUT entry {n} to {timer_period=}, {castor_code=}, {pollux_code=}."
                )

        log.info("Committing LUT to NVM...")
        gem.write_lut()

        checksum = 0
        for dac_code in castor_calibration.values():
            checksum ^= dac_code

        log.success(f"Calibration table written, checksum: {checksum:04x}")

    else:
        log.warning("Dry run enabled, calibration table not saved to device.")

    gem.close()

    print("")
    log.success("Done!")
def run(save):
    # Oscilloscope setup.
    resource_manager = visa.ResourceManager("@ivi")
    scope = oscilloscope.Oscilloscope(resource_manager)
    scope.reset()

    scope.set_vertical_division("c1", 2)
    scope.set_vertical_cursor("c1", 0, 3.3)
    scope.set_trigger_level("c1", 1)

    # Gemini setup
    gem = gemini.Gemini()
    gem.enter_calibration_mode()

    # Calibrate both oscillators
    print("--------- Calibrating Castor ---------")
    input("Connect to RAMP A and press enter to start.")
    castor_calibration = _calibrate_oscillator(gem, scope, 0)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    print(
        f"Lowest voltage: {lowest_voltage:.2f}, Highest: {highest_voltage:.2f}"
    )

    print("--------- Calibrating Pollux ---------")
    input("Connect to RAMP B and press enter to start.")
    pollux_calibration = _calibrate_oscillator(gem, scope, 1)

    lowest_voltage = _code_to_volts(min(castor_calibration.values()))
    highest_voltage = _code_to_volts(max(castor_calibration.values()))
    print(
        f"Lowest voltage: {lowest_voltage:.2f}, Highest: {highest_voltage:.2f}"
    )

    print("--------- Saving calibration table ---------")

    local_copy = pathlib.Path(
        "calibrations") / f"{gem.serial_number}.ramp.json"
    local_copy.parent.mkdir(parents=True, exist_ok=True)

    with local_copy.open("w") as fh:
        data = {
            "castor": castor_calibration,
            "pollux": pollux_calibration,
        }
        json.dump(data, fh)

    print(f"Saved local copy to {local_copy}")

    if save:
        for o, table in enumerate([castor_calibration, pollux_calibration]):
            for n, dac_code in enumerate(table.values()):
                print(f"> Set oscillator {o} entry {n} to {dac_code}.")
                gem.write_lut_entry(n, o, dac_code)

        print("Writing LUT to NVM")
        gem.write_lut()

        checksum = 0
        for dac_code in castor_calibration.values():
            checksum ^= dac_code

        print(f"Calibration table written, checksum: {checksum:04x}")

    else:
        print(
            "WARNING: Dry run enabled, calibration table not saved to device.")

    gem.close()
    print("Done")