Plugin development, how to implement persistent variables

Hey Chris,

I am brainstorming the best solution to the following problem:

I’m in the process of developing a plugin that works with a circuit board I’ve developed to drive stepper motor peristaltic pumps. It’s based on your “RotarySpeedSensor” plugin. I am currently looking for a solution on how I can run a calibration experiment that, let’s say, rotates the pump for 10 full turns and then asks to measure and input the amount extruded. Afterward, I would then like the plugin to somehow save the extruded volume value to look it up in any future experiments. Using the saved value, I could then offer an input like “milliliters to extrude” which I can then translate into pump turns for execution.

I found that mWorks already uses files like “setup_variables.xml” which seam related, but I’m not sure how I can register a new persistent variable file for my plugin within my plugin.

Many thanks in advance for your valuable help!
Louis

Hi Louis,

I would probably just add an optional “milliliters_per_turn” parameter to the device. Then the device could take either turns or milliliters as input, with the latter being allowed only if milliliters_per_turn is set.

If you really want to use a “persistent” variable, the closest thing is a saved variable in a variable set. For more info, please see “Variable sets” in Saving and restoring client state. My idea is that you would still have a milliliters_per_turn parameter, but the experiment would assign a variable as its value, and the value of that variable would be saved in a variable set.

Cheers,
Chris

Hey Chris,

thank you very much!

So as far as I understand, my two options are:

  1. Creating experiment variables and providing them to the IO device as parameter:

    var reward_ml_per_rotation = 0.09175
    

or

  1. Creating experiment variables and passing them to the IO device as parameters, but making them persistent so that when I load the experiment the newer content is loaded?

    var reward_ml_per_rotation = 0.09175 (persistant = 1)
    

Or did you mean reading variables from the experiments directly? For example, something like:

double display_left = mainDisplayInfo->getValue().getElement("display_left").getFloat();

Sorry if I misunderstood, could I ask you to provide me a few code examples?

Cheers,
Louis

Hi Louis,

You understood correctly. I meant (1) or (2), as you described. Just be aware that (2) requires that you either (a) explicitly load the saved variable set after loading your experiment, or (b) load your experiment via a workspace that includes the saved variable set.

The machinery that loads setup_variables.xml isn’t going to help you, as there’s no way for your plugin to add new saved variables to it. You could always come up with an ad-hoc solution, e.g. your plugin could read and write a custom configuration file. Personally, I’d prefer adding a parameter, as I described, but it’s up to you.

could I ask you to provide me a few code examples?

I’m not sure exactly what you want. Can you be more specific about what you want to see?

Thanks,
Chris

Hey!

So this is my mIO.cpp:

//
//  mIO.cpp
//  mIO
//
//  Created by Louis Frank on 2024-02-08.
//  Based on the work of Philipp Schwedheln, Christopher Stawarz & Ralf Brockhausen
//  Copyright 2024 DPZ. All rights reserved.
//

#include "mIO.hpp"

BEGIN_NAMESPACE_MW

const std::string mIO::SERIAL_PORT("serial_port");
const std::string mIO::RECONNECT_INTERVAL("reconnect_interval");

const std::string mIO::TAG("tag");
const std::string mIO::UPDATE_PERIOD("data_interval");

const std::string mIO::REWARDA("reward_a");
const std::string mIO::REWARDB("reward_b");
const std::string mIO::MANUALA("manual_a");
const std::string mIO::MANUALB("manual_b");

const std::string mIO::TOUCHXRAW("touch_x_raw");
const std::string mIO::TOUCHYRAW("touch_y_raw");
const std::string mIO::TOUCHZRAW("touch_z_raw");
const std::string mIO::TOUCHXCALIB("touch_x_calib"); // Not implemented and not needed with new firmware
const std::string mIO::TOUCHYCALIB("touch_y_calib"); // Not implemented and not needed with new firmware

const std::string mIO::BUTTONA("button_a");
const std::string mIO::BUTTONB("button_b");
const std::string mIO::BUTTONC("button_c");
const std::string mIO::BUTTOND("button_d");

const std::string mIO::LASERA("laser_a");
const std::string mIO::LASERB("laser_b");

const std::string mIO::SENDWORD("sendword");

const std::string mIO::JOYSTICKXRAW("joystick_x_raw");
const std::string mIO::JOYSTICKYRAW("joystick_y_raw");
const std::string mIO::JOYSTICKXCALIB("joystick_x_calib");
const std::string mIO::JOYSTICKYCALIB("joystick_y_calib");
const std::string mIO::JOYSTICKDIRECTION("joystick_direction");
const std::string mIO::JOYSTICKSTRENGTH("joystick_strength");

const std::string mIO::JOYSTICKBXRAW("joystick_b_x_raw");
const std::string mIO::JOYSTICKBYRAW("joystick_b_y_raw");
const std::string mIO::JOYSTICKBXCALIB("joystick_b_x_calib");
const std::string mIO::JOYSTICKBYCALIB("joystick_b_y_calib");
const std::string mIO::JOYSTICKBDIRECTION("joystick_b_direction");
const std::string mIO::JOYSTICKBSTRENGTH("joystick_b_strength");

const std::string mIO::JOYSTICKSAMPLINGRATE("joystick_samplingrate");

const std::string mIO::TTLINA("ttl_in_a");
const std::string mIO::TTLINB("ttl_in_b");
const std::string mIO::TTLOUT("ttl_out");

