<?php
/**
 * для штрих алгоритм такой:
 * - отправляем пакет с командой: STX, <LEN>, <PASS>, <MSG>, <CRC>
 * - ждем ответа ФР, должен быть ACK
 * - пауза, возможно очень маленькая, возможно большая, пока ФР готовит ответ
 * - читаем ответ ФР
 * - отвечаем ACK
 *
 * формат запроса, например гудок: 0x13
 * STX, 05, 13, 1E 00 00 00, 08
 * формат ответа такой же:
 * ACK, STX, 03 13 00 1E 0E
 *
 * Если в процессе печати документа произошёл обрыв ленты, то на ней, печатается строчка
«**ОБРЫВ БУМАГИ ДОКУМЕНТ НЕЗАВЕРШЕН**» и печать приостанавливается. АСПД
переходит в подрежим 2 «Активное отсутствие бумаги». Оператору требуется установить
новый рулон в АСПД согласно инструкции по заправке бумаги (см. соответствующий раздел
выше). При этом АСПД переходит в подрежим 3 «После активного отсутствия бумаги».
Затем оператор должен подать команду B0h «Продолжение печати» (все другие команды,
связанные с печатью, блокируются в подрежиме 3). После подачи команды продолжения
печати прерванный документ повторяется
 *
 * 25h - отрезка чека
 *
 * @author  mexxval
 * @link    http://blog.sci-smart.ru
 */

class ShtrihM_HiLevel extends KKM {
    // номера битов в битовых полях состояния ККМ
    const FLAGS2_SESSION_OPENED = 6; // 0 - closed, 1 - opened
    const FLAGS2_24HOUR_PASS = 7; // 0 - no, 1 - yes
    const FLAGS_MONEYBOX_OPENED = 11; // 0 - closed, 1 - opened
    const FLAGS_CASE_OPENED = 10; // 0 - опущена, 1 - поднята
    const FLAGS_PAPER_OUT = 7; // 0 - yes, 1 - no
    public $paper_width = 32; // Ширина бумаги
    public $fd = 0; // Номер ФД
    public $fp = 0; // Фискальный признак

    public function isReady () {
        $ans = $this->enq();
        if ($this->isNAK($ans)) {
            return true;
        } else if ($this->isACK($ans)) {
            // это может понадобиться, чтобы прочитать непереданных от ФР пакет.
            // такое может быть если наш ACK не успел дойти до ФР или еще какая беда приключилась со связью
            $this->readAnswer(); // читаем, освобождаем буфер ФР
            return true;
        }
        return false;
    }

    protected function init() {
        // оператор1 - пароль 1
        // оператор2 - пароль 2 [...]
        // оператор28 - пароль 28
        // администратор - пароль 29
        // системный администратор - пароль 30
        $this->setCashierPassword(30);
        parent::init();
    }

    // Формирование tlv
    protected function make_tlv($tag, $value, $s=5) {
        $value = iconv('UTF-8', 'cp866//IGNORE', $value);
        if($s==5) {
            $l=strlen($value);
            $value=$this->prepareText($value, $l);
        }
        elseif($s==1) {
            $l=1;
            $value=$this->prepareArg($value, 1);
        }
        elseif($s==2) {
            $l=2;
            $value=$this->prepareArg($value, 2);
        }
        elseif($s==3) {
            $l=4;
            $value=$this->prepareArg($value, 4);
        }
        else {
            $l=strlen($value)/2;
        }
        $tlv=$this->prepareArg($tag, 2).$this->prepareArg($l, 2).$value;
        return $tlv;
    }

    // Обработка команд согласно протоколу Qkkm
    public function wtf($s) {
        $s=trim($s);
        $p=explode(';', $s);
        $n=count($p);
        $this->results[]=array(true, 'txt'=>'Команда '.$s, 'code'=>0);
        if($p[$n-1]=='') {
            unset($p[$n-1]);
            $n-=1;
        }
        if($p[0]=='feed') {
            $res.=$this->feed($p[1]);
        }
        elseif($p[0]=='check_fnstatus') {
            $res=$this->check_fnstatus();
        }
        elseif($p[0]=='open_session') { // Открытие смены
            $res=$this->openSession($p[2]);
        }
        elseif($p[0]=='p' || $p[0]=='pm') { // Печать текста
            if(!$p[1]) $p[1]=' ';
            $res=$this->p($p[1]);
        }
        elseif($p[0]=='print_font') { // Печать строки заданным шрифтом
            if(!$p[2]) $p[2]=' ';
            $res=$this->print_font($p[1], $p[2]);
        }
        elseif($p[0]=='print_bold') { // Печать жирной строки
            if(!$p[1]) $p[1]=' ';
            $res=$this->print_bold($p[1]);
        }
        elseif($p[0]=='x') { // X отчет
            $res=$this->x();
        }
        elseif($p[0]=='z') { // Z отчет
            $res=$this->z($p[2]);
        }
        elseif($p[0]=='beep') { // гудок
            $res=$this->beep();
        }
        elseif($p[0]=='continue_print') { // Продолжить печать чека
            $res=$this->continue_print();
        }
        elseif($p[0]=='c') { // Печать копии последнего чека
            $res=$this->c();
        }
        elseif($p[0]=='g') { // Обнуление чека
            $res=$this->cancelCheck();
        }
        elseif($p[0]=='d') { // Статус ККМ
            $res=$this->d();
            $r=$this->writecmd('FF03');
            if($r!==false && $r!=='nak' && $r[0]!==false) {
                $r=unpack('H*', $r)[1];
                $d=str_split(mb_substr($r, 6), 2);
                $this->results[]=array(true, 'txt'=>'<b>Cрок действия ФН</b> - '.date('d.m.Y', strtotime(hexdec($d[0]).'-'.hexdec($d[1]).'-'.hexdec($d[2]))), 'code'=>0);
            }
            $r=$this->writecmd('FF3F');
            if($r!==false && $r!=='nak' && $r[0]!==false) {
                $r=unpack('H*', $r)[1];
                $r=mb_substr($r, 6, 4);
                $ofd_n=hexdec(implode('', array_reverse(str_split($r, 2))));
                $this->results[]=array(true, 'txt'=>'<b>Количество сообщений для ОФД</b> - '.$ofd_n, 'code'=>0);
            }
        }
        elseif($p[0]=='imde') { // Внесенеи или изъятие из кассы
            $res=$this->imde($p[1]);
        }
        elseif($p[0]=='set_tlv') { // Установка парметра TLV
            if($p[1]=='1008' && is_numeric($p[3]) && mb_strlen($p[3])==11) {
                $p[3]='+'.$p[3];
            }
            $res=$this->set_tlv($p[1], $p[3], $p[2]);
        }
        elseif($p[0]=='b') { // Открытие чека
            $res=$this->b($p[1], $p[2], $p[3]);
            $this->ticket_total=$p[4];
            $this->ticket_sum=$p[5];
        }
        elseif($p[0]=='smde') { // Добавление товара в чек
            $line=json_decode($p[1], true);
            if($line['partner'] && $this->base_settings['version']=='1.2') {
                $res=$this->add_kommitent($line['partner']);
            }
            if($line['mark'] && $res!==false && $res!='nak') {
                $res=$this->marking(str_replace('{{U+003B}}', ';', $line['mark']));
            }
            /*if(!$line['mark'] && $line['barcode']) {
                $code_type=array(
                    8=>'1301',
                    13=>'1302',
                    14=>'1303'
                );
                $this->set_tlv($code_type[mb_strlen($line['barcode'])]?$code_type[mb_strlen($line['barcode'])]:'1300', $line['barcode'], 5, true);
            }*/
            if($line['units'] && $res!==false && $res!='nak' && $this->base_settings['version']=='1.2') {
                $res=$this->set_tlv(2108, $line['units'], 1, true);
            }
            $res=$this->smde($line);
        }
        elseif($p[0]=='check_mark') {
            if(!$p[2]) $p[2]=1;
            if(!isset($p[3])) $p[3]=0;
            $res=$this->check_mark(str_replace('{{U+003B}}', ';', $p[1]), $p[2], $p[3]);
        }
        elseif($p[0]=='tmde') { // Оплата и закрытие чека
            if($n==3) {
                $res=$this->tmde_ffd_105(array($p[2]=>$p[1]), array($p[2]=>0)); // ффд 1.05
            }
            elseif($n==9) {
                $res=$this->tmde_ffd_105(array($p[1], $p[2], $p[3], $p[4]), array($p[5], $p[6], $p[7], $p[8])); // ффд 1.05
            }
            elseif($n==7) {
                $res=$this->tmde_ffd_11(array($p[1], $p[2], $p[3], $p[4], $p[5]), $p[6]); // ФФД 1.1
            }
            else {
                $res=array(false, 'txt'=>$this->getErrorMessagebyCode(9));
            }
        }
        elseif($p[0]=='cut_check') { // Отрезка чека
            $res=$this->cutCheck(1);
        }
        elseif($p[0]=='m') { // Запрос денежного регистра
            $res=$this->m($p[1]);
        }
        elseif($p[0]=='open_cash_box') {
            $res=$this->open_cash_box($p[1]); // Открытие денежного ящика
        }
        elseif($p[0]=='correction') { // Чек коррекции
            $res=$this->correction($p[1], $p[2]);
        }
        elseif($p[0]=='print_clishe') {
            $res=$this->print_clishe();
        }
        elseif($p[0]=='print_header') {
            $res=$this->print_header($p[1], $p[2]);
        }
        elseif($p[0]=='qr') {
            if(!isset($p[2])) {
                $p[2]=3;
            }
            $res=$this->print_qr($p[1], $p[2]);
        }
        elseif($p[0]=='barcode') {
            $res=$this->print_barcode($p[1]);
        }
        elseif($p[0]=='cmd') { // Произвольная команда
            $res=$this->cmd($p);
        }
        elseif($p[0]=='set_table') {
            $res=$this->set_table($p[1], $p[2], $p[3], $p[4]);
        }
        elseif($p[0]=='preitem' || $p[0]=='preitem_end' || $p[0]=='postitem' || $p[0]=='postitem_end') {
            // ничего не надо делать
        }
        elseif($p[0]) {
            $res=array(false, 'txt'=>$this->getErrorMessagebyCode(1));
        }
        elseif($p[0]=='') {
            $res='ack';
        }
        // Прежде чем отдать ответ, нужно дождаться конца печати
        $this->wait();

        // Теперь проверим состояние ККМ на наличие ошибок, связанных с ошибками печати
        $fr_submode=$this->getKKMState_short();
        if(!isset($fr_submode['fr_submode'])) {
            sleep(1);
            $fr_submode=$this->getKKMState_short();
        }

        if(!isset($fr_submode['fr_submode'])) {
            $res=$this->raiseError(999);
        }
        elseif($fr_submode['fr_submode']=='02') {
            $res=$this->raiseError(107);
        }

        return $res;
    }

