И так представьте, одним прекрасным днём к вам, как к системному администратору, поступает задача. За пару дней обновить все фискальные регистраторы во всех точках продаж, т.к. в маркировке очередные изменения и для их поддержки нужно перепрошить все ККМ новой прошивкой. И за одно, коль уж предстоит играться с ККМ, ещё добавить логотип компании и индивидуальную рекламную информацию в чек для каждой точки продаж. Вот моим решением такой интересной задачи, я хотел бы с вами поделиться. И рассказать о полезных моментах, которые вам могут пригодиться, в аналогичной ситуации.

Распределение ККМ и точек продаж по Московской области
Распределение ККМ и точек продаж по Московской области

Тут сразу становиться очевидно, что нужно автоматизировать данную задачу. Количество точек не маленькое и нет гарантий, что через месяц не прилетит аналогичная задача, но уже с новой версией прошивки и придётся делать всё заново. Да и кассы постоянно заменяются на точках и хочется, чтобы они настраивались сами и постоянно.

Хорошо что мы всегда были за унификацию оборудования и на всех точках у нас Штрих-М, но правда четырёх разных моделей. Работать с оборудованием одного производителя всегда проще.

Первым делом, для написания скриптов, разобьём задачу на подзадачи:

  1. Нужно обновить драйвер фискального регистратора.

  2. Нужно перепрошить сам фискальный регистратор.

  3. Нужно перенастроить фискальный регистратор.

Первая задача, обновление драйвера фискального регистратора, самая простая. Уже есть куча разных статей как через GPOWMIPowerShell или сторонним программным обеспечением (например через Kaspersky Security Center) установить тот или иной софт. На этом останавливается не будем, куда интереснее поиграться с самой железкой.

Скрипт перепрошивки фискального регистратора

Приводить здесь полный код скрипта не вижу смысла, он опубликован в моём профиле на GitHub, вот прямая ссылка. Давайте рассмотрим самые интересные моменты.

Во-первых, как взаимодействовать с ККМ? Тут всё оказалось просто. Драйвер фискального регистратора создаёт ActiveX объект, вот через него мы и будем строить своё взаимодействие. Поэтому на сайте производителя помимо свеженького установочного файла драйвера, нам ещё пригодится документ ШТРИХ-М: Драйвер ККТ А4.15. Руководство программиста. На моё удивление в этом документе всё очень чётко, структурированно и понятно расписано, какие свойства и методы есть у этого объекта, и как их вызывать.

// создаём объект для взаимодействия с кассой
if (!error) {// если нету ошибок
    try {// пробуем подключиться к кассе
        driver = new ActiveXObject("Addin.DrvFR");
        speed = driver.BaudRate;
        driver.Password = password;
        driver.GetECRStatus();
        if (!driver.ResultCode) {// если запрос выполнен
        } else error = 2;
    } catch (e) { error = 2; };
};

Пару слов, про то, как вызывать эти методы. Производитель реализовал достаточно интересный подход к API. Ты создаёшь экземпляр объекта, у него есть свойства и методы, но методы не принимают параметры, в привычном понимании этого. Вместо этого, ты просто перед вызовом метода у экземпляра объекта задаёшь необходимые свойства. Вызываешь метод без параметров. А затем читаешь значения из нужных тебе свойств. По началу данный подход кажется не привычным, но потом находишь его удобным, т.к. тебе не нужно парится об переменных, в которых хранить результаты выполнения методов. Ты просто всегда можешь обратиться к экземпляру объекта и прочитать нужные свойства.

Графический интерфейс тест-драйвера Штрих-М для ККМ
Графический интерфейс тест-драйвера Штрих-М для ККМ

Кстати, в графическом интерфейсе тест-драйвера при наведении на почти все кнопки и поля выскакивают подсказки с интересными названиями. Так вот, это как раз названия методов и свойств, которые нужно вызвать при программной реализации взаимодействия, для получения аналогичного результата. Такой любезное отношение к разработчикам очень порадовало, и сэкономило мне кучу времени на поиске нужного метода. За что производителю отдельное спасибо.

Второй интересный момент. Как загрузить файл прошивки в ККМ? Оказалась тут есть нюансы. Если в фискальный регистратор установлена обычная SD карта, то просто вызываем нужный метод, загружаем файл на эту карту и другим методом перезагружаем ККМ. При включении ККМ обновится.