const std::string mIO::PHOTODIODEA("photodiode_a");
const std::string mIO::PHOTODIODEB("photodiode_b");

// Time the mIO need for one operationloop
const std::string mIO::LOOPTIME("looptime");

const std::string mIO::COLORBUTTONA("button_a_color");
const std::string mIO::COLORBUTTONB("button_b_color");
const std::string mIO::COLORJOYSTICK("joystick_color");

void mIO::describeComponent(ComponentInfo &info)
{
    IODevice::describeComponent(info);

    info.setSignature("iodevice/mio");
    info.setDisplayName("mIO Plugin");
    info.setDescription("mIO Plugin for all IO you can imagine...");

    info.addParameter(SERIAL_PORT, false); // new

    info.addParameter(RECONNECT_INTERVAL, "0"); // new

    info.addParameter(UPDATE_PERIOD, true, "1ms");

    info.addParameter(REWARDA, "0");
    info.addParameter(REWARDB, "0");
    info.addParameter(MANUALA, "0");
    info.addParameter(MANUALB, "0");
    info.addParameter(TOUCHXRAW, "0");
    info.addParameter(TOUCHYRAW, "0");
    info.addParameter(TOUCHZRAW, "0");
    info.addParameter(TOUCHXCALIB, "0");
    info.addParameter(TOUCHYCALIB, "0");
    info.addParameter(BUTTONA, "0");
    info.addParameter(BUTTONB, "0");
    info.addParameter(BUTTONC, "0");
    info.addParameter(BUTTOND, "0");
    info.addParameter(LASERA, "0");
    info.addParameter(LASERB, "0");
    info.addParameter(SENDWORD, "0");
    info.addParameter(JOYSTICKXRAW, "0");
    info.addParameter(JOYSTICKYRAW, "0");
    info.addParameter(JOYSTICKXCALIB, "0");
    info.addParameter(JOYSTICKYCALIB, "0");
    info.addParameter(JOYSTICKDIRECTION, "0");
    info.addParameter(JOYSTICKSTRENGTH, "0");
    info.addParameter(JOYSTICKBXRAW, "0");
    info.addParameter(JOYSTICKBYRAW, "0");
    info.addParameter(JOYSTICKBXCALIB, "0");
    info.addParameter(JOYSTICKBYCALIB, "0");
    info.addParameter(JOYSTICKBDIRECTION, "0");
    info.addParameter(JOYSTICKBSTRENGTH, "0");
    info.addParameter(JOYSTICKSAMPLINGRATE, "10");
    info.addParameter(TTLINA, "0");
    info.addParameter(TTLINB, "0");
    info.addParameter(TTLOUT, "0");
    info.addParameter(PHOTODIODEA, "0");
    info.addParameter(PHOTODIODEB, "0");
    info.addParameter(LOOPTIME, "0");
    info.addParameter(COLORBUTTONA, "0.0,0.0,0.0");
    info.addParameter(COLORBUTTONB, "0.0,0.0,0.0");
    info.addParameter(COLORJOYSTICK, "0.0,0.0,0.0");
}

mIO::mIO(const ParameterValueMap &parameters)
    : IODevice(parameters),
      serialPortPath(optionalVariableOrText(parameters[SERIAL_PORT])),
      reconnectIntervalUS(parameters[RECONNECT_INTERVAL]),
      clock(Clock::instance()),
      running(false),
      connected(false),
      pump_1_timed_run(false),
      pump_2_timed_run(false),
      update_period(parameters[UPDATE_PERIOD]),
      rewardA(parameters[REWARDA]),
      rewardB(parameters[REWARDB]),
      manualA(parameters[MANUALA]),
      manualB(parameters[MANUALB]),
      touchXraw(parameters[TOUCHXRAW]),
      touchYraw(parameters[TOUCHYRAW]),
      touchZraw(parameters[TOUCHZRAW]),
      touchXcalib(parameters[TOUCHXCALIB]),
      touchYcalib(parameters[TOUCHYCALIB]),
      buttonA(parameters[BUTTONA]),
      buttonB(parameters[BUTTONB]),
      buttonC(parameters[BUTTONC]),
      buttonD(parameters[BUTTOND]),
      laserA(parameters[LASERA]),
      laserB(parameters[LASERB]),
      sendWord(parameters[SENDWORD]),
      joystickXraw(parameters[JOYSTICKXRAW]),
      joystickYraw(parameters[JOYSTICKYRAW]),
      joystickXcalib(parameters[JOYSTICKXCALIB]),
      joystickYcalib(parameters[JOYSTICKYCALIB]),
      joystickDirection(parameters[JOYSTICKDIRECTION]),
      joystickStrength(parameters[JOYSTICKSTRENGTH]),
      joystickBXraw(parameters[JOYSTICKBXRAW]),
      joystickBYraw(parameters[JOYSTICKBYRAW]),
      joystickBXcalib(parameters[JOYSTICKBXCALIB]),
      joystickBYcalib(parameters[JOYSTICKBYCALIB]),
      joystickBDirection(parameters[JOYSTICKBDIRECTION]),
      joystickBStrength(parameters[JOYSTICKBSTRENGTH]),
      joystickSampleRate(parameters[JOYSTICKSAMPLINGRATE]),
      ttlA(parameters[TTLINA]),
      ttlB(parameters[TTLINB]),
      ttlOut(parameters[TTLOUT]),
      photoDiodeA(parameters[PHOTODIODEA]),
      photoDiodeB(parameters[PHOTODIODEB]),
      looptime(parameters[LOOPTIME]),
      touchScreenMapper(point_type(-15.1858 + 2, 11.3894 - 2),  // displayTopLeft
                        point_type(15.1858 - 2, 11.3894 - 2),   // displayTopRight
                        point_type(-15.1858 + 2, -11.3894 + 2), // displayBottomLeft
                        point_type(15.1858 - 2, -11.3894 + 2),  // displayBottomRight
                        point_type(252, 3760),                  // touchscreenTopLeft
                        point_type(3875, 3725),                 // touchscreenTopRight
                        point_type(245, 330),                   // touchscreenBottomLeft
                        point_type(3855, 340)),                 // touchscreenBottomRight
      dcPumpCalculator(1., 1., 1.),                             // Seconds to reach max speed, Seconds to reach min speed, max speed ml per second
      stepperPumpCalculator(1.),                                // ml per full rotation
      joystick1Mapper({
          {5, 505, 1005, 50}, // X: min, center, max raw values, deadzone
          {5, 505, 1005, 50}  // Y: min, center, max raw values, deadzone
      }),
      joystick2Mapper({
          {5, 505, 1005, 50}, // X: min, center, max raw values, deadzone
          {5, 505, 1005, 50}  // Y: min, center, max raw values, deadzone
      })
{
    ParsedColorTrio colorButtonA(parameters[COLORBUTTONA]);
    buttonA_colR = colorButtonA.getR();
    buttonA_colG = colorButtonA.getG();
    buttonA_colB = colorButtonA.getB();

    ParsedColorTrio colorButtonB(parameters[COLORBUTTONB]);
    buttonB_colR = colorButtonB.getR();
    buttonB_colG = colorButtonB.getG();
    buttonB_colB = colorButtonB.getB();

    ParsedColorTrio colorJoystick(parameters[COLORJOYSTICK]);
    joystick_colR = colorJoystick.getR();
    joystick_colG = colorJoystick.getG();
    joystick_colB = colorJoystick.getB();
}