    function check_fnstatus() {
        $ofd_n=0;
        $fn_end=0;

        $r=$this->writecmd('FF03');
        if($r!==false && $r!=='nak' && $r[0]!==false) {
            $r=unpack('H*', $r)[1];
            $d=str_split(mb_substr($r, 6), 2);
            $fn_end=strtotime(hexdec($d[0]).'-'.hexdec($d[1]).'-'.hexdec($d[2]));
        }
        $r=$this->writecmd('FF3F');
        if($r!==false && $r!=='nak' && $r[0]!==false) {
            $r=unpack('H*', $r)[1];
            $r=mb_substr($r, 6, 4);
            $ofd_n=hexdec(implode('', array_reverse(str_split($r, 2))));
        }
        return array(
            'success'=>true,
            'kkm'=>'Штрих-М (Протокол 2)',
            'ffd'=>$this->base_settings['version'],
            'inn'=>$this->read_table(13, 1, 2, false),
            'kpp'=>'',
            'org_name'=>$this->read_table(13, 1, 7, false),
            'org_address'=>$this->read_table(13, 1, 9, false),
            'ofd_n'=>$ofd_n,
            'fn_end'=>$fn_end
        );
    }

    // Маркировка товара
    function get_marking($mark) {
        $part21=explode('-', $mark);
        $len=mb_strlen($mark);
        if($len==8 && ctype_digit($mark)) {
            $mark=base_convert($mark, 10, 16);
            $mark='4508'.implode('', str_split(sprintf('%012s', $mark), 2));
        }
        elseif ($len==13 && ctype_digit($mark)) {
            $mark=base_convert($mark, 10, 16);
            $mark='450D'.implode('', str_split(sprintf('%012s', $mark), 2));
        }
        elseif ($len==14 && ctype_digit($mark)) {
            $mark=base_convert($mark, 10, 16);
            $mark='490E'.implode('', str_split(sprintf("%012s", $mark), 2));
        }
        elseif($len!=8
            && $len!=13
            && $len!=14
            && preg_match("/[A-Za-z0-9\/\\!”%&’()*+-.,:;=<>?]/", $mark)
            && mb_substr($mark, 0, 2)=='01'
            && mb_substr($mark, 16, 2)=='21') {
            if(mb_strpos($mark, '')===false && mb_strpos($mark, '\\u001d')===false) {
                $mark=''.mb_substr($mark, 0, 31).''.mb_substr($mark, 31, 6).''.mb_substr($mark, 37);
            }
            if(mb_substr($mark, 0, 1)=='') {
                $mark=mb_substr($mark, 1);
            }
            elseif(mb_substr($mark, 0, 6)=='\\u001d') {
                $mark=mb_substr($mark, 6);
            }
            $part=explode('', $mark);
            if(count($part)==1) {
                $part=explode('\u001d', $mark);
            }
            $gtin=mb_substr($part[0], 2, 14);
            $serial=mb_substr($part[0], 18, 13);
            $unit='';
            $part_n=count($part);
            if($part_n>1) {
                for($i=1; $i<=$part_n; $i++) {
                    if(mb_substr($part[$i], 0, 4)=='8005') {
                        $unit=mb_substr($part[$i], 4, 6);
                        break;
                    }
                }
            }
            $gtin=base_convert($gtin, 10, 16);
            $gtin=implode('', str_split(sprintf('%012s', $gtin), 2));
            $serial=$this->prepareText($serial, mb_strlen($serial));
            if($unit!='') {
                $unit=$this->prepareText($unit, 6);
            }
            $mark='444D'.$gtin.$serial.$unit;
        }
        elseif($len==29
            && preg_match('/^[0-9a-zA-Z!”%&’()*+-.,:;=<>?]+$/' , $mark)
            && ctype_digit(mb_substr($mark, 0, 14))
            && mb_substr($mark, 0, 2)!='01') {
            $gtin=mb_substr($mark, 0, 14);
            $gtin=base_convert($gtin, 10, 16);
            $gtin=implode('', str_split(sprintf('%012s', $gtin), 2));
            $serial=mb_substr($mark, 14, 11);
            $serial=$this->prepareText($serial, 11).'2020';
            $mark='444D'.$gtin.$serial;
        }
        elseif($len==21
            && count($part21)==3
            && ctype_alnum($part21[0])
            && ctype_digit($part21[1])
            && ctype_alnum($part21[2])) {
            $mark='5246'.$this->prepareText($mark, mb_strlen($mark));
        }
        elseif($len==68 && preg_match('/^[0-9A-Z]+$/' , $mark) && mb_substr($mark, 0, 2)!='01') {
            $m=mb_substr($mark, 8, 23);
            $mark='C514'.$this->prepareText($m, mb_strlen($m));
        }
        elseif($len==150 && preg_match('/^[0-9A-Z]+$/' , $mark) && mb_substr($mark, 0, 2)!='01') {
            $m=mb_substr($mark, 0, 14);
            $mark='C51E'.$this->prepareText($m, mb_strlen($m));
        }
        else {
            $m=mb_substr($mark, 0, 30);
            $mark='0000'.$this->prepareText($m, mb_strlen($m));
        }
        $mark=mb_strtoupper($mark);
        return $mark;
    }

