Basic Introduction

This tutorial serves as your first step in learning how to use the zahner_link C++ library to control Zahner IM7 series potentiostats.

First, the CMakeLists.txt file is specified, which shows how the library is integrated into the project. Only a few cmake commands are required for this.

CMakeLists.txt

project(TestProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

list(APPEND CMAKE_PREFIX_PATH "C:/Users/max/Downloads/lib-for-install") # path to the downloaded library
find_package(zahner_link COMPONENTS zahner_link REQUIRED)
set(OPENSSL_ROOT_DIR "C:/Program Files/OpenSSL") # path to open ssl
find_package(OpenSSL REQUIRED)

if(MSVC)
    add_compile_options(/bigobj /utf-8)
endif()

include_directories(${PROJECT_SOURCE_DIR})
add_executable(TestProject main.cpp)

target_link_libraries(TestProject PUBLIC zahner_link::zahner_link)

main.cpp

#include <zahnerlinkexc.h>
#include <helpers/hardwaresettingshelper.h>
#include <jobs/control/jobs/switch_off/switch_offjob.h>
#include <jobs/control/jobs/switch_on/switch_onjob.h>
#include <jobs/calibration/jobs/calibration/calibratejob.h>
#include <jobs/dc/jobs/poga/pogajob.h>
#include <jobs/ac/jobs/eis/eisjob.h>
#include <xml/measurement.h>
#include <xml/zxmlexporter.h>
#include <iostream>
#include <memory>

/**
 * @brief Getting Started with the zahner_link C++ Library
 *
 * This tutorial serves as your first step in learning how to use the zahner_link C++ library
 * to control Zahner IM7 series potentiostats.
 *
 * Error handling is ignored in this example; this will be covered in a separate example.
 * The exception-based library is also used for clearer examples.
 *
 * It covers:
 * - Connecting to the device
 * - Performing calibration
 * - Basic Potentiostatic/Galvanostatic (POGA) measurements
 * - Electrochemical Impedance Spectroscopy (EIS)
 * - Data handling (saving and combining datasets)
 * - Proper shutdown procedure
 */

int main(int argc, char *argv[])
{
    // =============================================================================================
    // 1. Connecting to the IM7
    // =============================================================================================

    // Ensure your IM7 device is powered on and fully booted before connecting.
    // Create a ZahnerLinkExc object using the IM7's IP address (here "localhost")
    // and port number.
    // As an alternative, there is also the non-exception-based version of the ZahnerLink class.
    ZahnerLinkExc link("localhost", "1994");

    auto status = link.connect();

    if (status != ZahnerLinkServiceStatusEnum::SUCCESS_NO_ERROR)
    {
        std::cout << "Failed to connect to IM7. Status: " << static_cast<int>(status) << std::endl;
        return 1;
    }
    std::cout << "Successfully connected to IM7." << std::endl;

    // =============================================================================================
    // 2. Performing DC Calibration
    // =============================================================================================

    // After your IM7 has been warming up for at least 30 minutes, you should perform a DC calibration.
    // This ensures accurate measurements.
    std::cout << "Configuring DC Calibration..." << std::endl;
    CalibrateJob dcCalibrationJob({.calibration_type = CalibrationTypesEnum::DC});

    // Note: Calibration takes several minutes.
    // It is commented out here for quick execution of the example, but should be uncommented for real tests.
    // link.doJob(dcCalibrationJob);
    std::cout << "Skipped DC Calibration (uncomment in code to run)." << std::endl;

    // =============================================================================================
    // 3. Switching On the Potentiostat
    // =============================================================================================

    // Before taking any measurements, we need to switch on the potentiostat.
    // We set the initial state (potentiostatic 1.0 V).
    std::cout << "Switching On the Potentiostat..." << std::endl;
    SwitchOnJob switchOnJob({.potentiostat = "MAIN:1:POT",
                             .coupling = PotentiostatCoupling::POTENTIOSTATIC,
                             .bias = 1.0, // Start at 1.0 V
                             .voltage_range_index = 0,
                             .compliance_range_index = 0});
    link.doJob(switchOnJob);
    std::cout << "Potentiostat is ON." << std::endl;

    // =============================================================================================
    // 4. Running Measurements
    // =============================================================================================

    // ---------------------------------------------------------------------------------------------
    // Part A: Potentiostatic Polarization (DC)
    // ---------------------------------------------------------------------------------------------

    // We configure a PogaJob (Potentiostatic/Galvanostatic) for a 5-second measurement at 1.0 V.
    std::cout << "Starting first DC polarization measurement (1.0 V)..." << std::endl;
    PogaJob potentiostaticPolarizationJob({.bias = 1.0,
                                           .duration = 5.0,
                                           .output_data_rate = 25.0,
                                           .autorange = true,
                                           .current_range = 0.1,
                                           .ir_drop = 0.0});
    link.doJob(potentiostaticPolarizationJob);

    // Retrieve the data from the first run from the IM7.
    // getJobResultDataT returns a smart pointer to the dataset.
    auto dcDataset1 = link.getJobResultDataT(potentiostaticPolarizationJob);
    std::cout << "First DC measurement finished." << std::endl;

    // Reuse the job object for a second measurement, changing the bias to -1.0 V.
    std::cout << "Starting second DC polarization measurement (-1.0 V)..." << std::endl;
    potentiostaticPolarizationJob.parameters.bias = -1.0;
    link.doJob(potentiostaticPolarizationJob);

    auto dcDataset2 = link.getJobResultDataT(potentiostaticPolarizationJob);
    std::cout << "Second DC measurement finished." << std::endl;

    // Combine the two datasets into one.
    std::cout << "Combining DC datasets..." << std::endl;
    dcDataset1->append(dcDataset2);

    // Create a Measurement object from the combined dataset for export.
    ZXml::Measurement xmlMeasurement(dcDataset1);

    // Save to file.
    ZXml::ZXmlExporter exporter;
    exporter.setCompactXml(false); // logical formatting for readability
    exporter.saveAsFileStandalone(xmlMeasurement, "polarization.zmx");
    std::cout << "Saved combined DC data to 'polarization.zmx'." << std::endl;

    // ---------------------------------------------------------------------------------------------
    // Part B: Electrochemical Impedance Spectroscopy (EIS)
    // ---------------------------------------------------------------------------------------------

    // Configure an EIS sweep from 1 kHz to 10 kHz downto 100 Hz.
    // Using designated initializers for clear parameter naming.
    std::cout << "Starting EIS Generate Job (Frequency Sweep)..." << std::endl;
    EisGenerateJob eisGenerateJob(
        {.bias = 0,
         .min_frequency = 100,
         .max_frequency = 1e4,
         .start_frequency = 1e3,
         .points_per_decade_upper = 20,
         .points_per_decade_lower = 6,
         .pre_duration = 0.1,
         .pre_waves = 1,
         .meas_duration = 0.2,
         .meas_waves = 3,
         .amplitude = 0.01});
    link.doJob(eisGenerateJob);

    // Retrieve and save the first EIS result.
    auto eisDataset1 = link.getJobResultDataT(eisGenerateJob);
    exporter.saveAsFileStandalone(ZXml::Measurement(eisDataset1), "eis_generate.zmx");
    std::cout << "Saved EIS sweep data to 'eis_generate.zmx'." << std::endl;

    // ---------------------------------------------------------------------------------------------
    // Part C: EIS Frequency Table
    // ---------------------------------------------------------------------------------------------

    // Configure an EIS measurement with specific frequency points (1k Hz, 20 k Hz) defined in a table.
    std::cout << "Starting EIS Frequency Table Job..." << std::endl;
    EisFrequencyTableJob eisTableJob(
        {.bias = 0.0,
         .spectrum = {
             {.frequency = 1000,
              .amplitude = 0.01,
              .pre_duration = 0.1,
              .pre_waves = 1,
              .meas_duration = 0.3,
              .meas_waves = 3},
             {.frequency = 20e3,
              .amplitude = 0.1, // Difference amplitude for this point
              .pre_duration = 0.1,
              .pre_waves = 1,
              .meas_duration = 0.2,
              .meas_waves = 3},
         }});
    link.doJob(eisTableJob);

    // Retrieve and save the second EIS result.
    auto eisDataset2 = link.getJobResultDataT(eisTableJob);
    exporter.saveAsFileStandalone(ZXml::Measurement(eisDataset2), "eis_table.zmx");
    std::cout << "Saved EIS table data to 'eis_table.zmx'." << std::endl;

    // Combine both EIS datasets.
    std::cout << "Combining EIS datasets..." << std::endl;
    eisDataset1->append(eisDataset2);
    exporter.saveAsFileStandalone(ZXml::Measurement(eisDataset1), "eis_combined.zmx");
    std::cout << "Saved combined EIS data to 'eis_combined.zmx'." << std::endl;

    // =============================================================================================
    // 5. Switching Off and Disconnecting
    // =============================================================================================

    // Always switch off the potentiostat when finished.
    std::cout << "Switching Off the Potentiostat..." << std::endl;
    SwitchOffJob switchOffJob({.potentiostat = "MAIN:1:POT"});
    link.doJob(switchOffJob);

    // Disconnect cleanly from the service.
    // After disconnecting, existing data objects (like datasets) are still valid,
    // but no new jobs can be sent.
    link.disconnect();
    std::cout << "Disconnected from IM7." << std::endl;

    std::cout << "Tutorial finished successfully." << std::endl;
    return 0;
}