[PLUGIN] Touch Display

Hi, Thanks for this great plugin !
I have a cheap 3.5inch screen which probably not support dpms, I discover that the LCD backlight is always on but can be disabled with GPIO :
gpio mode 3 out and then
gpio write 3 1 to switch on the backlight
gpio write 3 0 to switch off

I just need to ON/OFF the screen so first I found the plugin “gpio control”. Thanks to gvolt it works on Volumio 3 and I’m able to switch off/on my backlight on play/pause, …

But I think that the screensaver is very conveniant so I would like to add these gpio commands next to the existing dpms commands in the Touch Display plugin…

Can I edit the /data/plugins/user_interface/touch_display/index.js file directly and restart the plugin to test ?

I’ve added this kind of commands :
exec('/usr/bin/gpio write 3 0');
just before dpms commands but it doesn’t seems to works.

My index.js file, click here to expanse

'use strict';

const libQ = require('kew');
const fs = require('fs-extra');
const { exec } = require('child_process');
const path = require('path');
const os = require('os');
const net = require('net');
const io = require('socket.io-client');
const unixDomSocket = new net.Socket();
const socket = io.connect('http://localhost:3000');
const blInterface = '/sys/devices/platform/rpi_backlight/backlight/rpi_backlight';
const als = '/etc/als'; // The plugin awaits the current value of an optional ambient light sensor (ALS) as a single number in /etc/als.
const configTxtGpuMemBanner = '#### Touch Display gpu_mem setting below: do not alter ####' + os.EOL;
const configTxtRotationBanner = '#### Touch Display rotation setting below: do not alter ####' + os.EOL;
const id = 'touch_display: ';
let rpiScreen = false;
let rpiBacklight = false;
let maxBrightness = 255;
const alsProgression = [];
let autoBrTimeoutCleared = false;
let currentlyAdjusting = false;
let uiNeedsUpdate = false;
let device, displayNumber, autoBrTimer, toggleBrTimer;

module.exports = TouchDisplay;

function TouchDisplay (context) {
  const self = this;

  self.context = context;
  self.commandRouter = self.context.coreCommand;
  self.logger = self.context.logger;
  self.configManager = self.context.configManager;
}

TouchDisplay.prototype.onVolumioStart = function () {
  const self = this;
  const configFile = self.commandRouter.pluginManager.getConfigurationFile(self.context, 'config.json');

  self.config = new (require('v-conf'))();
  self.config.loadFile(configFile);
  return libQ.resolve();
};

TouchDisplay.prototype.onVolumioShutdown = function () {
  const self = this;

  if (rpiBacklight) {
    // in order to have full brightness during the next boot up
    self.setBrightness(maxBrightness);
  }
  return libQ.resolve();
};

TouchDisplay.prototype.onVolumioReboot = function () {
  const self = this;

  if (rpiBacklight) {
    // in order to have full brightness during the next boot up
    self.setBrightness(maxBrightness);
  }
  return libQ.resolve();
};

