├── resource
└── example.png
├── example
├── example.receipt
├── delivery.receipt
├── label.receipt
└── receipt.receipt
├── test
├── topng.html
├── tosvg.html
├── totext.html
├── tocommand.html
├── print.html
└── receipt-serial-console.js
├── CHANGELOG.md
├── .gitignore
├── LICENSE
├── README.md
└── lib
└── receipt-serial.js
/resource/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/receiptline/receiptjs/HEAD/resource/example.png
--------------------------------------------------------------------------------
/example/example.receipt:
--------------------------------------------------------------------------------
1 | ^^^RECEIPT
2 |
3 | 03/18/2024, 12:34:56 PM
4 | Asparagus | 1| 1.00
5 | Broccoli | 2| 2.00
6 | Carrot | 3| 3.00
7 | ---
8 | ^TOTAL | ^6.00
--------------------------------------------------------------------------------
/example/delivery.receipt:
--------------------------------------------------------------------------------
1 | `^^^~~~Delivery~~~
2 |
3 | Mar. 18, 2024 12:34 PM
4 | Order #: ^56
5 | -
6 | {width:10,*}
7 | ^^^2 |^^Hamburger
8 | {width:12,*}
9 | ||Tomato
10 | ||Meat Sauce
11 | ||Onion
12 | ||Mayonnaise
13 | ||Mustard
14 | -
15 | {width:10,*}
16 | ^^^2 |^^Coffee
17 | {width:12,*}
18 | ||Soy Milk
19 | -
20 | {code:20240318123456;option:qrcode,5,h}
--------------------------------------------------------------------------------
/example/label.receipt:
--------------------------------------------------------------------------------
1 | {border:line}
2 | |^^^柿の種 ^^KAKINO TANE|
3 | -
4 | {width:8,*}
5 | |名 称| 米 菓 |
6 | -
7 | |原材料名|もち米、米、でん粉、しょうゆ、砂糖、食塩、風味調味料、唐辛子 |
8 | -
9 | |内 容 量|^100g|
10 | -
11 | |賞味期限|"2024年10月|
12 | -
13 | |保存方法|直射日光、高温多湿を避けてください。 |
14 | -
15 | |製 造 者|OFSC製菓株式会社 |
16 | ||東京都千代田区九段1-Y-X |
17 | {border:space;width:auto}
18 | {code:2012345678903;option:ean,hri}
--------------------------------------------------------------------------------
/example/receipt.receipt:
--------------------------------------------------------------------------------
1 | {image:iVBORw0KGgoAAAANSUhEUgAAAQAAAAA8AgMAAAD004yXAAAACVBMVEVwAJsAAAD///+esS7BAAAAAXRSTlMAQObYZgAAAZtJREFUSMftlkGOwyAMRW0J76kE97GlZu9KcP+rzCekKak6bWdGmrZS6KIG/7yAbQhEe+tN6qUpDZ1KPHT8SnhpeaP6FlA2wicBsgOeBpTF6gDfZHgj9Jt1sAMeAYYE/RFQngYMuX49gD8fMObjkwB+++h+DeB3x/qdvfD/AP4QwPXX+ceAUdV6Y7K/3Vr7rWhv73ZN9XVHzOUdlm6v9ay3pM1eLT+Ppv63BWjz8jJU0vpUWgGs5yfihrN451G+lq7i2bkF8Bagw3Qv0hEQKGTPPjnGxJtZrSIcJy5ciJ3JStbarjrgwOtVK7Z1iLFOOgOSR3WstTqMqCcVFlITDnhhNhMKzJNIA3iEBr1uaAfArA7bSWdAkRrIsJSMrwXbJBowEhogwe9F+NilC8D1oIyg43fA+1WwKpoktXUmsiOCjxFMR+gEBfwcaJ7qDBBWNTVSxKsZZhwkUgkppEiJ1VOIQdCFtAmNBQg9QWj9POQWFW4Bh0HVipRIHlpEUVzUAmslpgQpVcS0SklcrBqf03u/UvVhLd8H5Hffil/ia4Io3warBgAAAABJRU5ErkJggg==}
2 |
3 | Ichigaya Terminal
4 | 1-Y-X Kudan, Chiyoda-ku
5 | -------------------------------------
6 | 03-18-2024 12:34
7 | {border:line}
8 | ^RECEIPT
9 | {border:space}
10 | {width:*,2,10}
11 | HAMBURGER | 2| 24.00
12 | COFFEE | 2| 12.00
13 | -------------------------------------
14 | {width:*,20}
15 | ^TOTAL | ^36.00
16 | CASH | 40.00
17 | CHANGE | 4.00
18 | {code:20240318123456;option:code128,48}
--------------------------------------------------------------------------------
/test/topng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | toPNG()
6 |
7 |
8 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/tosvg.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | toSVG()
6 |
7 |
8 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/test/totext.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | toText()
6 |
7 |
8 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [4.0.0] - 2025-11-30
4 | ### Changed
5 | - Fonts for SVG/PNG output
6 | - SVG filter ID to include UUID
7 |
8 | ## [3.0.1] - 2025-11-01
9 | ### Added
10 | - Android support
11 |
12 | ## [3.0.0] - 2025-10-31
13 | ### Changed
14 | - Printer control sequence
15 |
16 | ## [2.1.0] - 2025-10-29
17 | ### Added
18 | - Quiet zone option to QR code generator
19 |
20 | ## [2.0.0] - 2025-10-25
21 | ### Changed
22 | - QR code generator
23 |
24 | ## [1.1.4] - 2025-10-18
25 | ### Removed
26 | - Unnecessary adjustments in Star Landscape
27 |
28 | ## [1.1.3] - 2025-10-16
29 | ### Fixed
30 | - Bugs with symbols in Star Landscape
31 | - Redundant code patterns
32 |
33 | ## [1.1.2] - 2025-10-13
34 | ### Fixed
35 | - Bugs in UPC and EAN for SII Landscape
36 | - SVG coordinates to integer values
37 | - Redundant code
38 |
39 | ## [1.1.1] - 2025-01-11
40 | ### Fixed
41 | - Incorrect drawer events
42 |
43 | ## [1.1.0] - 2025-01-10
44 | ### Added
45 | - Cash drawer status
46 |
47 | ### Fixed
48 | - Buffer length check
49 |
50 | ## [1.0.5] - 2024-04-14
51 | ### Fixed
52 | - README
53 |
54 | ## [1.0.4] - 2024-03-30
55 | ### Fixed
56 | - Shift_JIS table
57 |
58 | ## [1.0.3] - 2024-03-29
59 | ### Fixed
60 | - README
61 | - Status checks in StarPRNT mode
62 |
63 | ## [1.0.2] - 2024-03-21
64 | ### Fixed
65 | - README
66 |
67 | ## [1.0.1] - 2024-03-20
68 | ### Fixed
69 | - README
70 | - HTML for testing
71 |
72 | ## [1.0.0] - 2024-03-18
73 | ### Added
74 | - First edition
75 |
--------------------------------------------------------------------------------
/test/tocommand.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | toCommand()
6 |
7 |
8 |
9 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/test/print.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | print()
6 |
7 |
8 |
9 |
10 |
11 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Receipt.js
2 |
3 | JavaScript printing libraries for receipt printers, simple and easy with receipt markdown, printer status support.
4 |
5 | ```javascript
6 | const markdown = `^^^RECEIPT
7 |
8 | 03/18/2024, 12:34:56 PM
9 | Asparagus | 1| 1.00
10 | Broccoli | 2| 2.00
11 | Carrot | 3| 3.00
12 | ---
13 | ^TOTAL | ^6.00`;
14 |
15 | const receipt = Receipt.from(markdown, '-c 42 -l en');
16 | const png = await receipt.toPNG();
17 | ```
18 |
19 | 
20 |
21 |
22 | # Features
23 |
24 | Receipt.js is simple printing libraries for receipt printers that prints with easy markdown data for receipts and returns printer status. Even without a printer, it can output images.
25 |
26 | A development tool is provided to edit, preview, and print the receipt markdown.
27 | https://receiptline.github.io/receiptjs-designer/
28 |
29 | The details of the receipt markdown are explained at
30 | https://github.com/receiptline/receiptline
31 |
32 | # Convert to image or plain text
33 |
34 | The following files are required to use the Receipt API.
35 |
36 | - receipt.js
37 |
38 | ```html
39 |
40 | ```
41 |
42 | ```javascript
43 | const markdown = `^^^RECEIPT
44 |
45 | 03/18/2024, 12:34:56 PM
46 | Asparagus | 1| 1.00
47 | Broccoli | 2| 2.00
48 | Carrot | 3| 3.00
49 | ---
50 | ^TOTAL | ^6.00`;
51 |
52 | const receipt = Receipt.from(markdown, '-c 42 -l en');
53 | const png = await receipt.toPNG();
54 | ```
55 |
56 | ## Receipt.from(markdown[, options])
57 |
58 | The Receipt.from() static method creates a new Receipt instance.
59 |
60 | ### Parameters
61 |
62 | - `markdown` <string>
63 | - receipt markdown text
64 | - `options` <string>
65 | - `-c `: characters per line
66 | - range: `24`-`96`
67 | - default: `48`
68 | - `-l `: language of receipt markdown text
69 | - `en`, `fr`, `de`, `es`, `po`, `it`, `ru`, ...: Multilingual (cp437, 852, 858, 866, 1252 characters)
70 | - `ja`: Japanese (shiftjis characters)
71 | - `ko`: Korean (ksc5601 characters)
72 | - `zh-hans`: Simplified Chinese (gb18030 characters)
73 | - `zh-hant`: Traditional Chinese (big5 characters)
74 | - `th`: Thai
75 | - default: system locale
76 | - `-s`: paper saving (reduce line spacing)
77 |
78 | ### Return value
79 |
80 | - A new Receipt instance.
81 |
82 | ## receipt.toPNG()
83 |
84 | The toPNG() instance method converts to PNG.
85 | https://receiptline.github.io/receiptjs/test/topng.html
86 |
87 | ### Parameters
88 |
89 | - None.
90 |
91 | ### Return value
92 |
93 | - A Promise that fulfills with a string once the PNG in data URL format is ready to be used.
94 |
95 | ## receipt.toSVG()
96 |
97 | The toSVG() instance method converts to SVG.
98 | https://receiptline.github.io/receiptjs/test/tosvg.html
99 |
100 | ### Parameters
101 |
102 | - None.
103 |
104 | ### Return value
105 |
106 | - A Promise that fulfills with a string once the SVG is ready to be used.
107 |
108 | ## receipt.toText()
109 |
110 | The toText() instance method converts to plain text.
111 | https://receiptline.github.io/receiptjs/test/totext.html
112 |
113 | ### Parameters
114 |
115 | - None.
116 |
117 | ### Return value
118 |
119 | - A Promise that fulfills with a string once the plain text is ready to be used.
120 |
121 | ## receipt.toString()
122 |
123 | The toString() instance method returns a string representing the receipt markdown text.
124 |
125 | ### Parameters
126 |
127 | - None.
128 |
129 | ### Return value
130 |
131 | - A string representing the receipt markdown text.
132 |
133 |
134 | # Convert to printer commands
135 |
136 | The following files are required to use the Receipt Printer API.
137 |
138 | - receipt.js
139 | - receipt-printer.js
140 |
141 | ```html
142 |
143 |
144 | ```
145 |
146 | ```javascript
147 | const markdown = `^^^RECEIPT
148 |
149 | 03/18/2024, 12:34:56 PM
150 | Asparagus | 1| 1.00
151 | Broccoli | 2| 2.00
152 | Carrot | 3| 3.00
153 | ---
154 | ^TOTAL | ^6.00`;
155 |
156 | const receipt = Receipt.from(markdown, '-p generic -c 42');
157 | const command = await receipt.toCommand();
158 | ```
159 |
160 | ## Receipt.from(markdown[, options])
161 |
162 | The Receipt.from() static method creates a new Receipt instance.
163 |
164 | ### Parameters
165 |
166 | - `markdown` <string>
167 | - receipt markdown text
168 | - `options` <string>
169 | - `-p `: printer control language
170 | - `escpos`: ESC/POS (Epson)
171 | - `epson`: ESC/POS (Epson)
172 | - `sii`: ESC/POS (Seiko Instruments)
173 | - `citizen`: ESC/POS (Citizen)
174 | - `fit`: ESC/POS (Fujitsu)
175 | - `impact`: ESC/POS (TM-U220)
176 | - `impactb`: ESC/POS (TM-U220 Font B)
177 | - `generic`: ESC/POS (Generic) _Experimental_
178 | - `star`: StarPRNT
179 | - `starline`: Star Line Mode
180 | - `emustarline`: Command Emulator Star Line Mode
181 | - `stargraphic`: Star Graphic Mode
182 | - `starimpact`: Star Mode on dot impact printers _Experimental_
183 | - `starimpact2`: Star Mode on dot impact printers (Font 5x9 2P-1) _Experimental_
184 | - `starimpact3`: Star Mode on dot impact printers (Font 5x9 3P-1) _Experimental_
185 | - `-c `: characters per line
186 | - range: `24`-`96`
187 | - default: `48`
188 | - `-l `: language of receipt markdown text
189 | - `en`, `fr`, `de`, `es`, `po`, `it`, `ru`, ...: Multilingual (cp437, 852, 858, 866, 1252 characters)
190 | - `ja`: Japanese (shiftjis characters)
191 | - `ko`: Korean (ksc5601 characters)
192 | - `zh-hans`: Simplified Chinese (gb18030 characters)
193 | - `zh-hant`: Traditional Chinese (big5 characters)
194 | - `th`: Thai
195 | - default: system locale
196 | - `-s`: paper saving (reduce line spacing)
197 | - `-m [][,]`: print margin
198 | - range (left): `0`-`24`
199 | - range (right): `0`-`24`
200 | - default: `0,0`
201 | - `-u`: upside down
202 | - `-i`: print as image
203 | - `-n`: no paper cut
204 | - `-b `: image thresholding
205 | - range: `0`-`255`
206 | - default: error diffusion
207 | - `-g `: image gamma correction
208 | - range: `0.1`-`10.0`
209 | - default: `1.0`
210 | - `-v`: landscape orientation
211 | - device font support: `escpos`, `epson`, `sii`, `citizen`, `star`
212 | - `-r `: print resolution for ESC/POS, landscape, and device font
213 | - values: `180`, `203`
214 | - default: `203`
215 |
216 | ### Return value
217 |
218 | - A new Receipt instance.
219 |
220 | ## receipt.toCommand()
221 |
222 | The toCommand() instance method converts to printer commands.
223 | https://receiptline.github.io/receiptjs/test/tocommand.html
224 |
225 | ### Parameters
226 |
227 | - None.
228 |
229 | ### Return value
230 |
231 | - A Promise that fulfills with a string once the printer commands is ready to be used.
232 |
233 |
234 | # Print with the Web Serial API
235 |
236 | The following files are required to use the Receipt Serial API.
237 |
238 | - receipt.js
239 | - receipt-printer.js
240 | - receipt-serial.js
241 |
242 | ```html
243 |
244 |
245 |
246 | ```
247 |
248 | ```javascript
249 | const markdown = `^^^RECEIPT
250 |
251 | 03/18/2024, 12:34:56 PM
252 | Asparagus | 1| 1.00
253 | Broccoli | 2| 2.00
254 | Carrot | 3| 3.00
255 | ---
256 | ^TOTAL | ^6.00`;
257 |
258 | const conn = ReceiptSerial.connect({ baudRate: 19200 });
259 | conn.on('status', status => {
260 | console.log(status);
261 | });
262 | conn.on('ready', async () => {
263 | const result = await conn.print(markdown, '-c 42');
264 | });
265 | ```
266 |
267 | ## ReceiptSerial.connect([options])
268 |
269 | The ReceiptSerial.connect() static method creates a new connection using the Web Serial API.
270 |
271 | ### Parameters
272 |
273 | - `options` <object>
274 | - `baudRate`: baud rate to establish serial communication
275 | - default: `115200`
276 | - other values
277 | - parity: `none`
278 | - data bits: `8`
279 | - stop bits: `1`
280 | - flow control: `hardware`
281 |
282 | These options are for real serial ports.
283 |
284 | ### Return value
285 |
286 | - A new ReceiptSerial instance.
287 |
288 | ## receiptSerial.status
289 |
290 | The receiptSerial.status instance property is a string representing the printer status.
291 |
292 | ### Value
293 |
294 | - A string representing the printer status.
295 | - `online`: printer is online
296 | - `print`: printer is printing
297 | - `coveropen`: printer cover is open
298 | - `paperempty`: no receipt paper
299 | - `error`: printer error (except cover open and paper empty)
300 | - `offline`: printer is off or offline
301 | - `disconnect`: printer is not connected
302 |
303 | ## receiptSerial.print(markdown[, options])
304 |
305 | The print() instance method prints a receipt markdown text.
306 | https://receiptline.github.io/receiptjs/test/print.html
307 |
308 | ### Parameters
309 |
310 | - `markdown` <string>
311 | - receipt markdown text
312 | - `options` <string>
313 | - `-c `: characters per line
314 | - range: `24`-`96`
315 | - default: `48`
316 | - `-l `: language of receipt markdown text
317 | - `en`, `fr`, `de`, `es`, `po`, `it`, `ru`, ...: Multilingual (cp437, 852, 858, 866, 1252 characters)
318 | - `ja`: Japanese (shiftjis characters)
319 | - `ko`: Korean (ksc5601 characters)
320 | - `zh-hans`: Simplified Chinese (gb18030 characters)
321 | - `zh-hant`: Traditional Chinese (big5 characters)
322 | - `th`: Thai
323 | - default: system locale
324 | - `-s`: paper saving (reduce line spacing)
325 | - `-m [][,]`: print margin
326 | - range (left): `0`-`24`
327 | - range (right): `0`-`24`
328 | - default: `0,0`
329 | - `-u`: upside down
330 | - `-i`: print as image
331 | - `-n`: no paper cut
332 | - `-b `: image thresholding
333 | - range: `0`-`255`
334 | - default: error diffusion
335 | - `-g `: image gamma correction
336 | - range: `0.1`-`10.0`
337 | - default: `1.0`
338 | - `-p `: printer control language
339 | - `escpos`: ESC/POS (Epson)
340 | - `epson`: ESC/POS (Epson)
341 | - `sii`: ESC/POS (Seiko Instruments)
342 | - `citizen`: ESC/POS (Citizen)
343 | - `fit`: ESC/POS (Fujitsu)
344 | - `impact`: ESC/POS (TM-U220)
345 | - `impactb`: ESC/POS (TM-U220 Font B)
346 | - `generic`: ESC/POS (Generic) _Experimental_
347 | - `star`: StarPRNT
348 | - `starline`: Star Line Mode
349 | - `emustarline`: Command Emulator Star Line Mode
350 | - `stargraphic`: Star Graphic Mode
351 | - `starimpact`: Star Mode on dot impact printers _Experimental_
352 | - `starimpact2`: Star Mode on dot impact printers (Font 5x9 2P-1) _Experimental_
353 | - `starimpact3`: Star Mode on dot impact printers (Font 5x9 3P-1) _Experimental_
354 | - default: auto detection (`epson`, `sii`, `citizen`, `fit`, `impactb`, `generic`, `star`)
355 | - `-v`: landscape orientation
356 | - device font support: `escpos`, `epson`, `sii`, `citizen`, `star`
357 | - `-r `: print resolution for ESC/POS, landscape, and device font
358 | - values: `180`, `203`
359 | - default: `203`
360 |
361 | ### Return value
362 |
363 | - A Promise that fulfills with a string once the print result is ready to be used.
364 | - `success`: printing success
365 | - `print`: printer is printing
366 | - `coveropen`: printer cover is open
367 | - `paperempty`: no receipt paper
368 | - `error`: printer error (except cover open and paper empty)
369 | - `offline`: printer is off or offline
370 | - `disconnect`: printer is not connected
371 |
372 | ## receiptSerial.drawer
373 |
374 | The receiptSerial.drawer instance property is a string representing the cash drawer status.
375 |
376 | ### Value
377 |
378 | - A string representing the cash drawer status.
379 | - `drawerclosed`: drawer is closed
380 | - `draweropen`: drawer is open
381 | - `offline`: printer is off or offline
382 | - `disconnect`: printer is not connected
383 |
384 | ## receiptSerial.invertDrawerState(invert)
385 |
386 | The invertDrawerState() instance method inverts cash drawer state.
387 |
388 | ### Parameters
389 |
390 | - `invert` <boolean>
391 | - if true, invert drawer state
392 |
393 | ### Return value
394 |
395 | - None.
396 |
397 | ## receiptSerial.close()
398 |
399 | The close() instance method closes the connection.
400 | The current implementation also closes other open connections.
401 |
402 | ### Parameters
403 |
404 | - None.
405 |
406 | ### Return value
407 |
408 | - None.
409 |
410 | ## receiptSerial.on(name, listener)
411 |
412 | The on() instance method adds the `listener` function to the listeners array for the event named `name`.
413 |
414 | ### Parameters
415 |
416 | - `name`
417 | - event name
418 | - `status`: printer status updated
419 | - `ready`: ready to print
420 | - `online`: printer is online
421 | - `print`: printer is printing
422 | - `coveropen`: printer cover is open
423 | - `paperempty`: no receipt paper
424 | - `error`: printer error (except cover open and paper empty)
425 | - `offline`: printer is off or offline
426 | - `disconnect`: printer is not connected
427 | - `drawer`: drawer status updated
428 | - `drawerclosed`: drawer is closed
429 | - `draweropen`: drawer is open
430 | - `listener`
431 | - the listener function
432 |
433 | ### Return value
434 |
435 | - None.
436 |
437 | ## receiptSerial.off(name, listener)
438 |
439 | The off() instance method removes the `listener` function from the listeners array for the event named `name`.
440 |
441 | ### Parameters
442 |
443 | - `name`
444 | - event name
445 | - `status`: printer status updated
446 | - `ready`: ready to print
447 | - `online`: printer is online
448 | - `print`: printer is printing
449 | - `coveropen`: printer cover is open
450 | - `paperempty`: no receipt paper
451 | - `error`: printer error (except cover open and paper empty)
452 | - `offline`: printer is off or offline
453 | - `disconnect`: printer is not connected
454 | - `drawer`: drawer status updated
455 | - `drawerclosed`: drawer is closed
456 | - `draweropen`: drawer is open
457 | - `listener`
458 | - the listener function
459 |
460 | ### Return value
461 |
462 | - None.
463 |
464 |
465 | # Web browsers
466 |
467 | The print function is available on Chrome, Edge, and Opera that support the Web Serial API.
468 | (Windows, Linux, macOS, ChromeOS, and **Android**)
469 |
470 |
471 | # Receipt printers
472 |
473 | - Epson TM series
474 | - Seiko Instruments RP series
475 | - Star MC series
476 | - Citizen CT series
477 | - Fujitsu FP series
478 |
479 | Connect with the Web Serial API.
480 | (Bluetooth, virtual serial port, and serial port)
481 |
482 | Epson TM series (South Asia model) and Star MC series (StarPRNT model) can print with device font of Thai characters.
483 |
484 | ## Restrictions
485 |
486 | The Web Serial API has no write timeout, so if hardware flow control is enabled, opening the printer cover during printing may cause the browser to stop responding. In this case, close the printer cover or press the paper feed button. Alternatively, change the printer's busy condition setting from "Offline or receive buffer full" to "Receive buffer full".
487 |
--------------------------------------------------------------------------------
/lib/receipt-serial.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 Open Foodservice System Consortium
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // QR Code is a registered trademark of DENSO WAVE INCORPORATED.
18 |
19 | const ReceiptSerial = (() => {
20 | //
21 | // event dispatcher
22 | //
23 | const dispatch = (listener, ...args) => {
24 | setTimeout(() => {
25 | for (let callback of listener) callback(...args);
26 | });
27 | };
28 |
29 | //
30 | // serial port
31 | //
32 | const serialport = () => {
33 | let port;
34 | let reader;
35 | let writer;
36 | let opened;
37 | let rfcomm;
38 | const listeners = { open: [], data: [], drain: [], error: [], close: [] };
39 | return {
40 | // open serial port
41 | open(options) {
42 | // select port
43 | navigator.serial.requestPort().then(async p => {
44 | port = p;
45 | rfcomm = 'bluetoothServiceClassId' in port.getInfo();
46 | // open port
47 | for (let i = 0; i < 3; i++) {
48 | try {
49 | await port.open(options);
50 | break;
51 | }
52 | catch (e) {
53 | if (i === 2) {
54 | throw e;
55 | }
56 | }
57 | }
58 | opened = true;
59 | let enabled = false;
60 | setTimeout(() => {
61 | enabled = true;
62 | dispatch(listeners.open);
63 | }, 100);
64 | writer = port.writable.getWriter();
65 | // read data
66 | while (opened && port.readable) {
67 | reader = port.readable.getReader();
68 | try {
69 | while (opened) {
70 | const { value, done } = await reader.read();
71 | if (done) {
72 | break;
73 | }
74 | if (enabled) {
75 | dispatch(listeners.data, value);
76 | }
77 | }
78 | await new Promise(resolve => setTimeout(resolve, 100));
79 | }
80 | catch (e) {
81 | // non-fatal read error
82 | }
83 | finally {
84 | reader.releaseLock();
85 | }
86 | }
87 | writer.releaseLock();
88 | await port.close();
89 | dispatch(listeners.close);
90 | }).catch(e => {
91 | dispatch(listeners.close);
92 | });
93 | },
94 | // write data
95 | async write(data, encoding) {
96 | if (opened && port.writable) {
97 | const buffer = encoding !== 'binary' ? new TextEncoder().encode(data) : Uint8Array.from(data, c => c.charCodeAt(0));
98 | if (rfcomm) {
99 | try {
100 | await writer.ready;
101 | await writer.write(buffer);
102 | }
103 | catch (e) {
104 | dispatch(listeners.error, e);
105 | return false;
106 | }
107 | }
108 | else {
109 | for (let i = 0; i < buffer.length; i += 1024) {
110 | const count = Math.min(1024, buffer.length - i);
111 | const chunk = buffer.subarray(i, i + count);
112 | while (opened) {
113 | try {
114 | const signals = await port.getSignals();
115 | if (signals.clearToSend) {
116 | await writer.ready;
117 | await writer.write(chunk);
118 | break;
119 | }
120 | else {
121 | await new Promise(resolve => setTimeout(resolve, 100));
122 | }
123 | }
124 | catch (e) {
125 | dispatch(listeners.error, e);
126 | return false;
127 | }
128 | }
129 | }
130 | }
131 | dispatch(listeners.drain);
132 | }
133 | return true;
134 | },
135 | // close serial port
136 | close() {
137 | if (opened && port.readable) {
138 | reader.cancel();
139 | opened = false;
140 | }
141 | },
142 | // add event listener
143 | on(name, listener) {
144 | if (listeners[name]) {
145 | listeners[name].push(listener);
146 | }
147 | },
148 | // remove event listener
149 | off(name, listener) {
150 | if (listeners[name]) {
151 | listeners[name] = listeners[name].filter(c => c !== listener);
152 | }
153 | }
154 | };
155 | };
156 |
157 | // all states
158 | const state = {
159 | online: 'online',
160 | print: 'print',
161 | coveropen: 'coveropen',
162 | paperempty: 'paperempty',
163 | error: 'error',
164 | offline: 'offline',
165 | disconnect: 'disconnect',
166 | drawerclosed: 'drawerclosed',
167 | draweropen: 'draweropen'
168 | };
169 |
170 | // control commands
171 | const command = {
172 | hello: '\x10\x04\x02\x1b\x06\x01\x1b@', // DLE EOT n ESC ACK SOH ESC @
173 | siiasb: '\x1da\xff', // GS a n
174 | starasb: '\x1b\x1ea\x01\x17', // ESC RS a n ETB
175 | escptr: '\x10\x04\x02', // DLE EOT n
176 | escdrw: '\x10\x04\x01', // DLE EOT n
177 | escclr: '\x10\x14\x08\x01\x03\x14\x01\x06\x02\x08', // DLE DC4 n d1 d2 d3 d4 d5 d6 d7
178 | escasb: '\x1dI\x42\x1dI\x43\x1da\xff' // GS I n GS I n GS a n
179 | };
180 |
181 | return {
182 | /**
183 | * Create serial port connection.
184 | * @param {object} [options] serial port options
185 | * @returns {object} connection instance
186 | */
187 | connect(options = {}) {
188 | // listeners
189 | const listeners = {
190 | status: [],
191 | ready: [],
192 | online: [],
193 | print: [],
194 | coveropen: [],
195 | paperempty: [],
196 | error: [],
197 | offline: [],
198 | disconnect: [],
199 | drawer: [],
200 | drawerclosed: [],
201 | draweropen: []
202 | };
203 | // promise resolver
204 | let resolve = () => {};
205 | // status
206 | let status = state.offline;
207 | // ready
208 | let ready = false;
209 | // update status
210 | const update = newstatus => {
211 | if (newstatus !== status) {
212 | // print response
213 | if (status === state.print) {
214 | resolve(newstatus === state.online ? 'success' : newstatus);
215 | }
216 | // status event
217 | status = newstatus;
218 | dispatch(listeners.status, status);
219 | dispatch(listeners[status]);
220 | // ready event
221 | if (!ready && status === state.online) {
222 | dispatch(listeners.ready);
223 | ready = true;
224 | }
225 | }
226 | };
227 | // drawer status
228 | let drawer = state.offline;
229 | // invert drawer status
230 | let invertion = false;
231 | // update drawer status
232 | const updateDrawer = newstatus => {
233 | let d = newstatus;
234 | // invert drawer status
235 | if (invertion) {
236 | switch (d) {
237 | case state.drawerclosed:
238 | d = state.draweropen;
239 | break;
240 | case state.draweropen:
241 | d = state.drawerclosed;
242 | break;
243 | default:
244 | break;
245 | }
246 | }
247 | if (d !== drawer) {
248 | // status event
249 | drawer = d;
250 | dispatch(listeners.drawer, drawer);
251 | dispatch(listeners[drawer]);
252 | }
253 | };
254 | // timer
255 | let timeout = 0;
256 | // printer control language
257 | let printer = '';
258 | // receive buffer
259 | const buffer = [];
260 | // drain
261 | let drain = true;
262 | // connection
263 | const conn = serialport();
264 | // drain event
265 | conn.on('drain', () => {
266 | // write buffer is empty
267 | drain = true;
268 | });
269 | // open event
270 | conn.on('open', async () => {
271 | // hello to printer
272 | drain = await conn.write(command.hello, 'binary');
273 | // set timer
274 | timeout = setTimeout(async () => {
275 | // buffer clear
276 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
277 | // set timer
278 | timeout = setTimeout(async () => {
279 | // buffer clear
280 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
281 | // set timer
282 | timeout = setTimeout(async () => {
283 | // buffer clear
284 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
285 | }, 3000);
286 | }, 3000);
287 | }, 3000);
288 | });
289 | // error event
290 | conn.on('error', err => {
291 | // clear timer
292 | clearTimeout(timeout);
293 | // close port
294 | conn.close();
295 | });
296 | // close event
297 | conn.on('close', () => {
298 | // disconnect event
299 | update(state.disconnect);
300 | updateDrawer(state.disconnect);
301 | });
302 | // data event
303 | conn.on('data', async data => {
304 | // append data
305 | buffer.push(...data);
306 | // parse response
307 | let len;
308 | do {
309 | len = buffer.length;
310 | switch (printer) {
311 | case '':
312 | if ((buffer[0] & 0xf0) === 0xb0) {
313 | // sii: initialized response
314 | // clear data
315 | buffer.shift();
316 | // clear timer
317 | clearTimeout(timeout);
318 | // printer control language
319 | printer = 'sii';
320 | // enable automatic status
321 | drain = await conn.write(command.siiasb, 'binary');
322 | }
323 | else if ((buffer[0] & 0x91) === 0x01) {
324 | // star: automatic status
325 | if (len > 1) {
326 | const l = ((buffer[0] >> 2 & 0x18) | (buffer[0] >> 1 & 0x07)) + (buffer[1] >> 6 & 0x02);
327 | // check length
328 | if (l <= len) {
329 | // printer
330 | if ((buffer[2] & 0x20) === 0x20) {
331 | // cover open event
332 | update(state.coveropen);
333 | }
334 | else if ((buffer[5] & 0x08) === 0x08) {
335 | // paper empty event
336 | update(state.paperempty);
337 | }
338 | else if ((buffer[3] & 0x2c) !== 0 || (buffer[4] & 0x0a) !== 0) {
339 | // error event
340 | update(state.error);
341 | }
342 | else {
343 | // nothing to do
344 | }
345 | // cash drawer
346 | updateDrawer((buffer[2] & 0x04) === 0x04 ? state.draweropen : state.drawerclosed);
347 | // clear data
348 | buffer.splice(0, l);
349 | // clear timer
350 | clearTimeout(timeout);
351 | // printer control language
352 | printer = 'star';
353 | // enable automatic status
354 | drain = await conn.write(command.starasb, 'binary');
355 | }
356 | }
357 | }
358 | else if ((buffer[0] & 0x93) === 0x12) {
359 | // escpos: realtime status
360 | if ((buffer[0] & 0x97) === 0x16) {
361 | // cover open event
362 | update(state.coveropen);
363 | }
364 | else if ((buffer[0] & 0xb3) === 0x32) {
365 | // paper empty event
366 | update(state.paperempty);
367 | }
368 | else if ((buffer[0] & 0xd3) === 0x52) {
369 | // error event
370 | update(state.error);
371 | }
372 | else {
373 | // initial state
374 | status = state.offline;
375 | }
376 | // clear data
377 | buffer.shift();
378 | // clear timer
379 | clearTimeout(timeout);
380 | // printer control language
381 | printer = 'escpos';
382 | // get drawer status
383 | drain = await conn.write(command.escdrw, 'binary');
384 | }
385 | else if ((buffer[0] & 0x93) === 0x10) {
386 | // escpos: automatic status
387 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
388 | // clear data
389 | buffer.splice(0, 4);
390 | }
391 | }
392 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
393 | // escpos: block data
394 | const i = buffer.indexOf(0);
395 | // check length
396 | if (i > 0) {
397 | // clear data
398 | buffer.splice(0, i + 1);
399 | }
400 | }
401 | else {
402 | // other
403 | buffer.shift();
404 | }
405 | break;
406 |
407 | case 'sii':
408 | if ((buffer[0] & 0xf0) === 0x80) {
409 | // sii: status
410 | if (status === state.print && drain) {
411 | // online event
412 | update(state.online);
413 | }
414 | // clear data
415 | buffer.shift();
416 | }
417 | else if ((buffer[0] & 0xf0) === 0xc0) {
418 | // sii: automatic status
419 | if (len > 7) {
420 | // printer
421 | if ((buffer[1] & 0xf8) === 0xd8) {
422 | // cover open event
423 | update(state.coveropen);
424 | }
425 | else if ((buffer[1] & 0xf1) === 0xd1) {
426 | // paper empty event
427 | update(state.paperempty);
428 | }
429 | else if ((buffer[0] & 0x0b) !== 0) {
430 | // error event
431 | update(state.error);
432 | }
433 | else if (status !== state.print) {
434 | // online event
435 | update(state.online);
436 | }
437 | else {
438 | // nothing to do
439 | }
440 | // cash drawer
441 | updateDrawer((buffer[3] & 0xf8) === 0xd8 ? state.drawerclosed : state.draweropen);
442 | // clear data
443 | buffer.splice(0, 8);
444 | }
445 | }
446 | else {
447 | // sii: other
448 | buffer.shift();
449 | }
450 | break;
451 |
452 | case 'star':
453 | if ((buffer[0] & 0xf1) === 0x21) {
454 | // star: automatic status
455 | if (len > 1) {
456 | const l = ((buffer[0] >> 2 & 0x08) | (buffer[0] >> 1 & 0x07)) + (buffer[1] >> 6 & 0x02);
457 | // check length
458 | if (l <= len) {
459 | // printer
460 | if ((buffer[2] & 0x20) === 0x20) {
461 | // cover open event
462 | update(state.coveropen);
463 | }
464 | else if ((buffer[5] & 0x08) === 0x08) {
465 | // paper empty event
466 | update(state.paperempty);
467 | }
468 | else if ((buffer[3] & 0x2c) !== 0 || (buffer[4] & 0x0a) !== 0) {
469 | // error event
470 | update(state.error);
471 | }
472 | else if (status !== state.print) {
473 | // online event
474 | update(state.online);
475 | }
476 | else if (drain) {
477 | // online event
478 | update(state.online);
479 | }
480 | else {
481 | // nothing to do
482 | }
483 | // cash drawer
484 | updateDrawer((buffer[2] & 0x04) === 0x04 ? state.draweropen : state.drawerclosed);
485 | // clear data
486 | buffer.splice(0, l);
487 | }
488 | }
489 | }
490 | else {
491 | // star: other
492 | buffer.shift();
493 | }
494 | break;
495 |
496 | case 'escpos':
497 | if ((buffer[0] & 0x93) === 0x12) {
498 | // escpos: realtime status
499 | // cash drawer
500 | updateDrawer((buffer[0] & 0x97) === 0x16 ? state.drawerclosed : state.draweropen);
501 | // clear data
502 | buffer.shift();
503 | // clear timer
504 | clearTimeout(timeout);
505 | if (status !== state.offline) {
506 | // printer control language
507 | printer = '';
508 | // set timer
509 | timeout = setTimeout(async () => {
510 | // get printer status
511 | drain = await conn.write(command.escptr, 'binary');
512 | }, 3000);
513 | }
514 | else {
515 | // printer control language
516 | printer = 'generic';
517 | // get model info and enable automatic status
518 | drain = await conn.write(command.escasb, 'binary');
519 | // set timer
520 | timeout = setTimeout(async () => {
521 | // buffer clear
522 | drain = await conn.write(command.escclr, 'binary');
523 | // set timer
524 | timeout = setTimeout(async () => {
525 | // buffer clear
526 | drain = await conn.write('\x00'.repeat(65536) + command.escclr, 'binary');
527 | }, 3000);
528 | }, 3000);
529 | }
530 | }
531 | else if ((buffer[0] & 0x93) === 0x10) {
532 | // escpos: automatic status
533 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
534 | // clear data
535 | buffer.splice(0, 4);
536 | }
537 | }
538 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
539 | // escpos: block data
540 | const i = buffer.indexOf(0);
541 | // check length
542 | if (i > 0) {
543 | // clear data
544 | buffer.splice(0, i + 1);
545 | }
546 | }
547 | else {
548 | // escpos: other
549 | buffer.shift();
550 | }
551 | break;
552 |
553 | default:
554 | // check response type
555 | if ((buffer[0] & 0x90) === 0) {
556 | // escpos: status
557 | if (status === state.print && drain) {
558 | // online event
559 | update(state.online);
560 | }
561 | // clear data
562 | buffer.shift();
563 | }
564 | else if ((buffer[0] & 0x93) === 0x10) {
565 | // escpos: automatic status
566 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
567 | // printer
568 | if ((buffer[0] & 0x20) === 0x20) {
569 | // cover open event
570 | update(state.coveropen);
571 | }
572 | else if ((buffer[2] & 0x0c) === 0x0c) {
573 | // paper empty event
574 | update(state.paperempty);
575 | }
576 | else if ((buffer[1] & 0x2c) !== 0) {
577 | // error event
578 | update(state.error);
579 | }
580 | else if (status !== state.print) {
581 | // online event
582 | update(state.online);
583 | }
584 | else {
585 | // nothing to do
586 | }
587 | // cash drawer
588 | updateDrawer((buffer[0] & 0x04) === 0x04 ? state.drawerclosed : state.draweropen);
589 | // clear data
590 | buffer.splice(0, 4);
591 | }
592 | }
593 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
594 | // escpos: block data
595 | const i = buffer.indexOf(0);
596 | // check length
597 | if (i > 0) {
598 | // clear data
599 | const block = buffer.splice(0, i + 1);
600 | if (block[0] === 0x5f) {
601 | // clear timer
602 | clearTimeout(timeout);
603 | // model info
604 | const model = String.fromCharCode(...block.slice(1, -1)).toLowerCase();
605 | if (printer === 'generic' && /^(epson|citizen|fit)$/.test(model)) {
606 | // escpos thermal
607 | printer = model;
608 | }
609 | else if (printer === 'epson' && /^tm-u/.test(model)) {
610 | // escpos impact
611 | printer = 'impactb';
612 | }
613 | else {
614 | // nothing to do
615 | }
616 | }
617 | else if (block[0] === 0x3b) {
618 | // power on
619 | if (block[1] === 0x31) {
620 | // printer control language
621 | printer = '';
622 | // hello to printer
623 | drain = await conn.write(command.hello, 'binary');
624 | }
625 | // offline event
626 | update(state.offline);
627 | updateDrawer(state.offline);
628 | }
629 | else if (block[0] === 0x37) {
630 | // buffer clear
631 | if (block[1] === 0x25) {
632 | // clear timer
633 | clearTimeout(timeout);
634 | // get model info and enable automatic status
635 | drain = await conn.write(command.escasb, 'binary');
636 | }
637 | }
638 | else {
639 | // nothing to do
640 | }
641 | }
642 | }
643 | else if ((buffer[0] & 0x93) === 0x12) {
644 | // escpos: realtime status
645 | // clear timer
646 | clearTimeout(timeout);
647 | // cash drawer
648 | updateDrawer((buffer[0] & 0x97) === 0x16 ? state.drawerclosed : state.draweropen);
649 | // clear data
650 | buffer.shift();
651 | }
652 | else {
653 | // escpos: other
654 | buffer.shift();
655 | }
656 | break;
657 | }
658 | }
659 | while (buffer.length > 0 && buffer.length < len);
660 | });
661 | // open port
662 | conn.open({ ...{ baudRate: 115200, flowControl: 'hardware', bufferSize: 2048 }, ...options });
663 |
664 | return {
665 | /**
666 | * Printer status.
667 | * @type {string} printer status
668 | */
669 | get status() {
670 | return status;
671 | },
672 | /**
673 | * Cash drawer status.
674 | * @type {string} cash drawer status
675 | */
676 | get drawer() {
677 | return drawer;
678 | },
679 | /**
680 | * Invert cash drawer state.
681 | * @param {boolean} invert invert cash drawer state
682 | */
683 | invertDrawerState(invert) {
684 | invertion = !!invert;
685 | switch (drawer) {
686 | case state.drawerclosed:
687 | drawer = state.draweropen;
688 | break;
689 | case state.draweropen:
690 | drawer = state.drawerclosed;
691 | break;
692 | default:
693 | break;
694 | }
695 | },
696 | /**
697 | * Print receipt markdown.
698 | * @param {string} markdown receipt markdown
699 | * @param {string} [options] print options
700 | * @returns {string} print result
701 | */
702 | print(markdown, options) {
703 | // asynchronous printing
704 | return new Promise(async res => {
705 | // online or ready
706 | if (status === state.online) {
707 | // save resolver
708 | resolve = res;
709 | // print event
710 | update(state.print);
711 | // convert markdown to printer command
712 | const command = await Receipt.from(markdown, `-p ${printer} ${options}`).toCommand();
713 | // write command
714 | if (/^star$/.test(printer)) {
715 | // star
716 | drain = await conn.write(command
717 | .replace(/^(\x1b@)?\x1b\x1ea\x00/, '$1\x1b\x1ea\x01') // (ESC @) ESC RS a n
718 | .replace(/(\x1b\x1d\x03\x01\x00\x00\x04?|\x1b\x06\x01)$/, '\x17'), 'binary'); // ETB
719 | }
720 | else {
721 | // escpos
722 | drain = await conn.write(command.replace(/^\x1b@\x1da\x00/, '\x1b@\x1da\xff'), 'binary'); // ESC @ GS a n
723 | }
724 | }
725 | else {
726 | // print response
727 | res(status);
728 | }
729 | });
730 | },
731 | /**
732 | * Close serial port.
733 | */
734 | close() {
735 | // clear timer
736 | clearTimeout(timeout);
737 | // close port
738 | conn.close();
739 | },
740 | /**
741 | * Add event listener.
742 | * @param {string} name event name
743 | * @param {function} listener event listener
744 | */
745 | on(name, listener) {
746 | if (listeners[name]) {
747 | listeners[name].push(listener);
748 | }
749 | },
750 | /**
751 | * Remove event listener.
752 | * @param {string} name event name
753 | * @param {function} listener event listener
754 | */
755 | off(name, listener) {
756 | if (listeners[name]) {
757 | listeners[name] = listeners[name].filter(c => c !== listener);
758 | }
759 | }
760 | };
761 | }
762 | };
763 | })();
764 |
--------------------------------------------------------------------------------
/test/receipt-serial-console.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 Open Foodservice System Consortium
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // QR Code is a registered trademark of DENSO WAVE INCORPORATED.
18 |
19 | const ReceiptSerial = (() => {
20 | //
21 | // event dispatcher
22 | //
23 | const dispatch = (listener, ...args) => {
24 | setTimeout(() => {
25 | for (let callback of listener) callback(...args);
26 | });
27 | };
28 |
29 | //
30 | // serial port
31 | //
32 | const serialport = () => {
33 | let port;
34 | let reader;
35 | let writer;
36 | let opened;
37 | let rfcomm;
38 | const listeners = { open: [], data: [], drain: [], error: [], close: [] };
39 | return {
40 | // open serial port
41 | open(options) {
42 | // select port
43 | navigator.serial.requestPort().then(async p => {
44 | port = p;
45 | rfcomm = 'bluetoothServiceClassId' in port.getInfo();
46 | // open port
47 | for (let i = 0; i < 3; i++) {
48 | try {
49 | await port.open(options);
50 | break;
51 | }
52 | catch (e) {
53 | if (i === 2) {
54 | throw e;
55 | }
56 | }
57 | }
58 | opened = true;
59 | console.log(new Date().toISOString(), 'open');
60 | let enabled = false;
61 | setTimeout(() => {
62 | enabled = true;
63 | dispatch(listeners.open);
64 | }, 100);
65 | writer = port.writable.getWriter();
66 | // read data
67 | while (opened && port.readable) {
68 | reader = port.readable.getReader();
69 | try {
70 | while (opened) {
71 | const { value, done } = await reader.read();
72 | if (done) {
73 | break;
74 | }
75 | if (enabled) {
76 | console.log(new Date().toISOString(), 'read:', value);
77 | dispatch(listeners.data, value);
78 | }
79 | }
80 | await new Promise(resolve => setTimeout(resolve, 100));
81 | }
82 | catch (e) {
83 | // non-fatal read error
84 | }
85 | finally {
86 | reader.releaseLock();
87 | }
88 | }
89 | writer.releaseLock();
90 | await port.close();
91 | console.log(new Date().toISOString(), 'close');
92 | dispatch(listeners.close);
93 | }).catch(e => {
94 | dispatch(listeners.close);
95 | });
96 | },
97 | // write data
98 | async write(data, encoding) {
99 | if (opened && port.writable) {
100 | const buffer = encoding !== 'binary' ? new TextEncoder().encode(data) : Uint8Array.from(data, c => c.charCodeAt(0));
101 | if (rfcomm) {
102 | try {
103 | await writer.ready;
104 | await writer.write(buffer);
105 | console.log(new Date().toISOString(), 'write:', buffer);
106 | }
107 | catch (e) {
108 | dispatch(listeners.error, e);
109 | return false;
110 | }
111 | }
112 | else {
113 | for (let i = 0; i < buffer.length; i += 1024) {
114 | const count = Math.min(1024, buffer.length - i);
115 | const chunk = buffer.subarray(i, i + count);
116 | while (opened) {
117 | try {
118 | const signals = await port.getSignals();
119 | if (signals.clearToSend) {
120 | await writer.ready;
121 | await writer.write(chunk);
122 | console.log(new Date().toISOString(), 'write:', chunk);
123 | break;
124 | }
125 | else {
126 | console.log(new Date().toISOString(), 'write: busy');
127 | await new Promise(resolve => setTimeout(resolve, 100));
128 | }
129 | }
130 | catch (e) {
131 | dispatch(listeners.error, e);
132 | return false;
133 | }
134 | }
135 | }
136 | }
137 | dispatch(listeners.drain);
138 | }
139 | return true;
140 | },
141 | // close serial port
142 | close() {
143 | if (opened && port.readable) {
144 | reader.cancel();
145 | opened = false;
146 | }
147 | },
148 | // add event listener
149 | on(name, listener) {
150 | if (listeners[name]) {
151 | listeners[name].push(listener);
152 | }
153 | },
154 | // remove event listener
155 | off(name, listener) {
156 | if (listeners[name]) {
157 | listeners[name] = listeners[name].filter(c => c !== listener);
158 | }
159 | }
160 | };
161 | };
162 |
163 | // all states
164 | const state = {
165 | online: 'online',
166 | print: 'print',
167 | coveropen: 'coveropen',
168 | paperempty: 'paperempty',
169 | error: 'error',
170 | offline: 'offline',
171 | disconnect: 'disconnect',
172 | drawerclosed: 'drawerclosed',
173 | draweropen: 'draweropen'
174 | };
175 |
176 | // control commands
177 | const command = {
178 | hello: '\x10\x04\x02\x1b\x06\x01\x1b@', // DLE EOT n ESC ACK SOH ESC @
179 | siiasb: '\x1da\xff', // GS a n
180 | starasb: '\x1b\x1ea\x01\x17', // ESC RS a n ETB
181 | escptr: '\x10\x04\x02', // DLE EOT n
182 | escdrw: '\x10\x04\x01', // DLE EOT n
183 | escclr: '\x10\x14\x08\x01\x03\x14\x01\x06\x02\x08', // DLE DC4 n d1 d2 d3 d4 d5 d6 d7
184 | escasb: '\x1dI\x42\x1dI\x43\x1da\xff' // GS I n GS I n GS a n
185 | };
186 |
187 | return {
188 | /**
189 | * Create serial port connection.
190 | * @param {object} [options] serial port options
191 | * @returns {object} connection instance
192 | */
193 | connect(options = {}) {
194 | // listeners
195 | const listeners = {
196 | status: [],
197 | ready: [],
198 | online: [],
199 | print: [],
200 | coveropen: [],
201 | paperempty: [],
202 | error: [],
203 | offline: [],
204 | disconnect: [],
205 | drawer: [],
206 | drawerclosed: [],
207 | draweropen: []
208 | };
209 | // promise resolver
210 | let resolve = () => {};
211 | // status
212 | let status = state.offline;
213 | // ready
214 | let ready = false;
215 | // update status
216 | const update = newstatus => {
217 | if (newstatus !== status) {
218 | // print response
219 | if (status === state.print) {
220 | resolve(newstatus === state.online ? 'success' : newstatus);
221 | console.log(new Date().toISOString(), 'print', newstatus === state.online ? 'success' : newstatus);
222 | }
223 | // status event
224 | status = newstatus;
225 | dispatch(listeners.status, status);
226 | dispatch(listeners[status]);
227 | // ready event
228 | if (!ready && status === state.online) {
229 | dispatch(listeners.ready);
230 | ready = true;
231 | console.log(new Date().toISOString(), 'ready');
232 | }
233 | }
234 | console.log(new Date().toISOString(), 'status:', status);
235 | };
236 | // drawer status
237 | let drawer = state.offline;
238 | // invert drawer status
239 | let invertion = false;
240 | // update drawer status
241 | const updateDrawer = newstatus => {
242 | let d = newstatus;
243 | // invert drawer status
244 | if (invertion) {
245 | switch (d) {
246 | case state.drawerclosed:
247 | d = state.draweropen;
248 | break;
249 | case state.draweropen:
250 | d = state.drawerclosed;
251 | break;
252 | default:
253 | break;
254 | }
255 | }
256 | if (d !== drawer) {
257 | // status event
258 | drawer = d;
259 | dispatch(listeners.drawer, drawer);
260 | dispatch(listeners[drawer]);
261 | }
262 | console.log(new Date().toISOString(), 'status:', drawer);
263 | };
264 | // timer
265 | let timeout = 0;
266 | // printer control language
267 | let printer = '';
268 | // receive buffer
269 | const buffer = [];
270 | // drain
271 | let drain = true;
272 | // connection
273 | const conn = serialport();
274 | // drain event
275 | conn.on('drain', () => {
276 | // write buffer is empty
277 | drain = true;
278 | });
279 | // open event
280 | conn.on('open', async () => {
281 | // hello to printer
282 | drain = await conn.write(command.hello, 'binary');
283 | // set timer
284 | timeout = setTimeout(async () => {
285 | // buffer clear
286 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
287 | // set timer
288 | timeout = setTimeout(async () => {
289 | // buffer clear
290 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
291 | // set timer
292 | timeout = setTimeout(async () => {
293 | // buffer clear
294 | drain = await conn.write('\x00'.repeat(65536) + command.hello, 'binary');
295 | }, 3000);
296 | }, 3000);
297 | }, 3000);
298 | });
299 | // error event
300 | conn.on('error', err => {
301 | // clear timer
302 | clearTimeout(timeout);
303 | // close port
304 | conn.close();
305 | });
306 | // close event
307 | conn.on('close', () => {
308 | // disconnect event
309 | update(state.disconnect);
310 | updateDrawer(state.disconnect);
311 | });
312 | // data event
313 | conn.on('data', async data => {
314 | // append data
315 | buffer.push(...data);
316 | // parse response
317 | let len;
318 | do {
319 | len = buffer.length;
320 | switch (printer) {
321 | case '':
322 | if ((buffer[0] & 0xf0) === 0xb0) {
323 | // sii: initialized response
324 | console.log(new Date().toISOString(), 'sii: initialized response');
325 | // clear data
326 | buffer.shift();
327 | // clear timer
328 | clearTimeout(timeout);
329 | // printer control language
330 | printer = 'sii';
331 | console.log(new Date().toISOString(), `printer: ${printer}`);
332 | // enable automatic status
333 | drain = await conn.write(command.siiasb, 'binary');
334 | }
335 | else if ((buffer[0] & 0x91) === 0x01) {
336 | // star: automatic status
337 | if (len > 1) {
338 | const l = ((buffer[0] >> 2 & 0x18) | (buffer[0] >> 1 & 0x07)) + (buffer[1] >> 6 & 0x02);
339 | // check length
340 | if (l <= len) {
341 | console.log(new Date().toISOString(), 'star: automatic status');
342 | // printer
343 | if ((buffer[2] & 0x20) === 0x20) {
344 | // cover open event
345 | update(state.coveropen);
346 | }
347 | else if ((buffer[5] & 0x08) === 0x08) {
348 | // paper empty event
349 | update(state.paperempty);
350 | }
351 | else if ((buffer[3] & 0x2c) !== 0 || (buffer[4] & 0x0a) !== 0) {
352 | // error event
353 | update(state.error);
354 | }
355 | else {
356 | // nothing to do
357 | }
358 | // cash drawer
359 | updateDrawer((buffer[2] & 0x04) === 0x04 ? state.draweropen : state.drawerclosed);
360 | // clear data
361 | buffer.splice(0, l);
362 | // clear timer
363 | clearTimeout(timeout);
364 | // printer control language
365 | printer = 'star';
366 | console.log(new Date().toISOString(), `printer: ${printer}`);
367 | // enable automatic status
368 | drain = await conn.write(command.starasb, 'binary');
369 | }
370 | }
371 | }
372 | else if ((buffer[0] & 0x93) === 0x12) {
373 | // escpos: realtime status
374 | console.log(new Date().toISOString(), 'escpos: realtime status');
375 | if ((buffer[0] & 0x97) === 0x16) {
376 | // cover open event
377 | update(state.coveropen);
378 | }
379 | else if ((buffer[0] & 0xb3) === 0x32) {
380 | // paper empty event
381 | update(state.paperempty);
382 | }
383 | else if ((buffer[0] & 0xd3) === 0x52) {
384 | // error event
385 | update(state.error);
386 | }
387 | else {
388 | // initial state
389 | status = state.offline;
390 | }
391 | // clear data
392 | buffer.shift();
393 | // clear timer
394 | clearTimeout(timeout);
395 | // printer control language
396 | printer = 'escpos';
397 | console.log(new Date().toISOString(), `printer: ${printer}`);
398 | // get drawer status
399 | drain = await conn.write(command.escdrw, 'binary');
400 | }
401 | else if ((buffer[0] & 0x93) === 0x10) {
402 | // escpos: automatic status
403 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
404 | console.log(new Date().toISOString(), 'escpos: automatic status');
405 | // clear data
406 | buffer.splice(0, 4);
407 | }
408 | }
409 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
410 | // escpos: block data
411 | const i = buffer.indexOf(0);
412 | // check length
413 | if (i > 0) {
414 | console.log(new Date().toISOString(), 'escpos: block data');
415 | // clear data
416 | buffer.splice(0, i + 1);
417 | }
418 | }
419 | else {
420 | // other
421 | buffer.shift();
422 | }
423 | break;
424 |
425 | case 'sii':
426 | if ((buffer[0] & 0xf0) === 0x80) {
427 | // sii: status
428 | console.log(new Date().toISOString(), 'sii: status');
429 | if (status === state.print && drain) {
430 | // online event
431 | update(state.online);
432 | }
433 | // clear data
434 | buffer.shift();
435 | }
436 | else if ((buffer[0] & 0xf0) === 0xc0) {
437 | // sii: automatic status
438 | console.log(new Date().toISOString(), 'sii: automatic status');
439 | if (len > 7) {
440 | // printer
441 | if ((buffer[1] & 0xf8) === 0xd8) {
442 | // cover open event
443 | update(state.coveropen);
444 | }
445 | else if ((buffer[1] & 0xf1) === 0xd1) {
446 | // paper empty event
447 | update(state.paperempty);
448 | }
449 | else if ((buffer[0] & 0x0b) !== 0) {
450 | // error event
451 | update(state.error);
452 | }
453 | else if (status !== state.print) {
454 | // online event
455 | update(state.online);
456 | }
457 | else {
458 | // nothing to do
459 | }
460 | // cash drawer
461 | updateDrawer((buffer[3] & 0xf8) === 0xd8 ? state.drawerclosed : state.draweropen);
462 | // clear data
463 | buffer.splice(0, 8);
464 | }
465 | }
466 | else {
467 | // sii: other
468 | console.log(new Date().toISOString(), 'sii: other');
469 | buffer.shift();
470 | }
471 | break;
472 |
473 | case 'star':
474 | if ((buffer[0] & 0xf1) === 0x21) {
475 | // star: automatic status
476 | if (len > 1) {
477 | const l = ((buffer[0] >> 2 & 0x08) | (buffer[0] >> 1 & 0x07)) + (buffer[1] >> 6 & 0x02);
478 | // check length
479 | if (l <= len) {
480 | console.log(new Date().toISOString(), 'star: automatic status');
481 | // printer
482 | if ((buffer[2] & 0x20) === 0x20) {
483 | // cover open event
484 | update(state.coveropen);
485 | }
486 | else if ((buffer[5] & 0x08) === 0x08) {
487 | // paper empty event
488 | update(state.paperempty);
489 | }
490 | else if ((buffer[3] & 0x2c) !== 0 || (buffer[4] & 0x0a) !== 0) {
491 | // error event
492 | update(state.error);
493 | }
494 | else if (status !== state.print) {
495 | // online event
496 | update(state.online);
497 | }
498 | else if (drain) {
499 | // online event
500 | update(state.online);
501 | }
502 | else {
503 | // nothing to do
504 | }
505 | // cash drawer
506 | updateDrawer((buffer[2] & 0x04) === 0x04 ? state.draweropen : state.drawerclosed);
507 | // clear data
508 | buffer.splice(0, l);
509 | }
510 | }
511 | }
512 | else {
513 | // star: other
514 | console.log(new Date().toISOString(), 'star: other');
515 | buffer.shift();
516 | }
517 | break;
518 |
519 | case 'escpos':
520 | if ((buffer[0] & 0x93) === 0x12) {
521 | // escpos: realtime status
522 | // cash drawer
523 | updateDrawer((buffer[0] & 0x97) === 0x16 ? state.drawerclosed : state.draweropen);
524 | // clear data
525 | buffer.shift();
526 | // clear timer
527 | clearTimeout(timeout);
528 | if (status !== state.offline) {
529 | // printer control language
530 | printer = '';
531 | // set timer
532 | timeout = setTimeout(async () => {
533 | // get printer status
534 | drain = await conn.write(command.escptr, 'binary');
535 | }, 3000);
536 | }
537 | else {
538 | // printer control language
539 | printer = 'generic';
540 | console.log(new Date().toISOString(), `printer: ${printer}`);
541 | // get model info and enable automatic status
542 | drain = await conn.write(command.escasb, 'binary');
543 | // set timer
544 | timeout = setTimeout(async () => {
545 | // buffer clear
546 | drain = await conn.write(command.escclr, 'binary');
547 | // set timer
548 | timeout = setTimeout(async () => {
549 | // buffer clear
550 | drain = await conn.write('\x00'.repeat(65536) + command.escclr, 'binary');
551 | }, 3000);
552 | }, 3000);
553 | }
554 | }
555 | else if ((buffer[0] & 0x93) === 0x10) {
556 | // escpos: automatic status
557 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
558 | console.log(new Date().toISOString(), 'escpos: automatic status');
559 | // clear data
560 | buffer.splice(0, 4);
561 | }
562 | }
563 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
564 | // escpos: block data
565 | const i = buffer.indexOf(0);
566 | // check length
567 | if (i > 0) {
568 | console.log(new Date().toISOString(), 'escpos: block data');
569 | // clear data
570 | buffer.splice(0, i + 1);
571 | }
572 | }
573 | else {
574 | // escpos: other
575 | console.log(new Date().toISOString(), 'escpos: other');
576 | buffer.shift();
577 | }
578 | break;
579 |
580 | default:
581 | // check response type
582 | if ((buffer[0] & 0x90) === 0) {
583 | // escpos: status
584 | console.log(new Date().toISOString(), 'escpos: status');
585 | if (status === state.print && drain) {
586 | // online event
587 | update(state.online);
588 | }
589 | // clear data
590 | buffer.shift();
591 | }
592 | else if ((buffer[0] & 0x93) === 0x10) {
593 | // escpos: automatic status
594 | if (len > 3 && (buffer[1] & 0x90) === 0 && (buffer[2] & 0x90) === 0 && (buffer[3] & 0x90) === 0) {
595 | console.log(new Date().toISOString(), 'escpos: automatic status');
596 | // printer
597 | if ((buffer[0] & 0x20) === 0x20) {
598 | // cover open event
599 | update(state.coveropen);
600 | }
601 | else if ((buffer[2] & 0x0c) === 0x0c) {
602 | // paper empty event
603 | update(state.paperempty);
604 | }
605 | else if ((buffer[1] & 0x2c) !== 0) {
606 | // error event
607 | update(state.error);
608 | }
609 | else if (status !== state.print) {
610 | // online event
611 | update(state.online);
612 | }
613 | else {
614 | // nothing to do
615 | }
616 | // cash drawer
617 | updateDrawer((buffer[0] & 0x04) === 0x04 ? state.drawerclosed : state.draweropen);
618 | // clear data
619 | buffer.splice(0, 4);
620 | }
621 | }
622 | else if (buffer[0] === 0x35 || buffer[0] === 0x37 || buffer[0] === 0x3b || buffer[0] === 0x3d || buffer[0] === 0x5f) {
623 | // escpos: block data
624 | const i = buffer.indexOf(0);
625 | // check length
626 | if (i > 0) {
627 | console.log(new Date().toISOString(), 'escpos: block data');
628 | // clear data
629 | const block = buffer.splice(0, i + 1);
630 | if (block[0] === 0x5f) {
631 | // clear timer
632 | clearTimeout(timeout);
633 | // model info
634 | const model = String.fromCharCode(...block.slice(1, -1)).toLowerCase();
635 | console.log(new Date().toISOString(), 'escpos:', model);
636 | if (printer === 'generic' && /^(epson|citizen|fit)$/.test(model)) {
637 | // escpos thermal
638 | printer = model;
639 | console.log(new Date().toISOString(), `printer: ${printer}`);
640 | }
641 | else if (printer === 'epson' && /^tm-u/.test(model)) {
642 | // escpos impact
643 | printer = 'impactb';
644 | console.log(new Date().toISOString(), `printer: ${printer}`);
645 | }
646 | else {
647 | // nothing to do
648 | }
649 | }
650 | else if (block[0] === 0x3b) {
651 | // power on
652 | if (block[1] === 0x31) {
653 | console.log(new Date().toISOString(), 'escpos: power on');
654 | // printer control language
655 | printer = '';
656 | console.log(new Date().toISOString(), `printer: ${printer}`);
657 | // hello to printer
658 | drain = await conn.write(command.hello, 'binary');
659 | }
660 | // offline event
661 | update(state.offline);
662 | updateDrawer(state.offline);
663 | }
664 | else if (block[0] === 0x37) {
665 | // buffer clear
666 | if (block[1] === 0x25) {
667 | console.log(new Date().toISOString(), 'escpos: buffer clear');
668 | // clear timer
669 | clearTimeout(timeout);
670 | // get model info and enable automatic status
671 | drain = await conn.write(command.escasb, 'binary');
672 | }
673 | }
674 | else {
675 | // nothing to do
676 | }
677 | }
678 | }
679 | else if ((buffer[0] & 0x93) === 0x12) {
680 | // escpos: realtime status
681 | console.log(new Date().toISOString(), 'escpos: realtime status');
682 | // clear timer
683 | clearTimeout(timeout);
684 | // cash drawer
685 | updateDrawer((buffer[0] & 0x97) === 0x16 ? state.drawerclosed : state.draweropen);
686 | // clear data
687 | buffer.shift();
688 | }
689 | else {
690 | // escpos: other
691 | console.log(new Date().toISOString(), 'escpos: other');
692 | buffer.shift();
693 | }
694 | break;
695 | }
696 | }
697 | while (buffer.length > 0 && buffer.length < len);
698 | });
699 | // open port
700 | conn.open({ ...{ baudRate: 115200, flowControl: 'hardware', bufferSize: 2048 }, ...options });
701 |
702 | return {
703 | /**
704 | * Printer status.
705 | * @type {string} printer status
706 | */
707 | get status() {
708 | return status;
709 | },
710 | /**
711 | * Cash drawer status.
712 | * @type {string} cash drawer status
713 | */
714 | get drawer() {
715 | return drawer;
716 | },
717 | /**
718 | * Invert cash drawer state.
719 | * @param {boolean} invert invert cash drawer state
720 | */
721 | invertDrawerState(invert) {
722 | invertion = !!invert;
723 | switch (drawer) {
724 | case state.drawerclosed:
725 | drawer = state.draweropen;
726 | break;
727 | case state.draweropen:
728 | drawer = state.drawerclosed;
729 | break;
730 | default:
731 | break;
732 | }
733 | },
734 | /**
735 | * Print receipt markdown.
736 | * @param {string} markdown receipt markdown
737 | * @param {string} [options] print options
738 | * @returns {string} print result
739 | */
740 | print(markdown, options) {
741 | // asynchronous printing
742 | return new Promise(async res => {
743 | // online or ready
744 | if (status === state.online) {
745 | // save resolver
746 | resolve = res;
747 | // print event
748 | update(state.print);
749 | // convert markdown to printer command
750 | const command = await Receipt.from(markdown, `-p ${printer} ${options}`).toCommand();
751 | // write command
752 | if (/^star$/.test(printer)) {
753 | // star
754 | drain = await conn.write(command
755 | .replace(/^(\x1b@)?\x1b\x1ea\x00/, '$1\x1b\x1ea\x01') // (ESC @) ESC RS a n
756 | .replace(/(\x1b\x1d\x03\x01\x00\x00\x04?|\x1b\x06\x01)$/, '\x17'), 'binary'); // ETB
757 | }
758 | else {
759 | // escpos
760 | drain = await conn.write(command.replace(/^\x1b@\x1da\x00/, '\x1b@\x1da\xff'), 'binary'); // ESC @ GS a n
761 | }
762 | }
763 | else {
764 | // print response
765 | res(status);
766 | }
767 | });
768 | },
769 | /**
770 | * Close serial port.
771 | */
772 | close() {
773 | // clear timer
774 | clearTimeout(timeout);
775 | // close port
776 | conn.close();
777 | },
778 | /**
779 | * Add event listener.
780 | * @param {string} name event name
781 | * @param {function} listener event listener
782 | */
783 | on(name, listener) {
784 | if (listeners[name]) {
785 | listeners[name].push(listener);
786 | }
787 | },
788 | /**
789 | * Remove event listener.
790 | * @param {string} name event name
791 | * @param {function} listener event listener
792 | */
793 | off(name, listener) {
794 | if (listeners[name]) {
795 | listeners[name] = listeners[name].filter(c => c !== listener);
796 | }
797 | }
798 | };
799 | }
800 | };
801 | })();
802 |
--------------------------------------------------------------------------------