// загружаем прошивку на карту память
if (!error && !isUpdated) {// если нужно выполнить
    driver.FileType = 1;// прошивка
    driver.FileName = image;
    driver.LoadFileOnSDCard();
    if (!driver.ResultCode) {// если данные получены
    } else error = 11;
};
// перезагружаем кассу
if (!error && !isUpdated) {// если нужно выполнить
    driver.RebootKKT();
    if (!driver.ResultCode) {// если данные получены
    } else error = 12;
};

Но если SD карты нет, то нужен другой подход. И он зависит от того, как подключена ККМ к компьютеру. Если через USB, то нужно использовать режим DFU.

// обновляем прошивку через usb
if (!error && !isUpdated) {// если нужно выполнить
    driver.UpdateFirmwareMethod = 0;// DFU
    driver.FileName = image;
    driver.UpdateFirmware();
    if (!driver.ResultCode) {// если запрос выполнен
        while (1 == driver.UpdateFirmwareStatus) wsh.sleep(timeout);
        if (!driver.UpdateFirmwareStatus) isUpdated = true;
    };
};

Если ККМ подключена к компьютеру через COM порт, то нужно использовать режим XMODEM.

// обновляем прошивку через com
if (!error && !isUpdated) {// если нужно выполнить
    driver.UpdateFirmwareMethod = 1;// XMODEM
    driver.FileName = image;
    driver.UpdateFirmware();
    if (!driver.ResultCode) {// если запрос выполнен
        while (1 == driver.UpdateFirmwareStatus) wsh.sleep(timeout);
        if (!driver.UpdateFirmwareStatus) isUpdated = true;
    } else error = 8;
};

В-третьих, после обновления нужно сделать технологическое обнуление, а перед ним нужно сохранить все настройки ККМ и потом восстановить их. Но тут я решил сделать всё просто, сохранить таблицу значений в многомерный массив и потом восстановить её из этого массива.

Хорошо бы перед прошивкой кассы проверять текущую версию прошивки, чтобы не иметь возможность не прошивать уже прошитые кассы. Так же хорошо бы проверять ещё и модель, на случай разных файлов с прошивками для разных моделей. Ну и так же не нужно прошивать кассу если на ней открыта смена. Добавляем эти проверки необязательными параметрами и в итоге получаем скрипт firmware.min.js который можно запускать из командной строки и выполнять прошивку ККМ одной командой.

cscript firmware.min.js <image> [<build> [<model>]]

  • <image> - Путь к файлу с прошивкой кассы.

  • <build> - Номер сборки прошивки (если не совпадает, то касса обновляется).

  • <model> - Фильтр по модели кассы (если не совпадает, то касса не обновляется).

Скрипт настройки фискального регистратора

Приводить здесь полный код скрипта то же не вижу смысла, он опубликован в моём профиле на GitHub, вот прямая ссылка. Давайте так же рассмотрим самые интересные моменты, тут их то же не мало.

Уведомление пользователя с помощью команды shutdown
Уведомление пользователя с помощью команды shutdown

Во-первых, нужно предусмотреть возможность уведомления пользователя. Т.к. при подключении ККМ к компьютеру, что по USB, что по COM, c ККМ через драйвер может взаимодействовать только одна программа. И если точка работает круглосуточно, то в тихом режиме такую ККМ мы никогда не перенастроим.

Но как сообщить вошедшему пользователю, что нужно сделать трёхминутный перерыв? Ведь скрипт исполняется в контексте другого административного пользователя или в моём случае от имени системы. И показывать какие-то сообщения бесполезно, другой пользователь их не увидит. Тут я решил использовать простой и давно проверенной мной способ, вызываем через shutdown отложенную перезагрузку с сообщением, а затем отменяем её. Если вы знаете более интересные способы, напишите пожалуйста в комментариях, я думаю они всем пригодятся.

// прерываем работу пользователя
if (!silent) {// если можно прервать работу пользователя
    // показываем начальное сообщение пользователю
    if (!error) {// если нету ошибок
        value = // сообщение для пользователя
            "Через 2 минуты на компьютер будет установлено обновление для ККМ. " +
            "Нужно будет закрыть кассу программы еФарма. Закрывать смену при этом не нужно. " +
            "Установка займёт 3 минуты. После этого вы сможете работать.";
        command = 'shutdown /r /t 60 /c "' + value + '"';
        shell.run(command, 0, false);
        wsh.sleep(30 * 1000);
        command = "shutdown /a";
        shell.run(command, 0, true);
        wsh.sleep(90 * 1000);
    };
    // принудительно завершаем работу кассовой программы
    if (!error) {// если нету ошибок
        command = "taskkill /F /IM ePlus.ARMCasherNew.exe /T";
        shell.run(command, 0, true);
        wsh.sleep(2 * 1000);
    };
};