TouchDisplay.prototype.onStart = function () {
  const self = this;
  const defer = libQ.defer();
  let lastStateIsPlaying = false;
  let attempts = 0;

  self.commandRouter.loadI18nStrings();
  self.systemctl('daemon-reload')
    .then(self.systemctl.bind(self, 'start volumio-kiosk.service'))
    .then(function () {
      self.logger.info(id + 'Volumio Kiosk started');
      self.commandRouter.executeOnPlugin('system_controller', 'system', 'getSystemVersion', '')
        .then(function (infos) {
          device = infos.hardware;
          if (device === 'pi') {
            fs.readFile('/proc/modules', 'utf8', function (err, data) {
              if (err) {
                self.logger.error(id + 'Error reading /proc/modules: ' + err);
                self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_READ') + '/proc/modules: ' + err);
              } else {
                // detect Raspberry Pi Foundation original touch screen
                if (data.match(/^rpi_ft5406\b/gm) === null && data.match(/^raspberrypi_ts\b/gm) === null) {
                  self.logger.info(id + 'No Raspberry Pi Foundation touch screen detected.');
                } else {
                  rpiScreen = true;
                  self.logger.info(id + 'Raspberry Pi Foundation touch screen detected.');
                  // check for backlight module of Raspberry Pi Foundation original touch screen
                  if (data.match(/^rpi_backlight\b/gm) === null) {
                    self.logger.info(id + 'No backlight module of a Raspberry Pi Foundation touch screen detected.');
                  } else {
                    rpiBacklight = true;
                    self.logger.info(id + 'Backlight module of a Raspberry Pi Foundation touch screen detected.');
                    // screen brightness
                    fs.readFile(blInterface + '/max_brightness', 'utf8', function (err, data) {
                      if (err) {
                        self.logger.error(id + 'Error reading ' + blInterface + '/max_brightness: ' + err);
                        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_READ') + blInterface + '/max_brightness: ' + err);
                      } else {
                        maxBrightness = parseInt(data, 10);
                        exec('/usr/bin/sudo /bin/chmod a+w ' + blInterface + '/brightness', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
                          if (error !== null) {
                            self.logger.error(id + 'Error setting file permissions for backlight brightness control: ' + error);
                          } else {
                            self.logger.info(id + 'File permissions for backlight brightness control set.');
                            if (!self.config.get('autoMode')) {
                              if (self.config.get('br2StartTime') !== self.config.get('br1StartTime')) {
                                self.toggleBrightness();
                              } else {
                                self.setBrightness(self.config.get('manualBr'));
                              }
                            } else {
                              self.autoBrightness();
                            }
                          }
                        });
                      }
                    });
                  }
                }
              }
              // screen orientation
              self.setOrientation(self.config.get('angle'));
              // GPU memory size
              if (self.config.get('controlGpuMem')) {
                self.modBootConfig(configTxtGpuMemBanner + 'gpu_mem=.*', configTxtGpuMemBanner + 'gpu_mem=' + self.config.get('gpuMem'))
                  .then(self.modBootConfig.bind(self, '^gpu_mem', '#GPU_MEM'))
                  .fail(function () {
                    self.logger.info(id + 'Writing the touch display plugin\'s gpu_mem setting failed. Previous gpu_mem settings in /boot/config.txt have not been commented.');
                  });
              }
            });
          }
        });
      // screensaver
      if (self.commandRouter.volumioGetState().status === 'play') {
        lastStateIsPlaying = true;
      }
      unixDomSocket.connect('/tmp/.X11-unix/X' + self.getDisplaynumber());
      unixDomSocket.on('connect', function () {
        if ((self.config.get('afterPlay') && self.commandRouter.volumioGetState().status === 'play') || self.config.get('timeout') === 0) {
			exec('/usr/bin/gpio write 26 1');
          exec('/usr/bin/xset -display :' + displayNumber + ' s reset dpms force on', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
            if (error !== null) {
              self.logger.error(id + 'Error waking up the screen: ' + error);
            }
          });
          self.setScreenTimeout(0, false);
        } else {
          self.setScreenTimeout(self.config.get('timeout'), false);
        }
        attempts = 0;
      });
      unixDomSocket.on('error', function (data) {
      });
      unixDomSocket.on('close', function () {
        if (attempts < 100) {
          setTimeout(function () {
            unixDomSocket.connect('/tmp/.X11-unix/X' + self.getDisplaynumber());
          }, 100);
          attempts++;
        } else {
          self.logger.error(id + 'Connecting to the Xserver failed.');
          self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_CON_XSERVER'));
        }
      });
      // catch state related events and react to changes of the playing status
      socket.emit('getState', '');
      socket.on('pushState', function (state) {
        if (state.status === 'play' && !lastStateIsPlaying) {
          if (self.config.get('afterPlay')) {
			  exec('/usr/bin/gpio write 26 1');
            exec('/usr/bin/xset -display :' + displayNumber + ' s reset dpms force on', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
              if (error !== null) {
                self.logger.error(id + 'Error waking up the screen: ' + error);
              }
            });
            self.setScreenTimeout(0, true);
          }
          lastStateIsPlaying = true;
        } else if (state.status !== 'play' && lastStateIsPlaying) {
          self.setScreenTimeout(self.config.get('timeout'), true);
          lastStateIsPlaying = false;
        }
      });
      defer.resolve();
    })
    .fail(function () {
      defer.reject(new Error());
    });
  return defer.promise;
};