mIO::~mIO()
{
    if (receiveDataThread.joinable())
    {
        continueReceivingData.clear();
        receiveDataThread.join();
    }
    if (mIO_Initialized)
    {
        if (schedule_node)
        {
            schedule_node->cancel();
            schedule_node.reset();
        }
    }
    delete proto_handler;
}

bool mIO::initialize()
{
    if (serialPortPath)
    {
        path = serialPortPath->getValue().getString();
    }
    if (!serialPort.connect(path, baudRate))
    {
        return false;
    }
    connected = true;
    proto_handler = new MioProtoV1(serialPort);

    continueReceivingData.test_and_set();
    receiveDataThread = std::thread([this]()
                                    { receiveData(); });

    scheduler = Scheduler::instance();

    schedule_node = scheduler->scheduleUS(
        FILELINE, update_period, update_period, M_REPEAT_INDEFINITELY,
        [this]()
        {
            update();
            return nullptr;
        },
        M_DEFAULT_IODEVICE_PRIORITY, M_DEFAULT_IODEVICE_WARN_SLOP_US, M_DEFAULT_FAIL_SLOP_US,
        M_MISSED_EXECUTION_DROP);

    mprintf(M_IODEVICE_MESSAGE_DOMAIN, "mIO: loaded and started device %s", device_path.c_str());
    mprintf(M_IODEVICE_MESSAGE_DOMAIN, "mIO: update_period %lld", update_period);

    // TODO How can we permanently store variables?

    double display_left = -15.1858;
    double display_right = 15.1858;
    double display_top = 11.3894;
    double display_bottom = -11.3894;

    // TODO Set calibration values
    touchScreenMapper = TouchScreenMapper(point_type(display_left * 0.75, display_top * 0.75),     // displayTopLeft
                                          point_type(display_right * 0.75, display_top * 0.75),    // displayTopRight
                                          point_type(display_left * 0.75, display_bottom * 0.75),  // displayBottomLeft
                                          point_type(display_right * 0.75, display_bottom * 0.75), // displayBottomRight
                                          point_type(475, 3629),                                   // touchscreenTopLeft
                                          point_type(3591, 3639),                                  // touchscreenTopRight
                                          point_type(488, 526),                                    // touchscreenBottomLeft
                                          point_type(3559, 544));                                  // touchscreenBottomRight
    dcPumpCalculator = DCPumpCalculator(0., 0., 1.);                                               // Seconds to reach max speed, Seconds to reach min speed, max speed ml per second
    stepperPumpCalculator = StepperPumpCalculator(0.09175);                                        // ml per full rotation (6 half pulses)
    joystick1Mapper = JoystickMapper({
        {5, 505, 1005, 65}, // X: min, center, max raw values, deadzone
        {5, 505, 1005, 65}  // Y: min, center, max raw values, deadzone
    });
    joystick2Mapper = JoystickMapper({
        {5, 505, 1005, 65}, // X: min, center, max raw values, deadzone
        {5, 505, 1005, 65}  // Y: min, center, max raw values, deadzone
    });

    mIO_Initialized = true;
    return mIO_Initialized;
}

bool mIO::startDeviceIO()
{
    running = true;
    return true;
}