    // Ожидание готовности принтера
    public function wait() {
        if(isset($this->sets['Attempts'])) {
            $c=$this->sets['Attempts'];
        }
        else {
            $c=900;
        }
        $print=true;
        $open=true;
        while ($c>0) {
            $c--;
            if($this->isPrintInProgress()===false) {
                break;
            }
            if($this->sets['WaitTime']>0) {
                usleep($this->sets['WaitTime']);
            }
        }
    }

    // Форматирование текста
    function text_format($s, $w=null) {
        if(!$w)$w=$this->paper_width;
        $s=str_replace('#kkm_bold#', '', $s);
        // Заменяем #kkm_right# на пробелы
        $kkm_right="#kkm_right#"; # тэг сдвига вправо
        $space_n=$w-(mb_strlen($s)-mb_strlen($kkm_right)); // кол-во пробелов
        $s=str_replace($kkm_right, str_repeat(" ", max(0, $space_n)), $s); // заменяем тэг сдвига на пробелы

        // Заменяем #kkm_center# на пробелы
        $kkm_center="#kkm_center#"; // тэг сдвига в центр
        $space_n=floor(($w-(mb_strlen($s)-mb_strlen($kkm_center)))/2); // кол-во пробелов
        $s=str_replace($kkm_center, str_repeat(" ", max(0, $space_n)), $s); // заменяем тэг сдвига на пробелы
        if($s=='') $s=' ';
        return $s;
    }

    // Выполнение произвольной команды
    public function cmd($p) {
        $cmd=$p[1];
        $args='';
        $n=count($p);
        if($n>2) {
            for($i=2; $i<$n; $i+=2) {
                $l=$p[$i+1];
                if(is_numeric($p[$i])) {
                    $args.=$this->prepareArg($p[$i], $l);
                }
                else {
                    $args.=$this->prepareText($p[$i], $l);
                }
            }
        }
        return $this->writecmd($cmd, $args);
    }

    // Добавить код маркировки к товару
    function add_mark($mark) {
        $cmd='ff67';
        $l=mb_strlen($mark);
        $args=$this->prepareArg($l, 1);
        $args.=$this->prepareText($mark, $l);
        $r=$this->writecmd($cmd, $args);
        return $r;
    }

    // Проверка кода маркировки
    function check_mark($mark, $count=1, $unit=0) {
        $count=(float)str_replace(',', '.', $count);
        $cmd='ff61';
        $args='';
        $status=1;
        if($unit!=0) {
            $status=2;
        }
        else {
            $count=1;
        }
        $tlv=$this->make_tlv(2108, $unit, 1);

        $count_l=strlen($count);
        $floor_l=strlen((int)floor($count));
        $k=max($count_l-$floor_l-1, 0);
        if($k>0) {
            $count*=pow(10, $k);
        }

        $tlv.=$this->prepareArg(1023, 2).$this->prepareArg(2, 2).$this->prepareArg($k, 1).$this->prepareArg($count, 1);

        $l=strlen($mark);

        $args.=$this->prepareArg($status, 1);
        $args.='00';
        $args.=$this->prepareArg($l, 1);
        $args.=$this->prepareArg(strlen($tlv)/2, 1);
        $args.=$this->prepareText($mark, $l);
        $args.=$tlv;
        

        $r=$this->writecmd($cmd, $args);
        if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;

        $res=unpack('H*', $r{2})[1];
        $errors=array(
            '02'=>"ФН не содержит ключ проверки кода проверки этого КМ",
            '03'=>"Проверка невозможна, так как отсутствуют идентификаторы применения GS1 91 и/или 92 или их формат неверный.",
            '04'=>"Проверка КМ в ФН невозможна по иной причине"
        );
        if($res!='00') {
            $e=unpack('H*', $r{3})[1];
            $cmd='FF69';
            $args=$this->prepareArg(0, 1);
            $this->writecmd($cmd, $args);
            return array(
                'success'=>false,
                'txt'=>$errors[$e]
            );
        }

        $res=unpack('H*', $r{6})[1];
        if($res=='20') {
            $errors=array(
                '01'=>"Неверный фискальный признак ответа",
                '02'=>"Неверный формат реквизиов ответа",
                '03'=>"Неверный номер запроса в ответе",
                '04'=>"Неверный номер ФН",
                '05'=>"Неверный CRC блока данных",
                '07'=>"Неверная длина ответа"
            );
            $cmd='FF69';
            $args=$this->prepareArg(0, 1);
            $this->writecmd($cmd, $args);
            return array(
                'success'=>false,
                'txt'=>$errors[unpack('H*', $r{7})[1]]
            );
        }
        elseif($res=='ff') {
            $cmd='FF69';
            $args=$this->prepareArg(0, 1);
            $this->writecmd($cmd, $args);
            return array(
                'success'=>false,
                'txt'=>"Сервер не доступен"
            );
        }

        $res=unpack('H*', $r{8})[1];
        if(!$res) {
            $cmd='FF69';
            $args=$this->prepareArg(0, 1);
            $this->writecmd($cmd, $args);
            return array(
                'success'=>false,
                'txt'=>"Сервер проверки КМ не доступен"
            );
        }

        $errors=array(
            '00'=>'Проверка КП КМ не выполнена, статус товара ОИСМ не проверен',
            '01'=>'Проверка КП КМ выполнена в ФН с отрицательным результатом, статус товара ОИСМ не проверен',
            '03'=>'Проверка КП КМ выполнена с положительным результатом, статус товара ОИСМ не проверен',
            '10'=>'Проверка КП КМ не выполнена, статус товара ОИСМ не проверен (ККТ функционирует в автономном режиме)',
            '11'=>'Проверка КП КМ выполнена в ФН с отрицательным результатом, статус товара ОИСМ не проверен (ККТ функционирует в автономном режиме)',
            '13'=>'Проверка КП КМ выполнена в ФН с положительным результатом, статус товара ОИСМ не проверен (ККТ функционирует в автономном режиме)',
            '05'=>'Проверка КП КМ выполнена с отрицательным результатом, статус товара у ОИСМ некорректен',
            '07'=>'Проверка КП КМ выполнена с положительным результатом, статус товара у ОИСМ некорректен'
        );
        if(isset($errors[unpack('H*', $r{11})[1]])) {
            $cmd='FF69';
            $args=$this->prepareArg(0, 1);
            $this->writecmd($cmd, $args);
            return array(
                'result'=>false,
                'txt'=>$errors[$r{7}]
            );
        }
        $cmd='ff69';
        $args=$this->prepareArg(1, 1);
        $r=$this->writecmd($cmd, $args);
        if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;
        return array(
            'success'=>true
        );
    }

    // Печать заголовка чека
    public function print_header($title, $num) {
        $args=$this->prepareText($title, 30);
        $args.=$this->prepareArg($num, 2);
        return $this->writecmd('18', $args);
    }