TouchDisplay.prototype.onStop = function () {
  const self = this;
  const defer = libQ.defer();

  unixDomSocket.removeAllListeners();
  unixDomSocket.destroy();
  socket.off('pushState');
  if (device === 'pi') {
    self.setOrientation('0');
    if (self.config.get('controlGpuMem')) {
      self.modBootConfig('^#GPU_MEM', 'gpu_mem')
        .then(self.modBootConfig.bind(self, configTxtGpuMemBanner + 'gpu_mem=.*', ''))
        .fail(function () {
          self.logger.info(id + 'Restoring gpu_mem settings in /boot/config.txt failed. The touch display plugin\'s gpu_mem settings have been preserved.');
        });
    }
    clearTimeout(autoBrTimer);
    clearTimeout(toggleBrTimer);
    if (rpiBacklight) {
      self.setBrightness(maxBrightness);
    }
  }
  self.systemctl('stop volumio-kiosk.service')
    .fin(function () {
      defer.resolve();
    });
  return defer.promise;
};

// Configuration Methods -----------------------------------------------------------------------------

TouchDisplay.prototype.getUIConfig = function () {
  const self = this;
  const defer = libQ.defer();
  const langCode = self.commandRouter.sharedVars.get('language_code');

  self.commandRouter.i18nJson(path.join(__dirname, 'i18n', 'strings_' + langCode + '.json'),
    path.join(__dirname, 'i18n', 'strings_en.json'),
    path.join(__dirname, 'UIConfig.json'))
    .then(function (uiconf) {
      uiconf.sections[0].hidden = false;
      uiconf.sections[0].content[0].value = self.config.get('timeout');
      uiconf.sections[0].content[0].attributes = [
        {
          placeholder: 120,
          maxlength: Number.MAX_SAFE_INTEGER.toString().length,
          min: 0,
          max: Number.MAX_SAFE_INTEGER
        }
      ];
      uiconf.sections[0].content[1].value = self.config.get('afterPlay');
      if (rpiBacklight) {
        uiconf.sections[1].hidden = false;
        try {
          if (fs.existsSync(als)) {
            uiconf.sections[1].content[0].hidden = false;
            uiconf.sections[1].content[0].value = self.config.get('autoMode');
            uiconf.sections[1].content[1].value = self.config.get('minBr');
            uiconf.sections[1].content[1].attributes = [
              {
                placeholder: 15,
                maxlength: maxBrightness.toString().length,
                min: 0,
                max: maxBrightness
              }
            ];
            uiconf.sections[1].content[2].value = self.config.get('maxBr');
            uiconf.sections[1].content[2].attributes = [
              {
                placeholder: maxBrightness,
                maxlength: maxBrightness.toString().length,
                min: 0,
                max: maxBrightness
              }
            ];
            uiconf.sections[1].content[4].value = self.config.get('brightnessCurve');
            uiconf.sections[1].content[5].value = self.config.get('midBr');
            uiconf.sections[1].content[5].attributes = [
              {
                placeholder: maxBrightness,
                maxlength: maxBrightness.toString().length,
                min: 0,
                max: maxBrightness
              }
            ];
          }
        } catch (e) {
          self.logger.error(id + 'Error checking the existence of "/etc/als": ' + e);
        }
        uiconf.sections[1].content[7].value = self.config.get('manualBr');
        uiconf.sections[1].content[7].attributes = [
          {
            placeholder: maxBrightness,
            maxlength: maxBrightness.toString().length,
            min: 0,
            max: maxBrightness
          }
        ];
        uiconf.sections[1].content[8].value = self.config.get('br1StartTime');
        uiconf.sections[1].content[8].attributes = [
          {
            placeholder: 'hh:mm',
            maxlength: 5
          }
        ];
        uiconf.sections[1].content[9].value = self.config.get('manualBr2');
        uiconf.sections[1].content[9].attributes = [
          {
            placeholder: maxBrightness,
            maxlength: maxBrightness.toString().length,
            min: 0,
            max: maxBrightness
          }
        ];
        uiconf.sections[1].content[10].value = self.config.get('br2StartTime');
        uiconf.sections[1].content[10].attributes = [
          {
            placeholder: 'hh:mm',
            maxlength: 5
          }
        ];
      }
      if (device === 'pi') {
        uiconf.sections[2].hidden = false;
        uiconf.sections[2].content[0].value.value = self.config.get('angle');
        uiconf.sections[2].content[0].value.label = self.commandRouter.getI18nString('TOUCH_DISPLAY.' + self.config.get('angle'));
        uiconf.sections[3].hidden = false;
        uiconf.sections[3].content[0].value = self.config.get('controlGpuMem');
        uiconf.sections[3].content[1].value = self.config.get('gpuMem');
        uiconf.sections[3].content[1].attributes = [
          {
            placeholder: 32,
            maxlength: 3,
            min: 32,
            max: 128
          }
        ];
      }
      uiconf.sections[4].hidden = false;
      uiconf.sections[4].content[0].value = self.config.get('showPointer');
      uiconf.sections[5].hidden = false;
      uiconf.sections[5].content[0].value = self.config.get('scale');
      uiconf.sections[5].content[0].attributes = [
        {
          placeholder: 100,
          maxlength: 3,
          min: 10,
          max: 200
        }
      ];
      defer.resolve(uiconf);
    })
    .fail(function (e) {
      self.logger.error(id + 'Could not fetch UI configuration: ' + e);
      defer.reject(new Error());
    });
  return defer.promise;
};