Второй интересный момент. Как из скрипта работать с графическим файлом? В API драйвера фискального регистратора есть метод для загрузки логотипа и размещения его на каждом печатаемом чеке. Но этот метод ожидает не путь к файлу с логотипом, как это было с файлом прошивки, а мудрёную текстовую строку с данными о цвете каждых восьми пикселов этой картинки. Т.е. картинку нужно как-то прочитать и сделать это стандартными средствами операционной системы, чтобы не устанавливать дополнительное программное обеспечение. И если хорошо поискать, то всё-таки можно найти стандартный ActiveX объект, который умеет это делать. Вот его мы и будем использовать для наших целей.

// добавляем логотип в клише
if (logotype) {// если нужно выполнить
    // проверяем наличие файла логотипа
    if (!error) {// если нужно выполнить
        value = logotype;// получаем значение
        value = template(value, { model: model });
        value = fso.getAbsolutePathName(value);
        if (fso.fileExists(value)) {// если файл существует
            logotype = value;
        } else error = 15;
    };
    // получаем данные логотипа
    if (!error) {// если нету ошибок
        image = new ActiveXObject("WIA.ImageFile");
        image.loadFile(logotype);// читаем файл
        list = [];// список значений для байтов
        for (var y = 0, yLen = 128; y < yLen; y++) {// высота
            for (var x = 0, xLen = 512; x < xLen; x++) {// ширина
                // вычисляем значение пиксела
                if (x < image.width && y < image.height) {// если есть пиксел
                    value = image.ARGBData(x + y * image.width + 1);
                    value = -16777216 == value ? 1 : 0;// чёрный цвет
                } else value = 0;
                // формируем данные 8 пикселов
                char = x % 8 ? char : 0;
                char += value ? Math.pow(2, x % 8) : 0;
                if (7 == x % 8) list.push(dec2hex(char, 2));
            };
        };
    };
    // загружаем изображение
    if (!error) {// если нету ошибок
        driver.LineNumber = 0;
        driver.LineDataHex = list.join(" ");
        driver.WideLoadLineData();
        if (!driver.ResultCode) {// если изображение загружено
        } else error = 16;
    };
};

В-третьих, нужно предусмотреть возможность менять любой параметр кассы. В графическом интерфейсе тест-драйвер умеет сохранять все настройки таблицы ККМ в файл и загружать их из него, но, к сожалению, в API нет аналогичного метода. Но нечего страшного, файл с настройками — это текстовый файл, изучим его структуру и реализуем загрузку данных из файла самостоятельно. Зато у нас будет стандартный формат файла с настройками, и если будет необходимость, то мы сможем загрузить его вручную через графический интерфейс тест-драйвера.

// добавляем данные в таблицы
if (table) {// если нужно выполнить
    // проверяем наличие файла таблиц для импорта
    if (!error) {// если нужно выполнить
        value = table;// получаем значение
        value = template(value, { model: model });
        value = fso.getAbsolutePathName(value);
        if (fso.fileExists(value)) {// если файл существует
            table = value;
        } else error = 17;
    };
    // получаем содержимое файла
    if (!error) {// если нету ошибок
        stream = fso.openTextFile(table, 1, false, -1);
        if (!stream.atEndOfStream) {// если файл не пуст
            data = stream.readAll();
        } else error = 18;
        stream.close();
    };
    // преобразовываем содержимое в список
    if (!error) {// если нету ошибок
        list = data.split(dLine);
        for (var i = 0, iLen = list.length; i < iLen; i++) {
            value = list[i];// сохраняем значение строки
            list[i] = list[i].split(dCell);// разделяем значения в строке
            if (list[i].length > 3 && value.indexOf("//")) {
                value = value.split("','")[1] || "";
                value = value.substr(0, value.length - 1);
                value = template(value, variable || {});
                item = {// элимент данных
                    table: list[i][0],  // таблица
                    row: list[i][1],    // ряд
                    field: list[i][2],  // поле
                    value: value        // значение
                };
                list[i] = item;
            } else list[i] = null;
        };
    };
    // выполняем импорт значений таблиц
    if (!error) {// если нету ошибок
        for (var i = 0, iLen = list.length; i < iLen; i++) {
            item = list[i];// получаем очередной элимен
            // читаем текущее значение в таблице
            if (item) {// если не пустая строка таблицы
                driver.TableNumber = item.table;
                driver.RowNumber = item.row;
                driver.FieldNumber = item.field;
                driver.GetFieldStruct();
                if (!driver.ResultCode) {// если данные получены
                    driver.ReadTable();// получаем данные
                    if (!driver.ResultCode) {// если данные получены
                        if (driver.FieldType) value = driver.ValueOfFieldString;
                        else value = driver.ValueOfFieldInteger;
                        if (value != item.value) {// если нужно изменить данные
                            // изменяем значение в таблицы
                            if (driver.FieldType) driver.ValueOfFieldString = item.value;
                            else driver.ValueOfFieldInteger = item.value;
                            driver.WriteTable();// изменяем данные
                        };
                    };
                };
            };
        };
    };
};