    public function print_barcode($barcode) {
        /*$barcode=mb_substr($barcode, 0, 12);
        $barcode=dechex($barcode);
        $b=str_split($barcode, 2);
        $barcode=implode('', array_reverse($b));
        return $this->writecmd('C2', $barcode);*/
        usleep($this->sets['WaitTime']);
        $r=$this->print_qr($barcode, 3);
        if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;
        usleep($this->sets['WaitTime']);
        return $this->p('#kkm_center#'.$barcode);
    }

    public function print_qr($qr, $scale=3) {
        $qr_l=mb_strlen($qr);
        $step=64;
        for($i=0; $i*$step<$qr_l; $i++) {
            $args=$this->prepareArg(0, 1);
            $args.=$this->prepareArg($i, 1);
            $args.=$this->prepareText(mb_substr($qr, $i*$step, $step), $step);
            $r=$this->writecmd('DD', $args);
            if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;
        }

        if(is_numeric($scale)) {
            $scale=min(max(3, (int)$scale), 8);
        }
        $args=$this->prepareArg(3, 1);
        $args.=$this->prepareArg($qr_l, 2);
        $args.=$this->prepareArg(0, 1);
        
        $args.=$this->prepareArg(0, 1);
        $args.=$this->prepareArg(1, 1);
        $args.=$this->prepareArg($scale, 1);
        $args.=$this->prepareArg(0, 1);
        $args.=$this->prepareArg(3, 1);

        $args.=$this->prepareArg(1, 1);

        return $this->writecmd('DE', $args);
    }

    // Печать клише
    public function print_clishe() {
        return $this->writecmd('52');
    }

    // Чек коррекции
    public function correction($type, $sum) {
        $args=$this->prepareArg($sum, 5);
        if($type=='docIn') {
            $args.=$this->prepareArg(1, 1);
        }
        elseif($type=='docOut') {
            $args.=$this->prepareArg(2, 1);
        }
        elseif($type=='docRetIn') {
            $args.=$this->prepareArg(3, 1);
        }
        elseif($type=='docRetOut') {
            $args.=$this->prepareArg(4, 2);
        }
        else {
            return array(false, 'txt'=>$this->getErrorMessagebyCode(9));
        }
        return $this->writecmd('ff36', $args);
    }

    // Открытие денежного ящика
    public function open_cash_box($box) {
        $args=$this->prepareArg($box, 1);
        return $this->writecmd('28', $args);
    }

    // Запрос денежного регистра
    public function m($reg) {
        $res=$this->getKKMMoneyRegister($reg);
        $this->msg=$res;
        return 'ack';
    }

    // Оплата чека, ФФД 1.1
    public function tmde_ffd_11($pays, $sno) {
        $args='';
        // Оплата
        $args.=$this->prepareArg($pays[0], 5); // Налом
        $args.=$this->prepareArg($pays[1], 5); // Электронно
        $args.=str_repeat('0000000000', 11); // Еще способы, не используются. 11 способов по 5 байт
        $args.=$this->prepareArg($pays[2], 5); // Предоплата
        $args.=$this->prepareArg($pays[3], 5); // Постоплата
        $args.=$this->prepareArg($pays[4], 5); // Встречное предложение
        $args.=$this->prepareArg($this->ticket_sum-$this->ticket_total, 1); // Округление до рубля, нет

        if($this->base_settings['version']=='1.2') {
            $args.=str_repeat('0000000000', 6); // Налоги. 0 - расчитывается системой. 6 налогов по 5 байт
        }
        else {
            $args.=str_repeat('00', 6); // Налоги. 0 - расчитывается системой. 6 налогов по 1 байт
        }
        $sno_types=array(
            '01'=>1,
            '02'=>2,
            '04'=>4,
            '08'=>8,
            '10'=>16,
            '20'=>32,
        );
        $args.=$this->prepareArg($sno_types[$sno], 1); // Сно
        $args .= $this->prepareText('', 64);
        $request=$this->writecmd('ff45', $args);
        if($request===false || $request=='nak' || (is_array($request) && $request[0]===false)) return $request;
        $error_code=hexdec(unpack('H*', $request{3})[1]);
        if($error_code>0) {
            return array(
                'success'=>false,
                'code'=>$error_code,
                'txt'=>$this->getErrorMessagebyCode($error_code)
            );
        }
        $this->fd=hexdec(unpack('H*', $request{11}.$request{10}.$request{9}.$request{8})[1]);
        $this->fp=hexdec(unpack('H*', $request{15}.$request{14}.$request{13}.$request{12})[1]);
        if(mb_strlen(unpack('H*', $request)[1])/2>17) {
            $g=hexdec(unpack('H*', $request{20})[1]);
            $m=hexdec(unpack('H*', $request{19})[1]);
            $d=hexdec(unpack('H*', $request{18})[1]);
            $h=hexdec(unpack('H*', $request{17})[1]);
            $i=hexdec(unpack('H*', $request{16})[1]);
            $this->kkm_time=strtotime($g.'-'.$m.'-'.$d.'T'.$h.':'.$i);
        }
        else {
            $this->kkm_time=time();
        }
        return $request;
    }

    // Оплата чека, ФФД 1.05
    public function tmde_ffd_105($pays, $taxes) {
        $args='';

        // Способы оплаты
        for($i=0; $i<4; $i++) {
            if($pays[$i]) {
                $args.=$this->prepareArg($pays[$i], 5);
            }
            else {
                $args.=$this->prepareArg(0, 5);
            }
        }
        $args.='0000'; // Скидка или надбавка в %

        // Налоги
        for($i=0; $i<4; $i++) {
            if($taxes[$i]) {
                $args.=$this->prepareArg($taxes[$i], 1);
            }
            else {
                $args.=$this->prepareArg(0, 1);
            }
        }
        $args .= $this->prepareText('', 40);
        return $this->writecmd('85', $args);
    }

    // Добавляем товар в чек
    public function smde($line) {
        // Узнаем тип открытого докмента
        $status_kkm=$this->getKKMState_short();
        if($status_kkm['fr_mode']=='08' || $status_kkm['fr_mode']=='88') {
            if($line['ffd_version']=='1.1' || $line['ffd_version']=='1.2') {
                return $this->operation_v2(1, $line['price'], $line['quantity']/1000, $line['section'], $line['title'], $line['vat'], $line['psr'], $line['ppr']);
            }
            return $this->sell($line['price']/100, $line['quantity']/1000, $line['section'], $line['title'], $line['vat']);
        }
        elseif($status_kkm['fr_mode']=='28' || $status_kkm['fr_mode']=='a8') {
            if($line['ffd_version']=='1.1' || $line['ffd_version']=='1.2') {
                return $this->operation_v2(2, $line['price'], $line['quantity']/1000, $line['section'], $line['title'], $line['vat'], $line['psr'], $line['ppr']);
            }
            return $this->return_sell($line['price']/100, $line['quantity']/1000, $line['section'], $line['title'], $line['vat']);
        }
        return array(false, 'txt'=>$this->getErrorMessagebyCode(2));
    }

    public function set_table($table, $row, $field, $value) {
        $table=$this->prepareArg($table, 1);
        $row=$this->prepareArg($row, 2);
        $field=$this->prepareArg($field, 1);
        if(is_numeric($value)) {
            $value=$this->prepareArg($value, 40);
        }
        else {
            $value=$this->prepareText($value, 40);
        }
        return $this->writecmd('1E', $table.$row.$field.$value);
    }

