// PID controller add-on for ESP IDE

/*
{
  "id": "pid_controller",
  "name": "PID Controller",
  "description": "A simple smart PID controller with automatic timing, output limits, P/I/D term limits, and support for multiple instances.",
  "version": "1.0.0",
  "author": "Milan Spacek + Codex",
  "tags": ["PID", "regulator", "control"],
  "screenshots": ["pid_controller.png"],
  "changelog": "v1.0.0 - First version"
}
*/

// MicroPython library embedded as a JS string
// This code is sent to the board as /lib/pid_controller.py when the button is pressed
var pid_controller_lib = `
# ┌────────────────────────────────────────────┐
# │ ESP IDE  : FREE MicroPython WEB IDE        │
# │ AUTHOR   : Milan Spacek (2019-2026)        │
# │ WEB      : https://espide.eu               │
# │ LICENSE  : AGPL-3.0                        │
# │                                            │
# │ CODE IS OPEN - IMPROVEMENTS MUST STAY OPEN │
# │ Please contribute your improvements back   │
# └────────────────────────────────────────────┘
#
# Fast and safe PID controller for MicroPython.
# - automatic dt from system ticks
# - output limits + independent P/I/D term limits
# - anti-windup (integral clamped by I limits and output headroom)
# - optional enable/disable P, I, D terms

try:
    import utime as _time
except Exception:
    import time as _time


class PID:
    def __init__(
        self,
        kp=1.0,
        ki=0.0,
        kd=0.0,
        out_min=-100.0,
        out_max=100.0,
        sample_time_ms=0,
        d_filter_alpha=0.20,
    ):
        self.kp = float(kp)
        self.ki = float(ki)
        self.kd = float(kd)

        self.p_enabled = True
        self.i_enabled = True
        self.d_enabled = True

        self._p_term = 0.0
        self._i_term = 0.0
        self._d_term = 0.0
        self._d_state = 0.0
        self._output = 0.0

        self.p_min = None
        self.p_max = None
        self.i_min = None
        self.i_max = None
        self.d_min = None
        self.d_max = None

        self.out_min = None
        self.out_max = None
        self.set_output_limits(out_min, out_max)

        # Default term limits are safe and bounded.
        self.p_min = self.out_min
        self.p_max = self.out_max
        self.i_min = self.out_min
        self.i_max = self.out_max
        self.d_min = self.out_min
        self.d_max = self.out_max

        self.sample_time_us = 0
        self.set_sample_time_ms(sample_time_ms)

        self.d_filter_alpha = 0.20
        self.set_d_filter_alpha(d_filter_alpha)

        self._last_us = None
        self._last_measurement = 0.0
        self._last_error = 0.0

    # -----------------------
    # Configuration
    # -----------------------

    def set_tunings(self, kp=None, ki=None, kd=None):
        if kp is not None:
            self.kp = float(kp)
        if ki is not None:
            self.ki = float(ki)
        if kd is not None:
            self.kd = float(kd)

    def set_output_limits(self, out_min=None, out_max=None):
        if out_min is not None:
            out_min = float(out_min)
        if out_max is not None:
            out_max = float(out_max)
        if out_min is not None and out_max is not None and out_min > out_max:
            out_min, out_max = out_max, out_min
        self.out_min = out_min
        self.out_max = out_max
        self._output = self._clamp(self._output, self.out_min, self.out_max)
        self._i_term = self._clamp(self._i_term, self.i_min, self.i_max)

    def set_term_limits(
        self,
        p_min=None,
        p_max=None,
        i_min=None,
        i_max=None,
        d_min=None,
        d_max=None,
    ):
        self.p_min, self.p_max = self._ordered_limits(p_min, p_max)
        self.i_min, self.i_max = self._ordered_limits(i_min, i_max)
        self.d_min, self.d_max = self._ordered_limits(d_min, d_max)

        self._p_term = self._clamp(self._p_term, self.p_min, self.p_max)
        self._i_term = self._clamp(self._i_term, self.i_min, self.i_max)
        self._d_term = self._clamp(self._d_term, self.d_min, self.d_max)

    def enable_terms(self, p=None, i=None, d=None):
        if p is not None:
            self.p_enabled = bool(p)
        if i is not None:
            self.i_enabled = bool(i)
        if d is not None:
            self.d_enabled = bool(d)

    def set_sample_time_ms(self, sample_time_ms):
        sample_time_ms = int(sample_time_ms)
        if sample_time_ms < 0:
            sample_time_ms = 0
        self.sample_time_us = sample_time_ms * 1000

    def set_d_filter_alpha(self, alpha):
        alpha = float(alpha)
        if alpha < 0.0:
            alpha = 0.0
        if alpha > 1.0:
            alpha = 1.0
        self.d_filter_alpha = alpha

    def reset(self, output=0.0, integral=0.0):
        self._last_us = None
        self._last_measurement = 0.0
        self._last_error = 0.0
        self._p_term = 0.0
        self._d_term = 0.0
        self._d_state = 0.0
        self._i_term = self._clamp(float(integral), self.i_min, self.i_max)
        self._output = self._clamp(float(output), self.out_min, self.out_max)

    # -----------------------
    # Runtime
    # -----------------------

    def compute(self, setpoint, measurement):
        setpoint = float(setpoint)
        measurement = float(measurement)
        now = self._ticks_us()

        if self._last_us is None:
            self._last_us = now
            self._last_measurement = measurement
            self._last_error = setpoint - measurement
            self._update_terms(setpoint, measurement, 0.0, first=True)
            return self._output

        dt_us = self._ticks_diff(now, self._last_us)
        if dt_us <= 0:
            return self._output
        if self.sample_time_us > 0 and dt_us < self.sample_time_us:
            return self._output

        dt = dt_us * 0.000001
        self._last_us = now
        self._update_terms(setpoint, measurement, dt, first=False)
        return self._output

    def _update_terms(self, setpoint, measurement, dt, first=False):
        error = setpoint - measurement

        # P term
        if self.p_enabled and self.kp != 0.0:
            p = self.kp * error
        else:
            p = 0.0
        p = self._clamp(p, self.p_min, self.p_max)

        # D term (on measurement to avoid setpoint kick)
        if not first and self.d_enabled and self.kd != 0.0 and dt > 0.0:
            d_input = -(measurement - self._last_measurement) / dt
            alpha = self.d_filter_alpha
            if alpha > 0.0:
                self._d_state += alpha * (d_input - self._d_state)
                d_input = self._d_state
            d = self.kd * d_input
        else:
            d = 0.0
        d = self._clamp(d, self.d_min, self.d_max)

        # I term with anti-windup
        i = self._i_term
        if not first and self.i_enabled and self.ki != 0.0 and dt > 0.0:
            i += self.ki * error * dt
            i = self._clamp(i, self.i_min, self.i_max)

            # Also keep integral within output headroom.
            sum_pd = p + d
            dyn_i_min = None
            dyn_i_max = None
            if self.out_min is not None:
                dyn_i_min = self.out_min - sum_pd
            if self.out_max is not None:
                dyn_i_max = self.out_max - sum_pd

            if self.i_min is not None:
                if dyn_i_min is None or self.i_min > dyn_i_min:
                    dyn_i_min = self.i_min
            if self.i_max is not None:
                if dyn_i_max is None or self.i_max < dyn_i_max:
                    dyn_i_max = self.i_max

            i = self._clamp(i, dyn_i_min, dyn_i_max)
        elif not self.i_enabled:
            i = self._clamp(i, self.i_min, self.i_max)

        out = p + i + d
        out = self._clamp(out, self.out_min, self.out_max)

        self._p_term = p
        self._i_term = i
        self._d_term = d
        self._output = out
        self._last_error = error
        self._last_measurement = measurement

    # -----------------------
    # Introspection
    # -----------------------

    def last_output(self):
        return self._output

    def last_p(self):
        return self._p_term

    def last_i(self):
        return self._i_term

    def last_d(self):
        return self._d_term

    def last_error(self):
        return self._last_error

    def debug(self):
        return {
            "out": self._output,
            "p": self._p_term,
            "i": self._i_term,
            "d": self._d_term,
            "err": self._last_error,
            "p_en": self.p_enabled,
            "i_en": self.i_enabled,
            "d_en": self.d_enabled,
        }

    # -----------------------
    # Helpers
    # -----------------------

    @staticmethod
    def _ordered_limits(lo, hi):
        if lo is not None:
            lo = float(lo)
        if hi is not None:
            hi = float(hi)
        if lo is not None and hi is not None and lo > hi:
            lo, hi = hi, lo
        return lo, hi

    @staticmethod
    def _clamp(value, lo, hi):
        if lo is not None and value < lo:
            return lo
        if hi is not None and value > hi:
            return hi
        return value

    @staticmethod
    def _ticks_us():
        if hasattr(_time, "ticks_us"):
            return _time.ticks_us()
        if hasattr(_time, "ticks_ms"):
            return _time.ticks_ms() * 1000
        return int(_time.time() * 1000000)

    @staticmethod
    def _ticks_diff(a, b):
        if hasattr(_time, "ticks_diff"):
            return _time.ticks_diff(a, b)
        return a - b
`;