bool mIO::stopDeviceIO()
{
    running = false;
    return true;
}

void mIO::update()
{
    MWTime currentTime = clock->getCurrentTimeUS();
    (void)currentTime;
    if (!running)
    {
        return;
    }
    if (!connected)
    {
        return;
    }

    // Send
    std::uint16_t pump_1_reward_half_pulses = 0;
    if (rewardA->getValue().getFloat())
    {
        pump_1_timed_run.set(true, dcPumpCalculator.timeToRun_s(rewardA->getValue().getFloat()));
        pump_1_reward_half_pulses = stepperPumpCalculator.rewardToHalfPulses(rewardA->getValue().getFloat());
        merror(M_IODEVICE_MESSAGE_DOMAIN, "pump_1_timed_run %d", pump_1_timed_run);
        merror(M_IODEVICE_MESSAGE_DOMAIN, "dcPumpCalculator.timeToRun_s %f", dcPumpCalculator.timeToRun_s(rewardA->getValue().getFloat()));

        rewardA->setValue(0.0);
    }

    std::uint16_t pump_2_reward_half_pulses = 0;
    if (rewardB->getValue().getFloat())
    {
        pump_2_timed_run.set(true, dcPumpCalculator.timeToRun_s(rewardB->getValue().getFloat()));
        pump_2_reward_half_pulses = stepperPumpCalculator.rewardToHalfPulses(rewardB->getValue().getFloat());
        merror(M_IODEVICE_MESSAGE_DOMAIN, "pump_2_timed_run %d", pump_2_timed_run);
        merror(M_IODEVICE_MESSAGE_DOMAIN, "dcPumpCalculator.timeToRun_s %f", dcPumpCalculator.timeToRun_s(rewardB->getValue().getFloat()));

        rewardB->setValue(0.0);
    }

    std::uint8_t digital_outs = 0;
    proto_handler->writeFlag(digital_outs, MIO_PROTO_V1_DIGITAL_OUTS_REWARD_A_BIT_ID,
                             pump_1_timed_run.get());
    proto_handler->writeFlag(digital_outs, MIO_PROTO_V1_DIGITAL_OUTS_REWARD_B_BIT_ID,
                             pump_2_timed_run.get());
    proto_handler->writeFlag(digital_outs, MIO_PROTO_V1_DIGITAL_OUTS_REWARD_TTL_BIT_ID,
                             ttlOut->getValue().getBool());

    if (laserA->getValue().getInteger() > (uint16_t)-1)
    {
        merror(M_IODEVICE_MESSAGE_DOMAIN, "mIO: conversion of laserA value to uint16 overflow. Result unknown!");
    }
    std::uint16_t analog_out_1 = (uint16_t)(laserA->getValue().getInteger());
    if (laserB->getValue().getInteger() > (uint16_t)-1)
    {
        merror(M_IODEVICE_MESSAGE_DOMAIN, "mIO: conversion of laserB value to uint16 overflow. Result unknown!");
    }
    std::uint16_t analog_out_2 = (uint16_t)(laserB->getValue().getInteger());
    if (sendWord->getValue().getInteger() > (uint16_t)-1)
    {
        merror(M_IODEVICE_MESSAGE_DOMAIN, "mIO: conversion of sendWord value to uint16 overflow. Result unknown!");
    }
    std::uint16_t idc = (uint16_t)(sendWord->getValue().getInteger());
    mio_proto_v1_rgb rgb_button_A;
    rgb_button_A.red = (uint8_t)(buttonA_colR->getValue().getFloat() * 255.0);
    rgb_button_A.blue = (uint8_t)(buttonA_colB->getValue().getFloat() * 255.0);
    rgb_button_A.green = (uint8_t)(buttonA_colG->getValue().getFloat() * 255.0);
    mio_proto_v1_rgb rgb_button_B;
    rgb_button_B.red = (uint8_t)(buttonB_colR->getValue().getFloat() * 255.0);
    rgb_button_B.blue = (uint8_t)(buttonB_colB->getValue().getFloat() * 255.0);
    rgb_button_B.green = (uint8_t)(buttonB_colG->getValue().getFloat() * 255.0);
    mio_proto_v1_rgb rgb_joystick;
    rgb_joystick.red = (uint8_t)(joystick_colR->getValue().getFloat() * 255.0);
    rgb_joystick.blue = (uint8_t)(joystick_colB->getValue().getFloat() * 255.0);
    rgb_joystick.green = (uint8_t)(joystick_colG->getValue().getFloat() * 255.0);

    proto_handler->send(digital_outs, analog_out_1, analog_out_2, idc, rgb_button_A, rgb_button_B, rgb_joystick, pump_1_reward_half_pulses, pump_2_reward_half_pulses);
}