    // Открыть чек
    public function b($type, $cashier, $print) {
        if($this->sets['AutoCancel']==1) {
            if($this->isOpenReceipt()===true) {
                $r=$this->cancelCheck();
                if($r===false || $r=='nak') return $r;
            }
        }
        if($print==0) {
            $r=$this->set_table(17, 1, 7, 1);
            if($r===false || $r=='nak') return $r;
        }

        $cashier_data=explode('{{user_inn}}', $cashier);
        $r=$this->set_table(2, 30, 2, $cashier_data[0]);
        $types=array(0=>0, 1=>2, 128=>128, 130=>130);
        $r=$this->writecmd('8D', $this->prepareArg($types[$type], 1));
        if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;
        if($cashier_data[1] && mb_strlen($cashier_data[1])==12) {
            $r=$this->set_tlv(1203, $cashier_data[1], 5);
        }
        return $r;
    }

    // Установка tlv
    public function set_tlv($tag, $value, $s, $add_operation=false) {
        $tlv=$this->make_tlv($tag, $value, $s);
        $cmd='ff0c';
        if($add_operation) {
            $cmd='FF4D';
        }
        if($tag==1171 || $tag==1225)$tlv='C8041000'.$tlv;
        return $this->writecmd($cmd, mb_strtoupper($tlv));
    }

    function add_kommitent($partner) {
        $r=$this->set_tlv('1222', 32, 1, true);
        if($r===false || $r=='nak' || $r[0]===false) return $r;
        if(mb_strlen($partner['inn'])>=10) {
            if(mb_strlen($partner['inn'])<12) $partner['inn'].='  ';
            $r=$this->set_tlv('1226', $partner['inn'], 5, true);
            if($r===false || $r=='nak' || $r[0]===false) return $r;
        }
        return $this->set_tlv(1171, '+'.$partner['phone'], 5, true);
        //if($r===false || $r=='nak' || $r[0]===false) return $r;
        //return $this->set_tlv(1225, str_replace('"', '', $partner['name']), 5, true);
    }

    public function marking($mark) {
        if($this->base_settings['version']=='1.2') {
            return $this->add_mark($mark);
        }
        $mark=$this->get_marking($mark);
        return $this->set_tlv(1162, $mark, 4);
    }

    // Внесение денег в кассу
    public function imde($summ) {
        if($summ>0) {
            return $this->writecmd('50', $this->prepareArg(abs($summ), 5));
        }
        else {
            return $this->writecmd('51', $this->prepareArg(abs($summ), 5));
        }
    }

    // Запрос статуса ККМ
    public function d() {
        $res=$this->getKKMState();
        if(is_array($res)) {
            //$this->msg=$res;
            $scheme = array(
                'cashieridx' => 'Порядковый номер оператора',
                'fr_soft_ver' => 'Версия ПО ККТ',
                'fr_soft_build' => 'Сборка ПО ККТ',
                'fr_soft_date' => 'Дата ПО ККТ', // dd-mm-yy
                'idxinroom' => 'Номер в зале',
                'docno' => 'Сквозной номер текущего документа',
                'fr_flags_text' => 'Флаги ККТ',
                'fr_mode_text' => 'Режим ККТ',
                'fr_submode_text' => 'Подрежим ККТ',
                'fr_port' => 'Порт ККТ',
                'date' => 'Дата',
                'time' => 'Время',
                'plant_no' => 'Заводской номер',
                'last_smena_no' => 'Номер последней закрытой смены',
                'rereg' => 'Количество перерегистраций',
                'rereg_left' => 'Количество оставшихся перерегистраций',
                'inn' => 'ИНН',
            );
            $this->results=array(
                array(true, 'txt'=>'<b>Запрос полного статуса ККМ</b>', 'code'=>0)
            );
            foreach($scheme as $code=>$title) {
                if(isset($res[$code])) {
                    if(is_array($res[$code])) {
                        $this->results[]=array(true, 'txt'=>'<b>'.$title.'</b>', 'code'=>0);
                        foreach($res[$code] as $res_txt) {
                            $this->results[]=array(true, 'txt'=>$res_txt, 'code'=>0);
                        }
                    }
                    else {
                        $this->results[]=array(true, 'txt'=>'<b>'.$title.'</b> - '.$res[$code], 'code'=>0);
                    }
                }
            }
            return 'ack';
        }
        else {
            return 'nak';
        }
    }

    // Продолжение печати
    public function continue_print() {
        return $this->writecmd('B0');
    }

    // Печать копии последнего чека
    public function c() {
        return $this->writecmd('8C');
    }

    // Протяжка ленты
    public function feed($n) {
        return $this->writecmd('29',  $this->prepareArg('3', 1).$this->prepareArg($n, 1));
    }

    // Печать простой строки
    public function p($text) {
        $strs=explode('#kkm_br#', $text);
        if(!$this->sets['delayPrint']) $flag='03';
        else $flag='83';
        foreach ($strs as $s) {
            if(mb_substr($s, 0, mb_strlen('#kkm_bold#'))=='#kkm_bold#') {
                $res=$this->print_bold($s);
            }
            else {
                $s=$this->text_format($s);
                if(mb_strlen($s)>$this->paper_width) {
                    for($an=0; $an<mb_strlen($s); $an+=$this->paper_width) {
                        $ss=mb_substr($s, $an, $this->paper_width);
                        $res=$this->writecmd('17',  $flag.$this->prepareText($ss, 40));
                        if($res===false || $res=='nak' || (is_array($res) && $res[0]===false)) {
                            break;
                        }
                    }
                }
                else {
                    $res=$this->writecmd('17',  $flag.$this->prepareText($s, 40));
                }
            }
            if($res===false || $res=='nak' || (is_array($res) && $res[0]===false)) {
                break;
            }
        }
        if((int)$this->sets['delayPrint']==1) {
            $res=$this->writecmd('17',  '03'.$this->prepareText('', 40));
        }
        return $res;
    }

    // Печать строки заданным шрифтом
    public function print_font($font, $text) {
        $strs=explode('#kkm_br#', $text);
        foreach ($strs as $s) {
            $s=$this->text_format($s, floor($this->paper_width/$font));
            $res=$this->writecmd('2F',  $this->prepareArg('3', 1).$this->prepareArg($font, 1).$this->prepareText($s, floor(40/$font)));
            if($res===false || $res=='nak' || (is_array($res) && $res[0]===false)) {
                break;
            }
        }
        return $res;
    }

    // Печать жирной строки
    public function print_bold($text) {
        $strs=explode('#kkm_br#', $text);
        foreach ($strs as $s) {
            $s=$this->text_format($s, floor($this->paper_width/2));
            $res=$this->writecmd('12',  $this->prepareArg('3', 1).$this->prepareText($s, 20));
            if($res===false || $res=='nak' || (is_array($res) && $res[0]===false)) {
                break;
            }
        }
        return $res;
    }

    // x-отчет
    public function x() {
        $res=$this->writecmd('40');
        if(is_array($res) && $res[0]===false && $res['code']==55) {
            $res=$this->writecmd('42');
            if($res===false || $res=='nak' || (is_array($res) && $res[0]===false)) {
                return $res;
            }
            $res=$this->writecmd('43');
        }
        return $res;
    }

    // z-отчет
    public function z($cashier=false) {
        $r=$this->writecmd('FF42');
        if($r===false || $r=='nak' || (is_array($r) && $r[0]===false)) return $r;
        if($cashier) {
            $cashier_data=explode('{{user_inn}}', $cashier);
            $r=$this->set_table(2, 30, 2, $cashier_data[0]);
            if($cashier_data[1] && mb_strlen($cashier_data[1])==12) {
                $this->set_tlv('1203', $cashier_data[1], 5);
            }
        }
        $request=$this->writecmd('41');
        $error_code=hexdec(unpack('H*', $request{1})[1]);
        if($error_code>0) {
            return array(
                'success'=>false,
                'code'=>$error_code,
                'txt'=>$this->getErrorMessagebyCode($error_code)
            );
        }
        $this->fd=hexdec(unpack('H*', $request{5}.$request{4}.$request{3}.$request{2})[1]);
        $this->fp=hexdec(unpack('H*', $request{9}.$request{8}.$request{7}.$request{6})[1]);
        return $request;
    }

