目前,网上关于android 移植打印机驱动的基本是以下几个方向:
我的学习:
本来想一步到位 ,直接移植cups ,但发现依赖是在太多,7.1 的内核包含的库少 而且旧 ,cusp静态编译又一堆问题,最后放弃。
然后尝试ghostscript+hplip ,但是hplip也很庞杂,而且严重依赖python,但是在研究的过程中发现,hplip 中包含了hpijs 的最新代码,所以就采用了 ghostscript+hpijs(hplip)的方案
1 android7.1 的编译环境 交叉编译用
2 ubuntu 18 以上,(我用的18 ),最好不要用虚拟机,编译太慢
3 vscode : linux 上看代码必备,
4 源码下载:ghostscript 10, hplip3.23.3 这两项代码都好下载,下载最新的即可
1 配置;./configure --host=aarch64-linux
2 修改makefile :找到STDLIBS 在最后加上 -static
3 make 生成./bin/gs
ghostscript 的编译非常简单,生成后用readelf -d ./bin/gs 查看
Dynamic section at offset 0x1cae260 contains 34 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libXt.so.6]
0x0000000000000001 (NEEDED) 共享库:[libX11.so.6]
0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
0x0000000000000001 (NEEDED) 共享库:[libdl.so.2]
0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0]
0x0000000000000001 (NEEDED) 共享库:[libstdc++.so.6]
0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1]
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
非静态编译会显示依赖库,这是无法在android的linux 环境运行的
There is no dynamic section in this file.
静态编译会提示该信息
最好做两份 一份pc版,一份arm版,pc版主要后面调试用到。先在pc上调通,然后移植到arm板上,直接在板子上调是不太现实的。
./configure --host=aarch64-linux --enable-network-build=no --enable-libusb01_build --enable-scan-build=no --enable-dbus-build=no --enable-fax-build=no --enable-hpijs-only-build --enable-hpijs-install
主要是编译hpijs 所以用到–enable-hpijs-only-build
同时编译一份pc版,调试主要是在pc上调,通了后移植到板子上就只剩编译问题了。
pc上的配置 将–host=aarch64-linux去掉,也可以将–enable-hpijs-only-build 去掉,编译出来有很多工具,有兴趣可以研究研究。
hpijs_LINK = $(LIBTOOL) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) \
--mode=link $(CXXLD) $(hpijs_CXXFLAGS) $(CXXFLAGS) \
$(AM_LDFLAGS) $(LDFLAGS) -all-static -o $@
pc版可以不设置,pc上没有库依赖的问题
HP Co. Inkjet Server 3.23.3
Copyright (c) 2001-2004, HP Co.
出现该消息 说明编译成功 ,android 需要将bin文件放到 system/bin 下,
Device Drivers --->
[*] USB support --->
<*> USB Printer support
如此,在插上打印机后 会产生/dev/usb/lp0节点 ,有些打印机需要下载固件,在下载固件之前,它可能映射成其它设备,无法生存该节点,这种打印机目前没有办法。
运行命令,需要root权限,pc 上需要先sudo -i , sudo好像不行.
gs -sDEVICE=ijs -sIjsServer=hpijs -dIjsUseOutputFD -sDeviceManufacturer="HEWLETT-PACKARD" -sDeviceModel="DESKJET 825" -dNOPAUSE -dBATCH -dPARANOIDSAFER -dQUIET -sOutputFile="/dev/usb/lp0" ./examples/waterfal.ps
else if (!strcmp (key, "DeviceManufacturer")) {
if ((strncasecmp(svalue, "HEWLETT-PACKARD", 15) != 0) &&
(strncasecmp(svalue, "APOLLO", 6) != 0) && (strncasecmp(svalue, "HP", 2) != 0))
{
BUG("unable to set DeviceManufacturer=%s\n", svalue);
status = -1;
} }
class LJZjsMonoProxy : public PrinterProxy
{
public:
LJZjsMonoProxy() : PrinterProxy(
"LJZjsMono",
"HP LaserJet 1000\0"
"HP LaserJet 1005\0"
"HP LaserJet 1018\0"
"HP LaserJet 1020\0"
"HP LaserJet 1022\0"
"HP LaserJet P2035\0"
"HP LaserJet P1102\0" //**<--搜索到该打印机 说明支持**
"HP LaserJet P1566\0"
"HP LaserJet P1606\0"
"HP LaserJet Professional M1136\0"
"HP LaserJet Professional M1132\0"
"HP LaserJet Professional M1212nf\0"
) {m_iPrinterType = eLJZjsMono;}
inline Printer* CreatePrinter(SystemServices* pSS) const { return new LJZjsMono(pSS); }
inline PRINTER_TYPE GetPrinterType() const { return eLJZjsMono;}
inline unsigned int GetModelBit() const { return 0x40;}
};
不过可惜的是 HP LaserJet P1102 就是上面提到的需要下载固件的打印机,android上无法产生/dev/usb/lp0 节点。
关于该参数后面会详细分析。到这里很可能还是打印不成功的。
-dQUIET
完成后 退出gs 程序,否则会一直卡在gs 程序里,需要ctrl+c 退出。android上用app调用shell命令,该参数非常重要。
-sOutputFile=“/dev/usb/lp0”
指向 打印机节点。
./examples/waterfal.ps
命令最后是输入文件,该文件为 ghostscript 中examples 的一个postscript的示例,输入文件也可以是pdf文件
前面的工作做完了,但是打印不一定会成功,最主要的是DeviceModel设置的问题,那到底该参数是如何作用的呢?
既然代码都有,那就直接分析分析代码好了。了解以下ijs服务,对后期调试也很有帮助。
ijs 是一个比较简单的服务,主要函数就下面几个:
ijs_server_init();
ijs_server_install_status_cb ();
ijs_server_install_list_cb ();
ijs_server_install_enum_cb ();
ijs_server_install_set_cb ();
ijs_server_install_get_cb ();
ijs_server_get_page_header()
还有一个最核心的函数表:
ijs_server_proc ijs_server_procs[] = {
ijs_server_proc_ack,
ijs_server_proc_nak,
ijs_server_proc_ping,
ijs_server_proc_pong,
ijs_server_proc_open,
ijs_server_proc_close,
ijs_server_proc_begin_job,
ijs_server_proc_end_job,
ijs_server_proc_cancel_job,
ijs_server_proc_query_status,
ijs_server_proc_list_params,
ijs_server_proc_enum_param,
ijs_server_proc_set_param,
ijs_server_proc_get_param,
ijs_server_proc_begin_page,
ijs_server_proc_send_data_block,
ijs_server_proc_end_page,
ijs_server_proc_exit
};
fd_from = 0;
fd_to = 1;
ijs_recv_init (&ctx->recv_chan, fd_from);
ijs_send_init (&ctx->send_chan, fd_to);
初始化函数 主要工作就是创建了两个文件接口,一个收一个发,分别对应fd 0,fd 1。
-ijs_server_get_page_header()
该函数会读取fd_from 文件 接收从客户端发送的消息。
while (1)
{
if ((ret = ijs_server_get_page_header(ctx, &pSS->ph)) < 0)
{
BUG("unable to read client data err=%d\n", ret);
goto BUGOUT;
}
.............
....
....
}
如是,就启动了ijs常驻服务。
ijs_server_get_page_header 函数非常简单:
ijs_server_get_page_header (IjsServerCtx *ctx, IjsPageHeader *ph)
{
int status;
ctx->ph = ph;
ctx->in_page = FALSE;
do
{
status = ijs_server_iter (ctx);
}
while (status == 0 && !ctx->in_page);
ctx->ph = NULL;
return status;
}
该函数接收客户端消息,然后解析得到cmd_num ,然后查ijs_server_procs表,并调用相应处理函数。
这就是ijs 的总体处理结构,相当简单的服务。
int
ijs_server_iter (IjsServerCtx *ctx)
{
int cmd_num;
int status;
status = ijs_recv_buf (&ctx->recv_chan);
if (status < 0)
return status;
cmd_num = ijs_get_int (ctx->recv_chan.buf);
#ifdef VERBOSE
fprintf (stderr, "command %d, %d bytes\n", cmd_num, ctx->recv_chan.buf_size);
#endif
if (cmd_num < 0 ||
cmd_num >= (int)sizeof(ijs_server_procs) / sizeof(ijs_server_procs[0]))
return -1;
return ijs_server_procs[cmd_num] (ctx);
}
当客户端发送设置参数的命令时,就会调用ijs_server_procs 中的ijs_server_proc_set_param函数
该函数主要做一些会话验证工作,然后调用到ijs_server_set_param 函数;此函数处理ijs自身的参数设置命令,以及第三方参数设置命令。最主要的就是第三方设置命令,代码这里补贴了。
return ctx->set_cb (ctx->set_cb_data, ctx, job_id, key, value, value_size);
set_cb 就是第三方参数设置的回调函数接口,该结构在ijs_server_install_set_cb 中设置,hpijs的设置:
ijs_server_install_set_cb (ctx, hpijs_set_cb, pSS)
(r = pSS->pPC->SelectDevice(svalue)
当设置的打印机 在支持列表中时,设置正确,然后就可以打印了(至少现在是这么认为的)
ijs 的服务层 大概就是这样子了。
#define pPFI PrinterFactory::GetInstance()
PrinterFactory 中存放了所有支持的打印机的 family 每个family 对应一个PrinterProxy
比如:
class LJM1005Proxy : public PrinterProxy
{
public:
LJM1005Proxy() : PrinterProxy(
"LJM1005",
"LaserJet M1005\0"
"HP LaserJet M1005\0"
"HP LaserJet M1120\0"
"HP LaserJet M1319\0"
"HP LaserJet P1505\0"
"HP LaserJet P2010\0"
"HP LaserJet P2014\0"
"HP LaserJet P2014n\0"
"M1005\0"
) {m_iPrinterType = eLJM1005;}
inline Printer* CreatePrinter(SystemServices* pSS) const { return new LJM1005(pSS); }
inline PRINTER_TYPE GetPrinterType() const { return eLJM1005;}
inline unsigned int GetModelBit() const { return 0x40;}
};
每一个family 有一个family name 如:LJM1005,和szModelNames ,szModelNames是一个字符串数组,对应的就是具体打印机,而devicemodel 既可以是 family name 也可以是modename
所有的PrinterProxy 都以静态全局变量的形式 定义在 internal.h中 ,总共有如下family:
family:AP2xxx
family:AP21xx
family:AP2560
family:DJ3320
family:DJD2600
family:DJ4100
family:DJ3600
family:DJ350
family:DJ540
family:DJ600
family:DJ630
family:DJ6xx
family:DJ6xxPhoto
family:DJ850
family:DJ890
family:DJ8x5
family:DJ8xx
family:OJProKx50
family:DJ9xxVIP
family:DJ9xx
family:DJ55xx
family:GenericVIP
family:PS470
family:PS100
family:LJP1XXX
family:LJM1005
family:LJZjsColor
family:LJZjsMono
family:LJFastRaster
family:LJJetReady
family:ColorLaser
family:Mono Laser
PrinterProxy 在构造的时候 ,会调用PrinterFactory::Register(this); 将自己添加到支持列表中,
也就是前面提到的全局单例中(#define pPFI PrinterFactory::GetInstance())
PrinterProxy::PrinterProxy
(
const char* szFamilyName,
const char* szModelNames
) : m_szFamilyName(szFamilyName),
m_szModelNames(szModelNames)
{
unsigned int uCount = 0;
char* tPtr = const_cast<char*>(m_szModelNames);
while (*tPtr)
{
tPtr += strlen(tPtr)+1;
uCount++;
}
m_uModelCount = uCount;
PrinterFactory::Register(this);// <--将自己添加到支持列表中
TRACE("PP::PP family is %s and supports %d models\n", GetFamilyName(), GetModelCount());
} //PrinterProxy
基本结构就大致如此。
理论上讲,只要打印机在支持列表中,那么gs的打印命令中 -sDeviceModel 设置正确 就可以打印成功, 那这篇文章也就不存在了。因为我的HP LaserJet M1005 打印不成功,没办法只能 read the fucking source code。
明明列表中有HP LaserJet M1005 ,为啥不能打印?gs 提供的错误消息有限 ,而hpijs 作为服务端,无任何提示消息,hpijs.cpp在main中的一句话,给了一点点方向:
openlog("hpijs", LOG_PID, LOG_DAEMON);
然后:
#define BUG(args...) syslog(LOG_ERR, __FILE__ " " STRINGIZE(__LINE__) ": " args)
因此 它肯定会在/var/log/下留下痕迹
于是在/var/log 下 grep -r -i “hpijs”
"unable to set device=HP LaserJet M1005, err=48
if ((r = pSS->pPC->SelectDevice(svalue)) != NO_ERROR)
{
if (r == PLUGIN_LIBRARY_MISSING)
{
// call dbus here
const char *user_name = " ";
const char *title = " ";
const char *device_uri = getenv ("DEVICE_URI");
const char *printer = getenv ("PRINTER");
int job_id = 0;
if (device_uri == NULL)
device_uri = "";
if (printer == NULL)
printer = "";
SendDbusMessage (device_uri, printer,
EVENT_PRINT_FAILED_MISSING_PLUGIN,
user_name, job_id, title);
BUG("unable to set device=%s, err=%d\n", svalue, r); //<====这里
status = -1;
}
else
{
/* OfficeJet LX is not very unique, do separate check here. */
if (!strncmp(svalue,"OfficeJet", 10))
r = pSS->pPC->SelectDevice("DESKJET 540");
}
}
PLUGIN_LIBRARY_MISSING的定义正好是0x30(48)
PLUGIN_LIBRARY_MISSING = 0x30, //!< a required plugin (dynamic) library is missing
提示plugin 的库找不到
PLUGIN_LIBRARY_MISSING 用到的地方不多,最终确认是走的ljzjs.cpp
LJZjs::LJZjs (SystemServices* pSS, int numfonts, BOOL proto)
: Printer(pSS, numfonts, proto)
{
CMYMap = NULL;
#ifdef APDK_AUTODUPLEX
m_bRotateBackPage = FALSE; // Lasers don't require back side image to be rotated
#endif
m_pszInputRasterData = NULL;
m_dwCurrentRaster = 0;
m_bStartPageSent = FALSE;
m_iPrinterType = UNSUPPORTED;
#ifdef HAVE_LIBDL
HPLJJBGCompress = NULL;
m_hHPLibHandle = NULL;
m_hHPLibHandle = LoadPlugin ("lj.so");
if (m_hHPLibHandle)
{
dlerror ();
*(void **) (&HPLJJBGCompress) = dlsym (m_hHPLibHandle, "hp_encode_bits_to_jbig");
*(void **) (&HPLJSoInit) = dlsym (m_hHPLibHandle, "hp_init_lib");
if (!HPLJSoInit || (HPLJSoInit && !HPLJSoInit (1)))
{
constructor_error = PLUGIN_LIBRARY_MISSING;
}
}
#endif
if (HPLJJBGCompress == NULL)
{
constructor_error = PLUGIN_LIBRARY_MISSING;
}
//Issue: LJZJSMono class printers not printing in RHEL
//Cause: Since start page is common for LJZJSMono and LJZJSColor class, the items of
//LJZJSColor-2 format was used for LJZJSMono due to below variable not initialised
//Fix: Added initialisation so that correct LJZJSMono items are used.
//Variable is updated in LJZJSColor.
m_bLJZjsColor2Printer = FALSE;
}
LJZjs是一个printer 类 ,很多打印机继承与该类,HP LaserJet M1005 就是其中之一,这类打印机会加载lj.so库
m_hHPLibHandle = LoadPlugin (“lj.so”);
添加打印消息,确定全路径是./usr/share/hplip/prnt/plugins/lj.so
库是存在的,那就是 初始化失败
if (!HPLJSoInit || (HPLJSoInit && !HPLJSoInit (1)))
这就没有办法了,没有源码。
看代码发现仅仅用到了这个库中的一个函数 “hp_encode_bits_to_jbig” ,jbig是压缩编码,那是不是可以找到 jbig 的源码,然后实现这个函数呢?jbig 的源码倒是很好找
https://github.com/suzp1984/jbig-android
jbig-android/jbig-android-library/src/main/jni/ 目录下即 jbig 压缩源码,然而拿到jbig源码也不知道从何下手,因为不知道hp到底做了些什么处理。
然而幸运的是,公司以前移植过 apdk ,apdk中 有这一段源码,于是搬过来直接用了,接下来就是添加编译改错,编译成功后终于HP LaserJet M1005 打印成功!
由于HPLJJBGCompress 的源码并非我的工作成果,HP也将其做成插件 未开放,所以不能上传。与上面的jbig源码相差不大,通过它来解决这个问题也许 可行。
然而,接下来还有一个问题,另一款打印机hp-laserjet_pro_mfp_m126a 不在支持列表,但是PPD文件中有他的ppd文件,其中有一句话
FoomaticRIPOptionSetting Model=HP-Color_LaserJet_2600n: " -sDeviceManufactur&&
er="HEWLETT-PACKARD" -sDeviceModel="HP Color LaserJet 2600n""
就是说这款打印机的DeviceModel 对应的是HP Color LaserJet 2600n ,但设置为这个后,打印不成功,在 /var/log/下有消息:
prnt/hpijs/services.cpp 389: unable to write to output, fd=6, count=4096: No such device
gs 打印shell 命令:
gs -dIjsUseOutputFD -dNOPAUSE -dBATCH -dPARANOIDSAFER -dQUIET -sDEVICE=ijs -sIjsServer=hpijs -sOutputFile=/dev/usb/lp0 -sDeviceManufacturer=\"HEWLETT-PACKARD\" -sDeviceModel=\"HP LaserJet M1005\" test.pdf
test.pdf 是测试用的PDF文件,该命令在adb shell 下执行正常
但是用java 调用shell 命令的方法就是打印不出来:
String printerId = "";
Process process = null;
DataInputStream in = null;
DataOutputStream os = null;
mSupportPrinters.clear();
cmd ="gs -dIjsUseOutputFD -dNOPAUSE -dBATCH -dPARANOIDSAFER -dQUIET -sDEVICE=ijs -sIjsServer=hpijs -sOutputFile=/dev/usb/lp0 -sDeviceManufacturer="HEWLETT-PACKARD" -sDeviceModel="HP LaserJet M1005" test.pdf \n"
try {
process = Runtime.getRuntime().exec("su"); //切换到root帐号
os = new DataOutputStream(process.getOutputStream());
in = new DataInputStream(process.getInputStream());
os.writeBytes(cmd);
os.flush();
while (true) {
printerId = in.readLine();//获取输入流
if (printerId ==null)
break;
if(printerId.contains("END")){
printerId = in.readLine();
break;
}
mSupportPrinters.add(printerId.replace("ModeName:",""));
//printerId = printerId + in.readLine();
}
os.writeBytes("exit\n");
os.flush();
process.waitFor();
} catch (Exception e) {
} finally {
try {
if (os != null) {
os.close();
}
if (in != null) {
in.close();
}
process.destroy();
} catch (Exception e) {
}
}
摸索了很久后发现,java调用shell时,会用到临时文件夹“/tmp/”,但是Android没有这个目录,于是用jni函数写了一个设置$TEMPDIR 环境变量的函数:
#include
JNIEXPORT jint JNICALL Java_jni_fpgargb_1lib_setenv
(JNIEnv *env, jclass,jstring key, jstring val){
jboolean iscopy;
//jobject mFileDescriptor;
const char *key_val = (*env).GetStringUTFChars(key, &iscopy);
const char *val_val = (*env).GetStringUTFChars(val, &iscopy);
setenv(key_val,val_val,1);
(*env).ReleaseStringUTFChars(key, key_val);
(*env).ReleaseStringUTFChars(val, val_val);
return 0;
}
/
///调用,将$TEMPDIR 设置为当前app的临时目录
fpgargb_lib.setenv("TEMPDIR",getContext().getCacheDir().getAbsolutePath());
打印成功