// Button for uploading the library to the processor
demoWorkspace.registerButtonCallback("upload_lib_file_pid_controller", async function(button) {
  if (!isEditorConnected()) {
    Swal.fire("Error", "Device is not connected!", "error");
    return;
  }

  try {
    mp().mute_terminal = true;
    term.writeln("Uploading file: pid_controller.py");
    await delay(50);

    await mp().sendFile("lib/pid_controller.py", pid_controller_lib, false);
    await delay(250);
    await mp().sendData('\r\n');
    await delay(250);
    mp().mute_terminal = false;
    await mp().sendData('\r\n');

    Swal.fire("Done", "File was uploaded to the ESP!", "success");
  } catch (e) {
    Swal.fire("Error", e.message, "error");
  }
});

// Library import for generated code
var pid_controller_import = `
try:
    import pid_controller
except:
    print(terminal_color('Library pid_controller.py was not found on the device.',col=91))
    print(terminal_color(' Please upload the library to the device with the button',col=91))
    print(terminal_color('        Upload library to processor',col=91))
`;

function pidIdOptions() {
  return [
    ['1', '1'], ['2', '2'], ['3', '3'], ['4', '4'],
    ['5', '5'], ['6', '6'], ['7', '7'], ['8', '8']
  ];
}

function ynToPy(v) {
  return (v === 'ON') ? 'True' : 'False';
}