TouchDisplay.prototype.updateUIConfig = function () {
  const self = this;

  self.commandRouter.getUIConfigOnPlugin('user_interface', 'touch_display', {})
    .then(function (uiconf) {
      self.commandRouter.broadcastMessage('pushUiConfig', uiconf);
    });
  self.commandRouter.broadcastMessage('pushUiConfig');
  uiNeedsUpdate = false;
};

TouchDisplay.prototype.getConfigurationFiles = function () {
  return ['config.json'];
};

TouchDisplay.prototype.getI18nFile = function (langCode) {
  const self = this;
  const langFile = 'strings_' + langCode + '.json';

  try {
    const i18nFiles = fs.readdirSync(path.join(__dirname, 'i18n'));
    // check for i18n file fitting the system language
    if (i18nFiles.some(function (i18nFile) { return i18nFile === langFile; })) {
      return path.join(__dirname, 'i18n', langFile);
    }
    throw new Error('i18n file complementing the system language not found.');
  } catch (e) {
    self.logger.error(id + 'Fetching language file: ' + e);
    // return default i18n file
    return path.join(__dirname, 'i18n', 'strings_en.json');
  }
};

TouchDisplay.prototype.saveScreensaverConf = function (confData) {
  const self = this;
  let noChanges = true;

  if (Number.isNaN(parseInt(confData.timeout, 10)) || !isFinite(confData.timeout)) {
    uiNeedsUpdate = true;
    self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.TIMEOUT') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
  } else {
    confData.timeout = self.checkLimits('timeout', confData.timeout, 0, Number.MAX_SAFE_INTEGER);
    if (self.config.get('timeout') !== confData.timeout || self.config.get('afterPlay') !== confData.afterPlay) {
      self.config.set('timeout', confData.timeout);
      self.config.set('afterPlay', confData.afterPlay);
      if ((confData.afterPlay && self.commandRouter.volumioGetState().status === 'play') || confData.timeout === 0) {
		  exec('/usr/bin/gpio write 26 1');
        exec('/usr/bin/xset -display :' + displayNumber + ' s reset dpms force on', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
          if (error !== null) {
            self.logger.error(id + 'Error waking up the screen: ' + error);
          }
        });
        self.setScreenTimeout(0, true);
      } else {
        self.setScreenTimeout(confData.timeout, true);
      }
      noChanges = false;
    }
  }
  if (uiNeedsUpdate) {
    self.updateUIConfig();
  } else if (noChanges) {
    self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
  } else {
    self.commandRouter.pushToastMessage('success', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('COMMON.SETTINGS_SAVED_SUCCESSFULLY'));
  }
};