    public function beep() {
        $res=$this->writecmd('13');
        if(is_array($res) && $res[0]===false && $res['code']==55) {
            $this->p('Бип-бип');
        }
    }

    function getKKMState_short() {
        $answer = $this->writecmd('10');
        if($answer==='nak') {
            return 'nak';
        }
        if(is_array($answer) && $answer[0]===false ) {
            return $answer;
        }
        $scheme = array(
            'lastcmd' => 1,
            'errcode' => 1,
            'cashieridx' => 1,
            'fr_flags' => 2,
            'fr_mode' => 1,
            'fr_submode' => 1,
            'operation_count' => 1,
            'bat_v' => 1,
            '220_v' => 1,
            'figovina' => 1,
            'key_error_code' => 1,
            'operation_count_2' => 1,
            'temp' => 1,
            'pre_fr_mode' => 1,
            'key_up_status' => 1,
            'last_print' => 1
        );
        $curstate = 0;
        $result = array();
        foreach ($scheme as $var => $len) {
            $part = substr($answer, $curstate, $len);
            $result[$var] = $this->unpackBinaryString($part);
            $curstate += $len;
        }
        return $result;
    }

    public function getKKMState() {
        $answer = $this->writecmd('11');
        if($answer==='nak') {
            return 'nak';
        }
        if(is_array($answer) && $answer[0]===false ) {
            return $answer;
        }
        $scheme = array(
            'lastcmd' => 1,
            'errcode' => 1,
            'cashieridx' => 1,
            'fr_soft_ver' => 2,
            'fr_soft_build' => 2,
            'fr_soft_date' => 3, // dd-mm-yy
            'idxinroom' => 1,
            'docno' => 2,
            'fr_flags' => 2,
            'fr_mode' => 1,
            'fr_submode' => 1,
            'fr_port' => 1,
            'fp_soft_ver' => 2,
            'fp_soft_build' => 2,
            'fp_soft_date' => 3,
            'date' => 3,
            'time' => 3,
            'plant_no' => 4,
            'last_smena_no' => 2,
            'rereg' => 1,
            'rereg_left' => 1,
            'figovina' => 2,
            'inn' => 6,
            'plant_no_2'=>2
        );
        $curstate = 0;
        $result = array();
        foreach ($scheme as $var => $len) {
            $part = substr($answer, $curstate, $len);
            $result[$var] = $this->unpackBinaryString($part);
            $curstate += $len;
        }
        $result['fr_flags'] = sprintf("%08b", hexdec($result['fr_flags']));
        $result['fr_mode_text'] = $this->getModeTitleByCode($result['fr_mode']);
        $result['fr_submode_text'] = $this->getSubModeTitleByCode($result['fr_submode']);
        $result['cashieridx']=hexdec($result['cashieridx']);
        $result['fr_soft_ver']=chr(hexdec($result['fr_soft_ver'][0].$result['fr_soft_ver'][1])).'.'.chr(hexdec($result['fr_soft_ver'][2].$result['fr_soft_ver'][3]));
        $result['fr_soft_build']=hexdec($result['fr_soft_build'][2].$result['fr_soft_build'][3].$result['fr_soft_build'][0].$result['fr_soft_build'][1]);
        $dd=hexdec($result['fr_soft_date'][0].$result['fr_soft_date'][1]);
        $mm=hexdec($result['fr_soft_date'][2].$result['fr_soft_date'][3]);
        $gggg='20'.hexdec($result['fr_soft_date'][4].$result['fr_soft_date'][5]);
        $result['fr_soft_date']=sprintf('%02s', $dd).'.'.sprintf('%02s', $mm).'.'.$gggg;
        $result['idxinroom']=hexdec($result['idxinroom']);
        $result['docno']=hexdec($result['docno'][2].$result['docno'][3].$result['docno'][0].$result['docno'][1]);
        $result['fr_flags_text'][]='Рулон операционного журнала (контрольной ленты): '.($yn[$result['fr_flags'][0]]?'нет':'есть');
        $result['fr_flags_text'][]='Рулон чековой ленты: '.($yn[$result['fr_flags'][1]]?'нет':'есть');
        $result['fr_flags_text'][]='Оптический датчик операционного журнала (контрольной ленты): '.($yn[$result['fr_flags'][6]]?'бумаги нет':'бумага есть');
        $result['fr_flags_text'][]='Оптический датчик чековой ленты: '.($yn[$result['fr_flags'][7]]?'бумаги нет':'бумага есть');
        $result['fr_flags_text'][]='Рычаг термоголовки контрольной ленты: '.($yn[$result['fr_flags'][8]]?'поднят':'опущен');
        $result['fr_flags_text'][]='Рычаг термоголовки чековой ленты: '.($yn[$result['fr_flags'][9]]?'поднят':'опущен');
        $result['fr_flags_text'][]='Крышка корпуса ККТ: '.($yn[$result['fr_flags'][10]]?'поднята':'опущена');
        $result['fr_flags_text'][]='Денежный ящик: '.($yn[$result['fr_flags'][11]]?'открыт':'закрыт');
        $result['fr_flags_text'][]='Крышка корпуса ККТ контрольной ленты: '.($yn[$result['fr_flags'][12]]?'поднята':'опущена');
        $result['fr_port']=hexdec($result['fr_port'][0].$result['fr_port'][1]);
        $dd=hexdec($result['date'][0].$result['date'][1]);
        $mm=hexdec($result['date'][2].$result['date'][3]);
        $gggg='20'.hexdec($result['date'][4].$result['date'][5]);
        $result['date']=sprintf('%02s', $dd).'.'.sprintf('%02s', $mm).'.'.$gggg;
        $hh=hexdec($result['time'][0].$result['time'][1]);
        $mm=hexdec($result['time'][2].$result['time'][3]);
        $ss=hexdec($result['time'][4].$result['time'][5]);
        $result['time']=sprintf('%02s', $hh).':'.sprintf('%02s', $mm).':'.sprintf('%02s', $ss);
        $result['plant_no']=hexdec(rtrim($result['plant_no'][6].$result['plant_no'][7].$result['plant_no'][4].$result['plant_no'][5].$result['plant_no'][2].$result['plant_no'][3].$result['plant_no'][0].$result['plant_no'][1], '0'));
        $result['plant_no']=sprintf('%06s', $result['plant_no']);
        $result['last_smena_no']=hexdec($result['last_smena_no']);
        $result['rereg']=hexdec($result['rereg']);
        $result['rereg_left']=hexdec($result['rereg_left']);
        $result['inn']=hexdec(rtrim($result['inn'][10].$result['inn'][11].$result['inn'][8].$result['inn'][9].$result['inn'][6].$result['inn'][7].$result['inn'][4].$result['inn'][5].$result['inn'][2].$result['inn'][3].$result['inn'][0].$result['inn'][1], '0'));
        return $result;
    }
    public function is24HourPass() {
        return $this->getStateFlag(self::FLAGS2_24HOUR_PASS, 2) == 1;
    }
    public function isSessionOpened() {
        return $this->getStateFlag(self::FLAGS2_SESSION_OPENED, 2) == 1;
    }
    public function isMoneyBoxOpened() {
        return $this->getStateFlag(self::FLAGS_MONEYBOX_OPENED) == 1;
    }
    public function isPaperOut() {
        return $this->getStateFlag(self::FLAGS_PAPER_OUT) == 0;
    }
    public function isCaseOpened() {
        return $this->getStateFlag(self::FLAGS_CASE_OPENED) == 1;
    }
    public function openMoneyBox($num = 0) {
        return $this->writecmd('28', sprintf('%02d', $num));
    }
    public function setDateConfirm($ts = null) {
        return $this->setDate($ts, '23');
    }
    // печать отчета или чего еще
    public function isPrintInProgress() {
        $state = $this->getKKMState_short();
        // контролируем submode
        if($state=='nak') return 'nak';
        if(!isset($state['fr_submode'])) {
            $state['fr_submode']='05';
        }
        return $state['fr_submode'] == '05';
    }