// -----------------------
// Python code generator
// -----------------------

Blockly.Python['pid_ctrl_init'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;

  var id = block.getFieldValue('ID') || '1';
  var kp = Blockly.Python.valueToCode(block, 'KP', Blockly.Python.ORDER_ATOMIC) || '1.0';
  var ki = Blockly.Python.valueToCode(block, 'KI', Blockly.Python.ORDER_ATOMIC) || '0.0';
  var kd = Blockly.Python.valueToCode(block, 'KD', Blockly.Python.ORDER_ATOMIC) || '0.0';
  var outMin = Blockly.Python.valueToCode(block, 'OUT_MIN', Blockly.Python.ORDER_ATOMIC) || '-100';
  var outMax = Blockly.Python.valueToCode(block, 'OUT_MAX', Blockly.Python.ORDER_ATOMIC) || '100';
  var sampleMs = Blockly.Python.valueToCode(block, 'SAMPLE_MS', Blockly.Python.ORDER_ATOMIC) || '0';
  var dAlpha = Blockly.Python.valueToCode(block, 'D_ALPHA', Blockly.Python.ORDER_ATOMIC) || '0.20';

  var code = '';
  code += 'pid' + id + ' = pid_controller.PID(kp=' + kp + ', ki=' + ki + ', kd=' + kd + ', out_min=' + outMin + ', out_max=' + outMax + ', sample_time_ms=' + sampleMs + ', d_filter_alpha=' + dAlpha + ')\n';
  return code;
};

