А сейчас, мы с Вами сделаем эмулятор из готовых компонетов. Нам потребуется напильник и немного изоленты.
А сейчас, мы с Вами сделаем эмулятор из готовых компонетов. Нам потребуется напильник и немного изоленты.

Наш эмулятор должен решать следующие задачи:

  1. Автоматическое обнаружение в локальной сети

  2. Реализовать IPP протокол для общения

  3. Отобразить образ печатного документа.

И вот первая проблема, в том что пока мы не сделаем п.1 и частично п.2, встроенная служба печати Андроид не отобразит наш принтер. Это связано как я писал ранее, с тем что из mDNS берется минимум информации, а остальное запрашивается от принтера через IPP.

Как известно IPP живет на 631 порту. Славо богу для андроида это не стало проблемой, обойти которую было бы не возможно. Так нельзя поднять свой сервис на порту меньше 1024 (точнее можно, но только после рутования).

Эксперименты показали, что можно указывать любой номер из доступных. Поискав аналог из уже известных, подходящим оказался 10631.

IPP живет поверх HTTP протокола. Для реализации сервера я использовал апче коре компонентс (package org.apache.hc.core5).

Вторая проблема, из-за которой пришлось их форкать и допиливать напильником, это то, что сервер не хотел стартовать в режиме - слушать порт на всех ip без учета имени хоста.

ServerBootstrap serverBootstrap = ServerBootstrap.bootstrap();
serverBootstrap.setListenerPort(PORT);
// serverBootstrap.setCanonicalHostName("10.0.0.104"); 
// занулил метод в классе RequestHandlerRegistry
serverBootstrap.setSocketConfig(SocketConfig.custom()
                                .setSoTimeout(300, TimeUnit.SECONDS)
                                .setTcpNoDelay(true)
                                .setSoReuseAddress(true).build());

mDNS дает нам три варианта для формирования урла (hostname, ip4 и ip6)

Плюс, обращение к эмулятору с этого же устройства по 127.0.0.1

Замечание. Все эти вещи динамические. Имя хоста присваивается в порядке подключения к сети и может быть до Android-9.local. чтобы зафиксировать ip в DHCP на роутере, нужно убедиться что на телефоне не выбран вариант случайный мак - адрес.

С моей точке зрения, предпочтительнее просто назначить статический айпи за диапазоном dhcp.

Теперь нам нужно повесить хук на rp урл

serverBootstrap.register("/"+RP, (request, response, context) -> {
.... 
  // проверим 
     if(!request.containsHeader(CONTENT_TYPE)){
       // NO CONTENT TYPE;
       return;
     }
    if(!("application/ipp".equalsIgnoreCase(request.getHeader(CONTENT_TYPE).getValue()))){
        // WRONG CONTENT TYPE
        return;
    }
  // получим операцию
    Operation operation;
    IppPacketData ippPacketData;
    int ippVer;
    try {
       HttpEntity entity = request.getEntity();
       IppInputStream responseInput = new IppInputStream(entity.getContent());
       ippPacketData = new IppPacketData(responseInput.readPacket(), responseInput);
       ippVer = ippPacketData.getPacket().getVersionNumber();

      operation = ippPacketData.getPacket().getOperation();
                    lInfo("oper:"+ operation.getName());

    }catch (Exception e){
      lError(e.getMessage());
     return;
   }

....  
}

Здесь появляется библиотека от HP

https://github.com/HPInc/jipp

С моей точки зрения, данная библиотека слишком правильная. Она отказалась работать с принтером Pantum (падала на дефолтном значении гео координат принтера) и ругалась на cups MacOS при расшаривание USB принтера (атрибут указан дважды). Тоже пришлось форкать и глушить часть эксепшенов.

Как понимаете, не мне первому пришла идея написания эмулятора. Кто-то просто собрал в докер образ cups. кто-то написал на питоне, кто-то под npm. Но в основном утверждается, что требуется поддержка 5 команд, но андроид согласен работать и при поддержке всего 2х.

operationsSupported.of(
  Operation.printJob, // минимум андроида
  Operation.validateJob,
  //   Operation.createJob,
  //   Operation.sendDocument,
  Operation.cancelJob,
  Operation.getJobAttributes,
  //    Operation.getJobs,
        Operation.getPrinterAttributes, // минимум андроида
  //    Operation.cancelMyJobs,
  //    Operation.closeJob,
  Operation.identifyPrinter
),

Единственный момент тут с отловом завершения печати. В первые секунды после того как получено задание печати, нужно отвечать, что принтер PrinterState.processing и секунд через 5 снова idle.

На скриншоте выше запрос андроида, что он хочет максимально знать о принтере.

Фактически согласен, если будет указано

а) в document-format-supported один pdf и/или pwg. Я не выделяю тут отдельно PCLm - так как это кастрированный пдф формат, где на странице может быть только картинка порезанная на полоски до 128 точек высотой.

б) printer-name

в) uuid

г) printer-location

д) media-supported хотя бы с одним стандартным размером бумаги

Все остальное опционально.

В следующей статье о JPL , PDL

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