Poooli L3 感熱紙プリンタへの Fortran からの出力　備忘録（改訂版）

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 の取得
プリンタの個人情報をさらけ出すスタイルｗ
```
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 されていることが分かった。

## プリンタの情報

プリンタ情報をとり、いくつかのテストパターンを印刷するプログラム。

```fortran
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
```
#### プリンタ出力結果
![IMG_20220404_0004.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44522/df97cc1f-a33e-33f1-9aaf-33d1969c11ed.jpeg)
以下斜線が計 19.8㎝ 続く。

## 二値 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 っぽく静的に記憶領域を確保した。このあたりはもう少し調査が必要に思われる。また初期化ルーチンは、変数の記憶領域サイズを検査しているだけだったようなので、省くことにした。（以下では初期化しない利用という禁じ手を使っているｗ）
 
コンパイル & 実行
```
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 行毎に一つの単位として、データ長が短かくなるようにして圧縮・転送を行った。本来は数百行分をまとめて圧縮して送ることが可能である。その例は付録のプログラム例に与えた。 


```fortran 
    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 本の直線を出力。

![IMG_20220404_0003.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44522/6ed38d0b-8079-19ba-1311-d514a946858d.jpeg)

```
 　
```


## グレースケール  Bitmap 出力
Grayscale Bitmap の階調は、6 程度ではないかと推測される。普通の熱転写紙の場合は実質 5 階調である。専用紙を用いた場合については分からないが、著しく増えるようには思われない。

グレースケール印刷では、Bitmap 画像を各行ごとに
1. 印字命令
2. 行番号
3. miniLZO 圧縮データ長
4. miniLZO データ列
5. Checksum
の 5 つの要素からなるデータ列を構成し、これを Byte 毎に XOR 13 してからプリンタに送る。

データをすべて送り終わった最後に、
1. 終了コマンド
2. 出力する最終行番号
を送ると印刷される。

画像データはいったん全てプリンタに送られてバッファに溜められるようである。行番号付きのデータは順不同で、重複してもよいようである。また最後の出力する最終行番号は、送ったデータの最大順序番号以下であればよく、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) 用意する。グレースケールは、１行番号内にこの 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
```

```fortran
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
```


#### プリンタ出力例
![DSC_4184 - コピー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44522/f6ef0301-bf60-1117-704a-5c7241cbd1b9.jpeg)




# まとめと結論

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 上にも見当たらないので、本記事が多少とも参考になるのではないかと思う。

# 参考サイト等

## リバースエンジニアリング解析
以下の記事内の参照サイト

https://qiita.com/cure_honey/items/72124ff8effddc075e9b

## CRC32
https://qiita.com/mikecat_mixc/items/e5d236e3a3803ef7d3c5

## miniLZO
http://www.oberhumer.com/opensource/lzo/

https://qiita.com/nishemon/items/818cc64dc2f8577edd87



# 付録 二値 Bitmap 作画例
Phomemo M02S と同様のプログラム。


#### プリンタ出力図
![IMG_20220404_0006.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44522/827314f3-9f62-ce54-adf8-24e57f6bcb3d.jpeg)


#### プログラム
```fortran
    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
```