Blockly.Python['pid_ctrl_compute'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var setpoint = Blockly.Python.valueToCode(block, 'SETPOINT', Blockly.Python.ORDER_ATOMIC) || '0';
  var measurement = Blockly.Python.valueToCode(block, 'MEAS', Blockly.Python.ORDER_ATOMIC) || '0';
  return ['pid' + id + '.compute(' + setpoint + ', ' + measurement + ')', Blockly.Python.ORDER_NONE];
};

Blockly.Python['pid_ctrl_set_tunings'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var kp = Blockly.Python.valueToCode(block, 'KP', Blockly.Python.ORDER_ATOMIC) || '1.0';
  var ki = Blockly.Python.valueToCode(block, 'KI', Blockly.Python.ORDER_ATOMIC) || '0.0';
  var kd = Blockly.Python.valueToCode(block, 'KD', Blockly.Python.ORDER_ATOMIC) || '0.0';
  return 'pid' + id + '.set_tunings(kp=' + kp + ', ki=' + ki + ', kd=' + kd + ')\n';
};

Blockly.Python['pid_ctrl_set_output_limits'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var outMin = Blockly.Python.valueToCode(block, 'OUT_MIN', Blockly.Python.ORDER_ATOMIC) || '-100';
  var outMax = Blockly.Python.valueToCode(block, 'OUT_MAX', Blockly.Python.ORDER_ATOMIC) || '100';
  return 'pid' + id + '.set_output_limits(' + outMin + ', ' + outMax + ')\n';
};

Blockly.Python['pid_ctrl_set_term_limits'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var pMin = Blockly.Python.valueToCode(block, 'P_MIN', Blockly.Python.ORDER_ATOMIC) || '-100';
  var pMax = Blockly.Python.valueToCode(block, 'P_MAX', Blockly.Python.ORDER_ATOMIC) || '100';
  var iMin = Blockly.Python.valueToCode(block, 'I_MIN', Blockly.Python.ORDER_ATOMIC) || '-100';
  var iMax = Blockly.Python.valueToCode(block, 'I_MAX', Blockly.Python.ORDER_ATOMIC) || '100';
  var dMin = Blockly.Python.valueToCode(block, 'D_MIN', Blockly.Python.ORDER_ATOMIC) || '-100';
  var dMax = Blockly.Python.valueToCode(block, 'D_MAX', Blockly.Python.ORDER_ATOMIC) || '100';

  var code = '';
  code += 'pid' + id + '.set_term_limits(p_min=' + pMin + ', p_max=' + pMax + ', i_min=' + iMin + ', i_max=' + iMax + ', d_min=' + dMin + ', d_max=' + dMax + ')\n';
  return code;
};

Blockly.Python['pid_ctrl_enable_terms'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var pEn = ynToPy(block.getFieldValue('P_EN') || 'ON');
  var iEn = ynToPy(block.getFieldValue('I_EN') || 'ON');
  var dEn = ynToPy(block.getFieldValue('D_EN') || 'ON');
  return 'pid' + id + '.enable_terms(p=' + pEn + ', i=' + iEn + ', d=' + dEn + ')\n';
};

Blockly.Python['pid_ctrl_reset'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var out0 = Blockly.Python.valueToCode(block, 'OUT0', Blockly.Python.ORDER_ATOMIC) || '0';
  var i0 = Blockly.Python.valueToCode(block, 'I0', Blockly.Python.ORDER_ATOMIC) || '0';
  return 'pid' + id + '.reset(output=' + out0 + ', integral=' + i0 + ')\n';
};