void mIO::receiveData()
{
    while (continueReceivingData.test_and_set())
    {
        auto result = proto_handler->handleIncomingSerialCommunication();
        if (result == MIO_PROTO_HANDLE_ERROR_SERIAL)
        {
            connected = false;

            serialPort.disconnect();
            if (!reconnect())
            {
                return;
            }
        }
        // Recv message
        MWTime currentTime = clock->getCurrentTimeUS();

        if (!proto_handler->recvAvailable())
        {
            continue;
        }

        mio_proto_v1_data_protocol_from_mio buffer;
        proto_handler->recv(&buffer);

        // ==== DIGITAL_INS ====
        if (manualA->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_1_BIT_ID))
            manualA->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_1_BIT_ID),
                currentTime);
        if (manualB->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_2_BIT_ID))
            manualB->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_2_BIT_ID),
                currentTime);
        if (ttlA->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_3_BIT_ID))
            ttlA->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_3_BIT_ID),
                currentTime);
        if (ttlB->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_4_BIT_ID))
            ttlB->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_D_IN_4_BIT_ID),
                currentTime);
        if (buttonA->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_1_BIT_ID))
            buttonA->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_1_BIT_ID),
                currentTime);
        if (buttonB->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_2_BIT_ID))
            buttonB->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_2_BIT_ID),
                currentTime);
        if (buttonC->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_3_BIT_ID))
            buttonC->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_3_BIT_ID),
                currentTime);
        if (buttonD->getValue().getBool() != proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_4_BIT_ID))
            buttonD->setValue(
                proto_handler->readFlag(buffer.digital_ins, MIO_PROTO_V1_DIGITAL_INS_IO_TB_IN_4_BIT_ID),
                currentTime);

        // ==== TOUCHSCREEN ====
        point_type touch_calibrated = touchScreenMapper.touchscreenToDisplay(
            point_type(static_cast<double>(buffer.touchscreen_x), static_cast<double>(buffer.touchscreen_y)));
        if (touchXraw->getValue().getInteger() != buffer.touchscreen_x)
        {
            touchXraw->setValue(buffer.touchscreen_x, currentTime);
            touchXcalib->setValue(touch_calibrated.x(), currentTime);
        }
        if (touchYraw->getValue().getInteger() != buffer.touchscreen_y)
        {
            touchYraw->setValue(buffer.touchscreen_y, currentTime);
            touchYcalib->setValue(touch_calibrated.y(), currentTime);
        }
        if (touchZraw->getValue().getInteger() != buffer.touchscreen_z)
        {
            touchZraw->setValue(buffer.touchscreen_z, currentTime);
        }

        // ==== JOYSTICK A ====
        bool joystick_a_update_dir = false;
        JoystickMapper::Axis joystick_a_axis_raw = {static_cast<double>(buffer.joystick_a_x), static_cast<double>(buffer.joystick_a_y)};
        JoystickMapper::Axis joystick_a_axis = joystick1Mapper.mapAxis(joystick_a_axis_raw);
        if (joystickXraw->getValue().getFloat() != joystick_a_axis_raw.x)
        {
            joystickXraw->setValue(joystick_a_axis_raw.x, currentTime);
            joystickXcalib->setValue(joystick_a_axis.x, currentTime);
            joystick_a_update_dir = true;
        }
        if (joystickYraw->getValue().getFloat() != joystick_a_axis_raw.y)
        {
            joystickYraw->setValue(joystick_a_axis_raw.y, currentTime);
            joystickYcalib->setValue(joystick_a_axis.y, currentTime);
            joystick_a_update_dir = true;
        }
        if (joystick_a_update_dir)
        {
            // TODO test JoystickMapper angle function
            float joystick_a_dir = joystick1Mapper.angleAxis(joystick_a_axis); // Converting to degrees and shifting range to 0-360

            joystickDirection->setValue(static_cast<int>(joystick_a_dir), currentTime);
        }

        // ==== JOYSTICK B ====
        bool joystick_b_update_dir = false;
        JoystickMapper::Axis joystick_b_axis_raw = {static_cast<double>(buffer.joystick_b_x), static_cast<double>(buffer.joystick_b_y)};
        JoystickMapper::Axis joystick_b_axis = joystick1Mapper.mapAxis(joystick_b_axis_raw);
        if (joystickBXraw->getValue().getInteger() != joystick_b_axis_raw.x)
        {
            joystickBXraw->setValue(joystick_b_axis_raw.x, currentTime);
            joystickBXcalib->setValue(joystick_b_axis.x, currentTime);
            joystickStrength->setValue(joystick1Mapper.distanceAxis({0, 0}, joystick_a_axis), currentTime);
            joystick_b_update_dir = true;
        }
        if (joystickBYraw->getValue().getInteger() != joystick_b_axis_raw.y)
        {
            joystickBYraw->setValue(joystick_b_axis_raw.y, currentTime);
            joystickBYcalib->setValue(joystick_b_axis.y, currentTime);
            joystickBStrength->setValue(joystick2Mapper.distanceAxis({0, 0}, joystick_b_axis), currentTime);
            joystick_b_update_dir = true;
        }
        if (joystick_b_update_dir)
        {
            // TODO test JoystickMapper angle function
            float joystick_b_dir = joystick2Mapper.angleAxis(joystick_b_axis); // Converting to degrees and shifting range to 0-360
            joystickBDirection->setValue(static_cast<int>(joystick_b_dir), currentTime);
        }

        if (photoDiodeA->getValue().getInteger() != buffer.analog_in_1)
        {
            photoDiodeA->setValue(buffer.analog_in_1, currentTime);
        }
        if (photoDiodeB->getValue().getInteger() != buffer.analog_in_2)
        {
            photoDiodeB->setValue(buffer.analog_in_2, currentTime);
        }

        // ==== LOOPTIME ====
        looptime->setValue(static_cast<long>(buffer.mio_time), currentTime);
    }
}

