├── .gitignore
├── .idea
├── artifacts
│ └── webapp_hardware_bridge_jar.xml
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── modules.xml
├── modules
│ ├── webapp-hardware-bridge.iml
│ └── webapp-hardware-bridge.main.iml
└── vcs.xml
├── ADVANCED.md
├── BUILD.md
├── CHANGELOG.md
├── CONFIGURATION.md
├── HTTP_API.md
├── LICENSE
├── README.md
├── TROUBLESHOOT.md
├── build.gradle
├── demo
├── printer-advanced.htm
├── printer-annotation.htm
├── printer-basic.htm
├── serial-basic.html
├── serial-weigh.htm
├── websocket-printer.js
├── websocket-serial.js
└── websocket-weigh.js
├── gradlew
├── gradlew.bat
├── icon.ico
├── install.nsi
├── settings.gradle
├── src
└── main
│ ├── java
│ ├── module-info.java
│ └── tigerworkshop
│ │ └── webapphardwarebridge
│ │ ├── Constants.java
│ │ ├── GUI.java
│ │ ├── Server.java
│ │ ├── dtos
│ │ ├── Config.java
│ │ ├── NotificationDTO.java
│ │ ├── PrintServiceDTO.java
│ │ ├── SerialPortDTO.java
│ │ └── VersionDTO.java
│ │ ├── interfaces
│ │ ├── WebSocketServerInterface.java
│ │ └── WebSocketServiceInterface.java
│ │ ├── responses
│ │ ├── PrintDocument.java
│ │ └── PrintResult.java
│ │ ├── services
│ │ ├── ConfigService.java
│ │ └── DocumentService.java
│ │ ├── utils
│ │ ├── AnnotatedPrintable.java
│ │ ├── CertificateGenerator.java
│ │ ├── ImagePrintable.java
│ │ └── ThreadUtil.java
│ │ └── websocketservices
│ │ ├── PrinterWebSocketService.java
│ │ └── SerialWebSocketService.java
│ └── resources
│ ├── META-INF
│ └── MANIFEST.MF
│ ├── icon.png
│ ├── log4j2.xml
│ └── web
│ ├── css
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
│ ├── bootstrap.rtl.min.css
│ └── bootstrap.rtl.min.css.map
│ ├── index.html
│ └── js
│ ├── axios.min.js
│ ├── bootstrap.bundle.js
│ ├── bootstrap.bundle.js.map
│ ├── bootstrap.bundle.min.js
│ ├── bootstrap.bundle.min.js.map
│ ├── bootstrap.esm.js
│ ├── bootstrap.esm.js.map
│ ├── bootstrap.esm.min.js
│ ├── bootstrap.esm.min.js.map
│ ├── bootstrap.js
│ ├── bootstrap.js.map
│ ├── bootstrap.min.js
│ ├── bootstrap.min.js.map
│ └── petite-vue.js
└── tls
└── .gitignore
/.gitignore:
--------------------------------------------------------------------------------
1 | downloads/
2 | .gradle/
3 | gradle/
4 | log/
5 | out/
6 | jre/
7 | build/
8 | .idea/workspace.xml
9 | config.json
10 | *.exe
--------------------------------------------------------------------------------
/.idea/artifacts/webapp_hardware_bridge_jar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/out/artifacts/webapp_hardware_bridge_jar
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/modules/webapp-hardware-bridge.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules/webapp-hardware-bridge.main.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ADVANCED.md:
--------------------------------------------------------------------------------
1 | # Advanced Configurations
2 |
3 | ## Authentication
4 |
5 | ### Enable Authentication
6 |
7 | Authentication is disabled by default, that any website can connect to your bridge and access local resources.
8 |
9 | To prevent unauthorized access, set `server.authentication.enabled` to `true` and `server.authentication.token` to the value you want.
10 |
11 | When enabled, connections without correct token will be rejected.
12 |
13 | #### WebSocket
14 |
15 | Point url to `ws://127.0.0.1:12212/serial/WEIGH?token=1234567890` if your `token` is "1234567890"
16 |
17 | #### Web UI
18 |
19 | Enter `token` as `Password` when prompted and leave `Username` empty.
20 |
21 | #### Web API
22 |
23 | Use header `Authorization: Bearer 1234567890`
24 |
25 | ## HTTPS/WSS Support
26 |
27 | Some browser does not allow webpage with secure context (i.e. HTTPS)
28 | to connect non-secured WebSocket server.
29 |
30 | Either of below methods required to workaround this:
31 |
32 | ### Allow non-secure WebSocket server from HTTPS website (Not recommended)
33 |
34 | Warning: These setting can open a security hole, use on development environment only
35 |
36 | Firefox: Go to `about:config`, set `network.websocket.allowInsecureFromHTTPS` to `true`
37 |
38 | Chrome: Add `--allow-running-insecure-content` to launching argument
39 |
40 | ### Enable WebSocket Secure (WSS) with self-signed certificate
41 |
42 | WHB have built-in ability to generate self-signed certificate.
43 |
44 | Set `server.tls.enabled` to true, `server.tls.selfSigned` to true in `setting.json` and relaunch the application.
45 |
46 | Upon start, application should automatically generate a self-signed certificate
47 |
48 | and start listening on `wss://127.0.0.1:12212` with secured connection.
49 |
50 | On first setup, you must go to `https://127.0.0.1:12212` to accept that self-signed certificate.
51 |
52 | After change, point url to `wss://127.0.0.1:12212` instead of `ws://127.0.0.1:12212`
53 |
54 | ### Enable WebSocket Secure (WSS) with real, user-provided certificate
55 |
56 | Copy your certificate and private key to `tls` directory.
57 |
58 | Set `server.tls.enabled` to true, `server.tls.selfSigned` to false, `server.tls.cert` and `server.tls.key` in `setting.json` and relaunch the application.
59 |
60 | Upon start, application should pickup your certificate and start listening on `wss://127.0.0.1:12212` with secured connection.
61 |
62 | After change, point url to `wss://127.0.0.1:12212` instead of `ws://127.0.0.1:12212`
63 |
64 | #### How to obtain real TLS Certificate?
65 |
66 | WHB is usually listening on 127.0.0.1. It's normally impossible to obtain valid certificates signed for that.
67 |
68 | A common workaround is to point your (sub-)domain A Record to 127.0.0.1, and obtain certificate with that
69 |
70 | e.g. Point `local.tiger-workshop.com` to `127.0.0.1`, then point your WebApp to `wss://local.tiger-workshop.com:12212`
71 |
72 | ### Why we can't provide certificate for you
73 |
74 | Shipping private key with application is considered kind of "key-compromise".
75 |
76 | The certificate will be revoked by CA. It's very easily detected especially for open-source projects.
--------------------------------------------------------------------------------
/BUILD.md:
--------------------------------------------------------------------------------
1 | # Build Instructions
2 |
3 | ## Build from source
4 |
5 | - JDK 21, [Eclipse Temurin 21](https://adoptium.net/en-GB/temurin/releases/) Recommanded
6 | - Intelij IDEA (Both Community and Ultimate works)
7 |
8 | 1. An artifact config file is included in git repository.
9 |
10 | 2. Use Intelij IDEA to "Build artifact" to yield `out\artifacts\webapp_hardware_bridge_jar`.
11 |
12 | ## Windows Installer bundled with JRE
13 |
14 | - JRE 21, [Eclipse Temurin 21](https://adoptium.net/en-GB/temurin/releases/) Recommanded
15 | - [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/)
16 |
17 | 1. Follow "Build from source" instructions to yield `out\artifacts\webapp_hardware_bridge_jar`
18 |
19 | 2. Copy JRE 21 into `./jre` directory
20 |
21 | 3. Run `install.nsi` with NSIS to yield `whb.exe`
22 |
23 | ## How to run
24 |
25 | 1. Start application
26 | - GUI: `javaw -cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI`
27 | - Server: `java -cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server`
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelogs
2 |
3 | ## From 0.x to 1.0.0
4 |
5 | - 1.0 is a major rewrite, while maintain compatibility with existing WebApps
6 | - Settings will lost after upgrade, please reconfigure via "Web UI" or "Web API"
7 |
8 | ### Feature changes
9 | - Added per printer settings (Auto-rotate, DPI...)
10 | - Added per serial port settings (Baud-rate, data bits, stop bit, parity bit, charset. binary mode, multi-bytes mode)
11 | - Added "Web UI" for configuration, replacing "Configurator"
12 | - Added "Web API", a HTTP API for WebApp to configure directly without using "Web UI" or "Configurator"
13 | - Config file renamed from "setting.json" to "config.json", which is in different format
14 |
15 | ### Internal changes
16 | - Removed "Configurator"
17 | - Removed undocumented feature "Cloud Proxy"
18 | - Removed usage of JavaFX
19 | - Rewrite config code
20 | - Implementation of WebSocket changed from "Java-WebSocket" to "Javalin"
21 | - Internal dataflow optimization
22 | - Simplified code by using "Lombok"
23 | - Upgrade Java version from 8 to 21
24 | - Many dependencies upgrades and security fixes
25 |
--------------------------------------------------------------------------------
/CONFIGURATION.md:
--------------------------------------------------------------------------------
1 | # Configurations
2 |
3 | ## Web/WebSocket Server
4 |
5 | ### Bind
6 |
7 | - (Default) `127.0.0.1`
8 | - `127.0.0.1` for normal usage
9 | - `0.0.0.0` for open to internet access (Not recommended)
10 | - Other interface address accepted
11 |
12 | ### Address
13 |
14 | - (Default) `127.0.0.1`
15 | - `127.0.0.1` unless you need to allow internal/internet access
16 | - IP address / Domain name accepted
17 |
18 | ### Port
19 |
20 | - (Default) `12212`
21 | - Range: `1024` - `65535`
22 |
23 | ### Enable authentication
24 |
25 | [See Authentication for more detail](ADVANCED.md#authentication)
26 |
27 | - (Default) `false`
28 |
29 | #### Token
30 |
31 | - (Default) Blank
32 | - Accept any text value
33 |
34 | ### Enable TLS
35 |
36 | [See HTTPS/WSS Support for more detail](ADVANCED.md#httpswss-support)
37 |
38 | - (Default) `false`
39 |
40 | #### Self Signed
41 |
42 | - (Default) `true`
43 |
44 | #### Cert
45 |
46 | - (Default) `tls/default-cert.pem`
47 |
48 | #### Key
49 |
50 | - (Default) `tls/default-cert.pem`
51 |
52 | #### CA Bundle
53 |
54 | - (Default) Empty
55 |
56 | ## Downloader
57 |
58 | ### Path
59 |
60 | Directory to save downloaded files
61 |
62 | - (Default) `download`
63 |
64 | ### Timeout
65 |
66 | Seconds before download timeout
67 |
68 | - (Default) `30`
69 |
70 | ### Ignore TLS certificate error
71 |
72 | Ignore any TLS certificate error (self-signed, expired...) when downloading files
73 |
74 | Not recommended for normal usage, useful in some corporate networks where firewall doing MITM
75 |
76 | - (Default) `false`
77 |
78 | ## Printers
79 |
80 | ### Enabled
81 |
82 | - (Default) `true`
83 |
84 | ### Auto add unknown type
85 |
86 | Auto add type mapping to configuration when document received with unknown type
87 |
88 | - (Default) `false`
89 |
90 | ### Fallback to default printer if none matched
91 |
92 | Fallback to default printer if none of the printers matched in configuration
93 |
94 | - (Default) `false`
95 |
96 | #### Type
97 |
98 | Mapping key between WebApp and physically printer name in operating system
99 |
100 | #### Printer Name
101 |
102 | Printer name in operating system
103 |
104 | #### Auto Rotate
105 |
106 | Auto rotate portrait / landscape
107 |
108 | - (Default) `false`
109 |
110 | #### Reset imageable area
111 |
112 | Required by some printer to handle size correctly
113 |
114 | - (Default) `true`
115 |
116 | #### Force DPI
117 |
118 | Required by some printer/operating system to handle DPI correctly
119 |
120 | - (Default) `0`
121 | - `0` - Auto detect
122 | - Common values: `213`, `300`
123 |
124 | ## Serials
125 |
126 | ### Enabled
127 |
128 | - (Default) `true`
129 |
130 | #### Type
131 |
132 | Mapping key between WebApp and physically serial port name in operating system
133 |
134 | #### Serial Port
135 |
136 | Serial port name in operating system
137 |
138 | #### Baud Rate
139 |
140 | Auto-detect when leave blank
141 |
142 | - (Default) Blank
143 |
144 | #### Data Bits
145 |
146 | Auto-detect when leave blank
147 |
148 | - (Default) Blank
149 |
150 | #### Stop Bits
151 |
152 | Auto-detect when leave blank
153 |
154 | - (Default) Blank
155 |
156 | #### Parity
157 |
158 | Auto-detect when leave blank
159 |
160 | - (Default) Blank
161 |
162 | #### Read Charset
163 |
164 | Charset to decode data received from serial port
165 |
166 | Changing this may break compatibility with WebApp integrated with pre-1.0 version
167 |
168 | - (Default) `UTF-8`
169 | - `UTF-8` - Data will be sent to WebSocket as UTF-8 `string`
170 | - `US-ASCII` - Data will be sent to WebSocket as ASCII `string`
171 | - `BINARY` - Data will be sent to WebSocket as `blob`
172 |
173 | #### Read Multi-bytes
174 |
175 | Read all available bytes in serial port and send them to WebSocket at once
176 |
177 | Changing this may break compatibility with WebApp integrated with pre-1.0 version
178 |
179 | - (Default) `false`
--------------------------------------------------------------------------------
/HTTP_API.md:
--------------------------------------------------------------------------------
1 | # HTTP APIs
2 |
3 | All endpoints have CORS configured to allow requests from any origin.
4 |
5 | You can get or update the current configuration in your WebApp directly by using the `/config.json` endpoint.
6 |
7 | ## GET /config.json
8 |
9 | Get content of `config.json` file.
10 |
11 | ## PUT /config.json
12 |
13 | Update content of `config.json` file.
14 |
15 | ## GET /system/printers.json
16 |
17 | Return list of available printers.
18 |
19 | ## GET /system/serials.json
20 |
21 | Return list of available serial ports.
22 |
23 | ## POST /system/restart.json
24 |
25 | Restart WebSocket/Web server
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 imTigger
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebApp Hardware Bridge
2 |
3 | ## Introduction
4 |
5 | WebApp Hardware Bridge made it possible for WebApps to perform silent print and access to serial ports.
6 |
7 | Common use cases:
8 | - Web-based POS - PDF and ESC/POS receipt silent print
9 | - Web-based WMS - Serial weight scale real-time reading, delivery note/packing List silent print
10 | - Any WebApps need to read/write to serial ports
11 |
12 | ## Features
13 |
14 | - [x] Direct print from WebApps
15 | - [x] Serial port read/write from WebApps
16 | - [x] Support all modern browsers that implemented WebSocket (Chrome, Firefox, Edge... etc)
17 | - [x] [HTTP API](HTTP_API.md) to configure directly from your WebApp
18 | - [x] [JS SDK/Example included](demo)
19 |
20 | ### Direct Print
21 | - 0-click silent printing in web browsers
22 | - Download via URL / Base64 encoded file / Base64 encoded binary raw command
23 | - Support multiple printers, mapped by key
24 | - Support PDF/PNG/JPG Printing
25 | - Support RAW/ESC-POS Printing
26 | - Support adding annotation text to PDF/Image before printing
27 | - Per printer settings
28 |
29 | ### Serial Access
30 | - Bidirectional communication
31 | - Support multiple ports, mapped by key
32 | - Support multiple connection share same serial port
33 | - Serial weigh scale (AWH-SA30 supported out-of-box in JS SDK)
34 | - Per port settings (Baud rate, data bits, stop bit, parity bit)
35 |
36 | ## How to use?
37 |
38 | ### Client Side
39 |
40 | 1. Install and setup mapping via Web UI / API
41 |
42 | 2. Start "WebApp Hardware Bridge" and start using your WebApp
43 |
44 | ### WebApp Side
45 |
46 | 1. Check [JS SDK/Example](demo)
47 |
48 | ## How it works?
49 |
50 | WebApp Hardware Bridge is a Java based application, which have more access to underlying hardwares.
51 |
52 | It exposes a WebSocket server on localhost to accept print jobs and serial connections from browsers.
53 |
54 | ### Print Jobs
55 |
56 | - PDF/Images job are downloaded/decoded and then sent to mapped printer.
57 | - Raw job are sent to mapped printer directly.
58 |
59 | ### Serial Connections
60 |
61 | - Serial port are opened by Java and "proxied" as WebSocket stream
62 | - Serial port can be shared by multiple connections
63 | - Bidirectional communications possible
64 |
65 | ### Mappings
66 |
67 | Web UI / API are provided to set up mappings between keys and printers/serials.
68 |
69 | Therefore, WebApps do not need to care about the actual printer names.
70 |
71 | ## More documents
72 |
73 | - [Configurations](CONFIGURATION.md)
74 | - [HTTP APIs](HTTP_API.md)
75 | - [Advanced Configurations - Authentication](ADVANCED.md#authentication)
76 | - [Advanced Configurations - HTTPS/WSS Support](ADVANCED.md#httpswss-support)
77 | - [Build from source](BUILD.md)
78 | - [Troubleshooting](TROUBLESHOOT.md)
79 |
80 | ## Upgrade
81 |
82 | - Settings will lost after upgrade from 0.x to 1.0, please reconfigure via "Web UI" or "Web API"
83 |
84 | ## Changelogs
85 |
86 | - [Changelogs](CHANGELOG.md)
87 |
--------------------------------------------------------------------------------
/TROUBLESHOOT.md:
--------------------------------------------------------------------------------
1 | # Troubleshoot
2 |
3 | - Configurator/GUI do not run? Install [vc_redist.x64.exe](https://www.microsoft.com/en-US/download/details.aspx?id=48145)
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'application'
4 | }
5 |
6 | group 'webapp-hardware-bridge'
7 | version '1.0.1'
8 |
9 | sourceCompatibility = '21'
10 | targetCompatibility = '21'
11 |
12 | repositories {
13 | mavenCentral()
14 | }
15 |
16 | dependencies {
17 | // The production code uses the SLF4J logging API at compile time
18 | implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.23.1'
19 |
20 | implementation group: 'commons-io', name: 'commons-io', version: '2.16.1'
21 | implementation group: 'commons-codec', name: 'commons-codec', version: '1.17.1'
22 | implementation group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.2.5'
23 | implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.2'
24 | implementation group: 'com.fazecast', name: 'jSerialComm', version: '2.11.0'
25 | implementation group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.31'
26 | implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.78.1'
27 | implementation group: 'org.bouncycastle', name: 'bcpkix-jdk18on', version: '1.78.1'
28 | implementation group: 'io.javalin', name: 'javalin', version: '6.2.0'
29 | implementation group: 'io.javalin.community.ssl', name: 'ssl-plugin', version: '6.2.0'
30 |
31 | compileOnly 'org.projectlombok:lombok:1.18.34'
32 | annotationProcessor 'org.projectlombok:lombok:1.18.34'
33 |
34 | testCompileOnly 'org.projectlombok:lombok:1.18.34'
35 | testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'
36 |
37 | testImplementation group: 'junit', name: 'junit', version: '4.13.2'
38 | }
39 |
40 | application {
41 | mainClass = 'tigerworkshop.webapphardwarebridge.GUI'
42 | }
--------------------------------------------------------------------------------
/demo/printer-advanced.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | WebSocket Printer Advanced
12 |
13 |
14 |
15 | WebSocket Printer Advanced
16 |
17 |
18 |
19 | PDF by URL
20 |
21 |
22 |
23 |
24 | Connection Status
25 |
26 |
27 |
28 | Output
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Normal Print
37 |
38 |
39 |
40 |
41 |
42 |
Print (With Fallback)
43 |
Fallback to window.open() if WebApp Hardware Bridge is not running.
44 |
45 |
46 |
47 |
48 |
49 |
Print (With id)
50 |
If "id" is submitted, it will be echoed back in onUpdate() thus we can trace back to jobs we submitted.
51 |
52 |
53 |
54 |
55 |
62 |
63 |
If "qty" is submitted, specified copies of documents will be printed.
64 |
65 |
66 |
67 |
68 |
114 |
115 |
--------------------------------------------------------------------------------
/demo/printer-annotation.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | WebSocket Printer Annotation
12 |
13 |
14 |
15 | WebSocket Printer Annotation
16 |
17 |
18 |
19 | PDF by URL
20 |
21 |
22 |
23 |
24 | Connection Status
25 |
26 |
27 |
28 | Output
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 | The "extras" attribute is an array of object, allows adding annotation text on top of the PDF/Image before printing.
44 | Useful for adding extra text such as timestamp on pre-generated logistic labels.
45 |
46 |
47 |
Example:
48 |
49 | {
50 | "text": "Hello World!", // Mandatory
51 | "x": 10, // Mandatory
52 | "y": 10, // Mandatory
53 | "size": 12, // Optional, default 10
54 | "bold": true, // Optional, default false
55 | }
56 |
57 |
58 |
59 | Print
60 |
61 |
62 |
63 |
64 |
65 |
86 |
87 |
--------------------------------------------------------------------------------
/demo/printer-basic.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | WebSocket Printer Basic
12 |
13 |
14 |
15 | WebSocket Printer Basic
16 |
17 |
18 |
19 | PDF by URL
20 |
27 |
28 |
29 |
30 | Image by URL
31 |
38 |
39 |
40 |
41 | PDF by Base64
42 |
49 |
50 |
51 |
52 | Raw (ESC/POS)
53 |
60 |
61 |
62 |
63 |
95 |
96 |
--------------------------------------------------------------------------------
/demo/serial-basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | WebSocket Serial
12 |
13 |
14 |
15 | WebSocket Serial
16 |
17 |
30 |
31 |
32 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/demo/serial-weigh.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | WebSocket Serial Weigh (AWH-30)
12 |
13 |
14 |
15 | WebSocket Serial Weigh (AWH-30)
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
38 |
39 |
--------------------------------------------------------------------------------
/demo/websocket-printer.js:
--------------------------------------------------------------------------------
1 | function WebSocketPrinter(options) {
2 | var defaults = {
3 | url: "ws://127.0.0.1:12212/printer",
4 | onConnect: function () {
5 | },
6 | onDisconnect: function () {
7 | },
8 | onUpdate: function () {
9 | },
10 | };
11 |
12 | var settings = Object.assign({}, defaults, options);
13 | var websocket;
14 | var connected = false;
15 |
16 | var onMessage = function (evt) {
17 | settings.onUpdate(evt.data);
18 | };
19 |
20 | var onConnect = function () {
21 | connected = true;
22 | settings.onConnect();
23 | };
24 |
25 | var onDisconnect = function () {
26 | connected = false;
27 | settings.onDisconnect();
28 | reconnect();
29 | };
30 |
31 | var connect = function () {
32 | websocket = new WebSocket(settings.url);
33 | websocket.onopen = onConnect;
34 | websocket.onclose = onDisconnect;
35 | websocket.onmessage = onMessage;
36 | };
37 |
38 | var reconnect = function () {
39 | connect();
40 | };
41 |
42 | this.submit = function (data) {
43 | if (Array.isArray(data)) {
44 | data.forEach(function (element) {
45 | websocket.send(JSON.stringify(element));
46 | });
47 | } else {
48 | websocket.send(JSON.stringify(data));
49 | }
50 | };
51 |
52 | this.isConnected = function () {
53 | return connected;
54 | };
55 |
56 | connect();
57 | }
--------------------------------------------------------------------------------
/demo/websocket-serial.js:
--------------------------------------------------------------------------------
1 | function WebSocketSerial(options) {
2 | var defaults = {
3 | url: 'ws://127.0.0.1:12212/serial/DISPLAY',
4 | onConnect: function () {
5 | },
6 | onDisconnect: function () {
7 | },
8 | onMessage: function (message) {
9 | }
10 | };
11 |
12 | var settings = Object.assign({}, defaults, options);
13 | var websocket;
14 | var buffer = '';
15 |
16 | var onMessage = function (evt) {
17 | var chr = evt.data;
18 | settings.onMessage(chr);
19 | };
20 |
21 | var onConnect = function () {
22 | settings.onConnect();
23 | };
24 |
25 | var onDisconnect = function () {
26 | settings.onDisconnect();
27 | reconnect();
28 | };
29 |
30 | var connect = function () {
31 | websocket = new WebSocket(settings.url);
32 | websocket.onopen = onConnect;
33 | websocket.onclose = onDisconnect;
34 | websocket.onmessage = onMessage;
35 | };
36 |
37 | var reconnect = function () {
38 | connect();
39 | };
40 |
41 | this.send = function (message) {
42 | websocket.send(message);
43 | };
44 |
45 | connect();
46 | }
--------------------------------------------------------------------------------
/demo/websocket-weigh.js:
--------------------------------------------------------------------------------
1 | function WebSocketWeigh(options) {
2 | var defaults = {
3 | url: 'ws://127.0.0.1:12212/serial/WEIGH',
4 | weightRegex: new RegExp('([0-9]{1,2}\\.[0-9]{3})kg'),
5 | stableRegex: new RegExp('^ST.*\\s+'),
6 | onConnect: function () {
7 | },
8 | onDisconnect: function () {
9 | },
10 | onUpdate: function (weight, stable) {
11 |
12 | }
13 | };
14 |
15 | var settings = Object.assign({}, defaults, options);
16 | var websocket;
17 | var buffer = '';
18 |
19 | var onMessage = function (evt) {
20 | var chr = evt.data;
21 | if (chr == "\n") {
22 | var weightOutput = settings.weightRegex.exec(buffer);
23 | var stableOutput = settings.stableRegex.test(buffer);
24 |
25 | if (weightOutput != null) {
26 | settings.onUpdate(weightOutput[1], stableOutput);
27 | }
28 | buffer = '';
29 | } else {
30 | buffer = buffer + chr;
31 | }
32 | };
33 |
34 | var onConnect = function () {
35 | settings.onConnect();
36 | };
37 |
38 | var onDisconnect = function () {
39 | settings.onDisconnect();
40 | reconnect();
41 | };
42 |
43 | var connect = function () {
44 | websocket = new WebSocket(settings.url);
45 | websocket.onopen = onConnect;
46 | websocket.onclose = onDisconnect;
47 | websocket.onmessage = onMessage;
48 | };
49 |
50 | var reconnect = function () {
51 | connect();
52 | };
53 |
54 | connect();
55 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imTigger/webapp-hardware-bridge/c4d7be999ac7021c9f8db01af4b83302a8cd62b8/icon.ico
--------------------------------------------------------------------------------
/install.nsi:
--------------------------------------------------------------------------------
1 | ; The name of the installer
2 | Name "WebApp Hardware Bridge"
3 |
4 | ; The file to write
5 | OutFile "whb.exe"
6 |
7 | ; The default installation directory
8 | InstallDir "$LOCALAPPDATA\WebApp Hardware Bridge"
9 |
10 | ; Request application privileges for Windows Vista
11 | RequestExecutionLevel user
12 |
13 | ;--------------------------------
14 |
15 | ; Pages
16 |
17 | ;Page directory
18 | Page components
19 | Page instfiles
20 |
21 | ;--------------------------------
22 |
23 | ; The stuff to install
24 | Section "!Main Application" ;No components page, name is not important
25 | SectionIn RO
26 |
27 | ; Set output path to the installation directory.
28 | SetOutPath $INSTDIR
29 |
30 | ; Remove old version
31 | RMDir /r "$INSTDIR\jre"
32 | Delete "$INSTDIR\*.jar"
33 | Delete "$INSTDIR\setting.default.json"
34 | Delete "$DESKTOP\WebApp Hardware Bridge (GUI).lnk"
35 | Delete "$DESKTOP\WebApp Hardware Bridge (Configurator).lnk"
36 | Delete "$SMPROGRAMS\WebApp Hardware Bridge (GUI).lnk"
37 | Delete "$SMPROGRAMS\WebApp Hardware Bridge (Configurator).lnk"
38 |
39 | ; Put file there
40 | File /r out\artifacts\webapp_hardware_bridge_jar\*
41 | File /r jre
42 |
43 | File "install.nsi"
44 | File "icon.ico"
45 |
46 | ; Delete shortcuts
47 | Delete "$DESKTOP\WebApp Hardware Bridge.lnk"
48 | Delete "$DESKTOP\WebApp Hardware Bridge (CLI).lnk"
49 | Delete "$SMPROGRAMS\WebApp Hardware Bridge.lnk"
50 | Delete "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk"
51 |
52 | ; Create shortcuts
53 | CreateShortcut "$DESKTOP\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI" "$INSTDIR\icon.ico" 0
54 | CreateShortcut "$DESKTOP\WebApp Hardware Bridge (CLI).lnk" "$INSTDIR\jre\bin\java.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server" "$INSTDIR\icon.ico" 0
55 | CreateShortcut "$SMPROGRAMS\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI" "$INSTDIR\icon.ico" 0
56 | CreateShortcut "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk" "$INSTDIR\jre\bin\java.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server" "$INSTDIR\icon.ico" 0
57 |
58 | ; Write the installation path into the registry
59 | WriteRegStr HKCU "SOFTWARE\WebApp Hardware Bridge" "Install_Dir" "$INSTDIR"
60 |
61 | ; Write the uninstall keys for Windows
62 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "DisplayName" "WebApp Hardware Bridge"
63 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "UninstallString" '"$INSTDIR\uninstall.exe"'
64 | WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "NoModify" 1
65 | WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "NoRepair" 1
66 | WriteUninstaller "uninstall.exe"
67 |
68 | ; Auto close when finished
69 | SetAutoClose true
70 | SectionEnd ; end the section
71 |
72 | Section "Auto-start" autostart
73 | CreateShortcut "$SMSTARTUP\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI"
74 | SectionEnd
75 |
76 | Section "Uninstall"
77 | ; Remove registry keys
78 | DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge"
79 | DeleteRegKey HKCU "SOFTWARE\WebApp Hardware Bridge"
80 |
81 | ; Delete shortcuts
82 | Delete "$DESKTOP\WebApp Hardware Bridge.lnk"
83 | Delete "$DESKTOP\WebApp Hardware Bridge (CLI).lnk"
84 | Delete "$SMPROGRAMS\WebApp Hardware Bridge.lnk"
85 | Delete "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk"
86 |
87 | ; Remove files and uninstaller
88 | RMDir /r $INSTDIR
89 | SectionEnd
90 |
91 | Function .onInstSuccess
92 | ExecShell "" "$DESKTOP\WebApp Hardware Bridge.lnk"
93 | FunctionEnd
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'webapp-hardware-bridge'
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | module tigerworkshop.webapphardwarebridge {
2 | requires java.desktop;
3 | requires com.fazecast.jSerialComm;
4 | requires jdk.management;
5 | requires org.bouncycastle.provider;
6 | requires org.bouncycastle.pkix;
7 | requires org.apache.commons.io;
8 | requires org.slf4j;
9 | requires org.apache.pdfbox;
10 | requires org.apache.commons.codec;
11 | requires org.apache.httpcomponents.core5.httpcore5;
12 | requires org.apache.logging.log4j;
13 | requires io.javalin;
14 | requires com.fasterxml.jackson.databind;
15 | requires io.javalin.community.ssl;
16 | requires static lombok;
17 |
18 | opens tigerworkshop.webapphardwarebridge.dtos to com.fasterxml.jackson.databind;
19 | opens tigerworkshop.webapphardwarebridge.responses to com.fasterxml.jackson.databind;
20 | opens tigerworkshop.webapphardwarebridge.utils to com.fasterxml.jackson.databind;
21 |
22 | exports tigerworkshop.webapphardwarebridge;
23 | exports tigerworkshop.webapphardwarebridge.interfaces;
24 | }
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/Constants.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge;
2 |
3 | public class Constants {
4 | public static final String APP_NAME = "WebApp Hardware Bridge";
5 | public static final String APP_ID = "tigerworkshop.webapphardwarebridge";
6 | public static final String VERSION = "1.0.1";
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/GUI.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import lombok.extern.log4j.Log4j2;
5 | import tigerworkshop.webapphardwarebridge.dtos.Config;
6 | import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
7 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
8 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
9 | import tigerworkshop.webapphardwarebridge.services.ConfigService;
10 |
11 | import javax.imageio.ImageIO;
12 | import java.awt.*;
13 | import java.io.File;
14 | import java.net.URI;
15 | import java.util.Objects;
16 |
17 | @Log4j2
18 | public class GUI implements WebSocketServiceInterface {
19 | private static final ConfigService configService = ConfigService.getInstance();
20 |
21 | private final Server server = new Server();
22 | private Config config = configService.getConfig();
23 |
24 | Desktop desktop = Desktop.getDesktop();
25 | TrayIcon trayIcon;
26 | SystemTray tray;
27 |
28 | public static void main(String[] args) throws Exception {
29 | GUI gui = new GUI();
30 | gui.launch();
31 | }
32 |
33 | public void launch() throws Exception {
34 | server.start();
35 |
36 | // Create tray icon
37 | if (!SystemTray.isSupported()) {
38 | log.warn("SystemTray is not supported");
39 | return;
40 | }
41 |
42 | // Register service as notification listener
43 | if (config.getGui().getNotification().isEnabled()) {
44 | server.registerService(this);
45 | }
46 |
47 | MenuItem settingItem = new MenuItem("Web UI");
48 | settingItem.addActionListener(e -> {
49 | try {
50 | if (desktop == null || !desktop.isSupported(Desktop.Action.BROWSE)) {
51 | throw new Exception("Desktop browse is not supported");
52 | }
53 |
54 | desktop.browse(new URI(config.getServer().getUri()));
55 | } catch (Exception ex) {
56 | log.error("Failed to open Web UI", ex);
57 | }
58 | });
59 |
60 | MenuItem appDirectoryItem = new MenuItem("App Directory");
61 | appDirectoryItem.addActionListener(e -> {
62 | try {
63 | if (desktop == null || !desktop.isSupported(Desktop.Action.OPEN)) {
64 | throw new Exception("Desktop open is not supported");
65 | }
66 |
67 | desktop.open(new File("."));
68 | } catch (Exception ex) {
69 | log.error("Failed to open log folder", ex);
70 | }
71 | });
72 |
73 | MenuItem logDirectoryItem = new MenuItem("Log Directory");
74 | logDirectoryItem.addActionListener(e -> {
75 | try {
76 | if (desktop == null || !desktop.isSupported(Desktop.Action.OPEN)) {
77 | throw new Exception("Desktop open is not supported");
78 | }
79 |
80 | desktop.open(new File("log"));
81 | } catch (Exception ex) {
82 | log.error("Failed to open log folder", ex);
83 | }
84 | });
85 |
86 | MenuItem restartItem = new MenuItem("Restart");
87 | restartItem.addActionListener(e -> restart());
88 |
89 | MenuItem exitItem = new MenuItem("Exit");
90 | exitItem.addActionListener(e -> System.exit(0));
91 |
92 | // Add components to pop-up menu
93 | final PopupMenu popupMenu = new PopupMenu();
94 | popupMenu.add(settingItem);
95 | popupMenu.addSeparator();
96 | popupMenu.add(appDirectoryItem);
97 | popupMenu.add(logDirectoryItem);
98 | popupMenu.addSeparator();
99 | popupMenu.add(restartItem);
100 | popupMenu.add(exitItem);
101 |
102 | tray = SystemTray.getSystemTray();
103 |
104 | // Set icon
105 | Dimension trayIconSize = tray.getTrayIconSize();
106 | final Image image = ImageIO.read(Objects.requireNonNull(getClass().getClassLoader().getResource("icon.png")));
107 | final Image scaledImage = image.getScaledInstance(trayIconSize.width, trayIconSize.height, Image.SCALE_SMOOTH);
108 |
109 | trayIcon = new TrayIcon(scaledImage, Constants.APP_NAME);
110 | trayIcon.setPopupMenu(popupMenu);
111 |
112 | tray.add(trayIcon);
113 |
114 | notify(Constants.APP_NAME, " is running in background!", TrayIcon.MessageType.INFO);
115 | }
116 |
117 | public void notify(String title, String message, TrayIcon.MessageType messageType) {
118 | try {
119 | trayIcon.displayMessage(title, message, messageType);
120 | } catch (Exception e) {
121 | log.error("Failed to display notification", e);
122 | }
123 | }
124 |
125 | public void restart() {
126 | try {
127 | config = configService.getConfig();
128 |
129 | server.stop();
130 | server.start();
131 |
132 | notify("Restart", "Server restarted successfully", TrayIcon.MessageType.INFO);
133 | } catch (Exception e) {
134 | log.error("Failed to restart server", e);
135 | }
136 | }
137 |
138 | @Override
139 | public void start() {
140 |
141 | }
142 |
143 | @Override
144 | public void stop() {
145 |
146 | }
147 |
148 | @Override
149 | public void messageToService(String message) {
150 | try {
151 | log.debug("GUI Notification: {}", message);
152 |
153 | NotificationDTO notificationDTO = new ObjectMapper().readValue(message, NotificationDTO.class);
154 | notify(notificationDTO.getTitle(), notificationDTO.getMessage(), TrayIcon.MessageType.valueOf(notificationDTO.getType()));
155 | } catch (Exception e) {
156 | log.error("Failed to parse notification message", e);
157 | }
158 | }
159 |
160 | @Override
161 | public void messageToService(byte[] message) {
162 | }
163 |
164 | @Override
165 | public void onRegister(WebSocketServerInterface server) {
166 |
167 | }
168 |
169 | @Override
170 | public void onUnregister() {
171 | }
172 |
173 | @Override
174 | public String getChannel() {
175 | return "/notification";
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/Server.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.JsonNode;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import com.fazecast.jSerialComm.SerialPort;
7 | import io.javalin.Javalin;
8 | import io.javalin.community.ssl.SslPlugin;
9 | import io.javalin.http.ContentType;
10 | import io.javalin.plugin.bundled.CorsPluginConfig;
11 | import io.javalin.util.JavalinBindException;
12 | import io.javalin.websocket.WsContext;
13 | import lombok.extern.log4j.Log4j2;
14 | import tigerworkshop.webapphardwarebridge.dtos.*;
15 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
16 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
17 | import tigerworkshop.webapphardwarebridge.services.ConfigService;
18 | import tigerworkshop.webapphardwarebridge.utils.CertificateGenerator;
19 | import tigerworkshop.webapphardwarebridge.utils.ThreadUtil;
20 | import tigerworkshop.webapphardwarebridge.websocketservices.PrinterWebSocketService;
21 | import tigerworkshop.webapphardwarebridge.websocketservices.SerialWebSocketService;
22 |
23 | import javax.print.PrintService;
24 | import java.awt.print.PrinterJob;
25 | import java.nio.ByteBuffer;
26 | import java.util.*;
27 | import java.util.concurrent.ConcurrentHashMap;
28 | import java.util.concurrent.ConcurrentLinkedQueue;
29 | import java.util.concurrent.TimeUnit;
30 |
31 | @Log4j2
32 | public class Server implements WebSocketServerInterface {
33 | private Javalin javalinServer;
34 |
35 | private static final ObjectMapper objectMapper = new ObjectMapper();
36 | private static final ConfigService configService = ConfigService.getInstance();
37 |
38 | private final ConcurrentHashMap> socketChannelSubscriptions = new ConcurrentHashMap<>();
39 | private final ConcurrentHashMap> serviceChannelSubscriptions = new ConcurrentHashMap<>();
40 | private final ConcurrentLinkedQueue services = new ConcurrentLinkedQueue<>();
41 |
42 | public static void main(String[] args) {
43 | try {
44 | new Server().start();
45 | } catch (Exception e) {
46 | throw new RuntimeException(e);
47 | }
48 | }
49 |
50 | synchronized public void start() throws Exception {
51 | Config config = configService.getConfig();
52 |
53 | Config.Server serverConfig = config.getServer();
54 |
55 | // Create Javalin Server
56 | javalinServer = Javalin.create(cfg -> {
57 | cfg.showJavalinBanner = false;
58 | cfg.staticFiles.add(staticFiles -> staticFiles.directory = "web");
59 | cfg.bundledPlugins.enableCors(cors -> cors.addRule(CorsPluginConfig.CorsRule::anyHost));
60 |
61 | if (serverConfig.getTls().isEnabled()) {
62 | if (serverConfig.getTls().isSelfSigned()) {
63 | log.info("TLS Enabled with self-signed certificate");
64 |
65 | CertificateGenerator.generateSelfSignedCertificate(serverConfig.getAddress(), serverConfig.getTls().getCert(), serverConfig.getTls().getKey());
66 |
67 | log.info("For first time setup, open in browser and trust the certificate: {}", serverConfig.getUri());
68 | }
69 |
70 | SslPlugin plugin = new SslPlugin(conf -> {
71 | conf.insecure = false;
72 | conf.securePort = serverConfig.getPort();
73 | conf.pemFromPath(serverConfig.getTls().getCert(), serverConfig.getTls().getKey());
74 | conf.sniHostCheck = !serverConfig.getTls().isSelfSigned();
75 | });
76 | cfg.registerPlugin(plugin);
77 | }
78 | });
79 |
80 | // Add WebSocket Auth
81 | javalinServer.wsBefore(ctx -> {
82 | ctx.onConnect(wsConnectContext -> {
83 | wsConnectContext.session.getPolicy().setMaxBinaryMessageSize(-1);
84 | wsConnectContext.session.getPolicy().setMaxTextMessageSize(-1);
85 |
86 | wsConnectContext.enableAutomaticPings(5, TimeUnit.SECONDS);
87 |
88 | if (serverConfig.getAuthentication().isEnabled()) {
89 | if (Optional.ofNullable(wsConnectContext.queryParam("token")).orElse("").equals(serverConfig.getAuthentication().getToken())) {
90 | return;
91 | }
92 |
93 | wsConnectContext.closeSession(1003, "Invalid token");
94 | }
95 | });
96 | });
97 |
98 | // Add WebSocket Printer Service
99 | Config.Printer printerConfig = config.getPrinter();
100 | if (printerConfig.isEnabled()) {
101 | PrinterWebSocketService printerWebSocketService = new PrinterWebSocketService();
102 | printerWebSocketService.start();
103 |
104 | javalinServer.ws(printerWebSocketService.getChannel(), ws -> {
105 | ws.onConnect(ctx -> {
106 | log.info("{} connected to {}", ctx.host(), printerWebSocketService.getChannel());
107 |
108 | addSocketToChannel(printerWebSocketService.getChannel(), ctx);
109 | });
110 |
111 | ws.onClose(ctx -> {
112 | log.info("{} disconnected from {}", ctx.host(), printerWebSocketService.getChannel());
113 |
114 | removeSocketFromChannel(printerWebSocketService.getChannel(), ctx);
115 | });
116 |
117 | ws.onMessage(ctx -> {
118 | log.info("{} sent message to {}: {}", ctx.host(), printerWebSocketService.getChannel(), ctx.message());
119 |
120 | messageToService("/printer", ctx.message());
121 | });
122 | });
123 |
124 | registerService(printerWebSocketService);
125 | }
126 |
127 | // Add WebSocket Serial Service
128 | Config.Serial serialConfig = config.getSerial();
129 | if (serialConfig.isEnabled()) {
130 | serialConfig.getMappings().forEach(mapping -> {
131 | try {
132 | log.info("Starting SerialWebSocketService: {}", mapping.toString());
133 | SerialWebSocketService serialWebSocketService = new SerialWebSocketService(mapping);
134 | serialWebSocketService.start();
135 |
136 | registerService(serialWebSocketService);
137 |
138 | javalinServer.ws(serialWebSocketService.getChannel(), ws -> {
139 | ws.onConnect(ctx -> {
140 | log.info("{} connected to {}", ctx.host(), serialWebSocketService.getChannel());
141 |
142 | addSocketToChannel(serialWebSocketService.getChannel(), ctx);
143 | });
144 |
145 | ws.onClose(ctx -> {
146 | log.info("{} disconnected from {}", ctx.host(), serialWebSocketService.getChannel());
147 |
148 | removeSocketFromChannel(serialWebSocketService.getChannel(), ctx);
149 | });
150 |
151 | ws.onMessage(ctx -> {
152 | log.info("{} sent message to {}: {}", ctx.host(), serialWebSocketService.getChannel(), ctx.message());
153 |
154 | messageToService(serialWebSocketService.getChannel(), ctx.message());
155 | });
156 |
157 | ws.onBinaryMessage(ctx -> {
158 | log.info("{} sent binary message to {}: {}", ctx.host(), serialWebSocketService.getChannel(), ctx.data());
159 |
160 | messageToService(serialWebSocketService.getChannel(), ctx.data());
161 | });
162 | });
163 | } catch (Exception e) {
164 | String message = "Failed to start SerialWebSocketService for " + mapping.getType() + ": " + e.getMessage();
165 | log.error(message);
166 |
167 | try {
168 | messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("ERROR", "Serial", message)));
169 | } catch (JsonProcessingException ex) {
170 | log.error("Failed to send notification: {}", ex.getMessage());
171 | }
172 | }
173 | });
174 | }
175 |
176 | // Add HTTP Auth
177 | javalinServer.before(ctx -> {
178 | if (serverConfig.getAuthentication().isEnabled()) {
179 | try {
180 | // Bearer Token
181 | if (Optional.ofNullable(ctx.header("Authorization")).orElse("").endsWith(serverConfig.getAuthentication().getToken())) {
182 | return;
183 | }
184 |
185 | // Basic Auth
186 | if (ctx.basicAuthCredentials() != null && Objects.equals(ctx.basicAuthCredentials().getPassword(), serverConfig.getAuthentication().getToken())) {
187 | return;
188 | }
189 | } catch (Exception e) {
190 | // NOOP
191 | }
192 |
193 | ctx.header("WWW-Authenticate", "Basic realm=\"Token required\"");
194 | ctx.res().sendError(401, "Token mismatch");
195 | }
196 | });
197 |
198 | // Add HTTP Service
199 | javalinServer.get("/config.json", ctx -> {
200 | ctx.contentType(ContentType.APPLICATION_JSON).result(configService.getConfig().toJson());
201 | });
202 |
203 | javalinServer.put("/config.json", ctx -> {
204 | configService.loadFromJson(ctx.body());
205 | configService.save();
206 |
207 | messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Setting", "Setting saved successfully")));
208 |
209 | ctx.contentType(ContentType.APPLICATION_JSON).result(configService.getConfig().toJson());
210 | });
211 |
212 | javalinServer.get("/system/printers.json", ctx -> {
213 | ArrayList dtos = new ArrayList<>();
214 | for (PrintService service : PrinterJob.lookupPrintServices()) {
215 | dtos.add(new PrintServiceDTO(service.getName(), ""));
216 | }
217 |
218 | ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dtos));
219 | });
220 |
221 | javalinServer.get("/system/serials.json", ctx -> {
222 | ArrayList dtos = new ArrayList<>();
223 | for (SerialPort port : SerialPort.getCommPorts()) {
224 | dtos.add(new SerialPortDTO(port.getSystemPortName(), port.getPortDescription(), port.getManufacturer()));
225 | }
226 |
227 | ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dtos));
228 | });
229 |
230 | javalinServer.get("/system/version.json", ctx -> {
231 | VersionDTO dto = new VersionDTO(Constants.APP_NAME, Constants.APP_ID, Constants.VERSION);
232 |
233 | ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dto));
234 | });
235 |
236 | javalinServer.post("/system/restart.json", ctx -> {
237 | stop();
238 | ThreadUtil.silentSleep(500);
239 | start();
240 |
241 | messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Restart", "Server restarted successfully")));
242 | });
243 |
244 | try {
245 | javalinServer.start(serverConfig.getBind(), serverConfig.getPort());
246 | log.info("{} {} running on {}", Constants.APP_NAME, Constants.VERSION, serverConfig.getUri());
247 | } catch (JavalinBindException e) {
248 | log.info("Unable to bind port, another instance is already running?");
249 | System.exit(1);
250 | }
251 | }
252 |
253 | synchronized public void stop() throws Exception {
254 | for (Iterator it = services.iterator(); it.hasNext(); ) {
255 | WebSocketServiceInterface service = it.next();
256 | unregisterService(service);
257 | service.stop();
258 | it.remove();
259 | }
260 |
261 | javalinServer.stop();
262 | }
263 |
264 | /*
265 | * Service to Server listener
266 | */
267 | @Override
268 | public void messageToServer(String channel, String message) {
269 | log.debug("Received data from channel: {}, Data: {}", channel, message);
270 |
271 | ConcurrentLinkedQueue connectionList = socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());
272 |
273 | for (Iterator it = connectionList.iterator(); it.hasNext(); ) {
274 | try {
275 | WsContext conn = it.next();
276 | conn.send(message);
277 | } catch (Exception e) {
278 | log.warn("Exception {}: {}, removing connection from list", e.getClass().getSimpleName(), e.getMessage());
279 | it.remove();
280 | }
281 | }
282 | }
283 |
284 | @Override
285 | public void messageToServer(String channel, byte[] message) {
286 | log.debug("Received data from channel: {}, Data: {}", channel, message);
287 |
288 | ConcurrentLinkedQueue connectionList = socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());
289 |
290 | for (Iterator it = connectionList.iterator(); it.hasNext(); ) {
291 | WsContext conn = it.next();
292 | try {
293 | conn.send(ByteBuffer.wrap(message));
294 | } catch (Exception e) {
295 | log.warn("Exception: Removing connection from list");
296 | it.remove();
297 | }
298 | }
299 | }
300 |
301 | /*
302 | * Service to Service listener
303 | */
304 | @Override
305 | public void messageToService(String channel, String message) {
306 | ConcurrentLinkedQueue services = getServicesForChannel(channel);
307 | for (WebSocketServiceInterface service : services) {
308 | log.debug("Sending: {} to channel: {}, service = {}", message, channel, service.getClass().getSimpleName());
309 |
310 | service.messageToService(message);
311 | }
312 | }
313 |
314 | @Override
315 | public void messageToService(String channel, byte[] bytes) {
316 | ConcurrentLinkedQueue services = getServicesForChannel(channel);
317 | for (WebSocketServiceInterface service : services) {
318 | log.debug("Sending: {} to channel: {}, service = {}", bytes, channel, service.getClass().getSimpleName());
319 |
320 | service.messageToService(bytes);
321 | }
322 | }
323 |
324 | @Override
325 | public void registerService(WebSocketServiceInterface service) {
326 | service.onRegister(this);
327 | addServiceToChannel(service.getChannel(), service);
328 | }
329 |
330 | @Override
331 | public void unregisterService(WebSocketServiceInterface service) {
332 | service.onUnregister();
333 | removeServiceFromChannel(service.getChannel(), service);
334 | }
335 |
336 | /*
337 | * Socket to Channel operations
338 | */
339 | private ConcurrentLinkedQueue getSocketsForChannel(String channel) {
340 | return socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());
341 | }
342 |
343 | void addSocketToChannel(String channel, WsContext socket) {
344 | ConcurrentLinkedQueue connectionList = getSocketsForChannel(channel);
345 | connectionList.add(socket);
346 | socketChannelSubscriptions.put(channel, connectionList);
347 | }
348 |
349 | private void removeSocketFromChannel(String channel, WsContext socket) {
350 | ConcurrentLinkedQueue connectionList = getSocketsForChannel(channel);
351 | connectionList.remove(socket);
352 | socketChannelSubscriptions.put(channel, connectionList);
353 | }
354 |
355 | /*
356 | * Service to Channel operations
357 | */
358 | private ConcurrentLinkedQueue getServicesForChannel(String channel) {
359 | ConcurrentLinkedQueue services = new ConcurrentLinkedQueue<>();
360 |
361 | services.addAll(serviceChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>()));
362 | services.addAll(serviceChannelSubscriptions.getOrDefault("*", new ConcurrentLinkedQueue<>()));
363 |
364 | return services;
365 | }
366 |
367 | private void addServiceToChannel(String channel, WebSocketServiceInterface service) {
368 | ConcurrentLinkedQueue serviceList = serviceChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());
369 |
370 | serviceList.add(service);
371 | serviceChannelSubscriptions.put(channel, serviceList);
372 |
373 | if (!services.contains(service)) {
374 | services.add(service);
375 | }
376 | }
377 |
378 | private void removeServiceFromChannel(String channel, WebSocketServiceInterface service) {
379 | ConcurrentLinkedQueue serviceList = getServicesForChannel(channel);
380 | serviceList.remove(service);
381 | serviceChannelSubscriptions.put(channel, serviceList);
382 |
383 | services.remove(service);
384 | }
385 | }
386 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/dtos/Config.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.dtos;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import com.fasterxml.jackson.core.JsonProcessingException;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Data;
8 | import lombok.NoArgsConstructor;
9 |
10 | import java.nio.charset.StandardCharsets;
11 | import java.util.ArrayList;
12 |
13 | @Data
14 | @NoArgsConstructor
15 | public class Config {
16 | private GUI gui = new GUI();
17 | private Server server = new Server();
18 | private Downloader downloader = new Downloader();
19 | private Printer printer = new Printer();
20 | private Serial serial = new Serial();
21 |
22 | public String toJson() throws JsonProcessingException {
23 | return new ObjectMapper().writeValueAsString(this);
24 | }
25 |
26 | @Data
27 | @NoArgsConstructor
28 | public static class GUI {
29 | private Notification notification = new Notification();
30 | }
31 |
32 | @Data
33 | @NoArgsConstructor
34 | public static class Notification {
35 | private boolean enabled = true;
36 | }
37 |
38 | @Data
39 | @NoArgsConstructor
40 | public static class Server {
41 | private String address = "127.0.0.1";
42 | private String bind = "127.0.0.1";
43 | private int port = 12212;
44 | private Authentication authentication = new Authentication();
45 | private TLS tls = new TLS();
46 |
47 | @JsonIgnore
48 | public String getUri() {
49 | return (tls.isEnabled() ? "https://" : "http://") + address + ":" + port;
50 | }
51 | }
52 |
53 | @Data
54 | @NoArgsConstructor
55 | public static class Authentication {
56 | private boolean enabled = false;
57 | private String token = null;
58 | }
59 |
60 | @Data
61 | @NoArgsConstructor
62 | public static class TLS {
63 | private boolean enabled = false ;
64 | private boolean selfSigned = true;
65 | private String cert = "tls/default-cert.pem";
66 | private String key = "tls/default-key.pem";
67 | private String caBundle = null;
68 | }
69 |
70 | @Data
71 | @NoArgsConstructor
72 | public static class Downloader {
73 | private boolean ignoreTLSCertificateError = false;
74 | private double timeout = 30;
75 | private String path = "downloads";
76 | }
77 |
78 | @Data
79 | @NoArgsConstructor
80 | public static class Printer {
81 | private boolean enabled = true;
82 | private boolean autoAddUnknownType = false;
83 | private boolean fallbackToDefault = false;
84 | private ArrayList mappings = new ArrayList<>();
85 | }
86 |
87 | @Data
88 | @NoArgsConstructor
89 | public static class Serial {
90 | private boolean enabled = true;
91 | private ArrayList mappings = new ArrayList<>();
92 | }
93 |
94 | @Data
95 | @NoArgsConstructor
96 | @AllArgsConstructor
97 | public static class PrinterMapping {
98 | private String type;
99 | private String name;
100 |
101 | private boolean autoRotate = false;
102 | private boolean resetImageableArea = true;
103 | private int forceDPI = 0;
104 | }
105 |
106 | @Data
107 | @NoArgsConstructor
108 | @AllArgsConstructor
109 | public static class SerialMapping {
110 | private String type;
111 | private String name;
112 |
113 | private Integer baudRate;
114 | private Integer numDataBits;
115 | private Integer numStopBits;
116 | private Integer parity;
117 |
118 | private Boolean readMultipleBytes = false;
119 | private String readCharset = StandardCharsets.UTF_8.toString();
120 | }
121 | }
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/dtos/NotificationDTO.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.dtos;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @Getter
8 | @NoArgsConstructor
9 | @AllArgsConstructor
10 | public class NotificationDTO {
11 | public String type;
12 | public String title;
13 | public String message;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/dtos/PrintServiceDTO.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.dtos;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.NoArgsConstructor;
5 |
6 | @NoArgsConstructor
7 | @AllArgsConstructor
8 | public class PrintServiceDTO {
9 | public String name;
10 | public String description;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/dtos/SerialPortDTO.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.dtos;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.NoArgsConstructor;
5 |
6 | @NoArgsConstructor
7 | @AllArgsConstructor
8 | public class SerialPortDTO {
9 | public String name;
10 | public String description;
11 | public String manufacturer;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/dtos/VersionDTO.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.dtos;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.NoArgsConstructor;
5 |
6 | @NoArgsConstructor
7 | @AllArgsConstructor
8 | public class VersionDTO {
9 | public String appName;
10 | public String appId;
11 | public String version;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServerInterface.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.interfaces;
2 |
3 |
4 | public interface WebSocketServerInterface {
5 | void messageToServer(String channel, String message);
6 |
7 | void messageToServer(String channel, byte[] message);
8 |
9 | void messageToService(String channel, String message);
10 |
11 | void messageToService(String channel, byte[] message);
12 |
13 | void registerService(WebSocketServiceInterface service);
14 |
15 | void unregisterService(WebSocketServiceInterface service);
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServiceInterface.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.interfaces;
2 |
3 | public interface WebSocketServiceInterface {
4 | void start();
5 |
6 | void stop();
7 |
8 | void messageToService(String message);
9 |
10 | void messageToService(byte[] message);
11 |
12 | void onRegister(WebSocketServerInterface server);
13 |
14 | void onUnregister();
15 |
16 | String getChannel();
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintDocument.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.responses;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import lombok.Getter;
5 | import lombok.ToString;
6 | import tigerworkshop.webapphardwarebridge.utils.AnnotatedPrintable;
7 |
8 | import java.util.ArrayList;
9 | import java.util.UUID;
10 |
11 | @ToString
12 | @Getter
13 | public class PrintDocument {
14 | String type;
15 | String url;
16 | String id;
17 | UUID uuid = UUID.randomUUID();
18 | Integer qty = 1;
19 | @JsonProperty("file_content") String fileContent;
20 | @JsonProperty("raw_content") String rawContent;
21 | ArrayList extras = new ArrayList<>();
22 | }
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintResult.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.responses;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.NoArgsConstructor;
5 | import lombok.ToString;
6 |
7 | @ToString
8 | @NoArgsConstructor
9 | @AllArgsConstructor
10 | public class PrintResult {
11 | public Boolean success;
12 | public String message;
13 | public String id;
14 | public String printerName;
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/services/ConfigService.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.services;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.DeserializationFeature;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import lombok.Getter;
7 | import lombok.extern.log4j.Log4j2;
8 | import tigerworkshop.webapphardwarebridge.dtos.Config;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 |
13 | @Log4j2
14 | public class ConfigService {
15 | @Getter
16 | private static final ConfigService instance = new ConfigService();
17 |
18 | private static final String CONFIG_FILENAME = "config.json";
19 | private static final String PRINTER_PLACEHOLDER = "";
20 |
21 | private final ObjectMapper objectMapper = new ObjectMapper()
22 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
23 |
24 | @Getter
25 | private Config config = new Config();
26 |
27 | private ConfigService() {
28 | try {
29 | loadFromFile(CONFIG_FILENAME);
30 | } catch (Exception e) {
31 | log.warn("Failed loading config, creating new file");
32 | save();
33 | }
34 | }
35 |
36 | public void loadFromJson(String json) throws JsonProcessingException {
37 | log.info("Loading config from JSON: {}", json);
38 | config = objectMapper.readValue(json, Config.class);
39 | }
40 |
41 | public void loadFromFile(String filename) throws IOException {
42 | log.info("Loading config from file: {}", filename);
43 | config = objectMapper.readValue(new File(filename), Config.class);
44 | }
45 |
46 | public void save() {
47 | try {
48 | objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(CONFIG_FILENAME), config);
49 | } catch (Exception e) {
50 | log.error("Failed to save config file", e);
51 | }
52 | }
53 |
54 | public void addPrintTypeToList(String printType) {
55 | config.getPrinter().getMappings().add(new Config.PrinterMapping(printType, PRINTER_PLACEHOLDER, false, true, 0));
56 | save();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/services/DocumentService.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.services;
2 |
3 | import lombok.Getter;
4 | import lombok.extern.log4j.Log4j2;
5 | import org.apache.commons.io.FileUtils;
6 | import org.apache.commons.io.FilenameUtils;
7 | import tigerworkshop.webapphardwarebridge.dtos.Config;
8 | import tigerworkshop.webapphardwarebridge.responses.PrintDocument;
9 |
10 | import javax.net.ssl.HttpsURLConnection;
11 | import javax.net.ssl.SSLContext;
12 | import javax.net.ssl.TrustManager;
13 | import javax.net.ssl.X509TrustManager;
14 | import java.io.File;
15 | import java.io.IOException;
16 | import java.net.HttpURLConnection;
17 | import java.net.MalformedURLException;
18 | import java.net.URL;
19 | import java.net.URLConnection;
20 | import java.nio.file.Files;
21 | import java.security.cert.X509Certificate;
22 | import java.util.Base64;
23 |
24 | @Log4j2
25 | public class DocumentService {
26 | @Getter
27 | private static final DocumentService instance = new DocumentService();
28 | private static final Config.Downloader downloaderConfig = ConfigService.getInstance().getConfig().getDownloader();
29 |
30 | public File prepareDocument(PrintDocument printDocument) throws Exception {
31 | FileUtils.forceMkdir(new File(downloaderConfig.getPath()));
32 |
33 | if (printDocument.getUrl() == null && printDocument.getFileContent() == null) {
34 | throw new Exception("Both URL and File Content are null");
35 | }
36 |
37 | File output = getOutputFile(printDocument);
38 | if (printDocument.getFileContent() != null) {
39 | byte[] bytes = Base64.getDecoder().decode(printDocument.getFileContent());
40 | Files.write(output.toPath(), bytes);
41 | } else {
42 | URL url = new URL(printDocument.getUrl());
43 | download(url, getOutputFile(printDocument));
44 | }
45 |
46 | return output;
47 | }
48 |
49 | public void deleteDocument(PrintDocument printDocument) throws IOException {
50 | FileUtils.deleteQuietly(getOutputFile(printDocument));
51 | }
52 |
53 | private File getOutputFile(PrintDocument printDocument) throws MalformedURLException {
54 | File output;
55 | if (printDocument.getFileContent() != null) {
56 | output = new File(downloaderConfig.getPath() + "/" + printDocument.getUuid() + "-" + printDocument.getUrl());
57 | } else {
58 | URL url = new URL(printDocument.getUrl());
59 | output = new File(downloaderConfig.getPath() + "/" + printDocument.getUuid() + "-" + FilenameUtils.getName(url.getPath()));
60 | }
61 | return output;
62 | }
63 |
64 | private void download(URL url, File outputFile) throws Exception {
65 | log.info("Downloading file from: {}", url);
66 |
67 | long timeStart = System.currentTimeMillis();
68 |
69 | if (downloaderConfig.isIgnoreTLSCertificateError()) {
70 | TrustManager[] trustAllCerts = new TrustManager[]{
71 | new X509TrustManager() {
72 | public X509Certificate[] getAcceptedIssuers() {
73 | return null;
74 | }
75 |
76 | public void checkClientTrusted(X509Certificate[] certs, String authType) {
77 | }
78 |
79 | public void checkServerTrusted(X509Certificate[] certs, String authType) {
80 | }
81 |
82 | }
83 | };
84 |
85 | SSLContext sc = SSLContext.getInstance("SSL");
86 | sc.init(null, trustAllCerts, new java.security.SecureRandom());
87 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
88 | }
89 |
90 | URLConnection urlConnection = url.openConnection();
91 | urlConnection.setConnectTimeout((int) downloaderConfig.getTimeout() * 1000);
92 | urlConnection.setReadTimeout((int) downloaderConfig.getTimeout() * 1000);
93 | urlConnection.connect();
94 |
95 | int contentLength = urlConnection.getContentLength();
96 | int responseCode;
97 | if (urlConnection instanceof HttpsURLConnection) {
98 | responseCode = ((HttpsURLConnection) urlConnection).getResponseCode();
99 | } else {
100 | responseCode = ((HttpURLConnection) urlConnection).getResponseCode();
101 | }
102 |
103 | log.trace("Content Length: {}", contentLength);
104 | log.trace("Response Code: {}", responseCode);
105 |
106 | // Status code mismatch
107 | if (responseCode != 200) {
108 | throw new IOException("HTTP Status Code: " + responseCode);
109 | }
110 |
111 | FileUtils.copyInputStreamToFile(urlConnection.getInputStream(), outputFile);
112 |
113 | long timeFinish = System.currentTimeMillis();
114 | log.info("File {} downloaded in {} ms", outputFile.getName(), timeFinish - timeStart);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/utils/AnnotatedPrintable.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.utils;
2 |
3 | import lombok.Data;
4 | import lombok.extern.log4j.Log4j2;
5 |
6 | import java.awt.*;
7 | import java.awt.geom.AffineTransform;
8 | import java.awt.print.PageFormat;
9 | import java.awt.print.Printable;
10 | import java.awt.print.PrinterException;
11 | import java.util.ArrayList;
12 |
13 | @Log4j2
14 | public class AnnotatedPrintable implements Printable {
15 | private final Printable printable;
16 | private final ArrayList annotatedPrintableAnnotationArrayList = new ArrayList<>();
17 |
18 | private static final Double MM_TO_PPI = 2.8346457;
19 |
20 | public AnnotatedPrintable(Printable printable) {
21 | this.printable = printable;
22 | }
23 |
24 | public void addAnnotation(AnnotatedPrintableAnnotation annotatedPrintableAnnotation) {
25 | annotatedPrintableAnnotationArrayList.add(annotatedPrintableAnnotation);
26 | }
27 |
28 | @Override
29 | public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
30 | int result = printable.print(graphics, pageFormat, pageIndex);
31 |
32 | if (annotatedPrintableAnnotationArrayList.isEmpty()) {
33 | return result;
34 | }
35 |
36 | if (result == PAGE_EXISTS) {
37 | Graphics2D graphics2D = (Graphics2D) graphics;
38 |
39 | // On Windows we need getDefaultTransform() to print in correct scale
40 | // But on Mac it causes NullPointerException, however a blank AffineTransform works
41 | try {
42 | graphics2D.setTransform(graphics2D.getDeviceConfiguration().getDefaultTransform());
43 | } catch (Exception e) {
44 | graphics2D.setTransform(new AffineTransform());
45 | }
46 |
47 | float clipX = (float) graphics2D.getClipBounds().getX();
48 | float clipY = (float) graphics2D.getClipBounds().getY();
49 |
50 | // Catch Exceptions otherwise blank page occur while exceptions silently handled
51 | try {
52 | for (AnnotatedPrintableAnnotation annotatedPrintableAnnotation : annotatedPrintableAnnotationArrayList) {
53 | if (annotatedPrintableAnnotation.getText() == null) {
54 | log.warn("annotatedPrintableAnnotation.getText() is null");
55 | continue;
56 | }
57 |
58 | float realX = (float) (clipX + annotatedPrintableAnnotation.getX() * MM_TO_PPI);
59 | float realY = (float) (clipY + annotatedPrintableAnnotation.getY() * MM_TO_PPI);
60 |
61 | int isBold = annotatedPrintableAnnotation.getBold() != null ? Font.BOLD : Font.PLAIN;
62 | int fontSize = annotatedPrintableAnnotation.getSize() != null ? annotatedPrintableAnnotation.getSize() : 10;
63 |
64 | Font font = new Font("Sans-Serif", isBold, fontSize);
65 | graphics2D.setColor(Color.BLACK);
66 | graphics2D.setFont(font);
67 | graphics2D.drawString(annotatedPrintableAnnotation.getText(), realX, realY);
68 | }
69 | } catch (Exception e) {
70 | log.error(e.getMessage(), e);
71 | }
72 |
73 | }
74 |
75 | return result;
76 | }
77 |
78 | @Data
79 | public static class AnnotatedPrintableAnnotation {
80 | private String text;
81 | private Float x;
82 | private Float y;
83 | private Integer size;
84 | private Boolean bold;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/utils/CertificateGenerator.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.utils;
2 |
3 | import lombok.extern.log4j.Log4j2;
4 | import org.bouncycastle.asn1.x500.X500Name;
5 | import org.bouncycastle.asn1.x509.Extension;
6 | import org.bouncycastle.asn1.x509.GeneralName;
7 | import org.bouncycastle.asn1.x509.GeneralNames;
8 | import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
9 | import org.bouncycastle.cert.X509CertificateHolder;
10 | import org.bouncycastle.cert.X509v3CertificateBuilder;
11 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
12 | import org.bouncycastle.jce.provider.BouncyCastleProvider;
13 | import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
14 | import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
15 | import org.bouncycastle.operator.ContentSigner;
16 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
17 |
18 | import java.io.File;
19 | import java.io.FileWriter;
20 | import java.io.IOException;
21 | import java.math.BigInteger;
22 | import java.security.*;
23 | import java.security.cert.X509Certificate;
24 | import java.util.Date;
25 | import java.util.regex.Pattern;
26 |
27 | @Log4j2
28 | public class CertificateGenerator {
29 | private static final String CERTIFICATE_ALGORITHM = "RSA";
30 | private static final String CERTIFICATE_ISSUER = "CN=127.0.0.1";
31 | private static final String CERTIFICATE_DOMAIN = "CN=127.0.0.1";
32 | private static final int CERTIFICATE_BITS = 2048;
33 |
34 | private static final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
35 | private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);
36 |
37 | public static void generateSelfSignedCertificate(String address, String certificatePath, String keyPath) {
38 | Security.addProvider(new BouncyCastleProvider());
39 |
40 | if (!isCertificateAndKeyExist(certificatePath, keyPath)) {
41 | try {
42 | log.info("Certificate or private key does not exist, attempt to generate.");
43 |
44 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM);
45 | keyPairGenerator.initialize(CERTIFICATE_BITS, new SecureRandom());
46 | KeyPair keyPair = keyPairGenerator.generateKeyPair();
47 |
48 | X500Name issuer = new X500Name(CERTIFICATE_ISSUER);
49 | X500Name subject = new X500Name(CERTIFICATE_DOMAIN);
50 | BigInteger serialNumber = new BigInteger(64, new SecureRandom());
51 | Date validFrom = new Date();
52 | Date validTo = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10));
53 | SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
54 | ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(new BouncyCastleProvider()).build(keyPair.getPrivate());
55 |
56 | X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuer, serialNumber, validFrom, validTo, subject, subPubKeyInfo);
57 |
58 | final GeneralNames subjectAltNames;
59 | if (IPV4_PATTERN.matcher(address).matches()) {
60 | subjectAltNames = new GeneralNames(new GeneralName(GeneralName.iPAddress, address));
61 | } else {
62 | subjectAltNames = new GeneralNames(new GeneralName(GeneralName.dNSName, address));
63 | }
64 | certificateBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
65 |
66 | X509CertificateHolder certificateHolder = certificateBuilder.build(signer);
67 | X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
68 |
69 | log.info("Certificate and private key generated.");
70 |
71 | File directory = new File("tls");
72 | if (!directory.isDirectory()) {
73 | directory.mkdir();
74 | }
75 |
76 | saveCert(cert, certificatePath);
77 | saveKey(keyPair.getPrivate(), keyPath);
78 | } catch (Exception e) {
79 | log.error(e.getMessage(), e);
80 | }
81 | } else {
82 | log.info("Certificate and private key already exists.");
83 | }
84 | }
85 |
86 | public static Boolean isCertificateAndKeyExist(String certificatePath, String keyPath) {
87 | File certificate = new File(certificatePath);
88 | File privateKey = new File(keyPath);
89 |
90 | return certificate.exists() && privateKey.exists();
91 | }
92 |
93 | private static void saveCert(X509Certificate cert, String certificatePath) {
94 | try {
95 | JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(certificatePath));
96 | writer.writeObject(cert);
97 | writer.close();
98 | } catch (IOException e) {
99 | log.error(e.getMessage(), e);
100 | }
101 | }
102 |
103 | private static void saveKey(PrivateKey key, String keyPath) {
104 | try {
105 | JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(keyPath));
106 | writer.writeObject(new JcaPKCS8Generator(key, null));
107 | writer.close();
108 | } catch (IOException e) {
109 | log.error(e.getMessage(), e);
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/utils/ImagePrintable.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.utils;
2 |
3 | import java.awt.*;
4 | import java.awt.print.PageFormat;
5 | import java.awt.print.Printable;
6 |
7 | public class ImagePrintable implements Printable {
8 | private final Image image;
9 |
10 | public ImagePrintable(Image image) {
11 | this.image = image;
12 | }
13 |
14 | public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) {
15 | if (pageIndex >= 1) {
16 | return Printable.NO_SUCH_PAGE;
17 | }
18 |
19 | Graphics2D g2d = (Graphics2D) graphics;
20 | g2d.translate((int) pageFormat.getImageableX(), (int) pageFormat.getImageableY());
21 |
22 | double width = pageFormat.getImageableWidth();
23 | double height = pageFormat.getImageableHeight();
24 |
25 | g2d.drawImage(image, 0, 0, (int) width, (int) height, null, null);
26 |
27 | return Printable.PAGE_EXISTS;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/utils/ThreadUtil.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.utils;
2 |
3 | public class ThreadUtil {
4 | public static void silentSleep(long duration) {
5 | try {
6 | Thread.sleep(duration);
7 | } catch (Exception ignored) {
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/PrinterWebSocketService.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.websocketservices;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.extern.log4j.Log4j2;
7 | import org.apache.commons.codec.binary.Base64;
8 | import org.apache.commons.io.FilenameUtils;
9 | import org.apache.pdfbox.pdmodel.PDDocument;
10 | import org.apache.pdfbox.printing.PDFPrintable;
11 | import org.apache.pdfbox.printing.Scaling;
12 | import tigerworkshop.webapphardwarebridge.dtos.Config;
13 | import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
14 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
15 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
16 | import tigerworkshop.webapphardwarebridge.responses.PrintDocument;
17 | import tigerworkshop.webapphardwarebridge.responses.PrintResult;
18 | import tigerworkshop.webapphardwarebridge.services.ConfigService;
19 | import tigerworkshop.webapphardwarebridge.services.DocumentService;
20 | import tigerworkshop.webapphardwarebridge.utils.AnnotatedPrintable;
21 | import tigerworkshop.webapphardwarebridge.utils.ImagePrintable;
22 |
23 | import javax.imageio.ImageIO;
24 | import javax.print.*;
25 | import java.awt.*;
26 | import java.awt.print.*;
27 | import java.io.File;
28 | import java.util.Optional;
29 |
30 | @Log4j2
31 | public class PrinterWebSocketService implements WebSocketServiceInterface {
32 | private WebSocketServerInterface server;
33 |
34 | private static final ConfigService configService = ConfigService.getInstance();
35 | private static final DocumentService documentService = DocumentService.getInstance();
36 | private static final ObjectMapper objectMapper = new ObjectMapper();
37 |
38 | public PrinterWebSocketService() {
39 | log.info("Starting PrinterWebSocketService");
40 | }
41 |
42 | @Override
43 | public void start() {
44 |
45 | }
46 |
47 | @Override
48 | public void stop() {
49 |
50 | }
51 |
52 | @Override
53 | public void messageToService(String message) {
54 | try {
55 | PrintDocument printDocument = objectMapper.readValue(message, PrintDocument.class);
56 | printDocument(printDocument);
57 | } catch (Exception e) {
58 | log.error(e.getMessage(), e);
59 | }
60 | }
61 |
62 | @Override
63 | public void messageToService(byte[] message) {
64 | log.error("PrinterWebSocketService onDataReceived: binary data not supported");
65 | }
66 |
67 | @Override
68 | public void onRegister(WebSocketServerInterface server) {
69 | this.server = server;
70 | }
71 |
72 | @Override
73 | public void onUnregister() {
74 | this.server = null;
75 | }
76 |
77 | @Override
78 | public String getChannel() {
79 | return "/printer";
80 | }
81 |
82 | /**
83 | * Prints a PrintDocument
84 | */
85 | public void printDocument(PrintDocument printDocument) throws Exception {
86 | log.info("Printing Document {}, {}", printDocument.getType(), printDocument.getUrl());
87 |
88 | PrinterSearchResult printerSearchResult = null;
89 | try {
90 | printerSearchResult = searchPrinterForType(printDocument.getType());
91 |
92 | server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Printing " + printDocument.getType(), printDocument.getUrl())));
93 |
94 | if (isRaw(printDocument)) {
95 | printRaw(printDocument, printerSearchResult);
96 | } else if (isImage(printDocument)) {
97 | printImage(printDocument, printerSearchResult);
98 | } else if (isPDF(printDocument)) {
99 | printPDF(printDocument, printerSearchResult);
100 | } else {
101 | throw new Exception("Unknown file type: " + printDocument.getUrl());
102 | }
103 |
104 | server.messageToServer(getChannel(), objectMapper.writeValueAsString(new PrintResult(true, "Success", printDocument.getId(), printerSearchResult.getName())));
105 | } catch (Exception e) {
106 | String errorMessage = e.getMessage();
107 |
108 | if (e instanceof PrinterAbortException) {
109 | errorMessage = "Printing aborted";
110 | }
111 |
112 | log.error("Print Error: {}, {}", e.getClass().getName(), errorMessage);
113 |
114 | if (!isRaw(printDocument)) {
115 | log.error("Print Error: Deleting downloaded document");
116 | documentService.deleteDocument(printDocument);
117 | }
118 |
119 | server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("ERROR", "Print Error " + printDocument.getType(), errorMessage)));
120 |
121 | server.messageToServer(getChannel(), objectMapper.writeValueAsString(new PrintResult(false, errorMessage, printDocument.getId(), printerSearchResult != null ? printerSearchResult.getName() : null)));
122 | }
123 | }
124 |
125 | /**
126 | * Return if PrintDocument is raw
127 | */
128 | private Boolean isRaw(PrintDocument printDocument) {
129 | return printDocument.getRawContent() != null && !printDocument.getRawContent().isEmpty();
130 | }
131 |
132 | /**
133 | * Return if PrintDocument is image
134 | */
135 | private Boolean isImage(PrintDocument printDocument) {
136 | String filename = FilenameUtils.getName(printDocument.getUrl());
137 |
138 | return filename.matches("^.*\\.(jpg|jpeg|png|gif)$");
139 | }
140 |
141 | /**
142 | * Return if PrintDocument is PDF
143 | */
144 | private Boolean isPDF(PrintDocument printDocument) {
145 | String filename = FilenameUtils.getName(printDocument.getUrl());
146 |
147 | return filename.matches("^.*\\.(pdf)$");
148 | }
149 |
150 | /**
151 | * Prints raw bytes to specified printer.
152 | */
153 | private void printRaw(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws PrintException {
154 | log.debug("printRaw::{}", printDocument);
155 | long timeStart = System.currentTimeMillis();
156 |
157 | byte[] bytes = Base64.decodeBase64(printDocument.getRawContent());
158 |
159 | DocPrintJob docPrintJob = printerSearchResult.getDocPrintJob();
160 | Doc doc = new SimpleDoc(bytes, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
161 | docPrintJob.print(doc, null);
162 |
163 | long timeFinish = System.currentTimeMillis();
164 | log.info("printRaw finished in {} ms", timeFinish - timeStart);
165 | }
166 |
167 | /**
168 | * Prints image to specified printer.
169 | */
170 | private void printImage(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws Exception {
171 | log.debug("printImage::{}", printDocument);
172 |
173 | File file = documentService.prepareDocument(printDocument);
174 | String path = file.getPath();
175 | String filename = file.getName();
176 |
177 | long timeStart = System.currentTimeMillis();
178 |
179 | PrinterJob job = PrinterJob.getPrinterJob();
180 | job.setPrintService(printerSearchResult.getDocPrintJob().getPrintService());
181 |
182 | PageFormat pageFormat = getPageFormat(job, printerSearchResult);
183 |
184 | Image image = ImageIO.read(new File(path));
185 |
186 | Book book = new Book();
187 | AnnotatedPrintable printable = new AnnotatedPrintable(new ImagePrintable(image));
188 |
189 | for (AnnotatedPrintable.AnnotatedPrintableAnnotation printDocumentExtra : printDocument.getExtras()) {
190 | printable.addAnnotation(printDocumentExtra);
191 | }
192 |
193 | book.append(printable, pageFormat);
194 |
195 | job.setPageable(book);
196 | job.setJobName(filename);
197 | job.setCopies(printDocument.getQty());
198 | job.print();
199 |
200 | long timeFinish = System.currentTimeMillis();
201 |
202 | log.info("printImage {} finished in {} ms", filename, timeFinish - timeStart);
203 | }
204 |
205 | /**
206 | * Prints PDF to specified printer.
207 | */
208 | private void printPDF(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws Exception {
209 | log.debug("printPDF::{}", printDocument);
210 |
211 | File file = documentService.prepareDocument(printDocument);
212 | String path = file.getPath();
213 | String filename = file.getName();
214 |
215 | long timeStart = System.currentTimeMillis();
216 |
217 | DocPrintJob docPrintJob = printerSearchResult.getDocPrintJob();
218 |
219 | PrinterJob job = PrinterJob.getPrinterJob();
220 | job.setPrintService(docPrintJob.getPrintService());
221 |
222 | PageFormat pageFormat = getPageFormat(job, printerSearchResult);
223 |
224 | try (PDDocument document = PDDocument.load(new File(path))) {
225 | Book book = new Book();
226 | for (int i = 0; i < document.getNumberOfPages(); i += 1) {
227 | // Rotate Page Automatically
228 | PageFormat eachPageFormat = (PageFormat) pageFormat.clone();
229 |
230 | if (printerSearchResult.getMapping().isAutoRotate()) {
231 | if (document.getPage(i).getCropBox().getWidth() > document.getPage(i).getCropBox().getHeight()) {
232 | log.debug("Auto rotation result: LANDSCAPE");
233 | eachPageFormat.setOrientation(PageFormat.LANDSCAPE);
234 | } else {
235 | log.debug("Auto rotation result: PORTRAIT");
236 | eachPageFormat.setOrientation(PageFormat.PORTRAIT);
237 | }
238 | }
239 |
240 | PDFPrintable pdfPrintable = new PDFPrintable(document, Scaling.SHRINK_TO_FIT, false, printerSearchResult.getMapping().getForceDPI());
241 |
242 | // Annotate Printable
243 | AnnotatedPrintable annotatedPrintable = new AnnotatedPrintable(pdfPrintable);
244 | for (AnnotatedPrintable.AnnotatedPrintableAnnotation printDocumentExtra : printDocument.getExtras()) {
245 | annotatedPrintable.addAnnotation(printDocumentExtra);
246 | }
247 |
248 | book.append(annotatedPrintable, eachPageFormat);
249 | }
250 |
251 | job.setPageable(book);
252 | job.setJobName(filename);
253 | job.setCopies(printDocument.getQty());
254 | job.print();
255 |
256 | long timeFinish = System.currentTimeMillis();
257 |
258 | log.info("printPDF {} finished in {} ms", path, timeFinish - timeStart);
259 | }
260 | }
261 |
262 | private PageFormat getPageFormat(PrinterJob job, PrinterSearchResult printerSearchResult) {
263 | final PageFormat pageFormat = job.defaultPage();
264 |
265 | log.debug("PageFormat Size: {} x {}", pageFormat.getWidth(), pageFormat.getHeight());
266 | log.debug("PageFormat Imageable Size:{} x {}, XY: {}, {}", pageFormat.getImageableWidth(), pageFormat.getImageableHeight(), pageFormat.getImageableX(), pageFormat.getImageableY());
267 | log.debug("Paper Size: {} x {}", pageFormat.getPaper().getWidth(), pageFormat.getPaper().getHeight());
268 | log.debug("Paper Imageable Size: {} x {}, XY: {}, {}", pageFormat.getPaper().getImageableWidth(), pageFormat.getPaper().getImageableHeight(), pageFormat.getPaper().getImageableX(), pageFormat.getPaper().getImageableY());
269 |
270 | // Reset Imageable Area
271 | if (printerSearchResult.getMapping().isResetImageableArea()) {
272 | log.debug("PageFormat reset enabled");
273 | Paper paper = pageFormat.getPaper();
274 | paper.setImageableArea(0, 0, paper.getWidth(), paper.getHeight());
275 | pageFormat.setPaper(paper);
276 | }
277 |
278 | log.debug("Final Paper Size: {} x {}", pageFormat.getPaper().getWidth(), pageFormat.getPaper().getHeight());
279 | log.debug("Final Paper Imageable Size: {} x {}, XY: {}, {}", pageFormat.getPaper().getImageableWidth(), pageFormat.getPaper().getImageableHeight(), pageFormat.getPaper().getImageableX(), pageFormat.getPaper().getImageableY());
280 |
281 | return pageFormat;
282 | }
283 |
284 | /**
285 | * Get PrinterSearchResult for specified type
286 | */
287 | private PrinterSearchResult searchPrinterForType(String type) throws PrinterException {
288 | Optional printerMappingOptional = configService.getConfig().getPrinter().getMappings().stream().filter(it -> it.getType().equals(type)).findFirst();
289 |
290 | if (printerMappingOptional.isPresent()) {
291 | Config.PrinterMapping printerMapping = printerMappingOptional.get();
292 | PrintService[] printServices = PrinterJob.lookupPrintServices();
293 |
294 | for (PrintService printService : printServices) {
295 | if (printService.getName().equalsIgnoreCase(printerMapping.getName())) {
296 | log.info("Sending print job type: {} to printer: {}", type, printService.getName());
297 |
298 | return new PrinterSearchResult(printService.getName(), printerMapping, printService.createPrintJob(), false);
299 | }
300 | }
301 | }
302 |
303 | if (configService.getConfig().getPrinter().isAutoAddUnknownType()) {
304 | // Add unknown type does not already exist
305 | if (configService.getConfig().getPrinter().getMappings().stream().noneMatch(it -> it.getType().equals(type))) {
306 | configService.addPrintTypeToList(type);
307 | }
308 | }
309 |
310 | if (configService.getConfig().getPrinter().isFallbackToDefault()) {
311 | log.info("No mapped print job type: {}, falling back to default printer", type);
312 |
313 | PrintService printService = PrintServiceLookup.lookupDefaultPrintService();
314 |
315 | if (printService == null) {
316 | throw new PrinterException("No default printer found");
317 | }
318 |
319 | return new PrinterSearchResult(printService.getName(), new Config.PrinterMapping(), printService.createPrintJob(), true);
320 | }
321 |
322 | throw new PrinterException("No matched printer: " + type);
323 | }
324 |
325 | @Getter
326 | @AllArgsConstructor
327 | private static class PrinterSearchResult {
328 | private String name;
329 | private Config.PrinterMapping mapping;
330 | private DocPrintJob docPrintJob;
331 | private Boolean isDefault;
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/SerialWebSocketService.java:
--------------------------------------------------------------------------------
1 | package tigerworkshop.webapphardwarebridge.websocketservices;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.fazecast.jSerialComm.SerialPort;
6 | import lombok.extern.log4j.Log4j2;
7 | import org.apache.commons.codec.binary.Hex;
8 | import tigerworkshop.webapphardwarebridge.dtos.Config;
9 | import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
10 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
11 | import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
12 | import tigerworkshop.webapphardwarebridge.utils.ThreadUtil;
13 |
14 | import java.nio.charset.Charset;
15 | import java.util.Objects;
16 |
17 | @Log4j2
18 | public class SerialWebSocketService implements WebSocketServiceInterface {
19 | private WebSocketServerInterface server;
20 |
21 | private static final ObjectMapper objectMapper = new ObjectMapper();
22 |
23 | private final Config.SerialMapping mapping;
24 | private final SerialPort serialPort;
25 | private byte[] writeBuffer = {};
26 |
27 | private Thread readThread;
28 | private Thread writeThread;
29 | private Thread monitorThread;
30 |
31 | private Boolean isRunning = true;
32 |
33 | private static final String BINARY = "BINARY";
34 |
35 | public SerialWebSocketService(Config.SerialMapping newMapping) {
36 | log.info("Starting SerialWebSocketService on {}", newMapping.getName());
37 |
38 | this.mapping = newMapping;
39 |
40 | this.serialPort = SerialPort.getCommPort(newMapping.getName());
41 |
42 | if (mapping.getBaudRate() != null) serialPort.setBaudRate(mapping.getBaudRate());
43 | if (mapping.getNumDataBits() != null) serialPort.setNumDataBits(mapping.getNumDataBits());
44 | if (mapping.getNumStopBits() != null) serialPort.setNumStopBits(mapping.getNumStopBits());
45 | if (mapping.getParity() != null) serialPort.setParity(mapping.getParity());
46 | }
47 |
48 | @Override
49 | public void start() {
50 | isRunning = true;
51 |
52 | readThread = new Thread(() -> {
53 | log.debug("Serial Read Thread started for {}", mapping.getName());
54 |
55 | while (isRunning) {
56 | if (serialPort.isOpen()) {
57 | int bytesAvailable = serialPort.bytesAvailable();
58 | if (bytesAvailable == 0) {
59 | // No data coming from COM portName
60 | ThreadUtil.silentSleep(10);
61 | continue;
62 | } else if (bytesAvailable == -1) {
63 | // Check if portName closed unexpected (e.g. Unplugged)
64 | serialPort.closePort();
65 |
66 | try {
67 | server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("WARNING", "Serial Port", "Serial " + mapping.getName() + "(" + mapping.getType() + ") unplugged")));
68 | } catch (JsonProcessingException e) {
69 | log.error("Failed to send notification: {}", e.getMessage());
70 | }
71 |
72 | log.warn("Serial {} unplugged", mapping.getName());
73 |
74 | continue;
75 | }
76 |
77 | int bytesToRead = mapping.getReadMultipleBytes() ? bytesAvailable : 1;
78 |
79 | byte[] receivedData = new byte[bytesToRead];
80 | serialPort.readBytes(receivedData, bytesToRead);
81 |
82 | if (server != null) {
83 | if (Objects.equals(mapping.getReadCharset(), BINARY)) server.messageToServer(getChannel(), receivedData);
84 | else server.messageToServer(getChannel(), new String(receivedData, Charset.forName(mapping.getReadCharset())));
85 | }
86 | }
87 | }
88 |
89 | log.debug("Serial Read Thread stopped for {}", mapping.getName());
90 | });
91 |
92 | writeThread = new Thread(() -> {
93 | log.debug("Serial Write Thread started for {}", mapping.getName());
94 |
95 | while (isRunning) {
96 | if (serialPort.isOpen()) {
97 | if (writeBuffer.length > 0) {
98 | log.trace("Bytes: {}", Hex.encodeHexString(writeBuffer));
99 |
100 | serialPort.writeBytes(writeBuffer, writeBuffer.length);
101 | writeBuffer = new byte[]{};
102 | }
103 | ThreadUtil.silentSleep(10);
104 | }
105 | }
106 |
107 | log.debug("Serial Write Thread stopped for {}", mapping.getName());
108 | });
109 |
110 | monitorThread = new Thread(() -> {
111 | log.debug("Serial Monitor Thread started for {}", mapping.getName());
112 |
113 | while (isRunning) {
114 | if (serialPort.isOpen()) {
115 | ThreadUtil.silentSleep(1000);
116 | } else {
117 | log.info("Trying to connect to serial @ {}", serialPort.getSystemPortName());
118 | serialPort.openPort(1000);
119 |
120 | if (serialPort.isOpen()) {
121 | log.info("Serial {} is now open", mapping.getName());
122 | }
123 | }
124 | }
125 |
126 | log.debug("Serial Monitor Thread stopped for {}", mapping.getName());
127 | });
128 |
129 | readThread.start();
130 | writeThread.start();
131 | monitorThread.start();
132 | }
133 |
134 | @Override
135 | public void stop() {
136 | log.info("Stopping SerialWebSocketService");
137 |
138 | isRunning = false;
139 |
140 | readThread.interrupt();
141 | writeThread.interrupt();
142 | monitorThread.interrupt();
143 |
144 | serialPort.closePort();
145 |
146 | log.info("Stopped SerialWebSocketService");
147 | }
148 |
149 | @Override
150 | public void messageToService(String message) {
151 | messageToService(message.getBytes());
152 | }
153 |
154 | @Override
155 | public void messageToService(byte[] message) {
156 | writeBuffer = message;
157 | }
158 |
159 | @Override
160 | public void onRegister(WebSocketServerInterface newServer) {
161 | this.server = newServer;
162 | }
163 |
164 | @Override
165 | public void onUnregister() {
166 | this.server = null;
167 | }
168 |
169 | @Override
170 | public String getChannel() {
171 | return "/serial/" + mapping.getType();
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 | Main-Class: tigerworkshop.webapphardwarebridge.GUI
3 | Class-Path: commons-io-2.16.1.jar jetty-security-11.0.21.jar sslcontext-
4 | kickstart-for-pem-8.3.6.jar commons-logging-1.2.jar kotlin-stdlib-jdk7-
5 | 1.9.24.jar kotlin-stdlib-jdk8-1.9.24.jar websocket-jetty-api-11.0.21.ja
6 | r jetty-xml-11.0.21.jar jetty-alpn-server-11.0.21.jar websocket-core-co
7 | mmon-11.0.21.jar log4j-core-2.23.1.jar bcpkix-jdk18on-1.78.1.jar websoc
8 | ket-servlet-11.0.21.jar log4j-api-2.23.1.jar javalin-6.2.0.jar websocke
9 | t-jetty-server-11.0.21.jar jetty-util-11.0.21.jar bcutil-jdk18on-1.78.1
10 | .jar websocket-jetty-common-11.0.21.jar pdfbox-2.0.31.jar jetty-alpn-co
11 | nscrypt-server-11.0.21.jar sslcontext-kickstart-8.3.6.jar http2-server-
12 | 11.0.21.jar bcprov-jdk18on-1.78.1.jar jetty-io-11.0.21.jar kotlin-stdli
13 | b-1.9.24.jar jetty-webapp-11.0.21.jar websocket-core-server-11.0.21.jar
14 | jetty-jakarta-servlet-api-5.0.2.jar sslcontext-kickstart-for-jetty-8.3
15 | .6.jar jetty-server-11.0.21.jar commons-codec-1.17.1.jar jSerialComm-2.
16 | 11.0.jar http2-common-11.0.21.jar jackson-annotations-2.17.2.jar conscr
17 | ypt-openjdk-uber-2.5.2.jar httpcore5-5.2.5.jar jackson-core-2.17.2.jar
18 | slf4j-api-2.0.13.jar annotations-13.0.jar fontbox-2.0.31.jar http2-hpac
19 | k-11.0.21.jar ssl-plugin-6.2.0.jar jetty-alpn-java-server-11.0.21.jar j
20 | etty-servlet-11.0.21.jar log4j-slf4j2-impl-2.23.1.jar jackson-databind-
21 | 2.17.2.jar jetty-http-11.0.21.jar
22 |
23 |
--------------------------------------------------------------------------------
/src/main/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imTigger/webapp-hardware-bridge/c4d7be999ac7021c9f8db01af4b83302a8cd62b8/src/main/resources/icon.png
--------------------------------------------------------------------------------
/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/web/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */
7 | :root {
8 | --bs-blue: #0d6efd;
9 | --bs-indigo: #6610f2;
10 | --bs-purple: #6f42c1;
11 | --bs-pink: #d63384;
12 | --bs-red: #dc3545;
13 | --bs-orange: #fd7e14;
14 | --bs-yellow: #ffc107;
15 | --bs-green: #198754;
16 | --bs-teal: #20c997;
17 | --bs-cyan: #0dcaf0;
18 | --bs-black: #000;
19 | --bs-white: #fff;
20 | --bs-gray: #6c757d;
21 | --bs-gray-dark: #343a40;
22 | --bs-gray-100: #f8f9fa;
23 | --bs-gray-200: #e9ecef;
24 | --bs-gray-300: #dee2e6;
25 | --bs-gray-400: #ced4da;
26 | --bs-gray-500: #adb5bd;
27 | --bs-gray-600: #6c757d;
28 | --bs-gray-700: #495057;
29 | --bs-gray-800: #343a40;
30 | --bs-gray-900: #212529;
31 | --bs-primary: #0d6efd;
32 | --bs-secondary: #6c757d;
33 | --bs-success: #198754;
34 | --bs-info: #0dcaf0;
35 | --bs-warning: #ffc107;
36 | --bs-danger: #dc3545;
37 | --bs-light: #f8f9fa;
38 | --bs-dark: #212529;
39 | --bs-primary-rgb: 13, 110, 253;
40 | --bs-secondary-rgb: 108, 117, 125;
41 | --bs-success-rgb: 25, 135, 84;
42 | --bs-info-rgb: 13, 202, 240;
43 | --bs-warning-rgb: 255, 193, 7;
44 | --bs-danger-rgb: 220, 53, 69;
45 | --bs-light-rgb: 248, 249, 250;
46 | --bs-dark-rgb: 33, 37, 41;
47 | --bs-white-rgb: 255, 255, 255;
48 | --bs-black-rgb: 0, 0, 0;
49 | --bs-body-color-rgb: 33, 37, 41;
50 | --bs-body-bg-rgb: 255, 255, 255;
51 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
52 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
53 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
54 | --bs-body-font-family: var(--bs-font-sans-serif);
55 | --bs-body-font-size: 1rem;
56 | --bs-body-font-weight: 400;
57 | --bs-body-line-height: 1.5;
58 | --bs-body-color: #212529;
59 | --bs-body-bg: #fff;
60 | --bs-border-width: 1px;
61 | --bs-border-style: solid;
62 | --bs-border-color: #dee2e6;
63 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
64 | --bs-border-radius: 0.375rem;
65 | --bs-border-radius-sm: 0.25rem;
66 | --bs-border-radius-lg: 0.5rem;
67 | --bs-border-radius-xl: 1rem;
68 | --bs-border-radius-2xl: 2rem;
69 | --bs-border-radius-pill: 50rem;
70 | --bs-link-color: #0d6efd;
71 | --bs-link-hover-color: #0a58ca;
72 | --bs-code-color: #d63384;
73 | --bs-highlight-bg: #fff3cd;
74 | }
75 |
76 | *,
77 | *::before,
78 | *::after {
79 | box-sizing: border-box;
80 | }
81 |
82 | @media (prefers-reduced-motion: no-preference) {
83 | :root {
84 | scroll-behavior: smooth;
85 | }
86 | }
87 |
88 | body {
89 | margin: 0;
90 | font-family: var(--bs-body-font-family);
91 | font-size: var(--bs-body-font-size);
92 | font-weight: var(--bs-body-font-weight);
93 | line-height: var(--bs-body-line-height);
94 | color: var(--bs-body-color);
95 | text-align: var(--bs-body-text-align);
96 | background-color: var(--bs-body-bg);
97 | -webkit-text-size-adjust: 100%;
98 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
99 | }
100 |
101 | hr {
102 | margin: 1rem 0;
103 | color: inherit;
104 | border: 0;
105 | border-top: 1px solid;
106 | opacity: 0.25;
107 | }
108 |
109 | h6, h5, h4, h3, h2, h1 {
110 | margin-top: 0;
111 | margin-bottom: 0.5rem;
112 | font-weight: 500;
113 | line-height: 1.2;
114 | }
115 |
116 | h1 {
117 | font-size: calc(1.375rem + 1.5vw);
118 | }
119 |
120 | @media (min-width: 1200px) {
121 | h1 {
122 | font-size: 2.5rem;
123 | }
124 | }
125 |
126 | h2 {
127 | font-size: calc(1.325rem + 0.9vw);
128 | }
129 |
130 | @media (min-width: 1200px) {
131 | h2 {
132 | font-size: 2rem;
133 | }
134 | }
135 |
136 | h3 {
137 | font-size: calc(1.3rem + 0.6vw);
138 | }
139 |
140 | @media (min-width: 1200px) {
141 | h3 {
142 | font-size: 1.75rem;
143 | }
144 | }
145 |
146 | h4 {
147 | font-size: calc(1.275rem + 0.3vw);
148 | }
149 |
150 | @media (min-width: 1200px) {
151 | h4 {
152 | font-size: 1.5rem;
153 | }
154 | }
155 |
156 | h5 {
157 | font-size: 1.25rem;
158 | }
159 |
160 | h6 {
161 | font-size: 1rem;
162 | }
163 |
164 | p {
165 | margin-top: 0;
166 | margin-bottom: 1rem;
167 | }
168 |
169 | abbr[title] {
170 | -webkit-text-decoration: underline dotted;
171 | text-decoration: underline dotted;
172 | cursor: help;
173 | -webkit-text-decoration-skip-ink: none;
174 | text-decoration-skip-ink: none;
175 | }
176 |
177 | address {
178 | margin-bottom: 1rem;
179 | font-style: normal;
180 | line-height: inherit;
181 | }
182 |
183 | ol,
184 | ul {
185 | padding-left: 2rem;
186 | }
187 |
188 | ol,
189 | ul,
190 | dl {
191 | margin-top: 0;
192 | margin-bottom: 1rem;
193 | }
194 |
195 | ol ol,
196 | ul ul,
197 | ol ul,
198 | ul ol {
199 | margin-bottom: 0;
200 | }
201 |
202 | dt {
203 | font-weight: 700;
204 | }
205 |
206 | dd {
207 | margin-bottom: 0.5rem;
208 | margin-left: 0;
209 | }
210 |
211 | blockquote {
212 | margin: 0 0 1rem;
213 | }
214 |
215 | b,
216 | strong {
217 | font-weight: bolder;
218 | }
219 |
220 | small {
221 | font-size: 0.875em;
222 | }
223 |
224 | mark {
225 | padding: 0.1875em;
226 | background-color: var(--bs-highlight-bg);
227 | }
228 |
229 | sub,
230 | sup {
231 | position: relative;
232 | font-size: 0.75em;
233 | line-height: 0;
234 | vertical-align: baseline;
235 | }
236 |
237 | sub {
238 | bottom: -0.25em;
239 | }
240 |
241 | sup {
242 | top: -0.5em;
243 | }
244 |
245 | a {
246 | color: var(--bs-link-color);
247 | text-decoration: underline;
248 | }
249 |
250 | a:hover {
251 | color: var(--bs-link-hover-color);
252 | }
253 |
254 | a:not([href]):not([class]), a:not([href]):not([class]):hover {
255 | color: inherit;
256 | text-decoration: none;
257 | }
258 |
259 | pre,
260 | code,
261 | kbd,
262 | samp {
263 | font-family: var(--bs-font-monospace);
264 | font-size: 1em;
265 | }
266 |
267 | pre {
268 | display: block;
269 | margin-top: 0;
270 | margin-bottom: 1rem;
271 | overflow: auto;
272 | font-size: 0.875em;
273 | }
274 |
275 | pre code {
276 | font-size: inherit;
277 | color: inherit;
278 | word-break: normal;
279 | }
280 |
281 | code {
282 | font-size: 0.875em;
283 | color: var(--bs-code-color);
284 | word-wrap: break-word;
285 | }
286 |
287 | a > code {
288 | color: inherit;
289 | }
290 |
291 | kbd {
292 | padding: 0.1875rem 0.375rem;
293 | font-size: 0.875em;
294 | color: var(--bs-body-bg);
295 | background-color: var(--bs-body-color);
296 | border-radius: 0.25rem;
297 | }
298 |
299 | kbd kbd {
300 | padding: 0;
301 | font-size: 1em;
302 | }
303 |
304 | figure {
305 | margin: 0 0 1rem;
306 | }
307 |
308 | img,
309 | svg {
310 | vertical-align: middle;
311 | }
312 |
313 | table {
314 | caption-side: bottom;
315 | border-collapse: collapse;
316 | }
317 |
318 | caption {
319 | padding-top: 0.5rem;
320 | padding-bottom: 0.5rem;
321 | color: #6c757d;
322 | text-align: left;
323 | }
324 |
325 | th {
326 | text-align: inherit;
327 | text-align: -webkit-match-parent;
328 | }
329 |
330 | thead,
331 | tbody,
332 | tfoot,
333 | tr,
334 | td,
335 | th {
336 | border-color: inherit;
337 | border-style: solid;
338 | border-width: 0;
339 | }
340 |
341 | label {
342 | display: inline-block;
343 | }
344 |
345 | button {
346 | border-radius: 0;
347 | }
348 |
349 | button:focus:not(:focus-visible) {
350 | outline: 0;
351 | }
352 |
353 | input,
354 | button,
355 | select,
356 | optgroup,
357 | textarea {
358 | margin: 0;
359 | font-family: inherit;
360 | font-size: inherit;
361 | line-height: inherit;
362 | }
363 |
364 | button,
365 | select {
366 | text-transform: none;
367 | }
368 |
369 | [role=button] {
370 | cursor: pointer;
371 | }
372 |
373 | select {
374 | word-wrap: normal;
375 | }
376 |
377 | select:disabled {
378 | opacity: 1;
379 | }
380 |
381 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
382 | display: none !important;
383 | }
384 |
385 | button,
386 | [type=button],
387 | [type=reset],
388 | [type=submit] {
389 | -webkit-appearance: button;
390 | }
391 |
392 | button:not(:disabled),
393 | [type=button]:not(:disabled),
394 | [type=reset]:not(:disabled),
395 | [type=submit]:not(:disabled) {
396 | cursor: pointer;
397 | }
398 |
399 | ::-moz-focus-inner {
400 | padding: 0;
401 | border-style: none;
402 | }
403 |
404 | textarea {
405 | resize: vertical;
406 | }
407 |
408 | fieldset {
409 | min-width: 0;
410 | padding: 0;
411 | margin: 0;
412 | border: 0;
413 | }
414 |
415 | legend {
416 | float: left;
417 | width: 100%;
418 | padding: 0;
419 | margin-bottom: 0.5rem;
420 | font-size: calc(1.275rem + 0.3vw);
421 | line-height: inherit;
422 | }
423 |
424 | @media (min-width: 1200px) {
425 | legend {
426 | font-size: 1.5rem;
427 | }
428 | }
429 |
430 | legend + * {
431 | clear: left;
432 | }
433 |
434 | ::-webkit-datetime-edit-fields-wrapper,
435 | ::-webkit-datetime-edit-text,
436 | ::-webkit-datetime-edit-minute,
437 | ::-webkit-datetime-edit-hour-field,
438 | ::-webkit-datetime-edit-day-field,
439 | ::-webkit-datetime-edit-month-field,
440 | ::-webkit-datetime-edit-year-field {
441 | padding: 0;
442 | }
443 |
444 | ::-webkit-inner-spin-button {
445 | height: auto;
446 | }
447 |
448 | [type=search] {
449 | outline-offset: -2px;
450 | -webkit-appearance: textfield;
451 | }
452 |
453 | /* rtl:raw:
454 | [type="tel"],
455 | [type="url"],
456 | [type="email"],
457 | [type="number"] {
458 | direction: ltr;
459 | }
460 | */
461 | ::-webkit-search-decoration {
462 | -webkit-appearance: none;
463 | }
464 |
465 | ::-webkit-color-swatch-wrapper {
466 | padding: 0;
467 | }
468 |
469 | ::-webkit-file-upload-button {
470 | font: inherit;
471 | -webkit-appearance: button;
472 | }
473 |
474 | ::file-selector-button {
475 | font: inherit;
476 | -webkit-appearance: button;
477 | }
478 |
479 | output {
480 | display: inline-block;
481 | }
482 |
483 | iframe {
484 | border: 0;
485 | }
486 |
487 | summary {
488 | display: list-item;
489 | cursor: pointer;
490 | }
491 |
492 | progress {
493 | vertical-align: baseline;
494 | }
495 |
496 | [hidden] {
497 | display: none !important;
498 | }
499 |
500 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/src/main/resources/web/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
7 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/src/main/resources/web/css/bootstrap-reboot.rtl.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */
7 | :root {
8 | --bs-blue: #0d6efd;
9 | --bs-indigo: #6610f2;
10 | --bs-purple: #6f42c1;
11 | --bs-pink: #d63384;
12 | --bs-red: #dc3545;
13 | --bs-orange: #fd7e14;
14 | --bs-yellow: #ffc107;
15 | --bs-green: #198754;
16 | --bs-teal: #20c997;
17 | --bs-cyan: #0dcaf0;
18 | --bs-black: #000;
19 | --bs-white: #fff;
20 | --bs-gray: #6c757d;
21 | --bs-gray-dark: #343a40;
22 | --bs-gray-100: #f8f9fa;
23 | --bs-gray-200: #e9ecef;
24 | --bs-gray-300: #dee2e6;
25 | --bs-gray-400: #ced4da;
26 | --bs-gray-500: #adb5bd;
27 | --bs-gray-600: #6c757d;
28 | --bs-gray-700: #495057;
29 | --bs-gray-800: #343a40;
30 | --bs-gray-900: #212529;
31 | --bs-primary: #0d6efd;
32 | --bs-secondary: #6c757d;
33 | --bs-success: #198754;
34 | --bs-info: #0dcaf0;
35 | --bs-warning: #ffc107;
36 | --bs-danger: #dc3545;
37 | --bs-light: #f8f9fa;
38 | --bs-dark: #212529;
39 | --bs-primary-rgb: 13, 110, 253;
40 | --bs-secondary-rgb: 108, 117, 125;
41 | --bs-success-rgb: 25, 135, 84;
42 | --bs-info-rgb: 13, 202, 240;
43 | --bs-warning-rgb: 255, 193, 7;
44 | --bs-danger-rgb: 220, 53, 69;
45 | --bs-light-rgb: 248, 249, 250;
46 | --bs-dark-rgb: 33, 37, 41;
47 | --bs-white-rgb: 255, 255, 255;
48 | --bs-black-rgb: 0, 0, 0;
49 | --bs-body-color-rgb: 33, 37, 41;
50 | --bs-body-bg-rgb: 255, 255, 255;
51 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
52 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
53 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
54 | --bs-body-font-family: var(--bs-font-sans-serif);
55 | --bs-body-font-size: 1rem;
56 | --bs-body-font-weight: 400;
57 | --bs-body-line-height: 1.5;
58 | --bs-body-color: #212529;
59 | --bs-body-bg: #fff;
60 | --bs-border-width: 1px;
61 | --bs-border-style: solid;
62 | --bs-border-color: #dee2e6;
63 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
64 | --bs-border-radius: 0.375rem;
65 | --bs-border-radius-sm: 0.25rem;
66 | --bs-border-radius-lg: 0.5rem;
67 | --bs-border-radius-xl: 1rem;
68 | --bs-border-radius-2xl: 2rem;
69 | --bs-border-radius-pill: 50rem;
70 | --bs-link-color: #0d6efd;
71 | --bs-link-hover-color: #0a58ca;
72 | --bs-code-color: #d63384;
73 | --bs-highlight-bg: #fff3cd;
74 | }
75 |
76 | *,
77 | *::before,
78 | *::after {
79 | box-sizing: border-box;
80 | }
81 |
82 | @media (prefers-reduced-motion: no-preference) {
83 | :root {
84 | scroll-behavior: smooth;
85 | }
86 | }
87 |
88 | body {
89 | margin: 0;
90 | font-family: var(--bs-body-font-family);
91 | font-size: var(--bs-body-font-size);
92 | font-weight: var(--bs-body-font-weight);
93 | line-height: var(--bs-body-line-height);
94 | color: var(--bs-body-color);
95 | text-align: var(--bs-body-text-align);
96 | background-color: var(--bs-body-bg);
97 | -webkit-text-size-adjust: 100%;
98 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
99 | }
100 |
101 | hr {
102 | margin: 1rem 0;
103 | color: inherit;
104 | border: 0;
105 | border-top: 1px solid;
106 | opacity: 0.25;
107 | }
108 |
109 | h6, h5, h4, h3, h2, h1 {
110 | margin-top: 0;
111 | margin-bottom: 0.5rem;
112 | font-weight: 500;
113 | line-height: 1.2;
114 | }
115 |
116 | h1 {
117 | font-size: calc(1.375rem + 1.5vw);
118 | }
119 |
120 | @media (min-width: 1200px) {
121 | h1 {
122 | font-size: 2.5rem;
123 | }
124 | }
125 |
126 | h2 {
127 | font-size: calc(1.325rem + 0.9vw);
128 | }
129 |
130 | @media (min-width: 1200px) {
131 | h2 {
132 | font-size: 2rem;
133 | }
134 | }
135 |
136 | h3 {
137 | font-size: calc(1.3rem + 0.6vw);
138 | }
139 |
140 | @media (min-width: 1200px) {
141 | h3 {
142 | font-size: 1.75rem;
143 | }
144 | }
145 |
146 | h4 {
147 | font-size: calc(1.275rem + 0.3vw);
148 | }
149 |
150 | @media (min-width: 1200px) {
151 | h4 {
152 | font-size: 1.5rem;
153 | }
154 | }
155 |
156 | h5 {
157 | font-size: 1.25rem;
158 | }
159 |
160 | h6 {
161 | font-size: 1rem;
162 | }
163 |
164 | p {
165 | margin-top: 0;
166 | margin-bottom: 1rem;
167 | }
168 |
169 | abbr[title] {
170 | -webkit-text-decoration: underline dotted;
171 | text-decoration: underline dotted;
172 | cursor: help;
173 | -webkit-text-decoration-skip-ink: none;
174 | text-decoration-skip-ink: none;
175 | }
176 |
177 | address {
178 | margin-bottom: 1rem;
179 | font-style: normal;
180 | line-height: inherit;
181 | }
182 |
183 | ol,
184 | ul {
185 | padding-right: 2rem;
186 | }
187 |
188 | ol,
189 | ul,
190 | dl {
191 | margin-top: 0;
192 | margin-bottom: 1rem;
193 | }
194 |
195 | ol ol,
196 | ul ul,
197 | ol ul,
198 | ul ol {
199 | margin-bottom: 0;
200 | }
201 |
202 | dt {
203 | font-weight: 700;
204 | }
205 |
206 | dd {
207 | margin-bottom: 0.5rem;
208 | margin-right: 0;
209 | }
210 |
211 | blockquote {
212 | margin: 0 0 1rem;
213 | }
214 |
215 | b,
216 | strong {
217 | font-weight: bolder;
218 | }
219 |
220 | small {
221 | font-size: 0.875em;
222 | }
223 |
224 | mark {
225 | padding: 0.1875em;
226 | background-color: var(--bs-highlight-bg);
227 | }
228 |
229 | sub,
230 | sup {
231 | position: relative;
232 | font-size: 0.75em;
233 | line-height: 0;
234 | vertical-align: baseline;
235 | }
236 |
237 | sub {
238 | bottom: -0.25em;
239 | }
240 |
241 | sup {
242 | top: -0.5em;
243 | }
244 |
245 | a {
246 | color: var(--bs-link-color);
247 | text-decoration: underline;
248 | }
249 |
250 | a:hover {
251 | color: var(--bs-link-hover-color);
252 | }
253 |
254 | a:not([href]):not([class]), a:not([href]):not([class]):hover {
255 | color: inherit;
256 | text-decoration: none;
257 | }
258 |
259 | pre,
260 | code,
261 | kbd,
262 | samp {
263 | font-family: var(--bs-font-monospace);
264 | font-size: 1em;
265 | }
266 |
267 | pre {
268 | display: block;
269 | margin-top: 0;
270 | margin-bottom: 1rem;
271 | overflow: auto;
272 | font-size: 0.875em;
273 | }
274 |
275 | pre code {
276 | font-size: inherit;
277 | color: inherit;
278 | word-break: normal;
279 | }
280 |
281 | code {
282 | font-size: 0.875em;
283 | color: var(--bs-code-color);
284 | word-wrap: break-word;
285 | }
286 |
287 | a > code {
288 | color: inherit;
289 | }
290 |
291 | kbd {
292 | padding: 0.1875rem 0.375rem;
293 | font-size: 0.875em;
294 | color: var(--bs-body-bg);
295 | background-color: var(--bs-body-color);
296 | border-radius: 0.25rem;
297 | }
298 |
299 | kbd kbd {
300 | padding: 0;
301 | font-size: 1em;
302 | }
303 |
304 | figure {
305 | margin: 0 0 1rem;
306 | }
307 |
308 | img,
309 | svg {
310 | vertical-align: middle;
311 | }
312 |
313 | table {
314 | caption-side: bottom;
315 | border-collapse: collapse;
316 | }
317 |
318 | caption {
319 | padding-top: 0.5rem;
320 | padding-bottom: 0.5rem;
321 | color: #6c757d;
322 | text-align: right;
323 | }
324 |
325 | th {
326 | text-align: inherit;
327 | text-align: -webkit-match-parent;
328 | }
329 |
330 | thead,
331 | tbody,
332 | tfoot,
333 | tr,
334 | td,
335 | th {
336 | border-color: inherit;
337 | border-style: solid;
338 | border-width: 0;
339 | }
340 |
341 | label {
342 | display: inline-block;
343 | }
344 |
345 | button {
346 | border-radius: 0;
347 | }
348 |
349 | button:focus:not(:focus-visible) {
350 | outline: 0;
351 | }
352 |
353 | input,
354 | button,
355 | select,
356 | optgroup,
357 | textarea {
358 | margin: 0;
359 | font-family: inherit;
360 | font-size: inherit;
361 | line-height: inherit;
362 | }
363 |
364 | button,
365 | select {
366 | text-transform: none;
367 | }
368 |
369 | [role=button] {
370 | cursor: pointer;
371 | }
372 |
373 | select {
374 | word-wrap: normal;
375 | }
376 |
377 | select:disabled {
378 | opacity: 1;
379 | }
380 |
381 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
382 | display: none !important;
383 | }
384 |
385 | button,
386 | [type=button],
387 | [type=reset],
388 | [type=submit] {
389 | -webkit-appearance: button;
390 | }
391 |
392 | button:not(:disabled),
393 | [type=button]:not(:disabled),
394 | [type=reset]:not(:disabled),
395 | [type=submit]:not(:disabled) {
396 | cursor: pointer;
397 | }
398 |
399 | ::-moz-focus-inner {
400 | padding: 0;
401 | border-style: none;
402 | }
403 |
404 | textarea {
405 | resize: vertical;
406 | }
407 |
408 | fieldset {
409 | min-width: 0;
410 | padding: 0;
411 | margin: 0;
412 | border: 0;
413 | }
414 |
415 | legend {
416 | float: right;
417 | width: 100%;
418 | padding: 0;
419 | margin-bottom: 0.5rem;
420 | font-size: calc(1.275rem + 0.3vw);
421 | line-height: inherit;
422 | }
423 |
424 | @media (min-width: 1200px) {
425 | legend {
426 | font-size: 1.5rem;
427 | }
428 | }
429 |
430 | legend + * {
431 | clear: right;
432 | }
433 |
434 | ::-webkit-datetime-edit-fields-wrapper,
435 | ::-webkit-datetime-edit-text,
436 | ::-webkit-datetime-edit-minute,
437 | ::-webkit-datetime-edit-hour-field,
438 | ::-webkit-datetime-edit-day-field,
439 | ::-webkit-datetime-edit-month-field,
440 | ::-webkit-datetime-edit-year-field {
441 | padding: 0;
442 | }
443 |
444 | ::-webkit-inner-spin-button {
445 | height: auto;
446 | }
447 |
448 | [type=search] {
449 | outline-offset: -2px;
450 | -webkit-appearance: textfield;
451 | }
452 |
453 | [type="tel"],
454 | [type="url"],
455 | [type="email"],
456 | [type="number"] {
457 | direction: ltr;
458 | }
459 |
460 | ::-webkit-search-decoration {
461 | -webkit-appearance: none;
462 | }
463 |
464 | ::-webkit-color-swatch-wrapper {
465 | padding: 0;
466 | }
467 |
468 | ::-webkit-file-upload-button {
469 | font: inherit;
470 | -webkit-appearance: button;
471 | }
472 |
473 | ::file-selector-button {
474 | font: inherit;
475 | -webkit-appearance: button;
476 | }
477 |
478 | output {
479 | display: inline-block;
480 | }
481 |
482 | iframe {
483 | border: 0;
484 | }
485 |
486 | summary {
487 | display: list-item;
488 | cursor: pointer;
489 | }
490 |
491 | progress {
492 | vertical-align: baseline;
493 | }
494 |
495 | [hidden] {
496 | display: none !important;
497 | }
498 |
499 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
--------------------------------------------------------------------------------
/src/main/resources/web/css/bootstrap-reboot.rtl.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
7 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
--------------------------------------------------------------------------------
/src/main/resources/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | WebApp Hardware Bridge Web UI
8 |
9 |
10 |
11 |
12 |
21 |
22 |
23 |
24 |
36 |
37 |
38 |
39 |
Web/WebSocket Server
40 |
41 |
47 |
48 |
54 |
55 |
61 |
62 |
68 |
69 |
79 |
80 |
86 |
87 |
88 |
TLS
89 |
95 |
96 |
102 |
103 |
109 |
110 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
Downloader
124 |
125 |
131 |
132 |
138 |
139 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | Printers
155 | +
156 |
157 |
158 |
159 |
160 |
166 |
167 |
173 |
174 |
180 |
181 |
182 |
183 |
184 | Printer {{ index + 1 }} ({{ printer.type }})
185 | -
186 |
187 |
188 |
194 |
195 |
203 |
204 |
210 |
211 |
217 |
218 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | Serials
235 | +
236 |
237 |
238 |
239 |
240 |
246 |
247 |
248 |
249 |
250 | Serial {{ index + 1 }} ({{ serial.type }})
251 | -
252 |
253 |
254 |
260 |
261 |
269 |
270 |
276 |
277 |
283 |
284 |
290 |
291 |
297 |
298 |
306 |
307 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
367 |
368 |
--------------------------------------------------------------------------------
/tls/.gitignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------