    // Открыт чек или закрыт
    public function isOpenReceipt() {
        $status_kkm=$this->getKKMState_short();
        if(isset($status_kkm['fr_mode'])) {
            $status_kkm=mb_substr($status_kkm['fr_mode'], 1, 1);
            if($status_kkm=='8') {
                $status_kkm;
                return true;
            }
        }
        return false;
    }

    public function setDate($ts = null, $cmd = '22') {
        if ($ts == null or !date('Ymd', $ts)) {
            $ts = time();
        }
        if (!in_array($cmd, array('22', '23'))) {
            $cmd = '22';
        }
        $time = sprintf('%02x%02x%02x', date('d', $ts), date('m', $ts), date('y', $ts));
        return $this->writecmd($cmd, $time);
    }
    public function setTime($ts = null) {
        if ($ts == null or !date('His', $ts)) {
            $ts = time();
        }
        $time = sprintf('%02x%02x%02x', date('H', $ts), date('i', $ts), date('s', $ts));
        return $this->writecmd('21', $time);
    }
    # аннулирование всего чека
    public function cancelCheck() {
        return $this->writecmd('88');
    }
    public function cutCheck($cut = 1) {
        $cut = (bool) $cut; // 1 - неполная, 0 - полная
        return $this->writecmd('25', sprintf('%02d', $cut));
    }
    /*
     * Тип документа (1 байт): 0 – продажа;
    1 – покупка;
    2 – возврат продажи;
    3 – возврат покупки
     */
    public function openNewCheck($type = 1) {
        return $this->writecmd('8D', sprintf('%02d', $type));
    }
    # открыть смену
    public function openSession($cashier=false) {
        /*$ts=time();
        $this->setDate($ts, '22');
        $this->setDate($ts, '23');
        $this->setTime($ts);*/
        $r=$this->writecmd('FF41');
        if($r===false || $r=='nak') return $r;
        if($cashier) {
            $cashier_data=explode('{{user_inn}}', $cashier);
            $r=$this->set_table(2, 30, 2, $cashier_data[0]);
            if($cashier_data[1] && mb_strlen($cashier_data[1])==12) {
                $this->set_tlv('1203', $cashier_data[1], 5);
            }
        }
        else {
            $r=$this->set_table(2, 30, 2, 'Сист. администратор');
        }
        $request=$this->writecmd('E0');
        $error_code=hexdec(unpack('H*', $request{1})[1]);
        if($error_code>0) {
            return array(
                'success'=>false,
                'code'=>$error_code,
                'txt'=>$this->getErrorMessagebyCode($error_code)
            );
        }
        $this->fd=hexdec(unpack('H*', $request{5}.$request{4}.$request{3}.$request{2})[1]);
        $this->fp=hexdec(unpack('H*', $request{9}.$request{8}.$request{7}.$request{6})[1]);
        return $request;
    }


    protected function getStateFlag($bit, $reg = 1) {
        if (!($state = $this->getKKMState_short())) {
            throw new Exception('Failed to get KKM state');
        }
        $reg = $reg == 2 ? 'fp_flags' : 'fr_flags';
        $tmp = strrev($state[$reg]);
        return $tmp{$bit};
    }
    protected function makeBinaryCmd($cmdcode, $data, $wPass = true) {
        $passwd = $wPass ? sprintf("%02x000000", $this->getCashierPassword()) : '';
        error_log($data);
        $cmd = pack('H*', $cmdcode . $passwd) . $this->escapeData(pack('H*', $data));
        $msg_len = pack('C', strlen($cmd));
        $cmdcrc = $this->makecrc($msg_len . $cmd);
        $cmd = pack('C*', self::STX) . $msg_len . $cmd . pack('C*', $cmdcrc);
        return $cmd;
    }
    protected function escapeData($data) {
        return $data;
    }
    public function writecmd($cmdcode, $data = '') {
        if($cmdcode!='11' && $cmdcode!='10') {
            $this->wait();
        }
        $wPass = !in_array($cmdcode, array('16'));
        $cmd = $this->makeBinaryCmd($cmdcode, $data, $wPass);
        $answer = false;
        try {
            $ack = $this->sendBinary($cmd, $this->sets['WaitTimeACK'], 1); // таймаут 500мс на ожидание ACK
            if (is_null($ack)) { // timeout?
                //throw new Exception('Response wait timeout. Link not ready');
                $answer=false;
            } else if ($this->isACK($ack)) {
                $answer = $this->readAnswer($cmdcode);
            } else {
                $answer='nak';
                $this->log('NOT ACK'); // что делать в этом случае?
                // Если в ответ на сообщение ФР получен NAK, сообщение не повторяется, ФР ждет уведомления ENQ для повторения ответа
            }
        }
        catch (Exception $e) {
            throw $e;
        }
        if(in_array($this->sets['PortType'], ['remote', 'socket'])) {
            //$this->readAnswer();
        }
        return $answer;
    }
    protected function readAnswer($cmdcode=null) {
        // пингуем enq-ами, если отвечает ack-ами, значит готовит данные, ждем
        $cycles = 100;
        // читаем первый байт ответа
        $ans = $this->readBinaryAnswer(1, $this->sets['WaitTimeFR']); // таймаут начала передачи от ФР. может быть большим
        do {
            if ($this->isSTX($ans)) {
                break;
            }
            else if ($this->isACK($ans)) { // пытаемся читать ответ
                $this->log('<B');
                $ans = $this->enq();
                continue;
            }
            else if (is_null($ans)) { // timeout
                $ans = $this->enq();
                continue;
            }
            else {
                $this->log('Expected STX in the beginning of answer. Got ' . $this->byteCode($ans));
                return false;
            }
        } while ($cycles-- > 0);

        $len_b = $this->readBinaryAnswer(1, $this->sets['WaitTimeFR']);
        $len = $this->byteCode($len_b);
        $ans = $this->readBinaryAnswer($len + 1, $this->sets['WaitTimeFR']);
        if ($this->checkCRC($len_b . $ans)) {
            $this->log('CRC MATCH');
            $this->ack();
            // проверим код ошибки
            $n=mb_strlen($cmdcode)/2;
            $errcode = $this->byteCode($ans{$n}); // длина пропущена, после нее код команды, а затем байт ошибки
            if ($errcode != 0) {
                $error=$this->raiseError($errcode);
                $this->results[]=$error['txt'];
                return $error;
            }
            elseif($cmdcode!='11' && $cmdcode!='FF03' && $cmdcode!='FF3F') {
                $this->results[]=array(true, 'txt'=>'Выполнено успешно', 'code'=>0);
            }
        } else {
            $this->nak();
        }
        return $ans;
    }
    // полное сообщение в бинарном виде без ведущего STX
    protected function checkCRC($answer) {
        $crc = 0;
        for ($i = 0, $len = strlen($answer); $i < $len; $i++) {
            $byte = $this->byteCode(substr($answer, $i, 1));
            if ($i == ($len - 1)) {
                break;
            }
            $crc ^= $byte;
        }
        return ($crc == $byte);
    }
    protected function raiseError($errorCode) {
        return array(false, 'txt'=>$this->getErrorMessagebyCode($errorCode), 'code'=>$errorCode);
    }
    protected function getSubModeTitleByCode($mode) {
        $const = 'SHTRIH_KKMSUBMODE_' . hexdec($mode);
        if (defined($const)) {
            return constant($const);
        }
        return 'Неизвестный подрежим';
    }
    protected function getModeTitleByCode($mode) {
        $mode = hexdec($mode);
        $submode = ($mode & 0xF0) >> 4;
        $mode = ($mode & 0x0F);
        $const = 'SHTRIH_KKMMODE_' . $mode . '_' . $submode;
        if (defined($const)) {
            return constant($const);
        }
        return 'Неизвестный режим';
    }
    protected function getErrorMessagebyCode($errorCode) {
        $errorconst = 'SHTRIH_ERROR_CODE_' . $errorCode;
        if (defined($errorconst)) {
            return constant($errorconst);
        }
        return 'unknown error';
    }
    // переводит 1000 (5 байт) в e803000000
    protected function prepareArg($num, $bytes) {
        return implode('', array_reverse(str_split(sprintf('%0' . ($bytes * 2) . 'x', $num), 2)));
    }
    protected function prepareText($text, $bytes = 40) {
        if (preg_match('//u', $text)) { // is UTF
            $text = iconv('UTF-8', 'cp1251//IGNORE', $text);
        }
        $text_code = '';
        for ($i = 0, $n = strlen($text); $i < $n; $i++) {
            $byte = sprintf('%x', ord($text{$i}));
            $text_code .= $byte;
        }
        $maxlen = $bytes * 2;
        return str_pad(substr($text_code, 0, $maxlen), $maxlen, '0', STR_PAD_RIGHT); // 40 bytes max!
    }