bool mIO::reconnect()
{
    if (reconnectIntervalUS <= 0)
    {
        // User doesn't want us to attempt reconnection.  Just abort.
        merror(M_IODEVICE_MESSAGE_DOMAIN, "Disconnected from mIO");
        return false;
    }

    merror(M_IODEVICE_MESSAGE_DOMAIN, "Disconnected from mIO; will try to reconnect in %g seconds",
           double(reconnectIntervalUS) / 1e6);

    while (true)
    {
        const auto waitStopTime = clock->getCurrentTimeUS() + reconnectIntervalUS;

        // Perform an "active" wait, so that we can exit promptly if the experiment is unloaded
        while (clock->getCurrentTimeUS() < waitStopTime)
        {
            if (!continueReceivingData.test_and_set())
            {
                return false;
            }
            clock->yield();
        }

        mprintf(M_IODEVICE_MESSAGE_DOMAIN, "Attempting to reconnect to mIO");

        if (serialPort.connect(path, baudRate))
        {
            mprintf(M_IODEVICE_MESSAGE_DOMAIN, "Successfully reconnected to mIO");
            connected = true;
            break;
        }

        merror(M_IODEVICE_MESSAGE_DOMAIN, "Failed to reconnect to mIO; will try again in %g seconds",
               double(reconnectIntervalUS) / 1e6);
    }

    return true;
}

END_NAMESPACE_MW

In line 255: // TODO How can we permanently store variables?

I would like a way to save the device-dependent calibration values for:

  • touchScreenMapper
  • dcPumpCalculator
  • stepperPumpCalculator
  • joystick1Mapper
  • joystick2Mapper

somewhere, since they depend on the hardware and not the experiment, I think it would be best if they were experiment and protocol independent. I would like to be able to automatically/manually define a setting or calibration value file that I can read and write to for this purpose. They are unlikely to change during the lifetime of the hardware, or only if the touchscreen, pump, joystick or other components are replaced.

Hi Louis,

OK, it sounds like you want a custom configuration file for your device.

JSON is probably a good choice for file format. You could use Boost.JSON, which is distributed (in header-only format) with MWorks, for encoding/decoding.

As for where to store the file, the directory $HOME/Library/Application Support/MWorks/Configuration would be fine. This is where the user-specific setup_variables.xml lives, and MWorks’ installer never messes with it. Your plugin can use the MWorksCore function prependUserPath to get the path, e.g.

auto configFilePath = prependUserPath("mio_config.json");

How does that sound?

Chris

Hey Chris,

yeah! That sounds pretty much exactly what I’m looking for. Thanks for the clarification! I was just worried that I might interfere with mworks if I went with a solution like this, so the prependUserPath function might be exactly the missing piece I was looking for.

Do I also need to add the following line
#include "MWorksCore/PlatformDependentServices.h"
to the files that use the function? It does not seem to be otherwise included for IODevices.

Thank you very much!
Louis

Yes, I think you do need to include the header:

#include <MWorksCore/PlatformDependentServices.h>

Cheers,
Chris

Hey Chris,

thanks for everything!

This is what I now settled for:

ConfigManager.hpp:

//
//  ConfigManager.hpp
//  mIO
//
//  Created by Louis Frank on 11.03.24.
//

#ifndef mIOConfigManager_h
#define mIOConfigManager_h

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/filesystem/convenience.hpp>
#include <memory>

// Short alias for this namespace
namespace pt = boost::property_tree;

template <typename T>
class property
{
private:
    std::shared_ptr<pt::ptree> tree;
    std::string property_path;
    T default_value;

public:
    property(std::shared_ptr<pt::ptree> tree, std::string property_path, T default_value) : tree(tree), property_path(property_path), default_value(default_value)
    {
    }

    operator T() const
    {
        if (!tree->get_optional<T>(property_path).is_initialized())
        {
            tree->put(property_path, default_value);
        }
        return tree->get<T>(property_path);
    };

    property<T> &operator=(const T &value)
    {
        tree->put(property_path, value);
        return *this;
    }
};

template <typename T>
class propertyCoordinate
{
private:
    std::shared_ptr<pt::ptree> tree;

public:
    propertyCoordinate(std::shared_ptr<pt::ptree> tree, property<T> x, property<T> y) : tree(tree), x(x), y(y){};

    property<T> x;
    property<T> y;
};

template <typename T>
class propertyAxisCalibration
{
private:
    std::shared_ptr<pt::ptree> tree;

public:
    propertyAxisCalibration(std::shared_ptr<pt::ptree> tree, property<T> min, property<T> center, property<T> max, property<T> deadzone)
        : tree(tree), min(min), center(center), max(max), deadzone(deadzone){};

    property<T> min;
    property<T> center;
    property<T> max;
    property<T> deadzone;
};

class mIOConfigManager
{
private:
    boost::filesystem::path filepath;
    std::shared_ptr<pt::ptree> tree;

public:
    explicit mIOConfigManager(boost::filesystem::path filepath) : filepath(filepath), tree(std::make_shared<pt::ptree>())
    {
        if (!boost::filesystem::exists(filepath))
        {
            // Create the file with empty content
            std::ofstream file(filepath.string());
            file << "{}";
            file.close();
        }

        // Load the json file into this ptree
        pt::read_json(filepath.string(), *tree);
    }

    void saveConfig()
    {
        pt::write_json(filepath.string(), *tree);
    }

