R4.4.11 追記:Poooli L3 プリンタのグレースケール印刷のためのデータフォーマットが分かったので、内容を改訂します。また一部間違いも訂正します。
要旨
Poooli 社の L3 感熱紙プリンタの bluetooth 出力の解析を行い、印字命令をある程度理解した。Bitmap データは miniLZO 圧縮され、コマンドやデータは基本的に xor 13 がとられてプリンタに送られている。これによって 2 階調の Bitmap 画像は任意のものが出力できるようになった。また 6? 階調の Grayscale Bitmap に関しても checksum の導出法とデータフォーマットが判し任意の Bitmap 画像が出力できるようになった。Poooli 社の感熱紙プリンタの解析は Github 上にも見当たらないので、本記事にも多少の価値があると考える。またこの結果は共通のスマホアプリを利用する L1/L1Pro, L2/L2 Pro プリンタ等にも適用できると思われる。
Poooli 社の感熱紙プリンタ
Poooli 社は中国の会社で、スマホアプリからの利用を主とする bluetooth プリンタを主に販売している。オウムをモチーフとした意匠が特徴となっている。
Poooli 社の感熱紙プリンタのうちいくつかは、感熱紙プリンタに一般的な 1bit 2 階調の印刷の他に、6 程度の階調のグレースケール印刷が可能になっている。L3 プリンタもその機能を持っていて、110㎜ 幅クラスの感熱紙プリンタでは珍しい存在になっている。ただグレースケール印刷で綺麗に出力するには専用の感熱紙が必要になる。
なお Poooli 社は Android や iPhone のアプリからの Bluetooth 接続による利用を前面に出しているが、Windows および Mac 用のプリンタドライバも提供しており USB から利用できる。さらに中国語版でのみではあるが Windows 用アプリケーションも提供している。
英語版
https://www.poooliprint.com/pages/poooli-a4-app
中国語版
https://www.poooli.com/blctxl-list.html
プリンタのハードウェア面に関しては、HPRT 社のツールに依存しているようである。
https://www.hprt.com/
解析手法
以前の Phomemo bluetooth プリンタで用いた方法をに依った。
https://qiita.com/cure_honey/items/72124ff8effddc075e9b
bluetooth
Poooli 社の Android アプリは記述にも関わらず Android 7 にはインストールできなかったため、Android 10 にインストールした。これに伴い Bluetooth log の取り方が変化した。今回は以下の記事を参考にした。
https://qiita.com/KentaHarada/items/42ed619e8f571d1de845
Windows 上の Wireshark によりパケットの中身を見た。
アプリ逆コンパイル
以下にある apk ファイルを逆コンパイルした。(現時点での最新版ではない。)
https://apkpure.com/poooli-smart-pocket-printer/com.poooli.beautifulprinter
生成されたソースプログラムの中で、以下のファイルが特に参考になった。
\classes-dex2jar\src\HPRTAndroidSDK\HPRTPrinterHelper.java
またこれと併せて HPRT 社のページからダウンロードできる "TSPL Programming Manual for HT100/HT130" も参考になった。
これらから、コマンドなどがそのままの形では使われていないことが分かった。また Bitmap 画像が miniLZO によって圧縮されてされているであろうこと、checksum としては CRC32 が使われているであろうことが推測できた。
Ubuntu からの Bluetooth 接続
以上の情報を元に、Windows の VMware 上の Ubuntu 20.04 から Bluetooth 接続によってプリンタと通信を試みた。
プリンタ mac address の取得
プリンタの個人情報をさらけ出すスタイルw
honey@ubuntu:~$ sudo hcitool scan
Scanning ...
00:15:82:93:3C:F9 Poooli_L3-3cf9
bluetooth channel の取得
Serial Port の channel は 6 番。
honey@ubuntu:~$ sdptool browse 00:15:82:93:3c:f9
Browsing 00:15:82:93:3C:F9 ...
Service Name: SerialPort
Service RecHandle: 0x1000f
Service Class ID List:
"Serial Port" (0x1101)
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 6
Profile Descriptor List:
"Serial Port" (0x1101)
Version: 0x0102
Service Name: Apple Inc.
Service RecHandle: 0x1000d
Service Class ID List:
UUID 128: 00000000-deca-fade-deca-deafdecacaff
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 15
Profile Descriptor List:
"Serial Port" (0x1101)
Version: 0x0100
Service Name: WeChat
Service RecHandle: 0x1000e
Service Class ID List:
UUID 128: e5b152ed-6b46-09e9-4678-665e9a972cbc
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 8
Profile Descriptor List:
"Serial Port" (0x1101)
Version: 0x0102
Bluetooth による仮想 Serial port 接続
rfcomm で -r を付けないと、0Ah が 0Dh に置き換えられてしまう。(マニュアルには 0Ah が 0Ah0Dh に置き換えられると書いてあるが、プリンタの反応的には 0Dh のみに置き換わっているように見える。)
connect の次の数字 0 は、仮想シリアルデバイス名の /dev/rfcomm0 の末尾の数字になる。
最後の 6 は上で調べた channel の番号。
honey@ubuntu:~$ sudo rfcomm -r connect 0 00:15:82:93:3C:F9 6
[sudo] password for honey:
Connected /dev/rfcomm0 to 00:15:82:93:3C:F9 on channel 6
Press CTRL-C for hangup
これで、別 terminal を開けば、Fortran からは /dev/rfcomm0 というファイル名でプリンタと双方向通信が可能になる。
sudo chmod 666 /dev/rfcomm0
解析結果
Poooli のスマホアプリを操作し、対応する Bluetooth 通信の内容を見てみると、いくつかの印字命令などが分かる。それをプログラムからプリンタに送り込むことで、プリンタを動作させられる。
ここでプリンタの自動電源オフまでの時間設定コマンドを利用して、与えたパラメータと実際に設定された時間を比較し、数値が XOR 13 されて送られていることが分かった。そうして HPRT TSPL の印字命令や引数も XOR 13 されていることが分かった。
プリンタの情報
プリンタ情報をとり、いくつかのテストパターンを印刷するプログラム。
program poooli
use, intrinsic :: iso_fortran_env
implicit none
character :: ok(2), info(64, 3), res(3), ip(17)
integer(int16) :: ienergy, itimer, idensity, iaa, ibb, ixx, iyy, izz
integer :: i
! open bluetooth
open(10, file = '/dev/rfcomm0', access = 'stream', action = 'readwrite')
!
! header
write(10) achar([z'1b', z'1c' ]), 'set mm', achar([05, 08])
read(10) ok
! set printer page type
write(10) achar([z'10', z'7e', z'68', z'79', z'7d', z'0d'])
read(10) ok
! set density [0..100]?
!write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'3a']) ! thin ! 55
!write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'46']) ! middle ! 75
write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'52']) ! thick ! 95
read(10) ok
print *, ok, ' print density'
! set paper width 12dots / mm
write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'ed', z'09']) ! long 1248 dots
!write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'9d', z'0e']) ! middle 912 dots
!write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'85', z'0f']) ! short 648 dots
read(10) ok
print *, ok, ' paper width'
! read printer info
write(10) achar([z'16', z'1f', z'69'])
read(10) info(:, 1)
print *, info(:, 1)
read(10) info(:, 2)
print *, info(:, 2)
read(10) info(:15, 3)
print *, info(:15, 3)
do i = 2, 2 ! 0, 256
if (i == 14) cycle ! hang
if (i == 17) cycle
if (i == 23) cycle
if (i == 28) cycle
if (i == 30) cycle
!set off timer
write(10) achar([z'10', z'7e', z'68', z'79', z'79']), achar(i), achar(13) ! 16bit integer little endian
! read status
write(10) achar([z'16', z'1f', z'7e'])
read(10) iaa, ibb, ienergy, itimer, idensity, ixx, iyy, izz, res
print *, iaa, ibb, ienergy, itimer, idensity, ixx, iyy, izz, res
print *, i, itimer, '(sec) off timer' !/ 256 ! number correspondence (i xor 13)
end do
write(10) achar([z'10', z'7e', z'68', z'79', z'79']), achar(13), achar(2) ! 0 + 15x256 = 3840s
print *, 'energy ', ienergy / 256.0, '% '
print *, 'print density ', idensity
!
! set print speed [0..10]?
!write(10) achar([z'10', z'7e', z'68', z'79', z'7e', z'07'])
!print *, 'speed'
! self test
write(10) achar([22, 22, 13])
! print checker graphics
write(10) achar([22, 22, 15])
! print H line
write(10) achar([22, 22, 10])
! print slash lines 198mm
! write(10) achar([22, 22, 9])
! FEED
! feed lines (1/12mm?) 16bit integer little endian
write(10) achar([22, 22, 12]), achar([7, 13]) ! 10lines feed
read(10) ok
print *, ok
end program poooli
画面出力結果
honey@ubuntu:~$ sudo chmod 666 /dev/rfcomm0 ; ./a.out
OK print density
OK paper width
Poooli_L3L3PLL30021
190947Poooli_L3-3cf9191127_r38990015
82933cf9noneEND
19279 0 25601 3840 95 0 0 24 END
2 3840 (sec) off timer
energy 100.003906 %
print density 95
プリンタ出力結果
二値 Bitmap 出力
Poooli 社のプリンタは HPRT 社のプリンタ制御命令を利用しており、Bitmap data を miniLZO を利用して圧縮して送っている。
画像データは左上から右下へ 1dot あたり 1bit、8dot 文を 1byte のデータとして並べたあと、これを miniLZO で圧縮し、印字命令・画像幅・データ長の後にくっつけて、XOR 13 した上でプリンタに送る。
LZO 圧縮
LZO は辞書式圧縮法の一つで、圧縮率よりも圧縮時間の短さに力点を置いている。このため書庫的な利用よりも、データ通信のような実時間での利用に向いている。LZO はパラメータ等が多いため、利用者の便宜を図って使い方を簡便化した miniLZO というサブセットが提供されている。
http://www.oberhumer.com/opensource/lzo/
Fortran からの miniLZO 利用は、C 言語関数への interface をいくつか書くだけよい。
最小実行例
miniLZO の C のルーチンは、記憶領域の割り付けなどで Modern Fortran の動的な機能と組み合わせられなかったので、FORTRAN77 っぽく静的に記憶領域を確保した。このあたりはもう少し調査が必要に思われる。また初期化ルーチンは、変数の記憶領域サイズを検査しているだけだったようなので、省くことにした。(以下では初期化しない利用という禁じ手を使っているw)
コンパイル & 実行
honey@ubuntu:~/minilzo-2.10$ gfortran minilzo.c pooominimal.f90
honey@ubuntu:~/minilzo-2.10$ sudo chmod 666 /dev/rfcomm0 ; ./a.out
ソース
miniLZO で圧縮したデータ長が長くなりすぎるとプリンタが受け付けなくなる。以下では簡便のため Bitmap 上から下に 1 行毎に一つの単位として、データ長が短かくなるようにして圧縮・転送を行った。本来は数百行分をまとめて圧縮して送ることが可能である。その例は付録のプログラム例に与えた。
module minilzo_m
use, intrinsic :: iso_c_binding
implicit none
interface
integer(c_int64_t) function lzo1x_1_compress(src, src_len, dst, dst_len, wrkmem) &
bind(c, name = 'lzo1x_1_compress')
import
integer(c_int64_t), value :: src_len
character(c_char), intent(in) :: src(src_len)
type(c_ptr), value :: dst
integer(c_int64_t), intent(out) :: dst_len
integer(c_int64_t) :: wrkmem(16384)
end function lzo1x_1_compress
end interface
end module minilzo_m
program test
use, intrinsic :: iso_fortran_env
use minilzo_m
implicit none
!
integer, parameter :: nbmp = 1248 / 8 * 120
integer(int16) :: mx, my, nmy
integer(int32) :: mlzo
integer :: iw, i, j, ix, iy, nx, ny
! miniLZO
character(c_char), save :: bmpi(nbmp)
character(c_char), save, target :: bmpo(nbmp + nbmp / 16 + 64 + 3)
integer(c_intptr_t), save :: wk(16384)
integer(c_int64_t) :: mi, mo, iret
!
open(newunit = iw, file = '/dev/rfcomm0', access = 'stream', action = 'readwrite')
! header
write(iw) achar([z'1b', z'1c' ]), 'set mm', achar([05, 08])
! page type
write(10) achar([z'10', z'7e', z'68', z'79', z'7d', z'0d'])
! set paper width
write(iw) achar([z'10', z'7e', z'68', z'79', z'7a']), achar([z'ed', z'09']) ! 1248pixel
! 0/1 BMP
nx = 1248
ny = 100
!
mx = nx / 8
my = 1
mi = int(mx) * int(my)
do iy = 1, ny / my
do ix = 1, mx
if (mod(iy, 10) == 0) then
bmpi(ix) = achar(255)
else
bmpi(ix) = achar(0)
end if
end do
!
! data compression
!
! miniLZO by Markus F.X.J. Oberhumer
! http://www.oberhumer.com/opensource/lzo/#minilzo
! GPL v2+ http://www.oberhumer.com/opensource/gpl.html
iret = lzo1x_1_compress(bmpi, mi, c_loc(bmpo), mo, wk)
mlzo = mo ! 32bit <= 64bit
!
write(iw) achar([z'10', z'7b', z'3d', z'3d']) ! command BMP Graphics
write(iw) ieor(mx, z'0d0d'), ieor(my, z'0d0d') ! bmp size nx/8 my XOR 13 int16 little endian
write(iw) ieor(mlzo, z'0d0d0d0d') ! miniLZO data size XOR 13 int32 little endian
write(iw) achar(ieor(iachar(bmpo(1:mlzo)), z'0d')) ! miniLZO data XOR 13 char~uint8
end do
! line feed
write(iw) achar([z'16', z'16', z'0c']), achar([z'57', z'0d'])
close(iw)
end program test
プリンタ出力例
長さ 1248dot の 10 本の直線を出力。
グレースケール Bitmap 出力
Grayscale Bitmap の階調は、6 程度ではないかと推測される。普通の熱転写紙の場合は実質 5 階調である。専用紙を用いた場合については分からないが、著しく増えるようには思われない。
グレースケール印刷では、Bitmap 画像を各行ごとに
- 印字命令
- 行番号
- miniLZO 圧縮データ長
- miniLZO データ列
- Checksum
の 5 つの要素からなるデータ列を構成し、これを Byte 毎に XOR 13 してからプリンタに送る。
データをすべて送り終わった最後に、
- 終了コマンド
- 出力する最終行番号
を送ると印刷される。
画像データはいったん全てプリンタに送られてバッファに溜められるようである。行番号付きのデータは順不同で、重複してもよいようである。また最後の出力する最終行番号は、送ったデータの最大順序番号以下であればよく、0 からその番号までの画像データが順番に出力される。行番号は順不同だが 0 から始まる飛びの無い整数でなければならない。
Checksum の求め方は CRC32 で、除数に当たるマジックナンバーは標準的な z'EDB88320' になっている。しかし初期値は標準的な z'FFFFFFFF' ではなく、z'00077812' (489490 十進) と取られていることが分かった。Checksum を取るデータは、印字命令+行番号+miniLZO 圧縮データ長+miniLZO データ列の 4 要素の、このままの並びである。
BMP data は二値の場合と同じく、左上から右下に向かって順に 1dot 当たり 1bit で 8dots を左を最上位 bit とする並びで束ねて 1byte とし、これを画像の横幅分(つまり width/8 bytes) 用意する。グレースケールは、1行番号内にこの bmp 並びを重ね書きのように複数回繰り返すことで濃淡を指定する。
Checksum 推定法
Checksum は行番号 n と、miniLZO のデータ列の内容の双方に伴って変化するが、miniLZO の内容を固定し Checksum だけを変化させた場合、Checksum の変化は比較的単調で、これを Bluetooth 解析の結果と比較すると、miniLZO のデータに依存する定数との XOR で相互に変換できることが分かった。逆に行番号 n を固定した場合、Checksum は miniLZO のデータのみに依存する変化をするはずであるが、CRC32 の仕組みから除数を変化させては相互変換が不可能になるので、初期値を変化させるたと推測できる。総当たり法で初期値を変化させ望む初期値を得た。
Grayscale プログラム例
以下に一例として、グレースケールモードで黒い棒を引く例を示す。
コンパイル & 実行
honey@ubuntu:~/minilzo-2.10$ gfortran minilzo.c pooogray.f90 -fno-range-check
cc1: warning: command line option ‘-fno-range-check’ is valid for Fortran but not for C
honey@ubuntu:~/minilzo-2.10$ sudo chmod 666 /dev/rfcomm0 ; ./a.out
module minilzo_m
use, intrinsic :: iso_c_binding
implicit none
interface
integer(c_int64_t) function lzo1x_1_compress(src, src_len, dst, dst_len, wrkmem) &
bind(c, name = 'lzo1x_1_compress')
import
integer(c_int64_t), value :: src_len
character(c_char), intent(in) :: src(src_len)
type(c_ptr), value :: dst
integer(c_int64_t), intent(out) :: dst_len
integer(c_int64_t) :: wrkmem(:)
end function lzo1x_1_compress
end interface
end module minilzo_m
program test
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env
use minilzo_m
implicit none
integer, parameter :: nbmp = 1248
character(c_char) :: bmpi(nbmp)
character(c_char), target :: bmpo(nbmp + nbmp / 16 + 64 + 3)
integer(c_intptr_t) :: wk(16384)
integer(c_int64_t) :: ni, no, iret
character :: ok(2)
character :: info(64, 3), res(3), ip(17)
integer(int16) :: ienergy, itimer, idensity, iaa, ibb, ixx, iyy, izz
integer(int16) :: nx, ny, k16
integer(int32) :: nlzo, k32, icrc
integer(int8) :: m(2 * nbmp)
integer :: i, j, k
open(10, file = '/dev/rfcomm0', access = 'stream', action = 'readwrite')
! lp0: not work
!open(10, file = '/dev/usb/lp0', access = 'stream', action = 'readwrite')
! header?
write(10) achar([z'1b', z'1c' ]), 'set mm', achar([05, 08])
!read(10) ok
!print *, ok
! set paper type
write(10) achar([z'10', z'7e', z'68', z'79', z'7d', z'0d'])
!read(10) ok
!print *, ok
! set print density
!write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'07']) ! 19-->20 07-->10
!write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'3a']) ! 3a-->55
!write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'46']) ! 46-->75
write(10) achar([z'10', z'7e', z'68', z'79', z'6e', z'52']) ! 52-->95
!read(10) ok
!print *, ok, ' print density'
! set paper width
write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'ed', z'09']) ! 1248pixel
!write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'9d', z'0e']) ! 912pixel
!write(10) achar([z'10', z'7e', z'68', z'79', z'7a', z'85', z'0f']) ! 648pixel
!read(10) ok
!print *, ok, ' paper width'
! !
! ! data compression
! !
! ! miniLZO by Markus F.X.J. Oberhumer
! ! http://www.oberhumer.com/opensource/lzo/#minilzo
! ! GPL v2+ http://www.oberhumer.com/opensource/gpl.html
! ! iret = lzo1x_1_compress(bmpi, ni, c_loc(bmpo), no, wk)
! ! print *, 'miniLZO: compressed size = ', no
! ! print *, iachar(bmpo(:no))
! ! nlzo = no ! 32bit <= 64bit
!
!
! xor 13
! 1f 75 0a ! commnad for grayscaled bmp
! 0d 0d ! line number
! 33 0d 0d 0d ! size of miniLZO data; 32bit little endian integer
! 0e 0d ..... ! miniLZO data
k = 0
do i = -7, 7
do j = 1, 40
bmpi = achar(0)
if (i /= 0) bmpi(1:156 * abs(i)) = achar(255) ! 156 * 8 = 1248
!if (i /= 0) bmpi(1:114 * abs(i)) = achar(255) ! 114 * 8 = 912
ni = size(bmpi)
iret = lzo1x_1_compress(bmpi, ni, c_loc(bmpo), no, wk)
nlzo = no ! 32bit <= 64bit
m(1:3) = iachar(achar([18, 120, 07]))
m(4) = int(mod(k, 256), int8) ! 16bit little endian
m(5) = int(k / 256, int8)
m(6) = int(modulo(shiftr(nlzo, 0), 256), int8) ! 32bit little endian
m(7) = int(modulo(shiftr(nlzo, 8), 256), int8)
m(8) = int(modulo(shiftr(nlzo, 16), 256), int8)
m(9) = int(modulo(shiftr(nlzo, 24), 256), int8)
m(10:nlzo + 9) = iachar(bmpo(1:nlzo))
write(10) ieor(m(1:nlzo + 9), 13_int8)
! add checksum
icrc = icrc32(m(1:nlzo + 9), 489490) ! 489490 = z'00077812'
write(10) achar(ieor( modulo(shiftr(icrc, 0), 256), 13)) ! crc32 xor 13
write(10) achar(ieor( modulo(shiftr(icrc, 8), 256), 13)) ! 32bit little endian
write(10) achar(ieor( modulo(shiftr(icrc, 16), 256), 13))
write(10) achar(ieor( modulo(shiftr(icrc, 24), 256), 13))
k = k + 1
end do
end do
!
! END OF gray scale BMP data
write(10) achar( [z'1f', z'75', z'04'] ) ! command
write(10) ieor(k - 1, (((13 * 256) + 13) * 256 + 13) * 256 + 13) ! last no of line to print ;(smaller than total number of lines)
!
!---------------------------------------------------------------------
!
! line feed
!write(10) achar([22, 22, 12]), achar([7, 13]) ! 10lines feed !2byte little endian
!read(10) ok
!print *, ok
close(10)
contains
function icrc32(m, icrc0) result(icrc)
integer(int8 ), intent(in) :: m(:)
integer(int32), intent(in) :: icrc0
integer(int32) :: icrc
integer(int32), parameter :: mgck = z'EDB88320' ! CRC32
! integer(int32), parameter :: mgck = z'82F63B78' ! CRC32C
integer :: i, j
logical :: qcrc
icrc = icrc0
do i = 1, size(m)
do j = 0, 7
if (btest(m(i), j)) icrc = ieor(icrc, 1)
qcrc = btest(icrc, 0)
icrc = shiftr(icrc, 1)
if (qcrc) icrc = ieor(icrc, mgck)
end do
end do
icrc = not(icrc)
end function icrc32
end program test
ここでは出力させなかったが、終了命令に対してはプリンタの応答があり、文字列と数値を送ってくる。但し一部 gray を gary と綴りを間違えている。
gray 00 00 ff ff ff ff
gary_finish
プリンタ出力例
まとめと結論
Poooli 社の L3 感熱紙プリンタの制御命令を、Bluetooth 信号の Log 解析と Andoroid スマートフォン・アプリの逆コンパイルという、リバースエンジニアリングの手法を用いて解読した。これにより、二値およびグレースケールの Bitmap 画像が、専用プログラムに依らずに出力可能になった。
プリンタの制御命令は HPRT 社のものを用いているようであったが、プリンタに送る信号は命令、データともども byte 毎に XOR 13 されて難読化されていることが分かった。画像データのフォーマットは、基本的に左上から右下に向かって、1dot 当たり 1bit を割り当てて、8dots 毎に 1byte のデータとしていた。グレースケールは、このデータ並びを複数回繰り返すことで「重ね打ち」のように濃度を変化させていた。画像データは miniLZO 圧縮によりデータを圧縮した上で送信されていた。
LZO という圧縮フォーマットの存在を今回初めて知ったが、プリンタのデータ転送に用いるのは(特にグレースケール印刷はデータ量が増えるので)合理的と思われた。グレースケールのデータには Checksum がつけられておりこれ自体は合理的と思われたが、標準的な初期値が使われておらず、XOR 13 をかけることとともに技術的な合理性の無い無用な難読化に思われた。
ここで得られた結果は、共通のスマホアプリを利用する Poooli L1/L1Pro, L2/L2 Pro 等の他の Poooli 製品にも適用できると思われる。Poooli 社の感熱紙プリンタの解析は Github 上にも見当たらないので、本記事が多少とも参考になるのではないかと思う。
参考サイト等
リバースエンジニアリング解析
以下の記事内の参照サイト
CRC32
miniLZO
付録 二値 Bitmap 作画例
Phomemo M02S と同様のプログラム。
プリンタ出力図
プログラム
module minilzo_m
use, intrinsic :: iso_c_binding
implicit none
interface
integer(c_int64_t) function lzo1x_1_compress(src, src_len, dst, dst_len, wrkmem) &
bind(c, name = 'lzo1x_1_compress')
import
integer(c_int64_t), value :: src_len
character(c_char), intent(in) :: src(src_len)
type(c_ptr), value :: dst
integer(c_int64_t), intent(out) :: dst_len
integer(c_int64_t) :: wrkmem(16384)
end function lzo1x_1_compress
end interface
end module minilzo_m
module bw_m
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env
use minilzo_m
implicit none
type :: bw_t
integer :: nx = 1248 / 8, ny = 1 ! Poooli L3
integer(int8), allocatable :: bw(:, :)
contains
procedure :: init => init_bw
procedure :: pr => pr_bw_poooli
procedure :: point => point_bw
procedure :: dot => dot_bw
procedure :: line => line_bw
procedure :: line_rel => line_rel_bw
procedure :: lines => lines_bw
end type bw_t
contains
subroutine init_bw(pic, nx, ny)
class (bw_t), intent(in out) :: pic
integer, intent(in) :: nx, ny
allocate(pic%bw(nx / 8, ny))
pic%nx = nx
pic%ny = ny
pic%bw = 0
end subroutine init_bw
subroutine pr_bw_poooli(pic, fn)
class (bw_t), intent(in) :: pic
character (len = *), intent(in) :: fn
character :: firmware(5), energy(3)
integer(int16) :: mx, my, nmy, k16
integer(int32) :: mlzo, k32
integer, parameter :: nbmp = 1248 / 8 * 4096
character(c_char), save :: bmpi(nbmp)
character(c_char), save, target :: bmpo(nbmp + nbmp / 16 + 64 + 3)
integer(c_intptr_t), save :: wk(16384)
integer(c_int64_t) :: mi, mo, iret
integer :: iw, i, j, ix, iy, kk
associate(nx => pic%nx, ny => pic%ny)
open(newunit = iw, file = fn, access = 'stream', action = 'readwrite')
! header
! write(iw) achar([z'1b', z'1c' ]), 'set mm', achar([05, 08])
write(iw) achar([27, 28]), 'set mm', achar([05, 08])
! page type
write(10) achar([z'10', z'7e', z'68', z'79', z'7d', z'0d'])
! concentration
!write(iw) achar([z'10', z'7e', z'68', z'79', z'6e', z'3a']) ! 3a-->55
!write(iw) achar([z'10', z'7e', z'68', z'79', z'6e', z'46']) ! 46-->75
!write(iw) achar([z'10', z'7e', z'68', z'79', z'6e', z'52']) ! 52-->95
write(iw) achar([16, 126, 104, 121, 110, 82]) ! 52-->95
! set paper width
write(iw) achar([16, 126, 104, 121, 122, 237, 09]) ! 1248pixel
!write(iw) achar([z'10', z'7e', z'68', z'79', z'7a', z'ed', z'09']) ! 1248pixel
!write(iw) achar([z'10', z'7e', z'68', z'79', z'7a', z'9d', z'0e']) ! 912pixel
!write(iw) achar([z'10', z'7e', z'68', z'79', z'7a', z'85', z'0f']) ! 648pixel
! set feeder speed 0..10 ? !30
!write(iw) achar([z'10', z'7e', z'68', z'79', z'7e', z'08']) ! 08 --> 05
!write(iw) achar([z'10', z'7e', z'68', z'79', z'7e', z'07']) ! 07 --> 10
write(iw) achar([16, 126, 104, 121, 126, 07]) ! 07 --> 10
! status info
! write(10) achar([z'16', z'1f', z'69'])
! 0/1 BMP
mx = nx / 8
my = 120
do kk = 0, ny / my
nmy = my
if (kk == ny / my) then ! remain part
nmy = mod(ny, my)
if (nmy == 0) exit
end if
j = 0
do iy = 1, nmy
do ix = 1, mx
j = j + 1
bmpi(j) = achar(pic%bw(ix, iy+kk*my))
end do
end do
!
! data compression
!
! miniLZO by Markus F.X.J. Oberhumer
! http://www.oberhumer.com/opensource/lzo/#minilzo
! GPL v2+ http://www.oberhumer.com/opensource/gpl.html
mi = int(mx) * int(nmy)
iret = lzo1x_1_compress(bmpi, mi, c_loc(bmpo), mo, wk)
mlzo = mo ! 32bit <= 64bit
!
k16 = 13 * 256 + 13
k32 = ((13 * 256 + 13) * 256 + 13) * 256 + 13
!write(iw) achar([z'10', z'7b', z'3d', z'3d']) ! command BMP Graphics
write(iw) achar([16, 123, 61, 61]) ! command
write(iw) ieor(k16, mx), ieor(k16, my) ! bmp size nx/8 ny XOR 13 int16 little endian
write(iw) ieor(k32, mlzo) ! miniLZO data size XOR 13 int32 little endian
write(iw) (achar(ieor(13, (iachar(bmpo(i))))), i = 1, mo) ! miniLZO data XOR 13 char~uint8
!flush(iw)
!print *, 'mo=', mo
end do
! self test
! write(10) achar([22, 22, 13])
! print checker pattern
! write(iw) achar([22, 22, 15])
! line feed
!write(10) achar([22, 22, 12]), achar([7, 13]) ! 10lines feed !2byte little endian
close(iw)
end associate
end subroutine pr_bw_poooli
subroutine point_bw(pic, ix, iy)
class (bw_t), intent(in out) :: pic
integer , intent(in) :: ix, iy
integer :: jx, mx
if (ix > 0 .and. ix <= pic%nx .and. iy > 0 .and. iy <=pic%ny) then
jx = (ix - 1) / 8 + 1
mx = 7 - mod(ix - 1, 8)
pic%bw(jx, iy) = ibset(pic%bw(jx, iy), mx)
end if
end subroutine point_bw
subroutine dot_bw(pic, ix, iy, nsize)
class (bw_t), intent(in out) :: pic
integer , intent(in) :: ix, iy, nsize
integer :: kx, ky
do kx = -nsize/2, nsize/2
do ky = -nsize/2, nsize/2
call pic%point(ix + kx, iy + ky)
end do
end do
end subroutine dot_bw
subroutine line_bw(pic, ix0, iy0, ix1, iy1, iwidth0)
class (bw_t), intent(in out) :: pic
integer , intent(in) :: ix0, iy0, ix1, iy1
integer, intent(in), optional :: iwidth0
integer :: ix, iy, mx, my, iwidth = 1
real :: dx, dy, x, y
if (present(iwidth0)) iwidth = iwidth0
mx = ix1 - ix0
my = iy1 - iy0
if (mx == 0 .and. my == 0) return
if (abs(mx) > abs(my)) then
dy = my / real(mx)
y = iy0
do ix = 0, mx, sign(1, mx)
y = dy * ix
iy = nint(y)
call pic%dot(ix0 + ix, iy0 + iy, iwidth)
end do
else
dx = mx / real(my)
x = ix0
do iy = 0, my, sign(1, my)
x = dx * iy
ix = nint(x)
call pic%dot(ix0 + ix, iy0 + iy, iwidth)
end do
end if
end subroutine line_bw
subroutine line_rel_bw(pic, ix, iy, ipen)
class (bw_t), intent(in out) :: pic
integer, intent(in) :: ix, iy, ipen
integer, save :: kx = 0, ky = 0
if (ipen == 1) call pic%line(kx, ky, kx + ix, ky - iy)
kx = kx + ix
ky = ky - iy ! reverse y-axis diection
end subroutine line_rel_bw
subroutine lines_bw(pic, pos, mag0)
class (bw_t), intent(in out) :: pic
integer, intent(in) :: pos(:)
integer, intent(in), optional :: mag0
integer :: i, ix, iy, ipen
integer, save :: mag = 1
if (present(mag0)) mag = mag0
do i = 1, size(pos) / 3
ix = pos(3 * i - 2) * mag
iy = pos(3 * i - 1) * mag
ipen = pos(3 * i)
call pic%line_rel(ix, iy, ipen)
end do
end subroutine lines_bw
end module bw_m
program test
use :: bw_m
implicit none
block
type(bw_t) :: fig1
integer :: nx = 1248, ny = 400
integer :: i, ix, iy
real :: x(10000, 2)
call fig1%init(nx, ny)
call random_number(x)
do i = 1, size(x, 1)
call fig1%point(int(nx * x(i, 1)), int(ny * x(i, 2)))
end do
call fig1%line( 1, 1, nx, 1)
call fig1%line( 1, 1, 1, ny)
call fig1%line( 1, ny, nx, ny)
call fig1%line(nx, 1, nx, ny)
call fig1%line( 1, 1, nx, ny )
call fig1%line(nx, 1, 1, ny)
call fig1%pr('/dev/rfcomm0')
end block
call sleep(1) ! non-standard iFortran
block
type(bw_t) :: fig2
integer :: nx = 1248, ny = 800
integer :: ix, iy, nr, ix0, ix1, iy0, iy1
call fig2%init(nx, ny)
nr = 100
do iy = 0, nr
ix = sqrt(real(nr**2 - iy**2))
ix0 = nx / 2 - ix
ix1 = nx / 2 + ix
iy0 = ny / 2 - iy
iy1 = ny / 2 + iy
call fig2%line(ix0, iy0, ix1, iy0)
call fig2%line(ix0, iy1, ix1, iy1)
end do
do ix = 1, 3 * nx / 18
ix0 = 0 + ix
ix1 = nx - ix
iy0 = ny
iy1 = 1
call fig2%line(ix0, iy0, ix1, iy1)
call fig2%line(ix1, iy0, ix0, iy1)
end do
do ix = nx / 4, 6 * nx / 17
ix0 = 0 + ix
ix1 = nx - ix
iy0 = ny
iy1 = 1
call fig2%line(ix0, iy0, ix1, iy1)
call fig2%line(ix1, iy0, ix0, iy1)
end do
do ix = 7 * nx / 16, nx / 2
ix0 = 0 + ix
ix1 = nx - ix
iy0 = ny
iy1 = 1
call fig2%line(ix0, iy0, ix1, iy1)
call fig2%line(ix1, iy0, ix0, iy1)
end do
do iy = ny / 7, 5 * ny / 17
ix0 = 1
ix1 = nx
iy0 = 1 + iy
iy1 = ny - iy
call fig2%line(ix0, iy0, ix1, iy1)
call fig2%line(ix1, iy0, ix0, iy1)
end do
do iy = 8 * ny / 19, ny / 2
ix0 = 1
ix1 = nx
iy0 = 1 + iy
iy1 = ny - iy
call fig2%line(ix0, iy0, ix1, iy1)
call fig2%line(ix1, iy0, ix0, iy1)
end do
call fig2%pr('/dev/rfcomm0')
end block
call sleep(1) ! non-standard Fortran
block
type(bw_t) :: fig3
integer :: nx = 1248, ny = 400
integer :: ix, iy, ix0, iy0, ix1, iy1
real :: x, y, pi = 4 * atan(1.0)
call fig3%init(nx, ny)
call fig3%line( 1, 1, nx, 1)
call fig3%line( 1, 2, nx, 2)
call fig3%line( 1, 1, 1, ny)
call fig3%line( 2, 1, 2, ny)
call fig3%line( 1, ny, nx, ny)
call fig3%line( 1, ny - 1, nx, ny - 1)
call fig3%line(nx, 1, nx, ny)
call fig3%line(nx - 1, 1, nx - 1, ny)
call fig3%line( 1, ny/2, nx, ny/2)
call fig3%line( 1, ny/2 + 1, nx, ny/2 + 1)
ix0 = 1
iy0 = int(300 * bessel_j0(0.0))
do ix = 1, nx - 1
x = ix / 20.0
y = 200 * bessel_j0(x)
ix1 = ix + 1
iy1 = -int(y) + ny / 2
call fig3%line(ix0, iy0, ix1, iy1)
call fig3%line(ix0, iy0+1, ix1, iy1+1)
call fig3%line(ix0, iy0-1, ix1, iy1-1)
ix0 = ix1
iy0 = iy1
end do
ix0 = 1
iy0 = int(300 * cos(-pi/4))
do ix = 1, nx - 1
x = ix / 20.0
y = 200 * sqrt(2 / (pi *x)) * cos(x - pi/4)
ix1 = ix + 1
iy1 = -int(y) + ny / 2
call fig3%line(ix0, iy0, ix1, iy1)
call fig3%line(ix0, iy0+1, ix1, iy1+1)
call fig3%line(ix0, iy0-1, ix1, iy1-1)
ix0 = ix1
iy0 = iy1
end do
call fig3%pr('/dev/rfcomm0')
end block
end program test