В-четвёртых, т.к. у нас стоит задача иметь возможность в зависимости от точки продаж загружать различную рекламную информацию, то добавляем ещё один параметр, который указывает на файл с данными для шаблонизатора. Эти данные будут загружаться из файла и постанавливается в шаблонизатор в зависимости от начала имени компьютера, на котором исполняется скрипт. А начало имени компьютера зависит от точки продаж. И уже после шаблонизатора мы будем загружать значение в таблицу ККМ.

Так же в шаблонизатор оборачиваем значения параметров, которые принимает скрипт. Что бы можно было использовать шаблоны в параметрах и в итоге получаем скрипт config.min.js который можно запускать из командной строки и выполнять расширенную настройку ККМ одной командой.

cscript config.min.js <install> <license> <variable> <logotype> <table> <silent>

  • <install> - Путь к файлу установки драйвера или false для пропуска.

  • <license> - Шаблон пути к файлу лицензий для касс или false для пропуска.

  • <variable> - Шаблон пути к файлу с переменными или false для пропуска.

  • <logotype> - Шаблон пути к BMP файлу логотипа для печати вверху чека.

  • <table> - Шаблон пути к файлу таблиц для импорта в формате Штрих-М.

  • <silent> - Выполнять тихую установку без остановки работы пользователя.

Использование скриптов

С самого начала я планировал использовать эти скрипты в планировщике Windows. Что бы они несколько раз в день проверяли прошивку и настройки, и при необходимости, перенастраивали и прошивали ККМ. Поэтому в скрипте firmware.min.js есть проверка на версию установленной прошивки, а в скрипте config.min.js изменение вносятся в ячейку таблицы, только если её текущее значение не соответствует нужному. За счёт этого, многократное исполнение скриптов не создаёт постоянной перепрошивки ККМ и перезаписи её настроек.

cscript firmware.min.js upd-19018.bin 19018 "ШТРИХ-ЛАЙТ-01Ф"
cscript config.min.js false false variable.tsv %model%.bmp %model%.csv true

Поэтому создаём необходимые задачи в планировщике Windows, экспортируем их в xml. Файлы xml вместе со скриптами упаковываем в установочный файл, например с помощью NSIS. И уже привычным нам способом через GPOWMIPowerShell или сторонним программным обеспечением распространяем на нужные нам компьютеры, включая компьютеры, которые будут установлены в будущем.

Использование планировщика для постоянного запуска скриптов
Использование планировщика для постоянного запуска скриптов

И нам остаётся только с помощью специализированного софта, который централизованно мониторит список установленных программ на компьютерах, периодически посматривать за распространением и покрытием.

Установочный пакет выгодно использовать, чтобы фалы с прошивками и картинками не гонялись при каждом запуске скриптами посети из SYSVOL, а были всегда рядом на локальном компьютере. И выездному инженеру, в случае необходимости, гораздо проще скачать из централизованного репозитория установочный файл, установить его и дальше запускать те задачи в планировщике которые он хочет принудительно выполнить, не дожидаясь их планового запуска.

Комментарии (2)


  1. Makoveyev
    07.11.2021 21:47
    +1

    А когда, на одном рабочем месте несколько фискальников? И подцеплены они через Ethernet?


    1. ViPiC Автор
      07.11.2021 21:50
      +1

      Хороший вопрос. ???? Общение с ККМ идёт через драйвер. Если в тест-драйвере корректно указано подключение к одной из касс по Ethernet, то эта касса должна управляется скриптом. Но этот в теории...