Blockly.Python['pid_ctrl_set_sample_time'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var ms = Blockly.Python.valueToCode(block, 'MS', Blockly.Python.ORDER_ATOMIC) || '0';
  return 'pid' + id + '.set_sample_time_ms(' + ms + ')\n';
};

Blockly.Python['pid_ctrl_term_value'] = function(block) {
  Blockly.Python.definitions_['import_pid_controller'] = pid_controller_import;
  var id = block.getFieldValue('ID') || '1';
  var term = block.getFieldValue('TERM') || 'OUT';

  if (term === 'P') return ['pid' + id + '.last_p()', Blockly.Python.ORDER_NONE];
  if (term === 'I') return ['pid' + id + '.last_i()', Blockly.Python.ORDER_NONE];
  if (term === 'D') return ['pid' + id + '.last_d()', Blockly.Python.ORDER_NONE];
  if (term === 'E') return ['pid' + id + '.last_error()', Blockly.Python.ORDER_NONE];
  return ['pid' + id + '.last_output()', Blockly.Python.ORDER_NONE];
};


// -----------------------
// Block definitions
// -----------------------

Blockly.Blocks['pid_ctrl_init'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('initialize PID controller')
      .appendField('ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('KP').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Kp');
    this.appendValueInput('KI').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Ki');
    this.appendValueInput('KD').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Kd');
    this.appendValueInput('OUT_MIN').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('output limit min');
    this.appendValueInput('OUT_MAX').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('output limit max');
    this.appendValueInput('SAMPLE_MS').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('minimum period [ms]');
    this.appendValueInput('D_ALPHA').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('D filter alpha 0..1');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Creates one PID controller instance.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_compute'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID compute ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');
    this.appendValueInput('SETPOINT').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('setpoint');
    this.appendValueInput('MEAS').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('measured value');

    this.setOutput(true, 'Number');
    this.setColour('#00979d');
    this.setTooltip('Computes the controller using the real dt and returns the output.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_set_tunings'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID set Kp Ki Kd ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('KP').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Kp');
    this.appendValueInput('KI').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Ki');
    this.appendValueInput('KD').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('Kd');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Changes controller gains at runtime.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_set_output_limits'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID output limits ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('OUT_MIN').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('min');
    this.appendValueInput('OUT_MAX').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('max');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Sets the controller output limits.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_set_term_limits'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID P I D term limits ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('P_MIN').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('P min');
    this.appendValueInput('P_MAX').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('P max');
    this.appendValueInput('I_MIN').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('I min');
    this.appendValueInput('I_MAX').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('I max');
    this.appendValueInput('D_MIN').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('D min');
    this.appendValueInput('D_MAX').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('D max');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Sets limits for each term separately.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_enable_terms'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID enable terms ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID')
      .appendField('P').appendField(new Blockly.FieldDropdown([['enable', 'ON'], ['disable', 'OFF']]), 'P_EN')
      .appendField('I').appendField(new Blockly.FieldDropdown([['enable', 'ON'], ['disable', 'OFF']]), 'I_EN')
      .appendField('D').appendField(new Blockly.FieldDropdown([['enable', 'ON'], ['disable', 'OFF']]), 'D_EN');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Allows disabling individual controller terms.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_reset'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID reset ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('OUT0').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('output after reset');
    this.appendValueInput('I0').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('integral after reset');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('Resets internal controller states.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_set_sample_time'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID compute period ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID');

    this.appendValueInput('MS').setCheck('Number').setAlign(Blockly.ALIGN_RIGHT).appendField('minimum period [ms]');

    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour('#00979d');
    this.setTooltip('0 = computes on every call, otherwise minimum period.');
    this.setHelpUrl('');
  }
};

