В этой статье мы рассмотрим решение сложной задачи по реализации протокола обмена данными с внешним оборудованием, в частности, каплеструйным принтером. Особое внимание уделим расчету контрольных сумм по алгоритму "дополнение до двух" (2's complement) и разберем нюансы передачи байтовых данных из 1С, которые могут приводить к ошибкам.
Протокол взаимодействия с оборудованием часто использует контрольные суммы для проверки целостности передаваемых данных. Чем сложнее алгоритм, тем меньше вероятность незамеченных ошибок. В нашем случае, описание алгоритма звучит следующим образом: "The data checksum is the 2s-complement value of the modulo-256 sum of all the bytes in the message." Разберем этот алгоритм по шагам, чтобы полностью понять его логику.
Нам необходимо сложить шестнадцатеричные значения всех байтов, составляющих передаваемое сообщение. Суммирование производится как для беззнаковых 8-битных чисел.
Рассмотрим пример сообщения из документации протокола:
1B 02 00 1D 01 01 0B 54 49 4A 20 50 72 69 6E 74 65 72 1B 03
Если мы сложим все эти шестнадцатеричные значения, то получим общую сумму. Для данного примера, сумма всех байтов в шестнадцатеричной системе счисления составляет 450h.
После получения общей суммы, которая может быть больше одного байта (например, 450h), нам нужно отбросить все "старшие" биты, оставив только младший байт. Эта операция эквивалентна применению "побитового И" с маской FFh (или взятию остатка от деления на 256 в десятичной системе). Фактически, мы оставляем только значение, которое помещается в один байт.
Для нашего примера, где общая сумма равна 450h:
450h AND 0FFh = 50h
Таким образом, младший байт суммы, который мы будем использовать для дальнейших расчетов, равен 50h.
Это финальный шаг, который преобразует полученный младший байт в итоговую контрольную сумму. Дополнение до двух – это распространенный метод представления отрицательных целых чисел в компьютерах, но в данном контексте он используется для получения контрольной суммы. Существует несколько способов его вычисления, которые приводят к одному и тому же результату.
Согласно описанию протокола, дополнение до двух для 8-битного значения можно вычислить, вычтя его значение из 100h (что эквивалентно 256 в десятичной системе). Это наиболее прямолинейный метод, описанный в документации.
Для нашего примера, где младший байт суммы равен 50h:
100h - 50h = B0h
Таким образом, контрольная сумма для данного сообщения составляет B0h. Эта сумма затем добавляется в конец передаваемого сообщения.
Этот метод является общепринятым способом вычисления дополнения до двух и широко используется в программировании. Он дает тот же результат, что и вычитание из 256. Давайте разберем его пошагово:
50h это 01010000b.01010000b это будет 10101111b (что в шестнадцатеричной системе равно AFh).10101111b + 1 будет 10110000b (что равно B0h).В коде этот метод часто записывается в виде выражения (~X + 1) & 0xFF, где X — это младший байт суммы. Давайте разберем каждый элемент этого выражения:
(result & 0xff): Эта операция гарантирует, что мы работаем только с младшим байтом суммы. Если переменная result содержит сумму, которая превышает 255 (например, 450h), эта операция "обрежет" ее до 50h, оставляя только младший байт.~(result & 0xff): Оператор ~ (тильда) выполняет побитовую инверсию всех битов байта. То есть, каждый 0 становится 1, а каждый 1 становится 0. Если наш байт 50h (01010000b), то после инверсии он станет AFh (10101111b).(~(result & 0xff) + 1): К инвертированному значению мы добавляем единицу. Для AFh + 1 получаем B0h.(... + 1) & 0xFF: Последняя операция & 0xFF (побитовое И с маской FFh) снова маскирует результат. Она нужна для того, чтобы гарантировать, что итоговая контрольная сумма останется в пределах одного байта. Например, если бы промежуточный результат был FFh, то прибавление единицы дало бы 100h. Маскирование & 0xFF преобразует 100h в 00h, что соответствует правильному дополнению до двух для 00h.Для расчета контрольной суммы в 1С мы можем использовать следующий подход. Преобразуем строковое представление байтов в числовые значения, суммируем их, а затем применяем описанные выше шаги. Важно помнить, что в 1С нет прямых побитовых операторов для чисел, но их можно эмулировать или использовать арифметические эквиваленты.
Рассмотрим пример из форума, где для сообщения, начинающегося с 1B 02 00 1D 02 01 0B и содержащего текст 54 49 4A 20 50 72 69 6E 74 65 72, а также данные 03 0A 30 34 2F 32 37 2F 32 30 31 39 1B 03, ожидается контрольная сумма ABh.
Для простоты представим, что мы можем получить десятичное значение каждого байта. Вот как могла бы выглядеть функция расчета:
// Функция для преобразования HEX-строки одного байта в десятичное число
// Например, "1B" -> 27, "50" -> 80
Функция HexВДесятичное(ЗначениеHex)
Возврат Integer(ЗначениеHex, 16); // В 1С такой функции нет напрямую,
// но можно реализовать через COM-объект или свой парсер.
// Для примера считаем, что она есть.
КонецФункции
// Основная функция расчета контрольной суммы
Функция РассчитатьКонтрольнуюСумму(СтрокаСообщенияВБайтахHEX)
СуммаБайтов = 0;
// Разбиваем строку на отдельные HEX-байты, например, "1B 02 00" -> ["1B", "02", "00"]
МассивБайтовHEX = СтрРазделить(СтрокаСообщенияВБайтахHEX, " ", Ложь);
Для Каждого БайтHEX Из МассивБайтовHEX Цикл
Если Не Пустая(БайтHEX) Тогда
Попытка
// Шаг 1: Суммирование всех байтов
ДесятичноеЗначениеБайта = HexВДесятичное(БайтHEX);
СуммаБайтов = СуммаБайтов + ДесятичноеЗначениеБайта;
Исключение
// Обработка ошибок, если БайтHEX некорректен
Продолжить;
КонецПопытки;
КонецЕсли;
КонецЦикла;
// Шаг 2: Получение младшего байта суммы (Modulo-256)
// Эквивалентно "СуммаБайтов AND 0FFh"
МладшийБайтСуммы = СуммаБайтов % 256;
// Шаг 3: Вычисление дополнения до двух
// Эквивалентно "100h - МладшийБайтСуммы" или "(~МладшийБайтСуммы + 1) & 0xFF"
// В 1С это можно реализовать так:
КонтрольнаяСумма = (256 - МладшийБайтСуммы) % 256;
Возврат КонтрольнаяСумма; // Вернет число от 0 до 255
КонецФункции
// Пример использования функции:
// Исходное сообщение для "TIJ Printer" из примера 0:
// СтрокаСообщения1 = "1B 02 00 1D 01 01 0B 54 49 4A 20 50 72 69 6E 74 65 72 1B 03";
// РасчетнаяКонтрольнаяСумма1 = РассчитатьКонтрольнуюСумму(СтрокаСообщения1); // Должно дать 176 (B0h)
// Исходное сообщение из примера 3:
// СтрокаСообщения2 = "1B 02 00 1D 02 01 0B 54 49 4A 20 50 72 69 6E 74 65 72 03 0A 30 34 2F 32 37 2F 32 30 31 39 1B 03";
// РасчетнаяКонтрольнаяСумма2 = РассчитатьКонтрольнуюСумму(СтрокаСообщения2); // Должно дать 171 (ABh)
Обратите внимание, что прямое преобразование HEX-строки в числовое значение в 1С (как HexВДесятичное() в примере) требует вспомогательных функций или использования COM-объектов, поскольку 1С не имеет встроенных функций типа Integer.Parse с указанием системы счисления. Функцию СтрКод() можно использовать только для получения ASCII-кода символа, а не для парсинга шестнадцатеричных строк. Для реального применения, мы бы реализовали свой парсер, который бы попарно читал символы HEX и преобразовывал их в десятичное число.
SendData и кодировкиПосле того как мы убедились в корректности расчета контрольной суммы на стороне 1С, мы столкнулись с неожиданной проблемой: устройство принимало некоторые сообщения, но отклоняло другие, хотя контрольная сумма была рассчитана по тому же алгоритму и на нашей стороне казалась верной. Ответ от устройства последовательно указывал на ошибку в контрольной сумме.
Это навело нас на мысль, что проблема кроется не в расчете, а в том, как 1С фактически передает байты через сокет. Мы выяснили, что при конкатенации строк и их отправке через метод Сокет.SendData(Сообщение + Символ(КонтрольнаяСумма)), 1С может некорректно преобразовывать символы в байты.
Внутренняя работа со строками в 1С (как и во многих Windows-приложениях) подразумевает использование Unicode (чаще всего UTF-16LE). Когда вы передаете строковую переменную в метод, который ожидает последовательность байтов (например, для TCP/IP сокета), происходит неявное преобразование кодировки. Если протокол ожидает строго определенные ASCII или однобайтовые UTF-8 символы, а 1С отправляет их в другой кодировке (например, добавляя "лишние" нулевые байты для UTF-16 символов), то получатель (наше внешнее устройство) будет интерпретировать входящий поток байтов не так, как задумано. Это приведет к тому, что устройство рассчитает свою контрольную сумму для "искаженного" сообщения, и она, естественно, не совпадет с той, что мы ему отправили.
В нашем случае, даже изменение одного символа в сообщении (например, с "TIJ Printer" на "TIJ Printep") приводило к отказу устройства, хотя математический расчет контрольной суммы на стороне 1С оставался полностью верным. Более того, при тестировании разных символов наблюдалась "случайная" картина: некоторые символы принимались, другие – нет. Это неопровержимо указывало на проблему с байтовым представлением отправляемых данных, а не с алгоритмом расчета.
Поскольку 1С, при работе со строками, могла вносить неочевидные искажения в байтовый поток, наиболее надежным решением стало использование методов, которые позволяют точно контролировать каждый отправляемый байт. Такой подход исключает любые неоднозначности кодировки, поскольку мы оперируем непосредственно байтами, а не символами, которые могут быть интерпретированы по-разному.
Самым эффективным способом оказалось создание и использование внешней компоненты (или прямое взаимодействие с .NET/COM объектами, если это возможно), которая позволяет формировать и отправлять данные в виде явных байтовых массивов (byte[]). Этот подход обходит внутренние механизмы 1С по работе со строками и гарантирует, что на уровне сокета будут переданы именно те байты, которые мы хотим.
В примере из форума автор успешно решил проблему, используя C# для формирования байтового массива и его отправки:
new byte[] { 0x1B, 0x02, 0x00, 0x1D, 0x01, 0x01, 0x0B, 0x54, 0x49, 0x4A, 0x20, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x65, 0x70, 0x1B, 0x03, 0xB2 }
Такой подход обеспечивает максимальный контроль над отправляемыми данными и исключает любые неоднозначности кодировки, поскольку мы оперируем непосредственно байтами. Для 1С это означает разработку внешней компоненты, которая будет принимать список чисел (байтов) и отправлять их через свой сокет.
ДвоичныеДанные.
Альтернативным решением, если нет возможности использовать внешние компоненты, может быть более тщательное формирование байтового потока средствами 1С, например, через объект ДвоичныеДанные. Мы можем создавать объект ДвоичныеДанные из массива чисел (где каждое число от 0 до 255 представляет собой байт) и затем использовать методы, которые могут работать с этими двоичными данными напрямую, если такие методы есть у сокета или другого объекта для передачи данных.
Пример такого концептуального подхода в 1С:
// Создаем массив десятичных значений байтов сообщения
МассивДесятичныхБайтов = Новый Массив();
// Добавляем служебные байты
МассивДесятичныхБайтов.Добавить(27); // 0x1B (ESC)
МассивДесятичныхБайтов.Добавить(2); // 0x02 (STX)
МассивДесятичныхБайтов.Добавить(0); // 0x00
МассивДесятичныхБайтов.Добавить(29); // 0x1D
МассивДесятичныхБайтов.Добавить(1); // 0x01
МассивДесятичныхБайтов.Добавить(1); // 0x01
МассивДесятичныхБайтов.Добавить(11); // 0x0B (Длина поля "TIJ Printer")
// Добавляем байты текста "TIJ Printep"
// 'T' -> 84, 'I' -> 73, 'J' -> 74, ' ' -> 32, 'P' -> 80,
// 'r' -> 114, 'i' -> 105, 'n' -> 110, 't' -> 116, 'e' -> 101, 'p' -> 112
МассивДесятичныхБайтов.Добавить(84);
МассивДесятичныхБайтов.Добавить(73);
МассивДесятичныхБайтов.Добавить(74);
МассивДесятичныхБайтов.Добавить(32);
МассивДесятичныхБайтов.Добавить(80);
МассивДесятичныхБайтов.Добавить(114);
МассивДесятичныхБайтов.Добавить(105);
МассивДесятичныхБайтов.Добавить(110);
МассивДесятичныхБайтов.Добавить(116);
МассивДесятичныхБайтов.Добавить(101);
МассивДесятичныхБайтов.Добавить(112); // Для "p" вместо "r"
// Добавляем служебные байты окончания сообщения
МассивДесятичныхБайтов.Добавить(27); // 0x1B (ESC)
МассивДесятичныхБайтов.Добавить(3); // 0x03 (ETX)
// Рассчитываем контрольную сумму для сформированного массива
// (Предполагаем наличие функции, которая суммирует элементы массива чисел)
ОбщаяСумма = 0;
Для Каждого БайтДесятичный Из МассивДесятичныхБайтов Цикл
ОбщаяСумма = ОбщаяСумма + БайтДесятичный;
КонецЦикла;
МладшийБайтСуммы = ОбщаяСумма % 256;
КонтрольнаяСумма = (256 - МладшийБайтСуммы) % 256;
// Добавляем контрольную сумму в конец массива
МассивДесятичныхБайтов.Добавить(КонтрольнаяСумма);
// Теперь формируем ДвоичныеДанные из массива чисел
// В 1С нет прямого конструктора ДвоичныеДанные(МассивЧисел),
// но можно использовать объект ПотокВПамяти и записывать туда байты.
// Или сохранять в файл и читать как ДвоичныеДанные.
Поток = Новый ПотокВПамяти();
Для Каждого БайтДесятичный Из МассивДесятичныхБайтов Цикл
Поток.ЗаписатьБайт(БайтДесятичный);
КонецЦикла;
Поток.УстановитьПозицию(0); // Перемещаем указатель в начало потока
ДвоичныеДанныеДляОтправки = Поток.Прочитать(); // Получаем ДвоичныеДанные
// Затем отправляем ДвоичныеДанные через сокет или другой механизм
// Например: Сокет.Отправить(ДвоичныеДанныеДляОтправки);
// Или записать в файл и передать файл, если есть такой способ работы с портом.
Важно убедиться, что используемый метод отправки данных (например, метод объекта сокета или другого объекта, работающего с портами) действительно принимает объект ДвоичныеДанные или корректно преобразует его в необходимый байтовый поток без дополнительных искажений. В случае отсутствия прямого метода, можно записать ДвоичныеДанные во временный файл и передать этот файл через соответствующий механизм, если он доступен.
Работа с низкоуровневыми протоколами и передачей байтовых данных из 1С требует особого внимания к деталям, особенно в части кодировок и преобразования строковых данных. Если расчет контрольной суммы на вашей стороне кажется верным, но внешнее устройство все равно отклоняет сообщения, следует в первую очередь подозревать проблемы с фактическим байтовым потоком, отправляемым программным обеспечением.
Использование явных байтовых массивов, формируемых через внешние компоненты или тщательно собранных с использованием объектов ДвоичныеДанные в 1С, может стать надежным решением подобных проблем. Это обеспечивает предсказуемость и корректность взаимодействия с внешним оборудованием, позволяя нам контролировать каждый отправляемый байт.
Мы успешно разобрались с расчетом контрольных сумм по методу "дополнение до двух" и выявили потенциальную ловушку при отправке байтовых последовательностей из 1С. Надеемся, это подробное руководство поможет вам избежать подобных сложностей в своих проектах и успешно настроить обмен данными с внешними устройствами.