#+TITLE: Setting up an Xprinter thermal receipt printer WiFi from Linux
#+DATE: 2021-10-12T15:57:12-04:00
#+DRAFT: true
#+DESCRIPTION:
#+TAGS[]:
#+KEYWORDS[]:
#+SLUG:
#+SUMMARY:

I recently purchased a thermal receipt printer off of [[https://web.archive.org/web/20211012195845/https://www.aliexpress.com/item/32842111016.html?spm=a2g0s.9042311.0.0.207d4c4d6xNWgO][AliExpress]] for a
project. It features both WiFi and USB connectivity which I thought
was really cool for the price.

To my dismay, I realized after purchasing that the drivers and
configuration application only run on Windows.

This wasn't a huge deal, as thermal printers generally use the
somewhat kinda standardized command set called [[https://github.com/escpos/escpos][ESC/POS]]. Unfortunately
while many of the formatting commands are shared between printers, the
commands to setup the WiFi connection don't seem to be documented
anywhere, and I suspect are device-specific.

Since booting into Windows every time I want to manage the printer's
network settings isn't ideal, I decided to reverse engineer the
WiFi configuration protocol.

Initially I tried to run the configuration tool in wine, but it
couldn't communicate with the printer over USB, which wasn't too
surprising.

I booted my spare laptop into windows and launched the config tool
there.

I then setup the WiFi through =Advanced -> Set Net=

#+ATTR_HTML: :title Xprinter config tool running in wine
#+ATTR_HTML: :alt Xprinter config tool running in wine
[[file:xprinter_setup_tool.png]]


#+ATTR_HTML: :title Xprinter advanced settings
#+ATTR_HTML: :alt Xprinter advanced settings
[[file:xprinter_advanced.png]]

#+ATTR_HTML: :title Xprinter net settings
#+ATTR_HTML: :alt Xprinter net settings
[[file:xprinter_set_net.png]]

At this point I noticed that the application supported configuring the
printer over the network, meaning I might be able to change the
settings under wine again, as network sockets should work normally.

After rebooting into Linux and testing my assumption, it turned out to
be true, I was able to run the configuration tool without issue as
long as it was connected through the network.

While I could have stopped there I decided to go one step further and
reverse the command sequence that configures the WiFi settings so I
could re-configure the printer's WiFi over USB if it ever got messed
up.

To do that, I sniffed the traffic going from the Xprinter application
and the printer's socket using Wireshark.

[[file:xprinter_wireshark.png]]

In the examples below I've encoded the decimal numbers to hex to make
it easier to understand the contents of the packets.

Based on the traffic, I was able to come up with the following.

* Setting IP address

  The following sets
  - IP =192.168.0.7=

  The IP hex:
  #+begin_src ruby
  [192, 168, 0, 7].map { _1.to_s(16).rjust(2, '0') }
  #+end_src

  #+RESULTS:
  | c0 | a8 | 00 | 07 |

  Packate contents from wireshark:

  =0000   1f 1b 1f 22 c0 a8 00 07=

  | Description    | Characters    |
  |----------------+---------------|
  | Unit separator | =1f=          |
  | Escape         | =1b=          |
  | Unit separator | =1f=          |
  | Command code   | =22=          |
  | IP             | =c0 a8 00 07= |

* Setting Subnet Mask

  The following sets
  - Subnet mask =255.255.255.0=

  Subnet mask to hex:
  #+begin_src ruby
  [255, 255, 255, 0].map { _1.to_s(16).rjust(2, '0') }
  #+end_src

  #+RESULTS:
  | ff | ff | ff | 00 |

  Packate contents from wireshark:

  =0000   1f 1b 1f b0 ff ff ff 00=

  | Description    | Character     |
  |----------------+---------------|
  | Unit separator | =1f=          |
  | Escape         | =1b=          |
  | Unit separator | =1f=          |
  | Command code   | =b0=          |
  | Subnet mask    | =ff ff ff 00= |

* Setting gateway

  The following sets
  - Gateway =192.168.0.1=

  Subnet mask to hex:
  #+begin_src ruby
  [192, 168, 0, 1].map { _1.to_s(16).rjust(2, '0') }
  #+end_src

  #+RESULTS:
  | c0 | a8 | 00 | 01 |

  Packate contents from wireshark:

  =0000   1f 1b 1f b1 c0 a8 00 01=

  | Description    | Character     |
  |----------------+---------------|
  | Unit separator | =1f=          |
  | Escape         | =1b=          |
  | Unit separator | =1f=          |
  | Command code   | =b1=          |
  | Net Mask        | =c0 a8 00 01= |

* Setting IP, netmask, and gateway

  The following sets
  - IP =192.168.0.1=
  - Subnet mask =255.255.255.0=
  - Gateway =192.168.0.1=

  Packate contents from wireshark:

  =0000   1f 1b 1f b2 c0 a8 00 07 ff ff ff 00 c0 a8 00 01=

  | Purpose        | Character     |
  |----------------+---------------|
  | Unit separator | =1f=          |
  | Escape         | =1b=          |
  | Unit separator | =1f=          |
  | Command code   | =b2=          |
  | IP             | =c0 a8 00 07= |
  | Subnet mask    | =ff ff ff 00= |
  | Gateway        | =c0 a8 00 01= |

* Setting WiFi network

  The following sets
  - SSID =SSID_HERE=
  - Key =PASSWORD_HERE=
  - Key Type =WPA2_AES_PSK=

  SSID to hex:
  #+begin_src ruby
  "SSID_HERE".bytes.map { _1.to_s(16) }
  #+end_src

  #+RESULTS:
  | 53 | 53 | 49 | 44 | 5f | 48 | 45 | 52 | 45 |

  Key to hex:
  #+begin_src ruby
  "PASSWORD_HERE".bytes.map { _1.to_s(16) }
  #+end_src

  #+RESULTS:
  | 50 | 41 | 53 | 53 | 57 | 4f | 52 | 44 | 5f | 48 | 45 | 52 | 45 |

  Packate contents from wireshark (including string representation):
    #+begin_src
    0000   1f 1b 1f b3 06 53 53 49 44 5f 48 45 52 45 00 50   .....SSID_HERE.P
    0010   41 53 53 57 4f 52 44 5f 48 45 52 45 00            ASSWORD_HERE.
    #+end_src

  | Purpose         | Character       |
  |-----------------+-----------------|
  | Unit separator  | =1f=            |
  | Escape          | =1b=            |
  | Unit separator  | =1f=            |
  | Command code    | =b3=            |
  | Key type        | =06=            |
  | SSID            | =SSID_HERE=     |
  | NUL-termination | =00=            |
  | Key             | =PASSWORD_HERE= |
  | NUL-termination | =00=            |

  If the WiFi key type is anything like the menu, the other key types
  are as follows

  | Key Type             | Value |
  |----------------------+-------|
  | =NULL=               | =00=  |
  | =WEP64=              | =01=  |
  | =WEP128=             | =02=  |
  | =WPA_AES_PSK=        | =03=  |
  | =WPA_TKIP_PSK=       | =04=  |
  | =WPA_TKIP_AES_PSK=   | =05=  |
  | =WPA2_AES_PSK=       | =06=  |
  | =WPA2_TKIP=          | =07=  |
  | =WPA2_TKIP_AES_PSK=  | =08=  |
  | =WPA_WPA2_MixedMode= | =09=  |

* Setting all network options

  The following sets
  - IP =192.168.0.7=
  - Subnet mask =255.255.255.0=
  - Gateway =192.168.0.1=
  - SSID =SSID_HERE=
  - Key =PASSWORD_HERE=
  - Key Type =WPA2_AES_PSK=

  Packate contents from wireshark (including string representation):

  #+begin_src
  0000   1f 1b 1f b4 c0 a8 00 07 ff ff ff 00 c0 a8 00 01   ................
  0010   06 53 53 49 44 5f 48 45 52 45 00 50 41 53 53 57   .SSID_HERE.PASSW
  0020   4f 52 44 5f 48 45 52 45 00                        ORD_HERE.
  #+end_src

  | Description     | Character       |
  |-----------------+-----------------|
  | Unit Separator  | =1f=            |
  | Escape          | =1b=            |
  | Unit Separator  | =1f=            |
  | Command Code    | =b4=            |
  | IP              | =c0 a8 00 07=   |
  | NetMask         | =ff ff ff 00=   |
  | Gateway         | =c0 a8 00 01=   |
  | Key Type        | =06=            |
  | SSID            | =SSID_HERE=     |
  | NUL-termination | =00=            |
  | Key             | =PASSWORD_HERE= |
  | NUL-termination | =00=            |