Blockly.Blocks['pid_ctrl_term_value'] = {
  init: function() {
    this.appendDummyInput()
      .appendField('PID value ID')
      .appendField(new Blockly.FieldDropdown(pidIdOptions()), 'ID')
      .appendField(new Blockly.FieldDropdown([
        ['output', 'OUT'],
        ['P term', 'P'],
        ['I term', 'I'],
        ['D term', 'D'],
        ['error', 'E']
      ]), 'TERM');

    this.setOutput(true, 'Number');
    this.setColour('#00979d');
    this.setTooltip('Controller diagnostic values.');
    this.setHelpUrl('');
  }
};


<!toolbox!>

<category name="PID Controller" colour="#00979d">
  <label text="Before first use, upload the library to the processor."></label>
  <button text="Upload library to processor" callbackKey="upload_lib_file_pid_controller"></button>

  <block type="pid_ctrl_init">
    <field name="ID">1</field>
    <value name="KP"><shadow type="math_number"><field name="NUM">1.0</field></shadow></value>
    <value name="KI"><shadow type="math_number"><field name="NUM">0.0</field></shadow></value>
    <value name="KD"><shadow type="math_number"><field name="NUM">0.0</field></shadow></value>
    <value name="OUT_MIN"><shadow type="math_number"><field name="NUM">-100</field></shadow></value>
    <value name="OUT_MAX"><shadow type="math_number"><field name="NUM">100</field></shadow></value>
    <value name="SAMPLE_MS"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
    <value name="D_ALPHA"><shadow type="math_number"><field name="NUM">0.20</field></shadow></value>
  </block>

  <block type="pid_ctrl_compute">
    <field name="ID">1</field>
    <value name="SETPOINT"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
    <value name="MEAS"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
  </block>

  <label text="Runtime settings"></label>
  <block type="pid_ctrl_set_tunings">
    <field name="ID">1</field>
    <value name="KP"><shadow type="math_number"><field name="NUM">1.0</field></shadow></value>
    <value name="KI"><shadow type="math_number"><field name="NUM">0.0</field></shadow></value>
    <value name="KD"><shadow type="math_number"><field name="NUM">0.0</field></shadow></value>
  </block>
  <block type="pid_ctrl_set_output_limits">
    <field name="ID">1</field>
    <value name="OUT_MIN"><shadow type="math_number"><field name="NUM">-100</field></shadow></value>
    <value name="OUT_MAX"><shadow type="math_number"><field name="NUM">100</field></shadow></value>
  </block>
  <block type="pid_ctrl_set_term_limits">
    <field name="ID">1</field>
    <value name="P_MIN"><shadow type="math_number"><field name="NUM">-100</field></shadow></value>
    <value name="P_MAX"><shadow type="math_number"><field name="NUM">100</field></shadow></value>
    <value name="I_MIN"><shadow type="math_number"><field name="NUM">-40</field></shadow></value>
    <value name="I_MAX"><shadow type="math_number"><field name="NUM">40</field></shadow></value>
    <value name="D_MIN"><shadow type="math_number"><field name="NUM">-100</field></shadow></value>
    <value name="D_MAX"><shadow type="math_number"><field name="NUM">100</field></shadow></value>
  </block>
  <block type="pid_ctrl_enable_terms">
    <field name="ID">1</field>
    <field name="P_EN">ON</field>
    <field name="I_EN">ON</field>
    <field name="D_EN">ON</field>
  </block>
  <block type="pid_ctrl_set_sample_time">
    <field name="ID">1</field>
    <value name="MS"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
  </block>
  <block type="pid_ctrl_reset">
    <field name="ID">1</field>
    <value name="OUT0"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
    <value name="I0"><shadow type="math_number"><field name="NUM">0</field></shadow></value>
  </block>

  <label text="Diagnostika"></label>
  <block type="pid_ctrl_term_value">
    <field name="ID">1</field>
    <field name="TERM">OUT</field>
  </block>
</category>
