├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── config.example.py ├── const.py ├── hardware.py ├── image_data.py ├── littleprinter.py ├── notes ├── Bt203.txt ├── RPi.txt ├── const.txt └── const_newest.txt ├── printer.py ├── proprietary_binaries ├── MIAO1.0.2.dex ├── MIAO1.0.4.dex ├── MIAO1.0.7.dex ├── MIAO2.0.3.dex └── mmj_p1_fw_v127.bin ├── requirements.txt └── testprint.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 14 * * 3' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v1 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | .idea 105 | 106 | config.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ihc童鞋@提不起劲 4 | Copyright (c) 2019 BroncoTc 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paperang(喵喵机) Python API, now with Little Printer integration! 2 | 3 | ### Requirements & Dependencies 4 | 5 | OS: OSX (tested on Catalina)/Linux (tested Debian Buster on a Raspberry Pi 4) 6 | Python: 3.5-3.7 (tested) 7 | 8 | Required debian packages: `libbluetooth-dev libhidapi-dev libatlas-base-dev python3-llvmlite python3-numba python-llvmlite llvm-dev` 9 | 10 | Python Modules: install with `pip3 install -r requirements.txt` 11 | 12 | ### macOS instructions 13 | 14 | #### Set up and test your printer 15 | You'll need python3 installed; check if you have it by typing `which python3` in Terminal or your favorite console application. 16 | 17 | 1. Install necessary python modules: 18 | ```sh 19 | pip3 install -r requirements.txt 20 | ``` 21 | 2. Ensure bluetooth is enabled on your computer. You do *not* need to connect your Paperang to your computer yet! We'll do that later, via the command line. 22 | 3. Turn on your Paperang and set it near your computer 23 | 4. Run the test script, which will tell your Paperang to print a self-test if it's successful: 24 | ```sh 25 | python3 testprint.py 26 | ``` 27 | If you've never paired your Paperang with your computer, you might get a dialog asking you to allow the Paperang to pair with your system. Click `connect`. You should only have to do this once. 28 | 29 | 5. If the test print was successful, the script will print out your device's MAC address on the console, as well as on the printer. You can enter that into the script to connect to your Paperang directly, avoiding the wait time for scanning for printers. 30 | 31 | If you need to look up your Paperang's MAC address quickly, you can use the `system_profiler` command to output information on all paired bluetooth devices: 32 | ```sh 33 | system_profiler SPBluetoothDataType 34 | ``` 35 | 36 | #### Print Little Printer data 37 | 38 | 39 | 40 | ### Establishing a connection 41 | 42 | `BtManager()` Leave the parameters blank to search for nearby paperang devices 43 | 44 | `BtManager("AA:BB:CC:DD:EE:FF")` Calling with a specific MAC address skips searching for devices, saving time 45 | 46 | ### Printing images 47 | 48 | The printer's API only accepts binary images for printing, so we need to convert text to images on the client side. 49 | 50 | The format of the printed image is binary data, each bit represents black (1) or white (0), and 384 dots per line. 51 | 52 | ```python 53 | mmj = BtManager() 54 | mmj.sendImageToBt(img) 55 | mmj.disconnect() 56 | ``` 57 | 58 | ### 其他杂项 59 | 60 | `registerCrcKeyToBt(key=123456)` 更改通信CRC32 KEY(不太懂这么做是为了啥,讲道理监听到这个包就能拿到key的) 61 | 62 | `sendPaperTypeToBt(paperType=0)` 更改纸张类型(疯狂卖纸呢) 63 | 64 | `sendPowerOffTimeToBt(poweroff_time=0)` 更改自动关机时间 65 | 66 | `sendSelfTestToBt()` 打印自检页面 67 | 68 | `sendDensityToBt(density)` 设置打印密度 69 | 70 | `sendFeedLineToBt(length)` 控制打印完后的padding 71 | 72 | `queryBatteryStatus()` 查询剩余电量 73 | 74 | `queryDensity()` 查询打印密度 75 | 76 | `sendFeedToHeadLineToBt(length)` 不太懂和 `sendFeedLineToBt` 有什么区别,但是看起来都是在打印后调用的。 77 | 78 | `queryPowerOffTime()` 查询自动关机时间 79 | 80 | `querySNFromBt()` 查询设备SN 81 | 82 | 其实还有挺多操作的,有兴趣的看着`const.py`猜一猜好了。 83 | 84 | ### 图像工具 85 | 86 | `ImageConverter.image2bmp(path)` 任意图像到可供打印的二进制数据转换 87 | 88 | `TextConverter.text2bmp(text)` 指定文字到可供打印的二进制数据转换 89 | 90 | ### 微信公众平台工具 91 | 92 | 两个小脚本,用来实现发送图片给微信公众号后自动打印。 93 | 94 | `wechat.php` 用于VPS接收腾讯数据,默认只允许指定用户打印。 95 | 96 | `printer_server.py` 放置于树莓派等有蓝牙的靠近喵喵机的机器上运行,可以使用`tinc`等建立VPN以供VPS直接访问。 97 | 98 | ### 吐槽 99 | 100 | 这玩意就不能增加一个多次打印的功能吗?以较低温度多次打印再走纸,应该可以实现打印灰度图的。 101 | 102 | 逆了好久的固件也没搞出来啥东西,真是菜。希望有大佬能告诉我一点人生的经验。 103 | 104 | 顺便丢两个芯片型号: `NUC123LD4BN0`, `STM32F071CBU6`,似乎是Cortex-M0。 105 | 106 | PS: 本代码仅供非盈利用途,如用于商业用途请另请高明。 107 | 108 | ### Acknowledgement 致谢 109 | Thanks for all the reverse engineering work done by the original author of this project. 110 | 111 | -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | macaddress = "00:11:22:33:44:55" 3 | width = 384 4 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*-coding:utf-8-*- 3 | 4 | from enum import Enum 5 | 6 | class BtCommandByte(): 7 | @staticmethod 8 | def findCommand(c): 9 | keys = list(filter(lambda x: not x.startswith("__") and BtCommandByte.__getattribute__(BtCommandByte, x) == c, 10 | dir(BtCommandByte))) 11 | return keys[0] if keys else "NO_MATCH_COMMAND" 12 | 13 | __fmversion__ = "1.2.7" 14 | PRT_PRINT_DATA = 0 15 | PRT_PRINT_DATA_COMPRESS = 1 16 | PRT_FIRMWARE_DATA = 2 17 | PRT_USB_UPDATE_FIRMWARE = 3 18 | PRT_GET_VERSION = 4 19 | PRT_SENT_VERSION = 5 20 | PRT_GET_MODEL = 6 21 | PRT_SENT_MODEL = 7 22 | PRT_GET_BT_MAC = 8 23 | PRT_SENT_BT_MAC = 9 24 | PRT_GET_SN = 10 25 | PRT_SENT_SN = 11 26 | PRT_GET_STATUS = 12 27 | PRT_SENT_STATUS = 13 28 | PRT_GET_VOLTAGE = 14 29 | PRT_SENT_VOLTAGE = 15 30 | PRT_GET_BAT_STATUS = 16 31 | PRT_SENT_BAT_STATUS = 17 32 | PRT_GET_TEMP = 18 33 | PRT_SENT_TEMP = 19 34 | PRT_SET_FACTORY_STATUS = 20 35 | PRT_GET_FACTORY_STATUS = 21 36 | PRT_SENT_FACTORY_STATUS = 22 37 | PRT_SENT_BT_STATUS = 23 38 | PRT_SET_CRC_KEY = 24 39 | PRT_SET_HEAT_DENSITY = 25 40 | PRT_FEED_LINE = 26 41 | PRT_PRINT_TEST_PAGE = 27 42 | PRT_GET_HEAT_DENSITY = 28 43 | PRT_SENT_HEAT_DENSITY = 29 44 | PRT_SET_POWER_DOWN_TIME = 30 45 | PRT_GET_POWER_DOWN_TIME = 31 46 | PRT_SENT_POWER_DOWN_TIME = 32 47 | PRT_FEED_TO_HEAD_LINE = 33 48 | PRT_PRINT_DEFAULT_PARA = 34 49 | PRT_GET_BOARD_VERSION = 35 50 | PRT_SENT_BOARD_VERSION = 36 51 | PRT_GET_HW_INFO = 37 52 | PRT_SENT_HW_INFO = 38 53 | PRT_SET_MAX_GAP_LENGTH = 39 54 | PRT_GET_MAX_GAP_LENGTH = 40 55 | PRT_SENT_MAX_GAP_LENGTH = 41 56 | PRT_GET_PAPER_TYPE = 42 57 | PRT_SENT_PAPER_TYPE = 43 58 | PRT_SET_PAPER_TYPE = 44 59 | PRT_GET_COUNTRY_NAME = 45 60 | PRT_SENT_COUNTRY_NAME = 46 61 | PRT_DISCONNECT_BT_CMD = 47 62 | PRT_MAX_CMD = 48 63 | 64 | -------------------------------------------------------------------------------- /hardware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*-coding:utf-8-*- 3 | 4 | import struct, zlib, logging 5 | from bluetooth import BluetoothSocket, find_service, RFCOMM, discover_devices 6 | from const import BtCommandByte 7 | from platform import system #so we can tell which OS we're using 8 | import codecs 9 | 10 | 11 | class Paperang: 12 | standardKey = 0x35769521 13 | padding_line = 300 14 | max_send_msg_length = 1536 15 | max_recv_msg_length = 1024 16 | service_uuid = "00001101-0000-1000-8000-00805F9B34FB" 17 | 18 | def __init__(self, address=None): 19 | self.address = address 20 | self.crckeyset = False 21 | self.connected = True if self.connect() else False 22 | 23 | def connect(self): 24 | if self.address is None and not self.scandevices(): 25 | return False 26 | if not self.scanservices(): 27 | return False 28 | logging.info("Service found. Connecting to \"%s\" on %s..." % (self.service["name"], self.service["host"])) 29 | self.sock = BluetoothSocket(RFCOMM) 30 | if system() == "Darwin": 31 | self.sock.connect((self.service["host"].decode('UTF-8'), self.service["port"])) 32 | else: 33 | self.sock.connect((self.service["host"], self.service["port"])) 34 | self.sock.settimeout(60) 35 | logging.info("Connected.") 36 | self.registerCrcKeyToBt() 37 | return True 38 | 39 | def disconnect(self): 40 | try: 41 | self.sock.close() 42 | except: 43 | pass 44 | logging.info("Disconnected.") 45 | 46 | def scandevices(self): 47 | logging.warning("Searching for devices...\n" 48 | "This will take some time; consider specifing a mac address to avoid a scan.") 49 | valid_names = ['MiaoMiaoJi', 'Paperang', 'Paperang_P2S'] 50 | nearby_devices = discover_devices(lookup_names=True) 51 | valid_devices = list(filter(lambda d: len(d) == 2 and d[1] in valid_names, nearby_devices)) 52 | if len(valid_devices) == 0: 53 | logging.error("Cannot find device with name %s." % " or ".join(valid_names)) 54 | return False 55 | elif len(valid_devices) > 1: 56 | logging.warning("Found multiple valid machines, the first one will be used.\n") 57 | logging.warning("\n".join(valid_devices)) 58 | else: 59 | if system() == "Darwin": 60 | logging.warning( 61 | "Found a valid machine with MAC %s and name %s" % (valid_devices[0][0].decode('UTF-8'), valid_devices[0][1]) 62 | ) 63 | self.address = valid_devices[0][0].decode('UTF-8') 64 | else: 65 | logging.warning( 66 | "Found a valid machine with MAC %s and name %s" % (valid_devices[0][0], valid_devices[0][1]) 67 | ) 68 | self.address = valid_devices[0][0] 69 | return True 70 | 71 | def scanservices(self): 72 | logging.info("Searching for services...") 73 | if system() == "Darwin": 74 | return self.scanservices_osx() 75 | 76 | # Example find_service() output on raspbian buster: 77 | # {'service-classes': ['1101'], 'profiles': [], 'name': 'Port', 'description': None, 78 | # 'provider': None, 'service-id': None, 'protocol': 'RFCOMM', 'port': 1, 79 | # 'host': 'A1:B2:C3:D4:E5:F6'} 80 | service_matches = find_service(uuid=self.service_uuid, address=self.address) 81 | valid_service = list(filter( 82 | lambda s: 'protocol' in s and 'name' in s and s['protocol'] == 'RFCOMM' and (s['name'] == 'SerialPort' or s['name'] == 'Port'), 83 | service_matches 84 | )) 85 | print(valid_service[0]) 86 | if len(valid_service) == 0: 87 | logging.error("Cannot find valid services on device with MAC %s." % self.address) 88 | return False 89 | logging.info("Found a valid service") 90 | self.service = valid_service[0] 91 | return True 92 | 93 | def scanservices_osx(self): 94 | # Example find_service() output on OSX 10.15.2: 95 | # [{'host': b'A1:B2:C3:D4:E5:F6', 'port': 1, 'name': 'Port', 'description': None, 96 | # 'provider': None, 'protocol': None, 'service-classes': [], 'profiles': [], 'service-id': None}] 97 | service_matches = find_service(address=self.address) 98 | # print("printing service matches...") 99 | # print(service_matches) 100 | # print("...done.") 101 | valid_services = list(filter( 102 | lambda s: 'name' in s and s['name'] == 'SerialPort', 103 | service_matches 104 | )) 105 | if len(valid_services) == 0: 106 | logging.error("Cannot find valid services on device with MAC %s." % self.address) 107 | return False 108 | self.service = valid_services[0] 109 | return True 110 | 111 | def sendMsgAllPackage(self, msg): 112 | # Write data directly to device 113 | sent_len = self.sock.send(msg) 114 | logging.info("Sending msg with length = %d..." % sent_len) 115 | 116 | def crc32(self, content): 117 | return zlib.crc32(content, self.crcKey if self.crckeyset else self.standardKey) & 0xffffffff 118 | 119 | def packPerBytes(self, bytes, control_command, i): 120 | result = struct.pack(' bytearray: 14 | # bits_str are human way (MSB:LSB) of representing binary numbers (e.g. "1010" means 12) 15 | if len(bits_str) % 8 != 0: 16 | raise ValueError("bits_str should have the length of ") 17 | partitioned_str = [bits_str[i:i + 8] for i in range(0, len(bits_str), 8)] 18 | int_str = [int(i, 2) for i in partitioned_str] 19 | return bytes(int_str) 20 | 21 | 22 | def binimage2bitstream(bin_image: np.ndarray): 23 | # bin_image is a numpy int array consists of only 1s and 0s 24 | # input follows thermal printer's mechanism: 1 is black (printed) and 0 is white (left untouched) 25 | assert bin_image.max() <= 1 and bin_image.min() >= 0 26 | return _pack_block(''.join(map(str, bin_image.flatten()))) 27 | 28 | 29 | def im2binimage(im, conversion="threshold"): 30 | # convert standard numpy array image to bin_image 31 | fixed_width = 384 32 | if hasattr(config, "width"): 33 | fixed_width = config.width 34 | if (len(im.shape) != 2): 35 | im = ski.color.rgb2gray(im) 36 | im = ski.transform.resize(im, (round( fixed_width /im.shape[1] * im.shape[0]), fixed_width)) 37 | if conversion == "threshold": 38 | ret = (im < ski.filters.threshold_li(im)).astype(int) 39 | elif conversion == "edge": 40 | ret = 1- (1 - (ski.feature.canny(im, sigma=2))) 41 | else: 42 | raise ValueError("Unsupported conversion method") 43 | return ret 44 | 45 | # this is straight from https://github.com/tgray/hyperdither 46 | @numba.jit 47 | def dither(num, thresh = 127): 48 | derr = np.zeros(num.shape, dtype=int) 49 | 50 | div = 8 51 | for y in range(num.shape[0]): 52 | for x in range(num.shape[1]): 53 | newval = derr[y,x] + num[y,x] 54 | if newval >= thresh: 55 | errval = newval - 255 56 | num[y,x] = 1. 57 | else: 58 | errval = newval 59 | num[y,x] = 0. 60 | if x + 1 < num.shape[1]: 61 | derr[y, x + 1] += errval / div 62 | if x + 2 < num.shape[1]: 63 | derr[y, x + 2] += errval / div 64 | if y + 1 < num.shape[0]: 65 | derr[y + 1, x - 1] += errval / div 66 | derr[y + 1, x] += errval / div 67 | if y + 2< num.shape[0]: 68 | derr[y + 2, x] += errval / div 69 | if x + 1 < num.shape[1]: 70 | derr[y + 1, x + 1] += errval / div 71 | return num[::-1,:] * 255 72 | 73 | def im2binimage2(im): 74 | basewidth = 384 75 | # resizer = pilkit.processors.ResizeToFit(fixed_width) 76 | # import in B&W, probably does not matter 77 | img = Image.open(im).convert('L') 78 | # img = Image.open(im) 79 | # img.show() 80 | 81 | wpercent = (basewidth/float(img.size[0])) 82 | hsize = int((float(img.size[1])*float(wpercent))) 83 | img = img.resize((basewidth,hsize), Image.ANTIALIAS) 84 | # img.save('test.pgm', format="PPM") 85 | # os.system('pamditherbw -atkinson test.pgm > test2.pgm') 86 | # os.system('pamtopnm test3.pbm') 87 | # img2 = Image.open('/Users/ktamas/Prog/python-paperang/test3.pbm') 88 | # img2 = Image.open('test3.pbm').convert('1') 89 | # img2.show() 90 | # os.system('') 91 | 92 | # img.show() 93 | # resize to the size paperang needs 94 | # new_img = resizer.process(img) 95 | # new_img.show() 96 | # do atkinson dithering 97 | # s = atk.atk(img.size[0], img.size[1], img.tobytes()) 98 | # o = Image.frombytes('L', img.size, s) 99 | # o = Image.fromstring('L', img.size, s) 100 | 101 | m = np.array(img)[:,:] 102 | m2 = dither(m) 103 | # out = Image.fromarray(m2[::-1,:]).convert('1') 104 | out = Image.fromarray(m2[::-1,:]) 105 | out.show() 106 | # the ditherer is stupid and does not make black and white images, just... almost so this fixes that 107 | enhancer = ImageEnhance.Contrast(out) 108 | enhanced_img = enhancer.enhance(4.0) 109 | enhanced_img.show() 110 | # now convert it to true black and white 111 | # blackandwhite_img = enhanced_img.convert('1') 112 | # blackandwhite_img.show() 113 | np_img = np.array(enhanced_img).astype(int) 114 | # flipping the ones and zeros 115 | np_img[np_img == 1] = 100 116 | np_img[np_img == 0] = 1 117 | np_img[np_img == 100] = 0 118 | 119 | return binimage2bitstream(np_img) 120 | 121 | def sirius(im): 122 | np_img = np.fromfile(im, dtype='uint8') 123 | # there must be a less stupid way to invert the array but i am baby 124 | np_img[np_img == 1] = 100 125 | np_img[np_img == 0] = 1 126 | np_img[np_img == 100] = 0 127 | return binimage2bitstream(np_img) 128 | -------------------------------------------------------------------------------- /littleprinter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | import hardware 4 | import image_data 5 | import tempfile 6 | import os 7 | from watchgod import watch 8 | import config 9 | from pathlib import Path 10 | 11 | class Paperang_Printer: 12 | def __init__(self): 13 | if hasattr(config, "macaddress"): 14 | self.printer_hardware = hardware.Paperang(config.macaddress) 15 | else: 16 | self.printer_hardware = hardware.Paperang() 17 | 18 | def print_sirius_image(self, path): 19 | if self.printer_hardware.connected: 20 | self.printer_hardware.sendImageToBt(image_data.sirius(path)) 21 | 22 | if __name__ == '__main__': 23 | mmj=Paperang_Printer() 24 | # `sirius-client` will write to this folder 25 | tmpdir = os.path.join(tempfile.gettempdir(), 'sirius-client') 26 | Path(tmpdir).mkdir(parents=True, exist_ok=True) 27 | 28 | for changes in watch(tmpdir): 29 | file = changes.pop()[1] 30 | print("Printing " + file) 31 | mmj.print_sirius_image(file) 32 | -------------------------------------------------------------------------------- /notes/Bt203.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/notes/Bt203.txt -------------------------------------------------------------------------------- /notes/RPi.txt: -------------------------------------------------------------------------------- 1 | Bitmap is sent to the printer, whose width must be 384. 2 | bitmap->[bytes]->{packPerBytes(byte, BtCommandByte.PRT_PRINT_DATA, i)->sendMsgAllPackage} loop i 3 | 4 | packPerBytes(list input, byte command, int id): 5 | result=[2, command, id] 6 | 7 | 8 | =================== 9 | 10 | Pre: 11 | 1. Learn using bluetooth in android. 12 | http://www.jianshu.com/p/f5f0570132f4 13 | 14 | 2. Collect serval versions of MiaoMiaoJi app. 15 | (1.0.2, 1.0.4, 1.0.7, 2.0.3 collected) 16 | 17 | 18 | DIY: 19 | 1. Prepare bluetooth environment on PRi 20 | 21 | 2. Break the 360 stub of MiaoMiaoJi apps and extract unencrypted classes.dex 22 | 23 | 3. Analyse code related with bluetooth. Three class is mainly reversed: BtManager, ConnectThread, SendMsgThread 24 | Also, the DevManageActivity, OfflineModeActivity should be analysed. 25 | 26 | 27 | Problem 1: 28 | invalid UUID 29 | Solution: 30 | https://stackoverflow.com/questions/17216264/bluetooth-connection-between-android-and-linux-rpi-lost-on-first-write-action 31 | 32 | Problem 2: 33 | bluetooth.btcommon.BluetoothError: (2, 'No such file or directory') 34 | Solution: 35 | https://stackoverflow.com/questions/33110992/python-code-for-bluetooth-throws-error-after-i-had-to-reset-the-adapter 36 | 37 | Problem 3: 38 | bluetooth.btcommon.BluetoothError: (2, 'No such file or directory') 39 | Solution: 40 | https://communities.intel.com/thread/76926 -------------------------------------------------------------------------------- /notes/const.txt: -------------------------------------------------------------------------------- 1 | BtCommand { 2 | public static final int SEND_TYPE_FEED = 0; 3 | public static final int SEND_TYPE_IMAGE = 5; 4 | public static final int SEND_TYPE_QUERY_FW_VERSION = 2; 5 | public static final int SEND_TYPE_QUERY_MAC_ADDRESS = 3; 6 | public static final int SEND_TYPE_QUERY_SN = 1; 7 | public static final int SEND_TYPE_SET_DENSITY = 4; 8 | } 9 | 10 | BtCommandByte.PRT_PRINT_DATA = 0; 11 | BtCommandByte.PRT_PRINT_DATA_COMPRESS = 1; 12 | BtCommandByte.PRT_FIRMWARE_DATA = 2; 13 | BtCommandByte.PRT_USB_UPDATE_FIRMWARE = 3; 14 | BtCommandByte.PRT_GET_VERSION = 4; 15 | BtCommandByte.PRT_SENT_VERSION = 5; 16 | BtCommandByte.PRT_GET_MODEL = 6; 17 | BtCommandByte.PRT_SENT_MODEL = 7; 18 | BtCommandByte.PRT_GET_BT_MAC = 8; 19 | BtCommandByte.PRT_SENT_BT_MAC = 9; 20 | BtCommandByte.PRT_GET_SN = 10; 21 | BtCommandByte.PRT_SENT_SN = 11; 22 | BtCommandByte.PRT_GET_STATUS = 12; 23 | BtCommandByte.PRT_SENT_STATUS = 13; 24 | BtCommandByte.PRT_GET_VOLTAGE = 14; 25 | BtCommandByte.PRT_SENT_VOLTAGE = 15; 26 | BtCommandByte.PRT_GET_BAT_STATUS = 16; 27 | BtCommandByte.PRT_SENT_BAT_STATUS = 17; 28 | BtCommandByte.PRT_GET_TEMP = 18; 29 | BtCommandByte.PRT_SENT_TEMP = 19; 30 | BtCommandByte.PRT_SET_FACTORY_STATUS = 20; 31 | BtCommandByte.PRT_GET_FACTORY_STATUS = 21; 32 | BtCommandByte.PRT_SENT_FACTORY_STATUS = 22; 33 | BtCommandByte.PRT_SENT_BT_STATUS = 23; 34 | BtCommandByte.PRT_SET_CRC_KEY = 24; 35 | BtCommandByte.PRT_SET_HEAT_DENSITY = 25; 36 | BtCommandByte.PRT_FEED_LINE = 26; 37 | BtCommandByte.PRT_MAX_CMD = 27; 38 | 39 | 40 | public final class BtCommand { 41 | public static final int SEND_TYPE_FEED = 0; 42 | public static final int SEND_TYPE_IMAGE = 5; 43 | public static final int SEND_TYPE_QUERY_FW_VERSION = 2; 44 | public static final int SEND_TYPE_QUERY_MAC_ADDRESS = 3; 45 | public static final int SEND_TYPE_QUERY_SN = 1; 46 | public static final int SEND_TYPE_SET_DENSITY = 4; 47 | } 48 | 49 | 50 | BtPrompt { 51 | public static final int BT_CONNECTED = 52; 52 | public static final int BT_CONNECTING = 51; 53 | public static final int BT_DISCONNECT = 53; 54 | public static final int BT_NOT_OPEN = 50; 55 | public static final int BT_RECONNECT = 54; 56 | public static final int MSG_NULL = 48; 57 | public static final int MSG_RECV = 49; 58 | } 59 | 60 | 61 | public static final String DEVICE_NAME = "MiaoMiaoJi"; 62 | public static final String SerialPortServiceClass_UUID = "00001101-0000-1000-8000-00805F9B34FB"; -------------------------------------------------------------------------------- /notes/const_newest.txt: -------------------------------------------------------------------------------- 1 | f3e15c3a845d48dc 2 | f3e15c3a845d48dc 3 | 4 | PRT_PRINT_DATA = 0; 5 | PRT_PRINT_DATA_COMPRESS = 1; 6 | PRT_FIRMWARE_DATA = 2; 7 | PRT_USB_UPDATE_FIRMWARE = 3; 8 | PRT_GET_VERSION = 4; 9 | PRT_SENT_VERSION = 5; 10 | PRT_GET_MODEL = 6; 11 | PRT_SENT_MODEL = 7; 12 | PRT_GET_BT_MAC = 8; 13 | PRT_SENT_BT_MAC = 9; 14 | PRT_GET_SN = 10; 15 | PRT_SENT_SN = 11; 16 | PRT_GET_STATUS = 12; 17 | PRT_SENT_STATUS = 13; 18 | PRT_GET_VOLTAGE = 14; 19 | PRT_SENT_VOLTAGE = 15; 20 | PRT_GET_BAT_STATUS = 16; 21 | PRT_SENT_BAT_STATUS = 17; 22 | PRT_GET_TEMP = 18; 23 | PRT_SENT_TEMP = 19; 24 | PRT_SET_FACTORY_STATUS = 20; 25 | PRT_GET_FACTORY_STATUS = 21; 26 | PRT_SENT_FACTORY_STATUS = 22; 27 | PRT_SENT_BT_STATUS = 23; 28 | PRT_SET_CRC_KEY = 24; 29 | PRT_SET_HEAT_DENSITY = 25; 30 | PRT_FEED_LINE = 26; 31 | PRT_PRINT_TEST_PAGE = 27; 32 | PRT_GET_HEAT_DENSITY = 28; 33 | PRT_SENT_HEAT_DENSITY = 29; 34 | PRT_SET_POWER_DOWN_TIME = 30; 35 | PRT_GET_POWER_DOWN_TIME = 31; 36 | PRT_SENT_POWER_DOWN_TIME = 32; 37 | PRT_FEED_TO_HEAD_LINE = 33; 38 | PRT_PRINT_DEFAULT_PARA = 34; 39 | PRT_GET_BOARD_VERSION = 35; 40 | PRT_SENT_BOARD_VERSION = 36; 41 | PRT_GET_HW_INFO = 37; 42 | PRT_SENT_HW_INFO = 38; 43 | PRT_SET_MAX_GAP_LENGTH = 39; 44 | PRT_GET_MAX_GAP_LENGTH = 40; 45 | PRT_SENT_MAX_GAP_LENGTH = 41; 46 | PRT_GET_PAPER_TYPE = 42; 47 | PRT_SENT_PAPER_TYPE = 43; 48 | PRT_SET_PAPER_TYPE = 44; 49 | PRT_GET_COUNTRY_NAME = 45; 50 | PRT_SENT_COUNTRY_NAME = 46; 51 | PRT_DISCONNECT_BT_CMD = 47; 52 | PRT_MAX_CMD = 48; 53 | 54 | 55 | 384 -------------------------------------------------------------------------------- /printer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import hardware 3 | import image_data 4 | import skimage.io 5 | import skimage as ski 6 | import config 7 | 8 | class Paperang_Printer: 9 | def __init__(self): 10 | if hasattr(config, "macaddress"): 11 | self.printer_hardware = hardware.Paperang(config.macaddress) 12 | else: 13 | self.printer_hardware = hardware.Paperang() 14 | 15 | def print_self_test(self): 16 | print("attempting test print to MAC address \"% s\""% config.macaddress) 17 | if self.printer_hardware.connected: 18 | self.printer_hardware.sendSelfTestToBt() 19 | 20 | def print_image_file(self, path): 21 | if self.printer_hardware.connected: 22 | self.printer_hardware.sendImageToBt(image_data.binimage2bitstream( 23 | image_data.im2binimage(ski.io.imread(path),conversion="threshold"))) 24 | 25 | def print_dithered_image(self, path): 26 | if self.printer_hardware.connected: 27 | self.printer_hardware.sendImageToBt(image_data.im2binimage2(path)) 28 | 29 | if __name__=="__main__": 30 | mmj=Paperang_Printer() 31 | mmj.print_self_test() 32 | # mmj.print_image_file("whatever") 33 | # mmj.print_dithered_image("/Users/ktamas/Downloads/frame.png") 34 | # mmj.print_dithered_image("/Users/ktamas/Pictures/hard-job-being-a-baby.jpeg") 35 | # mmj.print_dithered_image("/Users/ktamas/Desktop/-km49qIJ_400x400.png") 36 | # mmj.print_dithered_image("/Users/ktamas/Downloads/10827905_10152921795874452_6300515507948976079_o.jpg") 37 | # mmj.print_dithered_image("/Users/ktamas/Downloads/10827905_10152921795874452_6300515507948976079_o.jpg") 38 | -------------------------------------------------------------------------------- /proprietary_binaries/MIAO1.0.2.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/proprietary_binaries/MIAO1.0.2.dex -------------------------------------------------------------------------------- /proprietary_binaries/MIAO1.0.4.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/proprietary_binaries/MIAO1.0.4.dex -------------------------------------------------------------------------------- /proprietary_binaries/MIAO1.0.7.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/proprietary_binaries/MIAO1.0.7.dex -------------------------------------------------------------------------------- /proprietary_binaries/MIAO2.0.3.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/proprietary_binaries/MIAO2.0.3.dex -------------------------------------------------------------------------------- /proprietary_binaries/mmj_p1_fw_v127.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinyprinter/python-paperang/e164cf53553a40be9f941deabfb57e49da42ab36/proprietary_binaries/mmj_p1_fw_v127.bin -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cython 2 | numpy 3 | pybluez 4 | scikit-image 5 | scipy 6 | numba 7 | pilkit 8 | watchgod 9 | -------------------------------------------------------------------------------- /testprint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import config 3 | import time 4 | import hardware 5 | import image_data 6 | 7 | class Paperangg_Printer: 8 | def __init__(self): 9 | if hasattr(config, "macaddress"): 10 | print("attempting test print to MAC address \"% s\""% config.macaddress) 11 | self.printer_hardware = hardware.Paperang(config.macaddress) 12 | else: 13 | print("searching for printer for test print...") 14 | self.printer_hardware = hardware.Paperang() 15 | 16 | # having trouble connecting? uncomment the following line and input 17 | # your paperang's MAC address directly 18 | # self.printer_hardware = hardware.Paperang("AA:BB:CC:DD:EE:FF") 19 | 20 | def print_self_test(self): 21 | if self.printer_hardware.connected: 22 | self.printer_hardware.sendSelfTestToBt() 23 | self.printer_hardware.disconnect() 24 | else: 25 | print("printer not connected.") 26 | 27 | def print_sirius_image(self, path): 28 | if self.printer_hardware.connected: 29 | self.printer_hardware.sendImageToBt(image_data.sirius(path)) 30 | 31 | if __name__ == '__main__': 32 | mmj=Paperangg_Printer() 33 | mmj.print_self_test() 34 | --------------------------------------------------------------------------------