    public function read_table($table, $row, $cell, $unpack=true) {
        $args=$this->prepareArg($table, 1);
        $args.=$this->prepareArg($row, 2);
        $args.=$this->prepareArg($cell, 1);
        $res=$this->writecmd('1F', $args);
        if($unpack) {
            $n=strlen($res);
            $ans='';
            for($i=$n-2; $i>=2; $i-=1) {
                $ans.=unpack('H*', $res{$i})[1];
            }
        }
        else {
            $ans=mb_substr(iconv('cp1251', 'UTF-8', $res), 1, -1);
        }
        return $ans;
    }

    // Операция V2
    public function operation_v2($ticket_type, $price, $qty, $section, $text, $vt, $psr, $ppr) {
        $tax=$this->read_table(6, $vt, 1); // Узнаем ставку НДС в % из таблицы
        $tax=hexdec($tax)/100;
        $tax_c=2**($vt-1); // 2 возведенное в степень равную номеру налоговой группы уменьшенному на еденицу
        $ticket_type = $this->prepareArg($ticket_type, 1);
        $sum=$this->prepareArg(round($price*$qty), 5);
        $tax_sum=$this->prepareArg(round($price/(100+$tax)*$tax*$qty), 5);
        $tax_c = $this->prepareArg($tax_c, 1);
        $qty = $this->prepareArg($qty*1000000, 6);
        $price = $this->prepareArg($price, 5);
        $vt = $this->prepareArg($vt, 1);
        $section = $this->prepareArg($section, 1);
        $psr = $this->prepareArg($psr, 1);
        $ppr = $this->prepareArg($ppr, 1);
        $args=$ticket_type.$qty.$price.$sum.$tax_sum.$tax_c.$section.$psr.$ppr.$this->prepareText($text, 128);
        return $this->writecmd('ff46', $args);
    }

    // Продажа
    // цена передается как дробное. приводится внутри к копейкам
    // $qty передается как дробное, округлется до 3 знаков после запятой
    public function sell($price, $qty, $section, $text = 'sell', $vt=0) {
        $qty = $this->prepareArg($qty * 1000, 5);
        $price = $this->prepareArg(round($price * 100), 5);
        $vt = $this->prepareArg($vt, 1);
        $section = $this->prepareArg($section, 1);
        $args = sprintf('%10s%10s%02d%02d000000%80s', $qty, $price, $section, $vt, $this->prepareText($text, 40));
        return $this->writecmd('80', $args);
    }

    // Возврат продажи
    // цена передается как дробное. приводится внутри к копейкам
    // $qty передается как дробное, округлется до 3 знаков после запятой
    public function return_sell($price, $qty, $section, $text = 'sell', $vt=0) {
        $qty = $this->prepareArg($qty * 1000, 5);
        $price = $this->prepareArg(round($price * 100), 5);
        $vt = $this->prepareArg($vt, 1);
        $section = $this->prepareArg($section, 1);
        $args = sprintf('%10s%10s%02d%02d000000%80s', $qty, $price, $section, $vt, $this->prepareText($text, 40));
        return $this->writecmd('82', $args);
    }


    // сумма - дробная в рублях.
    public function closeCheck($sum, $print_line = '') {
        $args = $this->prepareArg($sum * 100, 5); // сумма наличных
        $args .= $this->prepareArg(0, 5);
        $args .= $this->prepareArg(0, 5);
        $args .= $this->prepareArg(0, 5); // сумма типа оплаты 4
        $args .= $this->prepareArg(0, 2); // скидка / надбавка
        $args .= '00000000'; // налоги
        $args .= $this->prepareText($print_line, 40);
        return $this->writecmd('85', $args);
    }
    public function getCheckSubtotal() {
        $ans = $this->writecmd('89');
        $ans = $this->unpackDigits(substr($ans, 3, 5));
        return ($ans / 100);
    }
    public function getKKMSumm() {
        return $this->getKKMMoneyRegister(241) / 100;
    }
    protected function getKKMMoneyRegister($no) {
        $ans = $this->writecmd('1A', sprintf('%x', $no));
        $ans = $this->unpackDigits(substr($ans, 3, 6));
        return $ans;
    }
    protected function getKKMOperRegister($no) {
        $ans = $this->writecmd('1B', sprintf('%x', $no));
        $ans = $this->unpackDigits(substr($ans, 3, 2));
        return $ans;
    }
    protected function unpackDigits($ans) {
        $ans = unpack('H*', implode('', array_reverse(str_split($ans))));
        $ans = reset($ans);
        return hexdec($ans);
    }
    public function getLastDocumentNumber() {
        return $this->getKKMOperRegister(152);
    }

    public function returnMoney($price, $qty, $section, $simulate = 0) {
    }

    protected function textToHex($text) {
        $text = iconv(CFG_SYSTEM_INTERNAL_ENCODING, 'ibm866//IGNORE', $text);
        $packed = $this->packString($text);
        return $this->unpackBinaryString($packed);
    }

    protected function BCD2num($bcd) {                  // Функция преобразования из BCD в человекочитаемый вид

        for($i=1,$res="";$i<strlen($bcd);++$i) {                // Перебираем побайтно
            $ln = sprintf("%08b",ord($bcd{$i}));                // Получаем строку 8 символов
            $resarr = str_split($ln, 4);                        // Разбиваем на 2 части по 4
            $result .= bindec($resarr[0]).bindec($resarr[1]);   // Переводим в нормальный вид и склеиваем
        }
        return (intval($result))/100;               // Возвращаем float вроде 1234.56
    }

    protected function num2BCD($num) {                  // Функция преобразования в BCD из человекочитаемого вида
        if (floatval(strlen($num)/2*2)!=(strlen($num)/2*2)) $num = "0".$num;

        for($i=0,$res="";$i<strlen($num);$i+=2) {               // Перебираем
            $result .= chr(bindec(decbin($num{$i}).decbin($num{$i+1})));
        }

        return $result;
    }
}