TouchDisplay.prototype.saveBrightnessConf = function (confData) {
  const self = this;
  const responseData = {
    title: self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'),
    message: self.commandRouter.getI18nString('TOUCH_DISPLAY.TEST_MSG'),
    size: 'lg',
    buttons: [
      {
        name: self.commandRouter.getI18nString('TOUCH_DISPLAY.TESTBRIGHTNESS'),
        class: 'btn btn-default',
        emit: 'callMethod',
        payload: { endpoint: 'user_interface/touch_display', method: 'testBrightness', data: Object.assign({}, confData) }
      },
      {
        name: self.commandRouter.getI18nString('COMMON.CONTINUE'),
        class: 'btn btn-info',
        emit: 'callMethod',
        payload: { endpoint: 'user_interface/touch_display', method: 'saveBrightnessConf', data: (function () { const data = Object.assign({}, confData); data.modalResult = true; return data; })() }
      },
      {
        name: self.commandRouter.getI18nString('COMMON.CANCEL'),
        class: 'btn btn-info',
        emit: 'callMethod',
        payload: { endpoint: 'user_interface/touch_display', method: 'saveBrightnessConf', data: (function () { const data = Object.assign({}, confData); data.modalResult = false; return data; })() }
      }
    ]
  };
  const timeValidator = /^([0-9]|0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$/;
  let noChanges = true;

  self.commandRouter.broadcastMessage('closeAllModals', '');
  if (self.config.get('autoMode') !== confData.autoMode) {
    noChanges = false;
  }
  if (confData.autoMode) {
    if (Number.isNaN(parseInt(confData.minBr, 10)) || !isFinite(confData.minBr)) {
      confData.minBr = self.config.get('minBr');
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.MINBR') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
    }
    confData.minBr = self.checkLimits('minBr', confData.minBr, 0, maxBrightness);
    if (confData.modalResult === undefined && confData.minBr < 15 && confData.minBr < self.config.get('minBr')) {
      responseData.message = responseData.message.replace('${}', confData.minBr);
      responseData.buttons[2].payload.data.minBr = self.config.get('minBr');
      self.commandRouter.broadcastMessage('openModal', responseData);
      return;
    } else {
      if (confData.modalResult === false) {
        uiNeedsUpdate = true;
      } else {
        if (self.config.get('minBr') !== confData.minBr) {
          self.config.set('minBr', confData.minBr);
          noChanges = false;
        }
      }
      if (Number.isNaN(parseInt(confData.maxBr, 10)) || !isFinite(confData.maxBr)) {
        confData.maxBr = self.config.get('maxBr');
        uiNeedsUpdate = true;
        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.MAXBR') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
      }
      confData.maxBr = self.checkLimits('maxBr', confData.maxBr, confData.minBr, maxBrightness);
      if (self.config.get('maxBr') !== confData.maxBr) {
        self.config.set('maxBr', confData.maxBr);
        noChanges = false;
      }
      if (confData.brightnessCurve) {
        if (Number.isNaN(parseInt(confData.midBr, 10)) || !isFinite(confData.midBr)) {
          confData.midBr = self.config.get('midBr');
          uiNeedsUpdate = true;
          self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.MIDBR') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
        }
        confData.midBr = self.checkLimits('midBr', confData.midBr, confData.minBr, confData.maxBr);
        if (self.config.get('midBr') !== confData.midBr) {
          self.config.set('midBr', confData.midBr);
          noChanges = false;
        }
      }
      // minAls and maxAls can only be the same value if the ALS range has not been determined before
      if (self.config.get('maxAls') <= self.config.get('minAls')) {
        if (confData.brightnessCurve) {
          self.getAlsValue({ confData: confData, action: 'minmaxmid' });
        } else {
          self.getAlsValue({ confData: confData, action: 'minmax' });
        }
      } else if (confData.brightnessCurve && (!self.config.has('midAls') || self.config.get('midAls') <= self.config.get('minAls') || self.config.get('midAls') >= self.config.get('maxAls'))) {
        self.getAlsValue({ confData: confData, action: 'mid' });
      } else {
        if (self.config.get('brightnessCurve') !== confData.brightnessCurve) {
          self.config.set('brightnessCurve', confData.brightnessCurve);
          noChanges = false;
        }
        self.config.set('autoMode', confData.autoMode);
        clearTimeout(toggleBrTimer);
        clearTimeout(autoBrTimer);
        autoBrTimeoutCleared = true;
        self.autoBrightness();
      }
    }
  } else {
    self.config.set('autoMode', confData.autoMode);
    clearTimeout(autoBrTimer);
    autoBrTimeoutCleared = true;
    if (Number.isNaN(parseInt(confData.manualBr, 10)) || !isFinite(confData.manualBr)) {
      confData.manualBr = self.config.get('manualBr');
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.MANUALBR') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
    }
    confData.manualBr = self.checkLimits('manualBr', confData.manualBr, 0, maxBrightness);
    if (Number.isNaN(parseInt(confData.manualBr2, 10)) || !isFinite(confData.manualBr2)) {
      confData.manualBr2 = self.config.get('manualBr2');
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.MANUALBR2') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
    }
    confData.manualBr2 = self.checkLimits('manualBr2', confData.manualBr2, 0, maxBrightness);
    if (confData.modalResult === undefined && ((confData.manualBr < 15 && confData.manualBr < self.config.get('manualBr') && confData.manualBr < self.config.get('manualBr2')) || (confData.manualBr2 < 15 && confData.manualBr2 < self.config.get('manualBr2') && confData.manualBr2 < self.config.get('manualBr')))) {
      if (confData.manualBr <= confData.manualBr2) {
        responseData.message = responseData.message.replace('${}', confData.manualBr);
        responseData.buttons[2].payload.data.manualBr = self.config.get('manualBr');
        if (confData.manualBr === confData.manualBr2) {
          responseData.buttons[2].payload.data.manualBr2 = self.config.get('manualBr2');
        }
      } else {
        responseData.message = responseData.message.replace('${}', confData.manualBr2);
        responseData.buttons[2].payload.data.manualBr2 = self.config.get('manualBr2');
      }
      self.commandRouter.broadcastMessage('openModal', responseData);
      return;
    } else {
      if (confData.modalResult === false) {
        uiNeedsUpdate = true;
        if (confData.manualBr !== confData.manualBr2) {
          if (confData.manualBr < 15 && confData.manualBr < self.config.get('manualBr')) {
            responseData.message = responseData.message.replace('${}', confData.manualBr);
            responseData.buttons[2].payload.data.manualBr = self.config.get('manualBr');
            self.commandRouter.broadcastMessage('openModal', responseData);
            return;
          } else if (confData.manualBr2 < 15 && confData.manualBr2 < self.config.get('manualBr2')) {
            responseData.message = responseData.message.replace('${}', confData.manualBr2);
            responseData.buttons[2].payload.data.manualBr2 = self.config.get('manualBr2');
            self.commandRouter.broadcastMessage('openModal', responseData);
            return;
          }
        } else {
          self.config.set('manualBr', confData.manualBr);
          self.config.set('manualBr2', confData.manualBr2);
          noChanges = false;
        }
      } else {
        self.config.set('manualBr', confData.manualBr);
        self.config.set('manualBr2', confData.manualBr2);
        noChanges = false;
      }
      if (confData.br1StartTime.match(timeValidator) === null) {
        uiNeedsUpdate = true;
        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.BR1STARTTIME') + self.commandRouter.getI18nString('TOUCH_DISPLAY.INVALID'));
      } else if (self.config.get('br1StartTime') !== confData.br1StartTime) {
        self.config.set('br1StartTime', confData.br1StartTime);
        noChanges = false;
      }
      if (confData.br2StartTime.match(timeValidator) === null) {
        uiNeedsUpdate = true;
        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.BR2STARTTIME') + self.commandRouter.getI18nString('TOUCH_DISPLAY.INVALID'));
      } else if (self.config.get('br2StartTime') !== confData.br2StartTime) {
        self.config.set('br2StartTime', confData.br2StartTime);
        noChanges = false;
      }
      if (self.config.get('br2StartTime') !== self.config.get('br1StartTime') && self.config.get('manualBr') !== self.config.get('manualBr2')) {
        self.toggleBrightness();
      } else {
        self.setBrightness(confData.manualBr);
        clearTimeout(toggleBrTimer);
      }
    }
  }
  if (uiNeedsUpdate) {
    self.updateUIConfig();
  } else if (noChanges) {
    self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
  } else {
    self.commandRouter.pushToastMessage('success', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('COMMON.SETTINGS_SAVED_SUCCESSFULLY'));
  }
};

TouchDisplay.prototype.saveOrientationConf = function (confData) {
  const self = this;
  const responseData = {
    title: self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'),
    message: self.commandRouter.getI18nString('TOUCH_DISPLAY.REBOOT_MSG'),
    size: 'lg',
    buttons: [
      {
        name: self.commandRouter.getI18nString('COMMON.RESTART'),
        class: 'btn btn-default',
        emit: 'reboot',
        payload: ''
      },
      {
        name: self.commandRouter.getI18nString('COMMON.CONTINUE'),
        class: 'btn btn-info',
        emit: 'closeModals',
        payload: ''
      }
    ]
  };

  if (self.config.get('angle') !== confData.angle.value) {
    self.config.set('angle', confData.angle.value);
    self.setOrientation(confData.angle.value)
      .then(function () {
        self.commandRouter.broadcastMessage('openModal', responseData);
      });
  } else {
    self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
  }
};

TouchDisplay.prototype.saveGpuMemConf = function (confData) {
  const self = this;
  const responseData = {
    title: self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'),
    message: self.commandRouter.getI18nString('TOUCH_DISPLAY.REBOOT_MSG'),
    size: 'lg',
    buttons: [
      {
        name: self.commandRouter.getI18nString('COMMON.RESTART'),
        class: 'btn btn-default',
        emit: 'reboot',
        payload: ''
      },
      {
        name: self.commandRouter.getI18nString('COMMON.CONTINUE'),
        class: 'btn btn-info',
        emit: 'closeModals',
        payload: ''
      }
    ]
  };

  if (confData.controlGpuMem) {
    if (Number.isNaN(parseInt(confData.gpuMem, 10)) || !isFinite(confData.gpuMem)) {
      confData.gpuMem = self.config.get('gpuMem');
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.GPUMEM') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
    } else {
      confData.gpuMem = self.checkLimits('gpuMem', confData.gpuMem, 32, 128);
    }
    if (self.config.get('gpuMem') !== confData.gpuMem || self.config.get('controlGpuMem') !== confData.controlGpuMem) {
      self.modBootConfig(configTxtGpuMemBanner + 'gpu_mem=.*', configTxtGpuMemBanner + 'gpu_mem=' + confData.gpuMem)
        .then(function () {
          self.config.set('gpuMem', confData.gpuMem);
          if (self.config.get('controlGpuMem') !== confData.controlGpuMem) {
            self.modBootConfig('^gpu_mem', '#GPU_MEM');
          }
        })
        .then(function () {
          self.config.set('controlGpuMem', confData.controlGpuMem);
          self.commandRouter.broadcastMessage('openModal', responseData);
        })
        .fail(function () {
          uiNeedsUpdate = true;
          self.logger.error(id + 'Changing gpu_mem settings failed.');
        })
        .done(function () {
          if (uiNeedsUpdate) {
            self.updateUIConfig();
          }
        });
    } else if (uiNeedsUpdate) {
      self.updateUIConfig();
    } else {
      self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
    }
  } else if (self.config.get('controlGpuMem') !== confData.controlGpuMem) {
    self.modBootConfig('^#GPU_MEM', 'gpu_mem')
      .then(function () {
        self.modBootConfig(configTxtGpuMemBanner + 'gpu_mem=.*', '');
      })
      .then(function () {
        self.config.set('controlGpuMem', confData.controlGpuMem);
        self.commandRouter.broadcastMessage('openModal', responseData);
      })
      .fail(function () {
        self.updateUIConfig();
        self.logger.error(id + 'Uncommenting gpu_mem settings in /boot/config.txt failed.');
      });
  } else {
    self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
  }
};

TouchDisplay.prototype.savePointerConf = function (confData) {
  const self = this;
  const defer = libQ.defer();
  const execStartLine = 'ExecStart=\\/usr\\/bin\\/startx \\/etc\\/X11\\/Xsession \\/opt\\/volumiokiosk.sh';
  const pointerOpt = confData.showPointer ? "'" : " -- -nocursor'";

  if (self.config.get('showPointer') !== confData.showPointer) {
    fs.stat('/tmp/.X11-unix/X' + displayNumber, function (err, stats) {
      if (err !== null || !stats.isSocket()) {
        self.updateUIConfig();
        self.logger.error(id + 'Pointer config cannot be applied: ' + err); // this can happen if the user applies a pointer setting which leads to a restart of the Xserver and then fastly (before the Xserver has completed its start) tries to apply a new pointer config
        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_SET_POINTER') + err);
        defer.reject(err);
      } else {
        self.config.set('showPointer', confData.showPointer);
        exec("/bin/echo volumio | /usr/bin/sudo -S /bin/sed -i -e '/" + execStartLine + '/c\\' + execStartLine + pointerOpt + ' /lib/systemd/system/volumio-kiosk.service', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
          if (error !== null) {
            self.logger.error(id + 'Error modifying /lib/systemd/system/volumio-kiosk.service: ' + error);
            self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_MOD') + '/lib/systemd/system/volumio-kiosk.service: ' + error);
            defer.reject(error);
          } else {
            self.systemctl('daemon-reload')
              .then(self.onStop.bind(self))
              .then(self.onStart.bind(self))
              .then(function () {
                self.commandRouter.pushToastMessage('success', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('COMMON.SETTINGS_SAVED_SUCCESSFULLY'));
                defer.resolve();
              })
              .fail(function () {
                defer.reject(new Error());
              });
          }
        });
      }
    });
  } else {
    self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
  }
  return defer.promise;
};

