├── .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 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 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 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |

36 | 37 |

38 | 39 |
40 | 41 |
42 | 43 |

Fallback to window.open() if WebApp Hardware Bridge is not running.

44 |
45 | 46 |
47 | 48 |
49 | 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 |
56 |
57 | 58 |
59 | 60 | 61 |
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 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 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 | 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 | 20 |
21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 | 45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | 56 |
57 | 58 |
59 |
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 |
18 |

Input:

19 |
20 | 21 | 22 |
23 | 24 |
25 |
26 | 27 |

Output:

28 | 29 |
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 |
13 |
14 | 15 | WebApp Hardware Bridge Web UI 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
GUI
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
Web/WebSocket Server
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 |
70 |
Authentication
71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 |
79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 | 87 |
88 |
TLS
89 |
90 | 91 |
92 | 93 |
94 |
95 | 96 |
97 | 98 |
99 | 100 |
101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 |
109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 |
117 |
118 |
119 |
120 | 121 |
122 |
123 |
Downloader
124 | 125 |
126 | 127 |
128 | 129 |
130 |
131 | 132 |
133 | 134 |
135 | 136 |
137 |
138 | 139 |
140 | 141 |
142 | 143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 |
151 |
152 |
153 |
154 | Printers 155 | 156 |
157 | 158 |
159 | 160 |
161 | 162 |
163 | 164 |
165 |
166 | 167 |
168 | 169 |
170 | 171 |
172 |
173 | 174 |
175 | 176 |
177 | 178 |
179 |
180 | 181 |
182 |
183 |
184 | Printer {{ index + 1 }} ({{ printer.type }}) 185 | 186 |
187 | 188 |
189 | 190 |
191 | 192 |
193 |
194 | 195 |
196 | 197 |
198 | 201 |
202 |
203 | 204 |
205 | 206 |
207 | 208 |
209 |
210 | 211 |
212 | 213 |
214 | 215 |
216 |
217 | 218 |
219 | 220 |
221 | 222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | 230 |
231 |
232 |
233 |
234 | Serials 235 | 236 |
237 | 238 |
239 | 240 |
241 | 242 |
243 | 244 |
245 |
246 | 247 |
248 |
249 |
250 | Serial {{ index + 1 }} ({{ serial.type }}) 251 | 252 |
253 | 254 |
255 | 256 |
257 | 258 |
259 |
260 | 261 |
262 | 263 |
264 | 267 |
268 |
269 | 270 |
271 | 272 |
273 | 274 |
275 |
276 | 277 |
278 | 279 |
280 | 281 |
282 |
283 | 284 |
285 | 286 |
287 | 288 |
289 |
290 | 291 |
292 | 293 |
294 | 295 |
296 |
297 | 298 |
299 | 300 |
301 | 304 |
305 |
306 | 307 |
308 | 309 |
310 | 311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 | 322 | 323 | 367 | 368 | -------------------------------------------------------------------------------- /tls/.gitignore: -------------------------------------------------------------------------------- 1 | * --------------------------------------------------------------------------------