    // TouchScreenMapper
    double display_left = -15.1858;
    double display_right = 15.1858;
    double display_top = 11.3894;
    double display_bottom = -11.3894;
    propertyCoordinate<double> displayTopLeft = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.displayTopLeft.x", display_left * 0.75),
        property<double>(tree, "touchScreenMapper.displayTopLeft.y", display_top * 0.75));
    propertyCoordinate<double> displayTopRight = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.displayTopRight.x", display_right * 0.75),
        property<double>(tree, "touchScreenMapper.displayTopRight.y", display_top * 0.75));
    propertyCoordinate<double> displayBottomLeft = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.displayBottomLeft.x", display_left * 0.75),
        property<double>(tree, "touchScreenMapper.displayBottomLeft.y", display_bottom * 0.75));
    propertyCoordinate<double> displayBottomRight = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.displayBottomRight.x", display_right * 0.75),
        property<double>(tree, "touchScreenMapper.displayBottomRight.y", display_bottom * 0.75));
    propertyCoordinate<double> touchscreenTopLeft = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.touchscreenTopLeft.x", 475),
        property<double>(tree, "touchScreenMapper.touchscreenTopLeft.y", 3629));
    propertyCoordinate<double> touchscreenTopRight = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.touchscreenTopRight.x", 3591),
        property<double>(tree, "touchScreenMapper.touchscreenTopRight.y", 3639));
    propertyCoordinate<double> touchscreenBottomLeft = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.touchscreenBottomLeft.x", 488),
        property<double>(tree, "touchScreenMapper.touchscreenBottomLeft.y", 526));
    propertyCoordinate<double> touchscreenBottomRight = propertyCoordinate(
        tree,
        property<double>(tree, "touchScreenMapper.touchscreenBottomRight.x", 3559),
        property<double>(tree, "touchScreenMapper.touchscreenBottomRight.y", 544));

    // DCPumpCalculator
    property<double> dc_pump_time_to_reach_max_flow = property<double>(tree, "dcPumpCalculator.time_to_reach_max_flow", 0.0);
    property<double> dc_pump_time_to_reach_min_flow = property<double>(tree, "dcPumpCalculator.time_to_reach_min_flow", 0.0);
    property<double> dc_pump_max_flow = property<double>(tree, "dcPumpCalculator.max_flow", 1.0);

    // StepperPumpCalculator
    property<double> stepper_pump_half_pulse_quantity = property<double>(tree, "stepperPumpCalculator.half_pulse_quantity", 0.09175);

    // joystick1Mapper
    propertyAxisCalibration<double> joystick_1_x = propertyAxisCalibration(
        tree,
        property<double>(tree, "joystick1Mapper.x.min", 5),
        property<double>(tree, "joystick1Mapper.x.center", 505),
        property<double>(tree, "joystick1Mapper.x.max", 1005),
        property<double>(tree, "joystick1Mapper.x.deadzone", 65));
    propertyAxisCalibration<double> joystick_1_y = propertyAxisCalibration(
        tree,
        property<double>(tree, "joystick1Mapper.y.min", 5),
        property<double>(tree, "joystick1Mapper.y.center", 505),
        property<double>(tree, "joystick1Mapper.y.max", 1005),
        property<double>(tree, "joystick1Mapper.y.deadzone", 65));

    // joystick2Mapper
    propertyAxisCalibration<double> joystick_2_x = propertyAxisCalibration(
        tree,
        property<double>(tree, "joystick2Mapper.x.min", 5),
        property<double>(tree, "joystick2Mapper.x.center", 505),
        property<double>(tree, "joystick2Mapper.x.max", 1005),
        property<double>(tree, "joystick2Mapper.x.deadzone", 65));
    propertyAxisCalibration<double> joystick_2_y = propertyAxisCalibration(
        tree,
        property<double>(tree, "joystick2Mapper.y.min", 5),
        property<double>(tree, "joystick2Mapper.y.center", 505),
        property<double>(tree, "joystick2Mapper.y.max", 1005),
        property<double>(tree, "joystick2Mapper.y.deadzone", 65));
};

#endif /* mIOConfigManager_h */


And my initialize() method is then as follows:

bool mIO::initialize()
{
    if (serialPortPath)
    {
        path = serialPortPath->getValue().getString();
    }
    if (!serialPort.connect(path, baudRate))
    {
        return false;
    }
    connected = true;
    proto_handler = new MioProtoV1(serialPort);

    continueReceivingData.test_and_set();
    receiveDataThread = std::thread([this]()
                                    { receiveData(); });

    scheduler = Scheduler::instance();

    schedule_node = scheduler->scheduleUS(
        FILELINE, update_period, update_period, M_REPEAT_INDEFINITELY,
        [this]()
        {
            update();
            return nullptr;
        },
        M_DEFAULT_IODEVICE_PRIORITY, M_DEFAULT_IODEVICE_WARN_SLOP_US, M_DEFAULT_FAIL_SLOP_US,
        M_MISSED_EXECUTION_DROP);

    mprintf(M_IODEVICE_MESSAGE_DOMAIN, "mIO: loaded and started device %s", device_path.c_str());
    mprintf(M_IODEVICE_MESSAGE_DOMAIN, "mIO: update_period %lld", update_period);

    // TODO How do we save variables for example. pump calibration permanently?

    // fixed_mIO_address = mainDisplayInfo->getValue().getElement("fixed_mIO_address").getString();

    // https://mworks.discourse.group/t/plugin-development-how-to-implement-persistent-variables/952/2
    boost::filesystem::path configFilePath = prependUserPath("mio_config.json");
    mIOConfigManager config_manager = mIOConfigManager(configFilePath);

    touchScreenMapper = TouchScreenMapper(point_type(config_manager.displayTopLeft.x, config_manager.displayTopLeft.y),                  // displayTopLeft
                                          point_type(config_manager.displayTopRight.x, config_manager.displayTopRight.y),                // displayTopRight
                                          point_type(config_manager.displayBottomLeft.x, config_manager.displayBottomLeft.y),            // displayBottomLeft
                                          point_type(config_manager.displayBottomRight.x, config_manager.displayBottomRight.y),          // displayBottomRight
                                          point_type(config_manager.touchscreenTopLeft.x, config_manager.touchscreenTopLeft.y),          // touchscreenTopLeft
                                          point_type(config_manager.touchscreenTopRight.x, config_manager.touchscreenTopRight.y),        // touchscreenTopRight
                                          point_type(config_manager.touchscreenBottomLeft.x, config_manager.touchscreenBottomLeft.y),    // touchscreenBottomLeft
                                          point_type(config_manager.touchscreenBottomRight.x, config_manager.touchscreenBottomRight.y)); // touchscreenBottomRight

    dcPumpCalculator = DCPumpCalculator(
        config_manager.dc_pump_time_to_reach_max_flow, // Seconds to reach max speed
        config_manager.dc_pump_time_to_reach_min_flow, // Seconds to reach min speed from max speed
        config_manager.dc_pump_max_flow                //  max speed ml per second
    );

    stepperPumpCalculator = StepperPumpCalculator(
        config_manager.stepper_pump_half_pulse_quantity // ml per half puls (1/6 full rotation)
    );

    joystick1Mapper = JoystickMapper({{// X: min, center, max raw values, deadzone
                                       config_manager.joystick_1_x.min,
                                       config_manager.joystick_1_x.center,
                                       config_manager.joystick_1_x.max,
                                       config_manager.joystick_1_x.deadzone},
                                      {// Y: min, center, max raw values, deadzone
                                       config_manager.joystick_1_y.min,
                                       config_manager.joystick_1_y.center,
                                       config_manager.joystick_1_y.max,
                                       config_manager.joystick_1_y.deadzone}});

    joystick2Mapper = JoystickMapper({{// X: min, center, max raw values, deadzone
                                       config_manager.joystick_2_x.min,
                                       config_manager.joystick_2_x.center,
                                       config_manager.joystick_2_x.max,
                                       config_manager.joystick_2_x.deadzone},
                                      {// Y: min, center, max raw values, deadzone
                                       config_manager.joystick_2_y.min,
                                       config_manager.joystick_2_y.center,
                                       config_manager.joystick_2_y.max,
                                       config_manager.joystick_2_y.deadzone}});

    config_manager.saveConfig();

    mIO_Initialized = true;
    return mIO_Initialized;
}

It’s creating and reading the following json file:

{
    "touchScreenMapper": {
        "displayTopLeft": {
            "x": "-11.38935",
            "y": "8.5420499999999997"
        },
        "displayTopRight": {
            "x": "11.38935",
            "y": "8.5420499999999997"
        },
        "displayBottomLeft": {
            "x": "-11.38935",
            "y": "-8.5420499999999997"
        },
        "displayBottomRight": {
            "x": "11.38935",
            "y": "-8.5420499999999997"
        },
        "touchscreenTopLeft": {
            "x": "475",
            "y": "3629"
        },
        "touchscreenTopRight": {
            "x": "3591",
            "y": "3639"
        },
        "touchscreenBottomLeft": {
            "x": "488",
            "y": "526"
        },
        "touchscreenBottomRight": {
            "x": "3559",
            "y": "544"
        }
    },
    "dcPumpCalculator": {
        "time_to_reach_max_flow": "0",
        "time_to_reach_min_flow": "0",
        "max_flow": "1"
    },
    "stepperPumpCalculator": {
        "half_pulse_quantity": "0.091749999999999998"
    },
    "joystick1Mapper": {
        "x": {
            "min": "5",
            "center": "505",
            "max": "1005",
            "deadzone": "65"
        },
        "y": {
            "min": "5",
            "center": "505",
            "max": "1005",
            "deadzone": "65"
        }
    },
    "joystick2Mapper": {
        "x": {
            "min": "5",
            "center": "505",
            "max": "1005",
            "deadzone": "65"
        },
        "y": {
            "min": "5",
            "center": "505",
            "max": "1005",
            "deadzone": "65"
        }
    }
}

One now can just transparently write and read values from the config manager:

 config_manager.joystick_2_y.min = 5;
 double min = config_manager.joystick_2_y.min;

and the config manager updates the file contents when you call:

    config_manager.saveConfig();

Note that there are some limitations as I have tried to mimic the CSharpish properties functionality (instead of getter and setter methods), but have not fully implemented it. Properties can only be assigned with “=” at the moment, operators like +=, ++, … are not implemented.


I’m sharing this as I think this might be helpful to others as well :slight_smile:

Cheers,
Louis

Looks good! Thanks for sharing.

Chris