TouchDisplay.prototype.saveScaleConf = function (confData) {
  const self = this;
  const defer = libQ.defer();

  if (Number.isNaN(parseInt(confData.scale, 10)) || !isFinite(confData.scale)) {
    uiNeedsUpdate = true;
    self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.SCALE') + self.commandRouter.getI18nString('TOUCH_DISPLAY.NAN'));
  } else {
    confData.scale = self.checkLimits('scale', confData.scale, 10, 200);
    if (self.config.get('scale') !== confData.scale) {
      fs.stat('/tmp/.X11-unix/X' + displayNumber, function (err, stats) {
        if (err !== null || !stats.isSocket()) {
          self.updateUIConfig();
          self.logger.error(id + 'Scale config cannot be applied: ' + err); // this can happen if the user applies a scale setting which leads to a restart of the Xserver and then fastly (before the Xserver has completed its start) tries to apply a new scale config
          self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_SET_SCALE') + err);
          defer.reject(err);
        } else {
          self.config.set('scale', confData.scale);
          exec('/usr/bin/chromium-browser -version', { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
            if (error !== null) {
              self.logger.error(id + 'Error requesting browser version.');
            } else {
              if (confData.scale < 100 && stdout.match(/\d*\./).toString().slice(0, -1) < 57) {
                self.commandRouter.pushToastMessage('warning', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.SCALE_WARN'));
              }
              exec("/bin/echo volumio | /usr/bin/sudo -S /bin/sed -i -e 's/factor=.* /factor=" + confData.scale / 100 + " /' /opt/volumiokiosk.sh", { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
                if (error !== null) {
                  self.logger.error(id + 'Error modifying /opt/volumiokiosk.sh: ' + error);
                  self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_MOD') + '/opt/volumiokiosk.sh: ' + error);
                  defer.reject(error);
                } else {
                  self.systemctl('daemon-reload')
                    .then(self.onStop.bind(self))
                    .then(self.onStart.bind(self))
                    .then(function () {
                      self.commandRouter.pushToastMessage('success', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('COMMON.SETTINGS_SAVED_SUCCESSFULLY'));
                      defer.resolve();
                    })
                    .fail(function () {
                      defer.reject(new Error());
                    });
                }
              });
            }
          });
        }
      });
    } else {
      self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.NO_CHANGES'));
    }
  }
  if (uiNeedsUpdate) {
    self.updateUIConfig();
  }
  return defer.promise;
};

// Plugin Methods ------------------------------------------------------------------------------------

TouchDisplay.prototype.checkLimits = function (item, value, min, max) {
  const self = this;

  if (value < min) {
    if (item !== '') {
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.' + item.toUpperCase()) + ': ' + self.commandRouter.getI18nString('TOUCH_DISPLAY.INFO_MIN'));
    }
    return min;
  }
  if (value > max) {
    if (item !== '') {
      uiNeedsUpdate = true;
      self.commandRouter.pushToastMessage('info', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.' + item.toUpperCase()) + ': ' + self.commandRouter.getI18nString('TOUCH_DISPLAY.INFO_MAX'));
    }
    return max;
  }
  return parseInt(value, 10);
};

TouchDisplay.prototype.setScreenTimeout = function (timeout, showErr) {
  const self = this;
  const defer = libQ.defer();

  fs.stat('/tmp/.X11-unix/X' + displayNumber, function (err, stats) {
    if (err !== null || !stats.isSocket()) {
      self.logger.error(id + 'Error setting screensaver timeout: ' + err);
      if (showErr) {
        self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_SET_TIMEOUT') + err);
      }
      defer.reject(err);
    } else {
		exec('/usr/bin/gpio write 26 0');
      exec('/usr/bin/xset -display :' + displayNumber + ' s off +dpms dpms 0 0 ' + timeout, { uid: 1000, gid: 1000 }, function (error, stdout, stderr) {
        if (error !== null) {
          self.logger.error(id + 'Error setting screensaver timeout: ' + error);
          if (showErr) {
            self.commandRouter.pushToastMessage('error', self.commandRouter.getI18nString('TOUCH_DISPLAY.PLUGIN_NAME'), self.commandRouter.getI18nString('TOUCH_DISPLAY.ERR_SET_TIMEOUT') + error);
          }
          defer.reject(error);
        } else {
          self.logger.info(id + 'Setting screensaver timeout to ' + timeout + ' seconds.');
          defer.resolve();
        }
      });
    }
  });
  return defer.promise;
};


........
code to long, cutted here
.........

A great alternative could be to authorise users to run custom scripts from events in settings of “Touch Display” plugin :stuck_out_tongue: