Приобрел я на днях термопринтер (кассовый аппарат, если изволите) жене для магазина и решил что смогу его обуздать как истинный программист и сделать свою веб кассу, вместо того, что-бы использовать платные решения, ведь задача простая, печатать чеки, но это оказалось не так то и просто...
Вот так выглядит сие чудо, было куплено в соседнем магазине, без опознавательных знаков, названия фирмы и без всего интересного.
Из полезной информации есть только название модели "AW-5890C".
Это усложнило весь процесс, но я решил пойти с малого. Первой задачей было синхронизироваться через браузер с USB девайсом, посредством USB API.
И тут меня ждал подвох...
Казалось бы, всем знакомый (тем кто работал с USB в браузере) сниппет кода, для открытия соединения с USB девайсом:
navigator.usb.requestDevice({filters: []})
.then(usbDevice => {
usbDevice.open()
.then(() => console.log('Success'))
.catch(() => console.error('Bye bye'));
});
В первой строке мы запрашиваем все девайсы в браузере, что-бы пользователь мог выбрать девайс. (Свойство "filters" обязательное как аргумент, потому пустой массив)
После того, как пользователь выбрал, мы вызываем у девайса метод "open", что-бы открыть соединение с ним.
В результате:
Видим наше устройство, уже отлично, выбираем его и....
"Это победа!"
На поиск истины у меня ушло более чем несколько часов, и вот она:
Windows 10 - единоличная операционная система...
Оказывается, если вы подключили какое-либо устройство и система определила его периферический тип и установила драйвера, то она тут же добавляет его в свою особенную службу, тем самым, делая устройство "занятым".
Это говорит о том, что вы можете послать файл на распечатку в принтер, который определился системой, но вы не можете воспользоваться USB портом, т.к. он занят принтером, который определила система.
Об этом же и говорит ошибка на скриншоте - "Access denied", т.к. устройство занято другим приложением (правильнее сказать службой операционной системы)
Решение этой проблемы довольно топорное, существует данная тулза, которая позволяет пере прошить драйвер USB, что-бы никакие службы не воровали устройство в свои владения.
Принцип прост, скачиваем, запускаем, выбираем наш порт, к которому подключено устройство, нажимаем "Install Driver"
На этом проблема исправилась, доступ к устройству был получен.
Следующий шаг был понять, как напечатать на принтере хоть что-нибудь...
На страничках гугла, stack overflow и т.п. была найдена информация что данные принтеры работают на 8 битных словах и можно просто отправить буфер 8 битных слов на принтер.
Так и было сделано:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
// Input endpoint
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Print
usbDevice.transferOut(endpoint, buffer)
.catch(error => { console.warn(error); })
Результат меня знатно заинтриговал тем, что он сработал!
Еще стоит пояснить за то, что я использовал "Input" канал для отправки сообщения в коде свыше:
Почти каждое USB устройство имеет по крайней мере 2 канала - Ввод и вывод.
Однако их именование относительное, в данном случае как и в большинстве НЕ китайских устройств, канал Ввода предназначен для ввода информации В устройство, а не как канал с которого данные поступают на ваш ПК.
В случае с миди клавиатурой, которую я подключал к своему ПК, я использовал канал Вывода для считывания нажатых клавиш на пианино (если кому интересно, могу сделать на это статью)
Радости моей не было конца и края!
Почти...
В наших реалиях, чеки будут печататься на русском языке, и тут меня снова ждал облом...
После такого, я начал задумываться о поиске документации для no name принтера.
Забив его модель в гугл и сопоставив все факты, а именно, что я живу в Украине и этот принтер владельцы соседнего магазина продали мне за гроши (как вероятно его сами и купили), то поиск не заставил себя долго ждать и был найден данный производитель.
У него в наличии есть практически такой же принтер с отличием в названии в 1 букву, а у них на сайте, так же можно найти и PDF с описанием контроллера и команд, которые я мог бы использовать.
Так же, по мимо этого, я нашел и документацию от более известного производителя HP
Но как базу я стал использовать PDF отечественного производителя, т.к. она показалась мне более достоверной в моих условиях.
После быстрого прочтения обеих документаций было найдено несколько интересных особенностей данных принтеров:
Однако у HP список включал в себя на 9 кодировок больше:
Судя по описанию команда выглядит как 3 последовательных слова, которые должны быть отправлены на девайс, а именно:
Точкой опоры естественно стали кодировки с первого скриншота, которые относятся к принтерам Украинского произодства.
Рассматривались различные кодировки, желательно те, которые учавствовали бы в обеих списках, что-бы при измении принтера на другой, не пришлось переписывать либо же сильно дополнять существующий код.
В качестве нужной кодировки у многих программистов взгляд мог сразу упасть на WPC 1252.
Но не тут то было, чистый обман глаз и никакого мошенничества.
Все помнят Notepad++ и его фишку с кодировками, но та самая что с русскими буквами и многим интересна есть 1251, в тот час как 1252 не содержит ничего интересного, кроме символов "Торговая марка" и нескольких валют, в ней даже знака Евро нет...
По данной причине взгляд пал на "PC866", она есть как в первом PDF так ив PDF от HP, что уже внушает доверие, причем в документе от HP она идет с приписью "Russian"
Что-бы установить кодировку, просто стоит отправить команду на девайс перед отправкой текста, выглядит это так:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b, 0x74, 17]))
.catch(error => { console.warn(error); })
// Print
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Стоит напомнить, что данные принтеры являются стековыми, что дает нам возможность записать предыдущий снипет кода в таком ввиде:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, new Uint8Array([0x74]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, new Uint8Array([17]))
.catch(error => { console.warn(error); })
// Print
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Данный факт является и проблемой в то же время, одна неверно описанная команда в вашем коде, ложит принтер.
Если вы ошиблись с кол-вом аргументов, то вы получите излишние аргументы на печать, а недостающие будут сьедены из данных которые вы пошлете после.
Как результат кода свыше, русских букв я так и не увидел...
Стоит напомнить, что данный принтер работает на 8 битных словах.
А буква "П" в фразе "Привет мир" имеет индекс 1055 в юникоде, что не совсем влазит в диапазон 0-255 так ведь?
Мое неумение работать с кодировками продлило мне задачу на несколько часов, но факт в том, что мало выставить кодировку на принтере, нужно еще и присылать ему данные в правильной кодировке.
И тут я потратил еще минут 10 на составление функции для перевода текста из JavaScript в 866 кодировку, я учел в ней только русские буквы, остальные символы в учет не брал:
function utf8_to_866 (aa) {
let c = 0;
let ab = new Uint8Array(aa.length);
for (let i = 0; i < aa.length; i++) {
c = aa.charCodeAt(i);
if (c >= 1040 && c <= 1087) {
ab = c - 912;
} else if (c >= 1088 && c <= 1105) {
ab = c - 864;
} else {
ab = aa.charCodeAt(i);
}
}
return ab;
}
И в боевом окружении выглядит это так:
let data = utf8_to_866('\n\nПривет Хабр!\n\n\n\n\n\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b, 0x74, 17]))
.catch(error => { console.warn(error); })
// Print
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Как результат:
Всем спасибо за внимание!
Вот так выглядит сие чудо, было куплено в соседнем магазине, без опознавательных знаков, названия фирмы и без всего интересного.
Из полезной информации есть только название модели "AW-5890C".
Это усложнило весь процесс, но я решил пойти с малого. Первой задачей было синхронизироваться через браузер с USB девайсом, посредством USB API.
И тут меня ждал подвох...
Казалось бы, всем знакомый (тем кто работал с USB в браузере) сниппет кода, для открытия соединения с USB девайсом:
navigator.usb.requestDevice({filters: []})
.then(usbDevice => {
usbDevice.open()
.then(() => console.log('Success'))
.catch(() => console.error('Bye bye'));
});
В первой строке мы запрашиваем все девайсы в браузере, что-бы пользователь мог выбрать девайс. (Свойство "filters" обязательное как аргумент, потому пустой массив)
После того, как пользователь выбрал, мы вызываем у девайса метод "open", что-бы открыть соединение с ним.
В результате:
Видим наше устройство, уже отлично, выбираем его и....
"Это победа!"
На поиск истины у меня ушло более чем несколько часов, и вот она:
Windows 10 - единоличная операционная система...
Оказывается, если вы подключили какое-либо устройство и система определила его периферический тип и установила драйвера, то она тут же добавляет его в свою особенную службу, тем самым, делая устройство "занятым".
Это говорит о том, что вы можете послать файл на распечатку в принтер, который определился системой, но вы не можете воспользоваться USB портом, т.к. он занят принтером, который определила система.
Об этом же и говорит ошибка на скриншоте - "Access denied", т.к. устройство занято другим приложением (правильнее сказать службой операционной системы)
Решение этой проблемы довольно топорное, существует данная тулза, которая позволяет пере прошить драйвер USB, что-бы никакие службы не воровали устройство в свои владения.
Принцип прост, скачиваем, запускаем, выбираем наш порт, к которому подключено устройство, нажимаем "Install Driver"
На этом проблема исправилась, доступ к устройству был получен.
Следующий шаг был понять, как напечатать на принтере хоть что-нибудь...
На страничках гугла, stack overflow и т.п. была найдена информация что данные принтеры работают на 8 битных словах и можно просто отправить буфер 8 битных слов на принтер.
Так и было сделано:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
// Input endpoint
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
usbDevice.transferOut(endpoint, buffer)
.catch(error => { console.warn(error); })
Результат меня знатно заинтриговал тем, что он сработал!
Еще стоит пояснить за то, что я использовал "Input" канал для отправки сообщения в коде свыше:
Почти каждое USB устройство имеет по крайней мере 2 канала - Ввод и вывод.
Однако их именование относительное, в данном случае как и в большинстве НЕ китайских устройств, канал Ввода предназначен для ввода информации В устройство, а не как канал с которого данные поступают на ваш ПК.
В случае с миди клавиатурой, которую я подключал к своему ПК, я использовал канал Вывода для считывания нажатых клавиш на пианино (если кому интересно, могу сделать на это статью)
Радости моей не было конца и края!
Почти...
В наших реалиях, чеки будут печататься на русском языке, и тут меня снова ждал облом...
После такого, я начал задумываться о поиске документации для no name принтера.
Забив его модель в гугл и сопоставив все факты, а именно, что я живу в Украине и этот принтер владельцы соседнего магазина продали мне за гроши (как вероятно его сами и купили), то поиск не заставил себя долго ждать и был найден данный производитель.
У него в наличии есть практически такой же принтер с отличием в названии в 1 букву, а у них на сайте, так же можно найти и PDF с описанием контроллера и команд, которые я мог бы использовать.
Так же, по мимо этого, я нашел и документацию от более известного производителя HP
Но как базу я стал использовать PDF отечественного производителя, т.к. она показалась мне более достоверной в моих условиях.
После быстрого прочтения обеих документаций было найдено несколько интересных особенностей данных принтеров:
- Они все работают на 8 битных словах
- Они стековые
- Если встречается слово "0x1b", то оно распознается как начало команды.
Однако у HP список включал в себя на 9 кодировок больше:
Судя по описанию команда выглядит как 3 последовательных слова, которые должны быть отправлены на девайс, а именно:
- 0x1B, 0x74, charset - в HEX формате.
- 27, 116, charset - в десятичном формате.
Точкой опоры естественно стали кодировки с первого скриншота, которые относятся к принтерам Украинского произодства.
Рассматривались различные кодировки, желательно те, которые учавствовали бы в обеих списках, что-бы при измении принтера на другой, не пришлось переписывать либо же сильно дополнять существующий код.
В качестве нужной кодировки у многих программистов взгляд мог сразу упасть на WPC 1252.
Но не тут то было, чистый обман глаз и никакого мошенничества.
Все помнят Notepad++ и его фишку с кодировками, но та самая что с русскими буквами и многим интересна есть 1251, в тот час как 1252 не содержит ничего интересного, кроме символов "Торговая марка" и нескольких валют, в ней даже знака Евро нет...
По данной причине взгляд пал на "PC866", она есть как в первом PDF так ив PDF от HP, что уже внушает доверие, причем в документе от HP она идет с приписью "Russian"
Что-бы установить кодировку, просто стоит отправить команду на девайс перед отправкой текста, выглядит это так:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b, 0x74, 17]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Стоит напомнить, что данные принтеры являются стековыми, что дает нам возможность записать предыдущий снипет кода в таком ввиде:
// Encode
let encoder = new TextEncoder();
let buffer = encoder.encode('\n\n\nHello world\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, new Uint8Array([0x74]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, new Uint8Array([17]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Данный факт является и проблемой в то же время, одна неверно описанная команда в вашем коде, ложит принтер.
Если вы ошиблись с кол-вом аргументов, то вы получите излишние аргументы на печать, а недостающие будут сьедены из данных которые вы пошлете после.
Как результат кода свыше, русских букв я так и не увидел...
Стоит напомнить, что данный принтер работает на 8 битных словах.
А буква "П" в фразе "Привет мир" имеет индекс 1055 в юникоде, что не совсем влазит в диапазон 0-255 так ведь?
Мое неумение работать с кодировками продлило мне задачу на несколько часов, но факт в том, что мало выставить кодировку на принтере, нужно еще и присылать ему данные в правильной кодировке.
И тут я потратил еще минут 10 на составление функции для перевода текста из JavaScript в 866 кодировку, я учел в ней только русские буквы, остальные символы в учет не брал:
function utf8_to_866 (aa) {
let c = 0;
let ab = new Uint8Array(aa.length);
for (let i = 0; i < aa.length; i++) {
c = aa.charCodeAt(i);
if (c >= 1040 && c <= 1087) {
ab = c - 912;
} else if (c >= 1088 && c <= 1105) {
ab = c - 864;
} else {
ab = aa.charCodeAt(i);
}
}
return ab;
}
И в боевом окружении выглядит это так:
let data = utf8_to_866('\n\nПривет Хабр!\n\n\n\n\n\n\n\n');
const endpoint = usbDevice.configuration.interfaces[0].alternate.endpoints[0].endpointNumber;
// Set 866
usbDevice.transferOut(endpoint, new Uint8Array([0x1b, 0x74, 17]))
.catch(error => { console.warn(error); })
usbDevice.transferOut(endpoint, data)
.catch(error => { console.warn(error); })
Как результат:
Всем спасибо за внимание!
Термо принтер и JavaScript
Приобрел я на днях термопринтер (кассовый аппарат, если изволите) жене для магазина и решил что смогу его обуздать как истинный программист и сделать свою веб кассу, вместо того, что-бы использовать...
habr.com