├── .DS_Store
├── .gitignore
├── .gitmodules
├── LICENSE
├── README.md
├── app.pem
├── app
├── app.html
├── css
│ ├── app.css
│ └── materialdesignicons.css
├── fonts
│ ├── materialdesignicons-webfont.eot
│ ├── materialdesignicons-webfont.ttf
│ ├── materialdesignicons-webfont.woff
│ └── materialdesignicons-webfont.woff2
├── img
│ └── icon.png
├── js
│ ├── app.js
│ ├── background.js
│ ├── remote.js
│ └── utils.js
├── manifest.json
├── package.json
├── remote.html
├── ts
│ ├── actions.ts
│ ├── app.ts
│ ├── background.ts
│ ├── global.d.ts
│ ├── remote.ts
│ ├── serial.ts
│ ├── store.ts
│ ├── utils.ts
│ └── view.ts
├── tsconfig.json
├── utils.html
├── webpack.config.js
├── yarn-error.log
└── yarn.lock
├── build.sh
├── flash.sh
├── inc
├── api.h
├── app.h
├── btn.h
├── def.h
├── led.h
└── net.h
├── lib
├── RGBLed
│ └── RGBLed.h
└── transport
│ └── transport.h
├── platformio.ini
└── src
├── main.cpp
├── receiver.h
└── transmitter.h
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pio
2 | .idea
3 | .vscode
4 | .vscode/.browse.c_cpp.db*
5 | .vscode/c_cpp_properties.json
6 | .vscode/launch.json
7 | .vscode/ipch
8 | node_modules
9 | **/dist
10 | **/build
11 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/EspRC"]
2 | path = lib/EspRC
3 | url = https://github.com/iphong/lib-esp-rc.git
4 | [submodule "lib/Button"]
5 | path = lib/Button
6 | url = https://github.com/iphong/lib-button.git
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Phong Vu
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 | [my_github]: https://github.com/iphong "My GitHub URL"
2 |
3 | # ESP VISUAL LED
4 |
5 | These project is focusing on using the ESP8266 to deliver visually complex and beautiful light shows.
6 |
7 | These are a few basic aspects in which we want to achieve in project:
8 |
9 | ### 1. Color Sequence
10 |
11 | Everything begins with a pre-composed light sequences created by show producers based on some audio tracks. A basic light sequences is a timeline of keyframes of color changes throughout the show. A light sequence runs at 1Khz frequency which should updates every milliseconds. A sequence is controlled and rendered individually, and each of these tasks of rendering light sequences from start to end is called an output channel.
12 |
13 | ### 2. Receiver Nodes
14 |
15 | A receiver node is a device module which may have one or more output channels, and should be able to render them simultenously. Multiple receiver can be combined if a large number of output channels are required in a set.
16 | These receivers need to synchronized in realtime.
17 |
18 | ### 3. Master Controller
19 |
20 | A light show may have tens or even hundreds of these output channels. A master computer which play the music track instruct when all receiver should begin playing light show and must be in perfect time sync with milliseconds accuracy throughout all devices.
21 |
22 | ***
23 |
24 | # Building the hardware
25 |
26 | The basic components for a LED driver circuit includes MCU, MOSFET and PSU. Everything needs to be very small as they will be mostly be mounted on instruments and clothes. size of a fullsize sim-card should be ideal.
27 |
28 | ### 1 - MCU
29 |
30 | Any esp8266 modules should be fine. And for this specific project, we use the ESP-12F for several reasons:
31 |
32 | * It has all the GPIO pins and yes, we definitly need all of them
33 | * It has 4MB of flash memory which can be used for OTA and light show uploads
34 | * It has builtin antenna which is good enough for our purpose
35 | * It is nicely packed in a small form factor with metal shields which is FCC certified
36 | * And they are insanely cheap
37 |
38 | ### 2 - PSU
39 |
40 | We need a 3.3v power supply for the ESP8266, a few resistors and a status indicator LED. For 5v power source, a linear LDO voltage regulator should be perfect. For power source greater than 9V, a switch regulator is required to keep the temperature low.
41 |
42 | ### 3 - MOSFET
43 |
44 | Average continuous current drawn of 2-4 amps running at 5V or 12V power sources or from 1-3 cells lithium batteries.
45 |
46 | A typical RGB led has 3 channels representing red, green and blue color. We controls the brightness of each channel using PWM.
47 | Most popular LED strip on the market has 4 pins with common anode terminal which connect directly to the main power source positive terminal. Then we use N-channel MOSFET w/ logic-level gate connect to the kathode terminal of each channels. To control set the gate level to HIGH to turn on the LED.
48 |
49 | There are some other uncommon LED with common kathode configuration which need a P-channel MOSFET to control. To keep things simple, we just going to use the common anode type products.
50 |
51 | ***
52 |
53 | ## Light Show Binary file format (.LSB)
54 |
55 | The content of this file is a sequence of 16 bytes frames. Which has the following structure:
56 |
57 | Type | RED | GREEN | BLUE | START | DURATION | TRANSITION
58 | ---- | --- | ----- | ---- | ----- | -------- | ----------
59 | UINT8(1) | UINT8(1) | UINT8(1) | UINT8(1) | UINT32(4) | UINT32(4) | UINT32(4)
60 |
61 | #### Frame types
62 |
63 | * 0x01 - RGB FRAME
64 | * 0x02 - END FRAME
65 | * 0x03 - LOOP FRAME
66 |
67 | ## GPIO Pin Mappings
68 |
69 | Function | Pin #
70 | ---------|------
71 | Battery Voltage | ***ADC0***
72 | Setup Button | ***GPIO0***
73 | Status LED | ***GPIO2***
74 | Serial TX | ***GPIO1***
75 | Serial RX | ***GPIO3***
76 | Channel 1 Green | ***GPIO12***
77 | Channel 1 Red | ***GPIO13***
78 | Channel 1 Blue | ***GPIO14***
79 | Channel 2 Green | ***GPIO15***
80 | Channel 2 Red | ***GPIO5***
81 | Channel 2 Blue | ***GPIO4***
82 |
--------------------------------------------------------------------------------
/app.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDMHJ3norw61sjx
3 | MxMhSLl3lEEpBzRgQvoqCPQgbCEDA3bdJolerxaeHCQyVt9hQJ3zOefZ3SJF62ik
4 | rnlui07iDEGgmT3iHmhyZTbZFPNspWSWkvqIqHlEtrbURBv8XJBA/IIUkNwn4IbC
5 | XmqdqR9/7/gn3QFGkkil0GSc3Ir0ZEjCoZtTVRNa6ICppAlxrAkURrhpG7SWuIpX
6 | uU28wnm1jUE4OX0b7YaI3tXgBX6VTZiEdumlhuRsM3sXK9yHBLCyaSbdhBLxDtIA
7 | cswbLNgeiImfFl7djyq3jQ3ux3dAxB3hO9cxmxZxpXgnJGfrI7nYPwpEvUOYG28S
8 | UFWygqHZAgMBAAECggEAOQT1h7QKaVV/JQJjobSzOxiLa267zvhm8j82E6ihQDpD
9 | hlwxm+XFp72szvM9y+lFNqFCh7Yi0O2GehrTiXIZQ0SxQLAOfZCJFQv5WoNjzUIW
10 | 64l7u5l05yEzyfCM8N7a3YysuhoNpoYunzXJCuooBpF+/YPby18qNEgXE0r49nRd
11 | t3Xg40S5kj94k7MNNzhMx1WmBi0UbA6c5NG/YTa27wLvzlNbetXXydSGgmL9zdaY
12 | eb0ot3ICOvPO6iZzhWbSHwLS6G0hvxe7Y/eT7BHbugw0/T9YcS+i/vK1plC0Bwz/
13 | 9oPFdAZ5krcPvNfZRq2eV1+FZx158Fir8dodGHr9kQKBgQDluBnhebfwnLbmbqP1
14 | que8qU3Vu2JJDLvnbfOEfWNKJlLgV8TPyR0PgaFcwRs3tDNFGO6GRc03p70kKMe7
15 | 3pbT/1ofKx8WcCfk622WL7UeN2sgUBxxdbEF+evxN9nOiq3sguwGosXSEKzhJ6Jt
16 | Rz/8KfkorpeiznadlKTHOXh6gwKBgQDjdopLQbyAcXDSZcsY1Z+bdGQIJ8ezyXuk
17 | 70XeA0WRIur4xR3mym8bXE7c+Wt6C5r+How2kGdgLu2PZR2EqoWVePz+QT5rPtNt
18 | VofsxBEYF+Nsa5vTjsuSVLu6NEtsiDVOAyWH2XJ4GV6TQIGdJIxcdRWoODJ6R/UT
19 | kSlcqD6zcwKBgH3mLlkC6qq2WQ9lp/qmVidx6rSu5CkBD6LBAeulBNvIsTc/IyB2
20 | KrUq6JL7Sr12x3qhNWjlrJlKF0FQEFeIoMVDd9MJQRp9EYBG/2KGdw8+dDnbbhtI
21 | 02JhHMyxPXATVUsAXfctEpoUhYtIu56EpC7BmkqPlY+m9B9dLgn6F2udAoGASs1A
22 | zV7gzpx/rbEsCQ63Xjf4bXYnhkhMEQFeyJPq3L/O5eBs5OjNgQHqLWEYpxoJ7me9
23 | VHRIiqjkFrP58RbitzUCfdqW3E3c2agLKyGPPY0djRoWNIxRBd43nhR0eUyRuwXt
24 | 4a7wpe4x29rqxPKv9ffLF3bjorLnNXgXUhFCDWECgYAi5HPTqEXBDmElDJSs713e
25 | HgxkP8RtU0PT3i8K5Jozrq67WwNJ+xDpLsnJgBazP/ZHCzNnVozmfgzLuEB6kO1Z
26 | VDxSuaAdqqLHtlnfLM/UP3rrYlpUUEYJ2ZEIjm7FHggnRkrQndt5p5PUITxHnXow
27 | bWagch2YY6QQJqBo+I2WCw==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/app/app.html:
--------------------------------------------------------------------------------
1 |
2 |
SDC LED TECHNOLOGY
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/css/app.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
4 | font-family: "system-ui", "Helvetica Neue", Verdana, Geneva, Tahoma, sans-serif;
5 | }
6 |
7 | *:not(input) {
8 | user-select: none;
9 | -webkit-user-select: none;
10 | -khtml-user-select: none;
11 | -moz-user-select: none;
12 | -ms-user-select: none;
13 | }
14 |
15 | header {
16 | -webkit-app-region: drag;
17 | }
18 |
19 | button, input, select, label {
20 | -webkit-app-region: no-drag;
21 | vertical-align: middle;
22 | }
23 |
24 | html {
25 | height: 100%;
26 | width: 100%;
27 | padding: 0;
28 | margin: 0;
29 | }
30 |
31 | body {
32 | display: grid;
33 | font-size: 14px;
34 | font-family: system-ui, serif;
35 | grid-template-rows: 60px auto;
36 | background: black;
37 | color: white;
38 | height: 100%;
39 | width: 100%;
40 | margin: 0;
41 | padding: 0;
42 | }
43 |
44 | button {
45 | font-size: 14px;
46 | white-space: nowrap;
47 | outline: none;
48 | transition: all ease 0.1s;
49 | }
50 | button.mdi {
51 | font-size: 20px;
52 | }
53 |
54 | nav {
55 | display: flex;
56 | flex-wrap: nowrap;
57 | }
58 |
59 | nav > * {
60 | vertical-align: middle;
61 | margin-left: 2px;
62 | }
63 | button, select {
64 | vertical-align: middle;
65 | height: 35px;
66 | min-width: 40px;
67 | padding: 0 10px;
68 | background: rgba(255,255,255,0.05);
69 | border: 1px solid rgba(0,0,0,0.1);
70 | color: white;
71 | -webkit-appearance: none;
72 | outline: none;
73 | border-radius: 5px;
74 | font-weight: 500;
75 | }
76 | button {
77 | }
78 | button:focus, input:focus, select:focus {
79 |
80 | }
81 |
82 | button:hover {
83 | text-shadow: 0 0 10px #c000ff;
84 | background: rgba(255,255,255,0.1);
85 | }
86 | button:active,
87 | button.selected {
88 | background: rgba(0,0,0,0.2);
89 | }
90 | button.selected {
91 | color: #d143ff;
92 | text-shadow: 0 0 10px #c000ff;
93 | }
94 |
95 | button[disabled],
96 | select[disabled] {
97 | opacity: 0.5;
98 | pointer-events: none;
99 | }
100 | select {
101 | max-width: 150px;
102 | padding: 2px 10px 0px 13px;
103 | }
104 |
105 | #header {
106 | position: relative;
107 | display: grid;
108 | padding: 5px 20px;
109 | grid-gap: 10px;
110 | background: #222326;
111 | grid-template-columns: 120px 1fr 2fr 4fr auto;
112 | align-items: center;
113 | justify-content: center;
114 | }
115 | #logo {
116 | text-shadow: 0 0 10px #bd00f1;
117 | }
118 | #logo strong {
119 | font-size: 30px;
120 | display: block;
121 | line-height: 1em;
122 | }
123 |
124 | #logo small {
125 | display: block;
126 | font-size: small;
127 | letter-spacing: 0.5em;
128 | line-height: 1em;
129 | }
130 |
131 | #main {
132 | position: relative;
133 | width: 100%;
134 | display: grid;
135 | /* grid-template-rows: auto 20px 60px; */
136 | grid-template-rows: 30px auto 0px 0px;
137 | overflow-x: hidden;
138 | background: #191919;
139 | }
140 |
141 | #tracks {
142 | display: grid;
143 | padding: 2px 0;
144 | grid-gap: 2px;
145 | box-sizing: border-box;
146 | /* background: url(../img/checker.jpg); */
147 | }
148 |
149 | #tracks .track {
150 | position: relative;
151 | background: #111;
152 | }
153 |
154 | #tracks .track .index {
155 | position: absolute;
156 | left: 0;
157 | top: 0;
158 | bottom: 0;
159 | display: flex;
160 | z-index: 10;
161 | color: white;
162 | text-shadow: 0 5px 5px #000;
163 | width: 30px;
164 | align-items: center;
165 | padding-left: 10px;
166 | }
167 |
168 | #tracks .track > span {
169 | display: block;
170 | position: absolute;
171 | /* top: 50%; */
172 | height: 100%;
173 | /* max-height: 20px; */
174 | /* box-sizing: border-box; */
175 | /* transform: translateY(-50%); */
176 | /* border-radius: 0px; */
177 | }
178 | #tracks .track:focus {
179 | z-index: 2;
180 | }
181 | #tracks .track.selected {
182 | /* background: #b905ff66; */
183 | }
184 | #tracks .track.selected:before {
185 | content: "";
186 | display: block;
187 | position: absolute;
188 | left: 0;
189 | top: 0;
190 | height: 100%;
191 | width: 100%;
192 | background: rgba(36, 36, 37, 0.712);
193 | outline: 1px solid rgb(255, 255, 255);
194 | outline-offset: 1px;
195 | z-index: 5;
196 | }
197 |
198 | #timeline {
199 | background: #111;
200 | color: white;
201 | white-space: nowrap;
202 | overflow: hidden;
203 | }
204 | #timeline span {
205 | height: 100%;
206 | display: inline-flex;
207 | align-items: center;
208 | font-size: 10px;
209 | border-left: 1px dashed #ffffff2e;
210 | margin-left: -1px;
211 | padding-left: 3px;
212 | color: #ffffff;
213 | }
214 |
215 | #tempo {
216 | white-space: nowrap;
217 | display: none;
218 | }
219 |
220 | #waveform {
221 | background: #222326;
222 | display: none;
223 | }
224 |
225 | #handle {
226 | position: fixed;
227 | left: 0;
228 | top: 60px;
229 | bottom: 0px;
230 | /* height: 34px; */
231 | width: 2px;
232 | /* border-radius: 50%; */
233 | background-color: white;
234 | box-shadow: 0px 0px 2px 2px #673ab7, 0px 0px 10px 2px #9c27b0;
235 | transform: translateX(-100%);
236 | z-index: 20;
237 | }
238 |
239 | .block {
240 | display: inline-block;
241 | height: 100%;
242 | border: 1px solid #222;
243 | box-sizing: border-box;
244 | background-color: #000;
245 | font-size: 12px;
246 | line-height: 18px;
247 | text-align: center;
248 | font-weight: bold;
249 | color: white;
250 | }
251 |
--------------------------------------------------------------------------------
/app/fonts/materialdesignicons-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.eot
--------------------------------------------------------------------------------
/app/fonts/materialdesignicons-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.ttf
--------------------------------------------------------------------------------
/app/fonts/materialdesignicons-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.woff
--------------------------------------------------------------------------------
/app/fonts/materialdesignicons-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.woff2
--------------------------------------------------------------------------------
/app/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/img/icon.png
--------------------------------------------------------------------------------
/app/js/background.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "./js/";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/background.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./ts/background.ts":
90 | /*!**************************!*\
91 | !*** ./ts/background.ts ***!
92 | \**************************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports) {
95 |
96 | chrome['app'].runtime.onLaunched.addListener(function () {
97 | chrome['app'].window.create('../app.html', {
98 | id: 'app',
99 | frame: "none",
100 | minWidth: 900,
101 | minHeight: 100
102 | });
103 | });
104 |
105 |
106 | /***/ })
107 |
108 | /******/ });
109 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvYmFja2dyb3VuZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO1FBQUE7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7OztRQUdBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSwwQ0FBMEMsZ0NBQWdDO1FBQzFFO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0Esd0RBQXdELGtCQUFrQjtRQUMxRTtRQUNBLGlEQUFpRCxjQUFjO1FBQy9EOztRQUVBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQSx5Q0FBeUMsaUNBQWlDO1FBQzFFLGdIQUFnSCxtQkFBbUIsRUFBRTtRQUNySTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLDJCQUEyQiwwQkFBMEIsRUFBRTtRQUN2RCxpQ0FBaUMsZUFBZTtRQUNoRDtRQUNBO1FBQ0E7O1FBRUE7UUFDQSxzREFBc0QsK0RBQStEOztRQUVySDtRQUNBOzs7UUFHQTtRQUNBOzs7Ozs7Ozs7Ozs7QUNsRkEsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDO0lBQzVDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLGFBQWEsRUFBRTtRQUMxQyxFQUFFLEVBQUUsS0FBSztRQUNULEtBQUssRUFBRSxNQUFNO1FBQ2IsUUFBUSxFQUFFLEdBQUc7UUFDYixTQUFTLEVBQUUsR0FBRztLQUNkLENBQUMsQ0FBQztBQUNKLENBQUMsQ0FBQyxDQUFDIiwiZmlsZSI6ImJhY2tncm91bmQuanMiLCJzb3VyY2VzQ29udGVudCI6WyIgXHQvLyBUaGUgbW9kdWxlIGNhY2hlXG4gXHR2YXIgaW5zdGFsbGVkTW9kdWxlcyA9IHt9O1xuXG4gXHQvLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuIFx0ZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXG4gXHRcdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuIFx0XHRpZihpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSkge1xuIFx0XHRcdHJldHVybiBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXS5leHBvcnRzO1xuIFx0XHR9XG4gXHRcdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG4gXHRcdHZhciBtb2R1bGUgPSBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSA9IHtcbiBcdFx0XHRpOiBtb2R1bGVJZCxcbiBcdFx0XHRsOiBmYWxzZSxcbiBcdFx0XHRleHBvcnRzOiB7fVxuIFx0XHR9O1xuXG4gXHRcdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuIFx0XHRtb2R1bGVzW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuIFx0XHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG4gXHRcdG1vZHVsZS5sID0gdHJ1ZTtcblxuIFx0XHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuIFx0XHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG4gXHR9XG5cblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGVzIG9iamVjdCAoX193ZWJwYWNrX21vZHVsZXNfXylcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubSA9IG1vZHVsZXM7XG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlIGNhY2hlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmMgPSBpbnN0YWxsZWRNb2R1bGVzO1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHsgZW51bWVyYWJsZTogdHJ1ZSwgZ2V0OiBnZXR0ZXIgfSk7XG4gXHRcdH1cbiBcdH07XG5cbiBcdC8vIGRlZmluZSBfX2VzTW9kdWxlIG9uIGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uciA9IGZ1bmN0aW9uKGV4cG9ydHMpIHtcbiBcdFx0aWYodHlwZW9mIFN5bWJvbCAhPT0gJ3VuZGVmaW5lZCcgJiYgU3ltYm9sLnRvU3RyaW5nVGFnKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFN5bWJvbC50b1N0cmluZ1RhZywgeyB2YWx1ZTogJ01vZHVsZScgfSk7XG4gXHRcdH1cbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywgeyB2YWx1ZTogdHJ1ZSB9KTtcbiBcdH07XG5cbiBcdC8vIGNyZWF0ZSBhIGZha2UgbmFtZXNwYWNlIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDE6IHZhbHVlIGlzIGEgbW9kdWxlIGlkLCByZXF1aXJlIGl0XG4gXHQvLyBtb2RlICYgMjogbWVyZ2UgYWxsIHByb3BlcnRpZXMgb2YgdmFsdWUgaW50byB0aGUgbnNcbiBcdC8vIG1vZGUgJiA0OiByZXR1cm4gdmFsdWUgd2hlbiBhbHJlYWR5IG5zIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDh8MTogYmVoYXZlIGxpa2UgcmVxdWlyZVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy50ID0gZnVuY3Rpb24odmFsdWUsIG1vZGUpIHtcbiBcdFx0aWYobW9kZSAmIDEpIHZhbHVlID0gX193ZWJwYWNrX3JlcXVpcmVfXyh2YWx1ZSk7XG4gXHRcdGlmKG1vZGUgJiA4KSByZXR1cm4gdmFsdWU7XG4gXHRcdGlmKChtb2RlICYgNCkgJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0JyAmJiB2YWx1ZSAmJiB2YWx1ZS5fX2VzTW9kdWxlKSByZXR1cm4gdmFsdWU7XG4gXHRcdHZhciBucyA9IE9iamVjdC5jcmVhdGUobnVsbCk7XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18ucihucyk7XG4gXHRcdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShucywgJ2RlZmF1bHQnLCB7IGVudW1lcmFibGU6IHRydWUsIHZhbHVlOiB2YWx1ZSB9KTtcbiBcdFx0aWYobW9kZSAmIDIgJiYgdHlwZW9mIHZhbHVlICE9ICdzdHJpbmcnKSBmb3IodmFyIGtleSBpbiB2YWx1ZSkgX193ZWJwYWNrX3JlcXVpcmVfXy5kKG5zLCBrZXksIGZ1bmN0aW9uKGtleSkgeyByZXR1cm4gdmFsdWVba2V5XTsgfS5iaW5kKG51bGwsIGtleSkpO1xuIFx0XHRyZXR1cm4gbnM7XG4gXHR9O1xuXG4gXHQvLyBnZXREZWZhdWx0RXhwb3J0IGZ1bmN0aW9uIGZvciBjb21wYXRpYmlsaXR5IHdpdGggbm9uLWhhcm1vbnkgbW9kdWxlc1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5uID0gZnVuY3Rpb24obW9kdWxlKSB7XG4gXHRcdHZhciBnZXR0ZXIgPSBtb2R1bGUgJiYgbW9kdWxlLl9fZXNNb2R1bGUgP1xuIFx0XHRcdGZ1bmN0aW9uIGdldERlZmF1bHQoKSB7IHJldHVybiBtb2R1bGVbJ2RlZmF1bHQnXTsgfSA6XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0TW9kdWxlRXhwb3J0cygpIHsgcmV0dXJuIG1vZHVsZTsgfTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kKGdldHRlciwgJ2EnLCBnZXR0ZXIpO1xuIFx0XHRyZXR1cm4gZ2V0dGVyO1xuIFx0fTtcblxuIFx0Ly8gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm8gPSBmdW5jdGlvbihvYmplY3QsIHByb3BlcnR5KSB7IHJldHVybiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqZWN0LCBwcm9wZXJ0eSk7IH07XG5cbiBcdC8vIF9fd2VicGFja19wdWJsaWNfcGF0aF9fXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnAgPSBcIi4vanMvXCI7XG5cblxuIFx0Ly8gTG9hZCBlbnRyeSBtb2R1bGUgYW5kIHJldHVybiBleHBvcnRzXG4gXHRyZXR1cm4gX193ZWJwYWNrX3JlcXVpcmVfXyhfX3dlYnBhY2tfcmVxdWlyZV9fLnMgPSBcIi4vdHMvYmFja2dyb3VuZC50c1wiKTtcbiIsImNocm9tZVsnYXBwJ10ucnVudGltZS5vbkxhdW5jaGVkLmFkZExpc3RlbmVyKGZ1bmN0aW9uICgpIHtcblx0Y2hyb21lWydhcHAnXS53aW5kb3cuY3JlYXRlKCcuLi9hcHAuaHRtbCcsIHtcblx0XHRpZDogJ2FwcCcsXG5cdFx0ZnJhbWU6IFwibm9uZVwiLFxuXHRcdG1pbldpZHRoOiA5MDAsXG5cdFx0bWluSGVpZ2h0OiAxMDBcblx0fSk7XG59KTtcbiJdLCJzb3VyY2VSb290IjoiIn0=
--------------------------------------------------------------------------------
/app/js/remote.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "./js/";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/remote.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./ts/remote.ts":
90 | /*!**********************!*\
91 | !*** ./ts/remote.ts ***!
92 | \**********************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports) {
95 |
96 | addEventListener('message', e => {
97 | console.debug('message', e.data);
98 | });
99 | addEventListener('click', e => {
100 | const target = e.target;
101 | const data = (target.dataset.send || '').split(' ').map(hex => parseInt(hex, 16));
102 | const app = chrome['app'].window.get('app').contentWindow;
103 | for (let i = 0; i < 1; i++) {
104 | setTimeout(function () {
105 | app.postMessage(new Uint8Array([2, ...data]));
106 | }, i * 10);
107 | }
108 | });
109 | addEventListener('load', e => {
110 | document.body.focus();
111 | });
112 |
113 |
114 | /***/ })
115 |
116 | /******/ });
117 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvcmVtb3RlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7UUFBQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTs7O1FBR0E7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLDBDQUEwQyxnQ0FBZ0M7UUFDMUU7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSx3REFBd0Qsa0JBQWtCO1FBQzFFO1FBQ0EsaURBQWlELGNBQWM7UUFDL0Q7O1FBRUE7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBLHlDQUF5QyxpQ0FBaUM7UUFDMUUsZ0hBQWdILG1CQUFtQixFQUFFO1FBQ3JJO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0EsMkJBQTJCLDBCQUEwQixFQUFFO1FBQ3ZELGlDQUFpQyxlQUFlO1FBQ2hEO1FBQ0E7UUFDQTs7UUFFQTtRQUNBLHNEQUFzRCwrREFBK0Q7O1FBRXJIO1FBQ0E7OztRQUdBO1FBQ0E7Ozs7Ozs7Ozs7OztBQ2pGQSxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDL0IsT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQztBQUNqQyxDQUFDLENBQUM7QUFDRixnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDN0IsTUFBTSxNQUFNLEdBQUcsQ0FBQyxDQUFDLE1BQTJCO0lBQzVDLE1BQU0sSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDakYsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsYUFBYTtJQUN6RCxLQUFLLElBQUksQ0FBQyxHQUFDLENBQUMsRUFBRSxDQUFDLEdBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFO1FBQ3ZCLFVBQVUsQ0FBQztZQUNWLEdBQUcsQ0FBQyxXQUFXLENBQUMsSUFBSSxVQUFVLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQzlDLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxDQUFDO0tBQ1Y7QUFDRixDQUFDLENBQUM7QUFVRixnQkFBZ0IsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDNUIsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUU7QUFDdEIsQ0FBQyxDQUFDIiwiZmlsZSI6InJlbW90ZS5qcyIsInNvdXJjZXNDb250ZW50IjpbIiBcdC8vIFRoZSBtb2R1bGUgY2FjaGVcbiBcdHZhciBpbnN0YWxsZWRNb2R1bGVzID0ge307XG5cbiBcdC8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG4gXHRmdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cbiBcdFx0Ly8gQ2hlY2sgaWYgbW9kdWxlIGlzIGluIGNhY2hlXG4gXHRcdGlmKGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdKSB7XG4gXHRcdFx0cmV0dXJuIGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdLmV4cG9ydHM7XG4gXHRcdH1cbiBcdFx0Ly8gQ3JlYXRlIGEgbmV3IG1vZHVsZSAoYW5kIHB1dCBpdCBpbnRvIHRoZSBjYWNoZSlcbiBcdFx0dmFyIG1vZHVsZSA9IGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdID0ge1xuIFx0XHRcdGk6IG1vZHVsZUlkLFxuIFx0XHRcdGw6IGZhbHNlLFxuIFx0XHRcdGV4cG9ydHM6IHt9XG4gXHRcdH07XG5cbiBcdFx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG4gXHRcdG1vZHVsZXNbbW9kdWxlSWRdLmNhbGwobW9kdWxlLmV4cG9ydHMsIG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG4gXHRcdC8vIEZsYWcgdGhlIG1vZHVsZSBhcyBsb2FkZWRcbiBcdFx0bW9kdWxlLmwgPSB0cnVlO1xuXG4gXHRcdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG4gXHRcdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbiBcdH1cblxuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZXMgb2JqZWN0IChfX3dlYnBhY2tfbW9kdWxlc19fKVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5tID0gbW9kdWxlcztcblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGUgY2FjaGVcbiBcdF9fd2VicGFja19yZXF1aXJlX18uYyA9IGluc3RhbGxlZE1vZHVsZXM7XG5cbiBcdC8vIGRlZmluZSBnZXR0ZXIgZnVuY3Rpb24gZm9yIGhhcm1vbnkgZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kID0gZnVuY3Rpb24oZXhwb3J0cywgbmFtZSwgZ2V0dGVyKSB7XG4gXHRcdGlmKCFfX3dlYnBhY2tfcmVxdWlyZV9fLm8oZXhwb3J0cywgbmFtZSkpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgbmFtZSwgeyBlbnVtZXJhYmxlOiB0cnVlLCBnZXQ6IGdldHRlciB9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRpZih0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2wudG9TdHJpbmdUYWcpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgU3ltYm9sLnRvU3RyaW5nVGFnLCB7IHZhbHVlOiAnTW9kdWxlJyB9KTtcbiBcdFx0fVxuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gY3JlYXRlIGEgZmFrZSBuYW1lc3BhY2Ugb2JqZWN0XG4gXHQvLyBtb2RlICYgMTogdmFsdWUgaXMgYSBtb2R1bGUgaWQsIHJlcXVpcmUgaXRcbiBcdC8vIG1vZGUgJiAyOiBtZXJnZSBhbGwgcHJvcGVydGllcyBvZiB2YWx1ZSBpbnRvIHRoZSBuc1xuIFx0Ly8gbW9kZSAmIDQ6IHJldHVybiB2YWx1ZSB3aGVuIGFscmVhZHkgbnMgb2JqZWN0XG4gXHQvLyBtb2RlICYgOHwxOiBiZWhhdmUgbGlrZSByZXF1aXJlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnQgPSBmdW5jdGlvbih2YWx1ZSwgbW9kZSkge1xuIFx0XHRpZihtb2RlICYgMSkgdmFsdWUgPSBfX3dlYnBhY2tfcmVxdWlyZV9fKHZhbHVlKTtcbiBcdFx0aWYobW9kZSAmIDgpIHJldHVybiB2YWx1ZTtcbiBcdFx0aWYoKG1vZGUgJiA0KSAmJiB0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmIHZhbHVlICYmIHZhbHVlLl9fZXNNb2R1bGUpIHJldHVybiB2YWx1ZTtcbiBcdFx0dmFyIG5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yKG5zKTtcbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KG5zLCAnZGVmYXVsdCcsIHsgZW51bWVyYWJsZTogdHJ1ZSwgdmFsdWU6IHZhbHVlIH0pO1xuIFx0XHRpZihtb2RlICYgMiAmJiB0eXBlb2YgdmFsdWUgIT0gJ3N0cmluZycpIGZvcih2YXIga2V5IGluIHZhbHVlKSBfX3dlYnBhY2tfcmVxdWlyZV9fLmQobnMsIGtleSwgZnVuY3Rpb24oa2V5KSB7IHJldHVybiB2YWx1ZVtrZXldOyB9LmJpbmQobnVsbCwga2V5KSk7XG4gXHRcdHJldHVybiBucztcbiBcdH07XG5cbiBcdC8vIGdldERlZmF1bHRFeHBvcnQgZnVuY3Rpb24gZm9yIGNvbXBhdGliaWxpdHkgd2l0aCBub24taGFybW9ueSBtb2R1bGVzXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm4gPSBmdW5jdGlvbihtb2R1bGUpIHtcbiBcdFx0dmFyIGdldHRlciA9IG1vZHVsZSAmJiBtb2R1bGUuX19lc01vZHVsZSA/XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0RGVmYXVsdCgpIHsgcmV0dXJuIG1vZHVsZVsnZGVmYXVsdCddOyB9IDpcbiBcdFx0XHRmdW5jdGlvbiBnZXRNb2R1bGVFeHBvcnRzKCkgeyByZXR1cm4gbW9kdWxlOyB9O1xuIFx0XHRfX3dlYnBhY2tfcmVxdWlyZV9fLmQoZ2V0dGVyLCAnYScsIGdldHRlcik7XG4gXHRcdHJldHVybiBnZXR0ZXI7XG4gXHR9O1xuXG4gXHQvLyBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGxcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubyA9IGZ1bmN0aW9uKG9iamVjdCwgcHJvcGVydHkpIHsgcmV0dXJuIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChvYmplY3QsIHByb3BlcnR5KTsgfTtcblxuIFx0Ly8gX193ZWJwYWNrX3B1YmxpY19wYXRoX19cbiBcdF9fd2VicGFja19yZXF1aXJlX18ucCA9IFwiLi9qcy9cIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi90cy9yZW1vdGUudHNcIik7XG4iLCJcbmFkZEV2ZW50TGlzdGVuZXIoJ21lc3NhZ2UnLCBlID0+IHtcblx0Y29uc29sZS5kZWJ1ZygnbWVzc2FnZScsIGUuZGF0YSlcbn0pXG5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGUgPT4ge1xuXHRjb25zdCB0YXJnZXQgPSBlLnRhcmdldCBhcyBIVE1MQnV0dG9uRWxlbWVudFxuXHRjb25zdCBkYXRhID0gKHRhcmdldC5kYXRhc2V0LnNlbmQgfHwgJycpLnNwbGl0KCcgJykubWFwKGhleCA9PiBwYXJzZUludChoZXgsIDE2KSlcblx0Y29uc3QgYXBwID0gY2hyb21lWydhcHAnXS53aW5kb3cuZ2V0KCdhcHAnKS5jb250ZW50V2luZG93XG5cdGZvciAobGV0IGk9MDsgaTwxOyBpKyspIHtcblx0XHRzZXRUaW1lb3V0KGZ1bmN0aW9uKCkge1xuXHRcdFx0YXBwLnBvc3RNZXNzYWdlKG5ldyBVaW50OEFycmF5KFsyLCAuLi5kYXRhXSkpXG5cdFx0fSwgaSAqIDEwKVxuXHR9XG59KVxuLy8gbGV0IHN0YXRlID0gZmFsc2Vcbi8vIGFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCBlID0+IHtcbi8vIFx0aWYgKHN0YXRlKSB7XG4vLyBcdFx0ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ29mZicpLmNsaWNrKClcbi8vIFx0fSBlbHNlIHtcbi8vIFx0XHRkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZ3JlZW4nKS5jbGljaygpXG4vLyBcdH1cbi8vIFx0c3RhdGUgPSAhc3RhdGVcbi8vIH0pXG5hZGRFdmVudExpc3RlbmVyKCdsb2FkJywgZSA9PiB7XG5cdGRvY3VtZW50LmJvZHkuZm9jdXMoKVxufSlcbiJdLCJzb3VyY2VSb290IjoiIn0=
--------------------------------------------------------------------------------
/app/js/utils.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "./js/";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/utils.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "./ts/utils.ts":
90 | /*!*********************!*\
91 | !*** ./ts/utils.ts ***!
92 | \*********************/
93 | /*! no static exports found */
94 | /***/ (function(module, exports) {
95 |
96 | const $txt = document.getElementById('txt');
97 | const $hex = document.getElementById('hex');
98 | const $dec = document.getElementById('dec');
99 | const $bin = document.getElementById('bin');
100 | const $int = document.getElementById('int');
101 | const $res = document.getElementById('res');
102 | const $array = document.getElementById('arr');
103 | const $message = document.getElementById('message');
104 | const $send = document.getElementById('send');
105 | chrome.storage.local.get('utils_txt', ({ utils_txt: codes }) => {
106 | if (!codes)
107 | return;
108 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ');
109 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ');
110 | $dec.innerHTML = codes.map(c => c.toString()).join(' ');
111 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('');
112 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ');
113 | });
114 | let codes = [];
115 | addEventListener('input', (e) => {
116 | let value = e.target.innerText.replace(/\s+/, '');
117 | switch (e.target.id) {
118 | case 'txt':
119 | codes = value ? value.split('').map(c => c.charCodeAt(0)) : [];
120 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ');
121 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ');
122 | $dec.innerHTML = codes.map(c => c.toString()).join(' ');
123 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ');
124 | break;
125 | case 'hex':
126 | value = e.target.innerText.replace(/[^0-9a-f ]/ig, '').trim();
127 | codes = value ? value.split(' ').map(c => parseInt(c, 16)) : [];
128 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ');
129 | $dec.innerHTML = codes.map(c => c.toString()).join(' ');
130 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('');
131 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ');
132 | break;
133 | case 'dec':
134 | value = e.target.innerText.replace(/[^0-9 ]/ig, '').trim();
135 | codes = value ? value.split(' ').map(c => parseInt(c, 10)) : [];
136 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ');
137 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ');
138 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('');
139 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ');
140 | break;
141 | case 'bin':
142 | value = e.target.innerText.replace(/[^01 ]/ig, '').trim();
143 | codes = value ? value.split(' ').map(c => parseInt(c, 2)) : [];
144 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ');
145 | $dec.innerHTML = codes.map(c => c.toString()).join(' ');
146 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('');
147 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ');
148 | break;
149 | case 'int':
150 | let res = '\n';
151 | value = e.target.innerText.replace(/[^0-9]/ig, '').trim();
152 | if (value) {
153 | const num = parseFloat(value);
154 | const buf = new Uint8Array(4);
155 | const view = new DataView(buf.buffer);
156 | view.setUint16(0, num);
157 | res += 'U16 BE = ' + toHEX(buf.slice(0, 2));
158 | view.setUint16(0, num, true);
159 | res += 'U16 LE = ' + toHEX(buf.slice(0, 2));
160 | view.setUint32(0, num);
161 | res += 'U32 BE = ' + toHEX(buf);
162 | view.setUint32(0, num, true);
163 | res += 'U32 LE = ' + toHEX(buf);
164 | view.setFloat32(0, num);
165 | res += 'F32 BE = ' + toHEX(buf);
166 | view.setFloat32(0, num, true);
167 | res += 'F32 LE = ' + toHEX(buf);
168 | }
169 | $res.innerHTML = res;
170 | break;
171 | default: return;
172 | }
173 | chrome.storage.local.set({ 'utils_txt': codes });
174 | });
175 | function toHEX(bytes) {
176 | return [...bytes].map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') + '\n';
177 | }
178 | $send.addEventListener('click', e => {
179 | chrome['app'].window.get('app').contentWindow.postMessage(new Uint8Array([1, ...codes]));
180 | });
181 |
182 |
183 | /***/ })
184 |
185 | /******/ });
186 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtRQUFBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBOzs7UUFHQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0EsMENBQTBDLGdDQUFnQztRQUMxRTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLHdEQUF3RCxrQkFBa0I7UUFDMUU7UUFDQSxpREFBaUQsY0FBYztRQUMvRDs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0EseUNBQXlDLGlDQUFpQztRQUMxRSxnSEFBZ0gsbUJBQW1CLEVBQUU7UUFDckk7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSwyQkFBMkIsMEJBQTBCLEVBQUU7UUFDdkQsaUNBQWlDLGVBQWU7UUFDaEQ7UUFDQTtRQUNBOztRQUVBO1FBQ0Esc0RBQXNELCtEQUErRDs7UUFFckg7UUFDQTs7O1FBR0E7UUFDQTs7Ozs7Ozs7Ozs7O0FDbEZBLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFzQjtBQUNoRSxNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBc0I7QUFDaEUsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQXNCO0FBQ2hFLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFzQjtBQUNoRSxNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBc0I7QUFDaEUsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQXNCO0FBQ2hFLE1BQU0sTUFBTSxHQUFHLFFBQVEsQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFzQjtBQUNsRSxNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsY0FBYyxDQUFDLFNBQVMsQ0FBc0I7QUFDeEUsTUFBTSxLQUFLLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQXNCO0FBRWxFLE1BQU0sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxFQUFFLFNBQVMsRUFBQyxLQUFLLEVBQUUsRUFBRSxFQUFFO0lBQzdELElBQUksQ0FBQyxLQUFLO1FBQUUsT0FBTTtJQUNsQixJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO0lBQ3ZGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7SUFDeEYsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztJQUN2RCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztJQUNoRSxNQUFNLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQztBQUNuRyxDQUFDLENBQUM7QUFDRixJQUFJLEtBQUssR0FBYSxFQUFFO0FBQ3hCLGdCQUFnQixDQUFDLE9BQU8sRUFBRSxDQUFDLENBQU0sRUFBRSxFQUFFO0lBQ3BDLElBQUksS0FBSyxHQUFXLENBQUMsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDO0lBQ3pELFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUU7UUFDcEIsS0FBSyxLQUFLO1lBQ1QsS0FBSyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUU7WUFDOUQsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztZQUN2RixJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3hGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDdkQsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsY0FBYyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRTtZQUM3RCxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRTtZQUMvRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3ZGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDdkQsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDaEUsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRTtZQUMxRCxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRTtZQUMvRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3ZGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDeEYsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDaEUsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRTtZQUN6RCxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRTtZQUM5RCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3hGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDdkQsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDaEUsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULElBQUksR0FBRyxHQUFHLElBQUk7WUFDZCxLQUFLLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUU7WUFDekQsSUFBSSxLQUFLLEVBQUU7Z0JBQ1YsTUFBTSxHQUFHLEdBQUcsVUFBVSxDQUFDLEtBQUssQ0FBQztnQkFDN0IsTUFBTSxHQUFHLEdBQUcsSUFBSSxVQUFVLENBQUMsQ0FBQyxDQUFDO2dCQUM3QixNQUFNLElBQUksR0FBRyxJQUFJLFFBQVEsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDO2dCQUVyQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUM7Z0JBQ3RCLEdBQUcsSUFBSSxXQUFXLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO2dCQUMzQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxHQUFHLEVBQUUsSUFBSSxDQUFDO2dCQUM1QixHQUFHLElBQUksV0FBVyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztnQkFFM0MsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDO2dCQUN0QixHQUFHLElBQUksV0FBVyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUM7Z0JBQy9CLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxFQUFFLEdBQUcsRUFBRSxJQUFJLENBQUM7Z0JBQzVCLEdBQUcsSUFBSSxXQUFXLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQztnQkFFL0IsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDO2dCQUN2QixHQUFHLElBQUksV0FBVyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUM7Z0JBQy9CLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQyxFQUFFLEdBQUcsRUFBRSxJQUFJLENBQUM7Z0JBQzdCLEdBQUcsSUFBSSxXQUFXLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQzthQUMvQjtZQUNELElBQUksQ0FBQyxTQUFTLEdBQUcsR0FBRztZQUNwQixNQUFLO1FBQ04sT0FBTyxDQUFDLENBQUMsT0FBTTtLQUNmO0lBQ0QsTUFBTSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxDQUFDO0FBQ2pELENBQUMsQ0FBQztBQUVGLFNBQVMsS0FBSyxDQUFDLEtBQWlCO0lBQy9CLE9BQU8sQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxJQUFJO0FBQzNGLENBQUM7QUFFRCxLQUFLLENBQUMsZ0JBQWdCLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxFQUFFO0lBQ25DLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLGFBQWEsQ0FBQyxXQUFXLENBQUMsSUFBSSxVQUFVLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQ3pGLENBQUMsQ0FBQyIsImZpbGUiOiJ1dGlscy5qcyIsInNvdXJjZXNDb250ZW50IjpbIiBcdC8vIFRoZSBtb2R1bGUgY2FjaGVcbiBcdHZhciBpbnN0YWxsZWRNb2R1bGVzID0ge307XG5cbiBcdC8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG4gXHRmdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cbiBcdFx0Ly8gQ2hlY2sgaWYgbW9kdWxlIGlzIGluIGNhY2hlXG4gXHRcdGlmKGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdKSB7XG4gXHRcdFx0cmV0dXJuIGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdLmV4cG9ydHM7XG4gXHRcdH1cbiBcdFx0Ly8gQ3JlYXRlIGEgbmV3IG1vZHVsZSAoYW5kIHB1dCBpdCBpbnRvIHRoZSBjYWNoZSlcbiBcdFx0dmFyIG1vZHVsZSA9IGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdID0ge1xuIFx0XHRcdGk6IG1vZHVsZUlkLFxuIFx0XHRcdGw6IGZhbHNlLFxuIFx0XHRcdGV4cG9ydHM6IHt9XG4gXHRcdH07XG5cbiBcdFx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG4gXHRcdG1vZHVsZXNbbW9kdWxlSWRdLmNhbGwobW9kdWxlLmV4cG9ydHMsIG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG4gXHRcdC8vIEZsYWcgdGhlIG1vZHVsZSBhcyBsb2FkZWRcbiBcdFx0bW9kdWxlLmwgPSB0cnVlO1xuXG4gXHRcdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG4gXHRcdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbiBcdH1cblxuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZXMgb2JqZWN0IChfX3dlYnBhY2tfbW9kdWxlc19fKVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5tID0gbW9kdWxlcztcblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGUgY2FjaGVcbiBcdF9fd2VicGFja19yZXF1aXJlX18uYyA9IGluc3RhbGxlZE1vZHVsZXM7XG5cbiBcdC8vIGRlZmluZSBnZXR0ZXIgZnVuY3Rpb24gZm9yIGhhcm1vbnkgZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kID0gZnVuY3Rpb24oZXhwb3J0cywgbmFtZSwgZ2V0dGVyKSB7XG4gXHRcdGlmKCFfX3dlYnBhY2tfcmVxdWlyZV9fLm8oZXhwb3J0cywgbmFtZSkpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgbmFtZSwgeyBlbnVtZXJhYmxlOiB0cnVlLCBnZXQ6IGdldHRlciB9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRpZih0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2wudG9TdHJpbmdUYWcpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgU3ltYm9sLnRvU3RyaW5nVGFnLCB7IHZhbHVlOiAnTW9kdWxlJyB9KTtcbiBcdFx0fVxuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gY3JlYXRlIGEgZmFrZSBuYW1lc3BhY2Ugb2JqZWN0XG4gXHQvLyBtb2RlICYgMTogdmFsdWUgaXMgYSBtb2R1bGUgaWQsIHJlcXVpcmUgaXRcbiBcdC8vIG1vZGUgJiAyOiBtZXJnZSBhbGwgcHJvcGVydGllcyBvZiB2YWx1ZSBpbnRvIHRoZSBuc1xuIFx0Ly8gbW9kZSAmIDQ6IHJldHVybiB2YWx1ZSB3aGVuIGFscmVhZHkgbnMgb2JqZWN0XG4gXHQvLyBtb2RlICYgOHwxOiBiZWhhdmUgbGlrZSByZXF1aXJlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnQgPSBmdW5jdGlvbih2YWx1ZSwgbW9kZSkge1xuIFx0XHRpZihtb2RlICYgMSkgdmFsdWUgPSBfX3dlYnBhY2tfcmVxdWlyZV9fKHZhbHVlKTtcbiBcdFx0aWYobW9kZSAmIDgpIHJldHVybiB2YWx1ZTtcbiBcdFx0aWYoKG1vZGUgJiA0KSAmJiB0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmIHZhbHVlICYmIHZhbHVlLl9fZXNNb2R1bGUpIHJldHVybiB2YWx1ZTtcbiBcdFx0dmFyIG5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yKG5zKTtcbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KG5zLCAnZGVmYXVsdCcsIHsgZW51bWVyYWJsZTogdHJ1ZSwgdmFsdWU6IHZhbHVlIH0pO1xuIFx0XHRpZihtb2RlICYgMiAmJiB0eXBlb2YgdmFsdWUgIT0gJ3N0cmluZycpIGZvcih2YXIga2V5IGluIHZhbHVlKSBfX3dlYnBhY2tfcmVxdWlyZV9fLmQobnMsIGtleSwgZnVuY3Rpb24oa2V5KSB7IHJldHVybiB2YWx1ZVtrZXldOyB9LmJpbmQobnVsbCwga2V5KSk7XG4gXHRcdHJldHVybiBucztcbiBcdH07XG5cbiBcdC8vIGdldERlZmF1bHRFeHBvcnQgZnVuY3Rpb24gZm9yIGNvbXBhdGliaWxpdHkgd2l0aCBub24taGFybW9ueSBtb2R1bGVzXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm4gPSBmdW5jdGlvbihtb2R1bGUpIHtcbiBcdFx0dmFyIGdldHRlciA9IG1vZHVsZSAmJiBtb2R1bGUuX19lc01vZHVsZSA/XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0RGVmYXVsdCgpIHsgcmV0dXJuIG1vZHVsZVsnZGVmYXVsdCddOyB9IDpcbiBcdFx0XHRmdW5jdGlvbiBnZXRNb2R1bGVFeHBvcnRzKCkgeyByZXR1cm4gbW9kdWxlOyB9O1xuIFx0XHRfX3dlYnBhY2tfcmVxdWlyZV9fLmQoZ2V0dGVyLCAnYScsIGdldHRlcik7XG4gXHRcdHJldHVybiBnZXR0ZXI7XG4gXHR9O1xuXG4gXHQvLyBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGxcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubyA9IGZ1bmN0aW9uKG9iamVjdCwgcHJvcGVydHkpIHsgcmV0dXJuIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChvYmplY3QsIHByb3BlcnR5KTsgfTtcblxuIFx0Ly8gX193ZWJwYWNrX3B1YmxpY19wYXRoX19cbiBcdF9fd2VicGFja19yZXF1aXJlX18ucCA9IFwiLi9qcy9cIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi90cy91dGlscy50c1wiKTtcbiIsImNvbnN0ICR0eHQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgndHh0JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRoZXggPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnaGV4JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRkZWMgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZGVjJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRiaW4gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnYmluJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRpbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnaW50JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRyZXMgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncmVzJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRhcnJheSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhcnInKSBhcyBIVE1MT3V0cHV0RWxlbWVudFxuY29uc3QgJG1lc3NhZ2UgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWVzc2FnZScpIGFzIEhUTUxPdXRwdXRFbGVtZW50XG5jb25zdCAkc2VuZCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdzZW5kJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcblxuY2hyb21lLnN0b3JhZ2UubG9jYWwuZ2V0KCd1dGlsc190eHQnLCAoeyB1dGlsc190eHQ6Y29kZXMgfSkgPT4ge1xuXHRpZiAoIWNvZGVzKSByZXR1cm5cblx0JGJpbi5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDIpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoOCwgJzAnKSkuam9pbignICcpXG5cdCRoZXguaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJylcblx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHQkdHh0LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IFN0cmluZy5mcm9tQ2hhckNvZGUoYykpLmpvaW4oJycpXG5cdCRhcnJheS5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiAnMHgnICsgYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcsICcpXG59KVxubGV0IGNvZGVzOiBudW1iZXJbXSA9IFtdXG5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsIChlOiBhbnkpID0+IHtcblx0bGV0IHZhbHVlOiBzdHJpbmcgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvXFxzKy8sICcnKVxuXHRzd2l0Y2ggKGUudGFyZ2V0LmlkKSB7XG5cdFx0Y2FzZSAndHh0Jzpcblx0XHRcdGNvZGVzID0gdmFsdWUgPyB2YWx1ZS5zcGxpdCgnJykubWFwKGMgPT4gYy5jaGFyQ29kZUF0KDApKSA6IFtdXG5cdFx0XHQkYmluLmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IGMudG9TdHJpbmcoMikudG9VcHBlckNhc2UoKS5wYWRTdGFydCg4LCAnMCcpKS5qb2luKCcgJylcblx0XHRcdCRoZXguaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJylcblx0XHRcdCRkZWMuaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygpKS5qb2luKCcgJylcblx0XHRcdCRhcnJheS5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiAnMHgnICsgYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcsICcpXG5cdFx0XHRicmVha1xuXHRcdGNhc2UgJ2hleCc6XG5cdFx0XHR2YWx1ZSA9IGUudGFyZ2V0LmlubmVyVGV4dC5yZXBsYWNlKC9bXjAtOWEtZiBdL2lnLCAnJykudHJpbSgpXG5cdFx0XHRjb2RlcyA9IHZhbHVlID8gdmFsdWUuc3BsaXQoJyAnKS5tYXAoYyA9PiBwYXJzZUludChjLCAxNikpIDogW11cblx0XHRcdCRiaW4uaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygyKS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDgsICcwJykpLmpvaW4oJyAnKVxuXHRcdFx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHRcdFx0JHR4dC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBTdHJpbmcuZnJvbUNoYXJDb2RlKGMpKS5qb2luKCcnKVxuXHRcdFx0JGFycmF5LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+ICcweCcgKyBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJywgJylcblx0XHRcdGJyZWFrXG5cdFx0Y2FzZSAnZGVjJzpcblx0XHRcdHZhbHVlID0gZS50YXJnZXQuaW5uZXJUZXh0LnJlcGxhY2UoL1teMC05IF0vaWcsICcnKS50cmltKClcblx0XHRcdGNvZGVzID0gdmFsdWUgPyB2YWx1ZS5zcGxpdCgnICcpLm1hcChjID0+IHBhcnNlSW50KGMsIDEwKSkgOiBbXVxuXHRcdFx0JGJpbi5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDIpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoOCwgJzAnKSkuam9pbignICcpXG5cdFx0XHQkaGV4LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IGMudG9TdHJpbmcoMTYpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoMiwgJzAnKSkuam9pbignICcpXG5cdFx0XHQkdHh0LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IFN0cmluZy5mcm9tQ2hhckNvZGUoYykpLmpvaW4oJycpXG5cdFx0XHQkYXJyYXkuaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gJzB4JyArIGMudG9TdHJpbmcoMTYpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoMiwgJzAnKSkuam9pbignLCAnKVxuXHRcdFx0YnJlYWtcblx0XHRjYXNlICdiaW4nOlxuXHRcdFx0dmFsdWUgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvW14wMSBdL2lnLCAnJykudHJpbSgpXG5cdFx0XHRjb2RlcyA9IHZhbHVlID8gdmFsdWUuc3BsaXQoJyAnKS5tYXAoYyA9PiBwYXJzZUludChjLCAyKSkgOiBbXVxuXHRcdFx0JGhleC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJyAnKVxuXHRcdFx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHRcdFx0JHR4dC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBTdHJpbmcuZnJvbUNoYXJDb2RlKGMpKS5qb2luKCcnKVxuXHRcdFx0JGFycmF5LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+ICcweCcgKyBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJywgJylcblx0XHRcdGJyZWFrXG5cdFx0Y2FzZSAnaW50Jzpcblx0XHRcdGxldCByZXMgPSAnXFxuJ1xuXHRcdFx0dmFsdWUgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvW14wLTldL2lnLCAnJykudHJpbSgpXG5cdFx0XHRpZiAodmFsdWUpIHtcblx0XHRcdFx0Y29uc3QgbnVtID0gcGFyc2VGbG9hdCh2YWx1ZSlcblx0XHRcdFx0Y29uc3QgYnVmID0gbmV3IFVpbnQ4QXJyYXkoNClcblx0XHRcdFx0Y29uc3QgdmlldyA9IG5ldyBEYXRhVmlldyhidWYuYnVmZmVyKVxuXG5cdFx0XHRcdHZpZXcuc2V0VWludDE2KDAsIG51bSlcblx0XHRcdFx0cmVzICs9ICdVMTYgQkUgPSAnICsgdG9IRVgoYnVmLnNsaWNlKDAsIDIpKVxuXHRcdFx0XHR2aWV3LnNldFVpbnQxNigwLCBudW0sIHRydWUpXG5cdFx0XHRcdHJlcyArPSAnVTE2IExFID0gJyArIHRvSEVYKGJ1Zi5zbGljZSgwLCAyKSlcblxuXHRcdFx0XHR2aWV3LnNldFVpbnQzMigwLCBudW0pXG5cdFx0XHRcdHJlcyArPSAnVTMyIEJFID0gJyArIHRvSEVYKGJ1Zilcblx0XHRcdFx0dmlldy5zZXRVaW50MzIoMCwgbnVtLCB0cnVlKVxuXHRcdFx0XHRyZXMgKz0gJ1UzMiBMRSA9ICcgKyB0b0hFWChidWYpXG5cblx0XHRcdFx0dmlldy5zZXRGbG9hdDMyKDAsIG51bSlcblx0XHRcdFx0cmVzICs9ICdGMzIgQkUgPSAnICsgdG9IRVgoYnVmKVxuXHRcdFx0XHR2aWV3LnNldEZsb2F0MzIoMCwgbnVtLCB0cnVlKVxuXHRcdFx0XHRyZXMgKz0gJ0YzMiBMRSA9ICcgKyB0b0hFWChidWYpXG5cdFx0XHR9XG5cdFx0XHQkcmVzLmlubmVySFRNTCA9IHJlc1xuXHRcdFx0YnJlYWtcblx0XHRkZWZhdWx0OiByZXR1cm5cblx0fVxuXHRjaHJvbWUuc3RvcmFnZS5sb2NhbC5zZXQoeyAndXRpbHNfdHh0JzogY29kZXMgfSlcbn0pXG5cbmZ1bmN0aW9uIHRvSEVYKGJ5dGVzOiBVaW50OEFycmF5KTogc3RyaW5nIHtcblx0cmV0dXJuIFsuLi5ieXRlc10ubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJykgKyAnXFxuJ1xufVxuXG4kc2VuZC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGUgPT4ge1xuXHRjaHJvbWVbJ2FwcCddLndpbmRvdy5nZXQoJ2FwcCcpLmNvbnRlbnRXaW5kb3cucG9zdE1lc3NhZ2UobmV3IFVpbnQ4QXJyYXkoWzEsIC4uLmNvZGVzXSkpXG59KVxuIl0sInNvdXJjZVJvb3QiOiIifQ==
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SDC",
3 | "description": "Light composer and player",
4 | "version": "2.1.2",
5 | "manifest_version": 2,
6 | "app": {
7 | "background": {
8 | "scripts": [
9 | "js/background.js"
10 | ]
11 | }
12 | },
13 | "icons": {
14 | "128": "img/icon.png"
15 | },
16 | "permissions": [
17 | "usb",
18 | "serial",
19 | "storage",
20 | {
21 | "fileSystem": [
22 | "write",
23 | "retainEntries",
24 | "directory"
25 | ]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@mdi/font": "^5.9.55",
4 | "jquery": "^3.5.1",
5 | "jquery-slim": "^3.0.0",
6 | "jszip": "^3.7.0",
7 | "lodash": "^4.17.21",
8 | "material-design-icons": "^3.0.1",
9 | "micromodal": "^0.4.10",
10 | "music-tempo": "^1.0.3",
11 | "style-loader": "^3.0.0",
12 | "styled-components": "^5.2.0",
13 | "unzip": "^0.1.11",
14 | "unzip-js": "^1.0.0",
15 | "url-loader": "^4.1.1",
16 | "webpack": "^4.44.1"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.11.6",
20 | "@types/chrome": "^0.0.125",
21 | "@types/jquery": "^3.5.4",
22 | "@types/unzip": "^0.1.1",
23 | "babel-loader": "^8.1.0",
24 | "css-loader": "^4.3.0",
25 | "file-loader": "^6.2.0",
26 | "ts-loader": "^8.0.5",
27 | "typescript": "^4.5.4",
28 | "webpack-cli": "^3.3.12"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/remote.html:
--------------------------------------------------------------------------------
1 |
2 | Utils
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/ts/actions.ts:
--------------------------------------------------------------------------------
1 | import { sendCommand, sendFile, sendSync, serialConnect, serialDisconnect } from './serial'
2 | export * from './serial'
3 | import {
4 | set,
5 | store,
6 | cache,
7 | openShowEntry,
8 | saveShowEntry,
9 | convertTracks,
10 | DEFAULT_STORE_DATA
11 | } from './store'
12 | import { $player, $tracks } from './view'
13 |
14 | export { logHex } from './store'
15 |
16 | export async function clear() {
17 | $player.src = ''
18 | cache.audio = null
19 | cache.show = null
20 | await set({ ...DEFAULT_STORE_DATA })
21 | }
22 | export async function save() {
23 | return new Promise(async (resolve) => {
24 | if (store.file) {
25 | chrome['fileSystem'].restoreEntry(store.file, async (entry: FileEntry) => {
26 | if (entry) await saveShowEntry(entry)
27 | resolve(null)
28 | })
29 | } else await saveAs()
30 | })
31 | }
32 | export async function saveAs() {
33 | return new Promise(async (resolve) => {
34 | chrome['fileSystem'].chooseEntry({ type: 'saveFile' }, async (entry: FileEntry) => {
35 | if (entry) await saveShowEntry(entry)
36 | resolve(null)
37 | })
38 | })
39 | }
40 |
41 | export async function open() {
42 | return new Promise((resolve) => {
43 | chrome['fileSystem'].chooseEntry({ type: 'openWritableFile' }, async (entry: FileEntry) => {
44 | if (entry) {
45 | await clear()
46 | await openShowEntry(entry)
47 | if (cache.audio) {
48 | $player.src = URL.createObjectURL(cache.audio)
49 | console.debug('set player src')
50 | }
51 | resolve(null)
52 | } else {
53 | resolve(null)
54 | }
55 | })
56 | })
57 | }
58 | export async function bind() {
59 | await sendCommand('#', 'PAIR')
60 | }
61 | export async function reset() {
62 | await sendCommand('#', 'RESET')
63 | }
64 | export async function restart() {
65 | await sendCommand('#', 'RESTART')
66 | }
67 | export async function play() {
68 | if (!store.connected) {
69 | await serialConnect()
70 | }
71 | await set('sync', true)
72 | $player.play()
73 | }
74 | export async function stop() {
75 | $player.currentTime = 0
76 | $player.pause()
77 | $player.src = $player.src
78 | await set({ sync: false, time: 0, ended: true, paused: false })
79 | await sendCommand('#', 'RESTART')
80 | }
81 | export async function connect() {
82 | if (store.connected)
83 | await serialConnect()
84 | else
85 | await serialDisconnect()
86 | }
87 | export async function openUtils() {
88 | chrome['app'].window.create('../utils.html', {
89 | id: 'utils',
90 | width: 270,
91 | height: 400
92 | })
93 | }
94 | export async function openRemote() {
95 | await stop()
96 | chrome['app'].window.create('../remote.html', {
97 | id: 'remote',
98 | width: 250,
99 | height: 250
100 | })
101 | }
102 | export async function sendToChannelA() {
103 | await uploadShowHandle('A')
104 | // await sendCommand('#', 'RESTART')
105 | }
106 | export async function sendToChannelB() {
107 | await uploadShowHandle('B')
108 | // await sendCommand('#', 'RESTART')
109 | }
110 | export async function sendToChannelAB() {
111 | await uploadShowHandle('A')
112 | await uploadShowHandle('B')
113 | // await sendCommand('#', 'RESTART')
114 | }
115 | export async function uploadShowHandle(channel) {
116 | let index = 0
117 | const $selected: HTMLElement = $tracks.querySelector('.selected')
118 | if ($selected) {
119 | index = parseInt($selected.dataset.index, 10)
120 | }
121 | await uploadFile(createLightShowBinaryFile(index), `/show/${store.slot}${channel}.lsb`)
122 | console.info('upload completed.', index, store.slot, channel)
123 | }
124 |
125 | const NODE_ADDR = '10.1.1.1'
126 | const NODE_PORT = '11111'
127 |
128 | export async function uploadFile(file: Blob, path: string) {
129 | return new Promise((resolve, reject) => {
130 | if (!file || !path) throw 'Missing file or path'
131 | if (!path.startsWith("/")) path = "/" + path;
132 | let req = new XMLHttpRequest();
133 | var form = new FormData();
134 | form.append("data", file, path);
135 | // req.timeout = 5000
136 | req.open("POST", `http://${NODE_ADDR}:${NODE_PORT}/edit`, true);
137 | req.send(form);
138 | const done = () => setTimeout(() => resolve(null), 500)
139 | const fail = (err:any) => setTimeout(() => reject(err), 500)
140 | req.addEventListener('load', done)
141 | req.addEventListener('loadend', done)
142 | req.addEventListener('abort', done)
143 | req.addEventListener('error', fail)
144 | req.addEventListener('timeout', fail)
145 | // setTimeout(() => {
146 | // if (req.readyState !== req.DONE) {
147 | // req.abort()
148 | // }
149 | // }, 5000)
150 | })
151 | }
152 |
153 | export function createLightShowBinaryFile(trackIndex: number) {
154 | return new Blob(convertTracks(store.tracks[trackIndex].frames))
155 | }
156 |
--------------------------------------------------------------------------------
/app/ts/app.ts:
--------------------------------------------------------------------------------
1 |
2 | import { init, set, store, parseAudioFile, parseShowFile, cache, openShowEntry } from './store'
3 | import { $player, $tracks, renderBeats, renderSerial, renderTimeline, renderTracks, renderWaveform, updateSize, updateTime } from './view'
4 | import { encodeMsg, sendRaw, sendSync } from './serial'
5 | import { serialConnect } from './serial'
6 | import * as actions from './actions'
7 | import $ from 'jquery'
8 | import { openRemote, openUtils } from './actions'
9 |
10 | Object.assign(window, actions)
11 | window['player'] = $player
12 |
13 | function update() {
14 | document.querySelectorAll('[data-key]').forEach((control: any) => {
15 | const key = control.dataset.key
16 | if ((control instanceof HTMLInputElement) || (control instanceof HTMLSelectElement)) {
17 | if (control.type === 'checkbox') {
18 | control['checked'] = store[key]
19 | } else
20 | control.value = store[key]
21 | } else {
22 | if (control instanceof HTMLButtonElement) {
23 | let selected = !!store[key]
24 | if (control.hasAttribute('value')) {
25 | selected = store[key] == control.getAttribute('value')
26 | }
27 | control.classList[selected ? 'add' : 'remove']('selected')
28 | }
29 | }
30 | })
31 | }
32 | chrome.serial.onReceiveError.addListener(({ connectionId: id }) => {
33 | if (id === store.serial_connection) {
34 | const current = chrome.app.window.current()
35 | chrome.app.window.getAll().forEach(view => {
36 | if (view !== current) view.close()
37 | })
38 | }
39 | })
40 |
41 | chrome.storage.onChanged.addListener(async (changes) => {
42 | update()
43 | if (changes.port || changes.connection || changes.connected) {
44 | await renderSerial()
45 | }
46 | if (changes.waveform || changes.beats) {
47 | await renderWaveform()
48 | await renderBeats()
49 | }
50 | if (changes.tracks) {
51 | await renderTracks()
52 | }
53 | })
54 |
55 | addEventListener('load', async () => {
56 | await init()
57 | await update()
58 | store.connected ? await serialConnect(true) : await renderSerial()
59 | await renderTracks()
60 | await renderWaveform()
61 | await renderBeats()
62 | requestAnimationFrame(function tick() {
63 | updateTime()
64 | requestAnimationFrame(tick)
65 | })
66 | setInterval(renderSerial, 1000)
67 | setInterval(() => {
68 | if (store.sync) {
69 | sendSync(store.time, store.slot, store.ended, store.paused)
70 | }
71 | }, 100)
72 | if (store.file) {
73 | chrome['fileSystem'].restoreEntry(store.file, async (entry: FileEntry) => {
74 | if (entry) {
75 | await openShowEntry(entry)
76 | if (cache.audio) {
77 | $player.src = URL.createObjectURL(cache.audio)
78 | console.debug('set player src')
79 | await renderTimeline()
80 | await renderTracks()
81 | await renderWaveform()
82 | await renderBeats()
83 | }
84 | } else {
85 | console.debug('unable to restore show entry', [store.file])
86 | await set('file', '')
87 | }
88 | })
89 | }
90 | })
91 |
92 | addEventListener('pause', async e => {
93 | store.paused = true
94 | store.ended = false
95 | }, true)
96 |
97 | addEventListener('play', async e => {
98 | store.paused = false
99 | store.ended = false
100 | }, true)
101 |
102 | addEventListener('ended', async e => {
103 | store.paused = false
104 | store.ended = true
105 | }, true)
106 |
107 | addEventListener('durationchange', async e => {
108 | await set('duration', Math.max($player.duration * 1000, store.duration))
109 | // store.duration = Math.max($player.duration * 1000, store.duration)
110 | await updateSize()
111 | }, true)
112 |
113 | addEventListener('click', async (e: MouseEvent) => {
114 | const target = e.target as HTMLElement
115 | if (target.closest('*[data-key]')) {
116 | let actionTarget = target.closest('*[data-key]') as HTMLElement
117 | if (actionTarget && actionTarget instanceof HTMLButtonElement) {
118 | const { key } = actionTarget.dataset
119 | let value:any = !store[key]
120 | if (actionTarget.hasAttribute('value')) {
121 | value = actionTarget.getAttribute('value')
122 | }
123 | await set(key, value)
124 | }
125 | }
126 | if (target.closest('*[data-action]')) {
127 | let actionTarget = target.closest('*[data-action]') as HTMLElement
128 | if (actionTarget) {
129 | const { action } = actionTarget.dataset
130 | if (typeof actions[action] === 'function') {
131 | const prevContent = actionTarget.innerHTML
132 | const prevClassNames = actionTarget.getAttribute('class')
133 | actionTarget.setAttribute('class', 'mdi mdi-loading mdi-spin')
134 | actionTarget.innerHTML = ''
135 | actionTarget.setAttribute('disabled', 'true')
136 |
137 | try {
138 | await actions[action].call(actionTarget, e)
139 | actionTarget.style.color = ''
140 | } catch(err) {
141 | actionTarget.style.color = 'red'
142 | }
143 |
144 | actionTarget.setAttribute('class', prevClassNames)
145 | actionTarget.innerHTML = prevContent
146 | actionTarget.removeAttribute('disabled')
147 | }
148 | }
149 | }
150 | })
151 |
152 | addEventListener('change', async (e) => {
153 | if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) {
154 | const { key } = e.target.dataset
155 | if (key) {
156 | if (e.target.type === 'checkbox') {
157 | await set(key, e.target['checked'])
158 | } else {
159 | await set(key, e.target.value)
160 | }
161 | }
162 | }
163 | })
164 |
165 | addEventListener('dragover', async (e) => {
166 | e.preventDefault()
167 | })
168 |
169 | addEventListener('drop', async (e: DragEvent) => {
170 | e.preventDefault()
171 | if (!e.dataTransfer) return
172 | for (let file of e.dataTransfer.files) {
173 | if (file.type.startsWith('audio/')) {
174 | const audio = await parseAudioFile(file)
175 | if (audio) {
176 | cache.audio = file
177 | $player.src = URL.createObjectURL(file)
178 | console.debug('set player src')
179 | }
180 | }
181 | if (file.name.endsWith('lt3')) {
182 | const show = await parseShowFile(file)
183 | if (show) await set(show)
184 | }
185 | }
186 | })
187 |
188 | addEventListener('wheel', (e) => {
189 | const delta = (e.deltaX + e.deltaY) / 2
190 | if ($player.src) $player.currentTime = Math.max(0, Math.min(store.duration / 1000, $player.currentTime + (delta / (e.altKey ? 200 : 10))))
191 | else store.time = Math.max(0, Math.min(store.duration, store.time + (delta * (e.altKey ? 200 : 10))))
192 | })
193 |
194 | addEventListener('mousedown', (e) => {
195 | const $track = e.target.closest('.track')
196 | if ($track) {
197 | const $selected = $track.parentElement.querySelector('.selected')
198 | if ($selected) {
199 | $selected.classList.remove('selected')
200 | }
201 | $track.classList.add('selected')
202 | $track.focus()
203 | }
204 | })
205 |
206 | addEventListener('keydown', (e) => {
207 | e.preventDefault()
208 | switch (e.key) {
209 | case ' ':
210 | store.ended || $player.paused ? actions.play() : $player.pause()
211 | // $player.paused || $player.ended ? $player.play() : $player.pause()
212 | break
213 | case 'ArrowLeft':
214 | $player.currentTime -= e.altKey ? 0.01 : 1
215 | break
216 | case 'ArrowRight':
217 | $player.currentTime += e.altKey ? 0.01 : 1
218 | break
219 | case 'ArrowUp':
220 | $player.volume = Math.min($player.volume + 0.1, 1)
221 | break
222 | case 'ArrowDown':
223 | $player.volume = Math.max($player.volume - 0.1, 0)
224 | break
225 | }
226 | })
227 |
228 | addEventListener('message', e => {
229 | const type = e.data[0]
230 | const data = e.data.slice(1)
231 | switch (type) {
232 | case 0:
233 | sendRaw(data)
234 | break
235 | case 1:
236 | sendRaw(encodeMsg(data))
237 | break
238 | case 2:
239 | sendRaw(encodeMsg([35, 62, ...data]))
240 | break
241 | }
242 | })
243 |
--------------------------------------------------------------------------------
/app/ts/background.ts:
--------------------------------------------------------------------------------
1 | chrome['app'].runtime.onLaunched.addListener(function () {
2 | chrome['app'].window.create('../app.html', {
3 | id: 'app',
4 | frame: "none",
5 | minWidth: 900,
6 | minHeight: 100
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/app/ts/global.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace chrome {
2 | const app: any
3 | const serial: any
4 | const storage: any
5 | }
6 |
7 | declare namespace window {}
8 |
9 | declare interface EventTarget extends EventTarget {
10 | closest(...args:any): any
11 | }
12 | declare interface HTMLSelectElement extends HTMLSelectElement {
13 | value: string|number
14 | }
15 |
16 | declare interface Track {
17 | [key: string]: any
18 | }
19 |
20 | declare interface TrackData {
21 | [key:string]: any
22 | }
23 |
24 | declare type WaveformData = [
25 | number, // average positive
26 | number, // average negative
27 | number, // max positive
28 | number, // max negative
29 | ]
30 |
31 | declare interface AudioData {
32 | name?: string
33 | duration?: number
34 | tempo?: number
35 | beats?: number[]
36 | waveform?: WaveformData[]
37 | }
38 |
39 | declare interface ShowData {
40 | [key:string]: any
41 | }
42 |
43 | declare interface StoreData {
44 |
45 | port: string
46 | connection: number
47 | connected: boolean
48 |
49 | sync: boolean
50 | file: string
51 | time: number
52 | slot: number
53 | channel: string
54 | duration: number
55 | paused: boolean
56 | ended: boolean
57 |
58 | tempo: number
59 | division: number
60 |
61 | beats: number[]
62 | tracks: TrackData[]
63 | waveform: WaveformData[]
64 |
65 | show: ShowData
66 | audio: AudioData
67 | }
68 |
69 | declare interface CacheData {
70 | audio: file,
71 | show: file
72 | }
73 |
74 | declare function map(value: number, fromMin: number, fromMax: number, toMin: number, toMax: number): number
75 | declare function constrain(value: number, min: number, max: number): number
76 |
--------------------------------------------------------------------------------
/app/ts/remote.ts:
--------------------------------------------------------------------------------
1 |
2 | addEventListener('message', e => {
3 | console.debug('message', e.data)
4 | })
5 | addEventListener('click', e => {
6 | const target = e.target as HTMLButtonElement
7 | const data = (target.dataset.send || '').split(' ').map(hex => parseInt(hex, 16))
8 | const app = chrome['app'].window.get('app').contentWindow
9 | for (let i=0; i<1; i++) {
10 | setTimeout(function() {
11 | app.postMessage(new Uint8Array([2, ...data]))
12 | }, i * 10)
13 | }
14 | })
15 | // let state = false
16 | // addEventListener('keydown', e => {
17 | // if (state) {
18 | // document.getElementById('off').click()
19 | // } else {
20 | // document.getElementById('green').click()
21 | // }
22 | // state = !state
23 | // })
24 | addEventListener('load', e => {
25 | document.body.focus()
26 | })
27 |
--------------------------------------------------------------------------------
/app/ts/serial.ts:
--------------------------------------------------------------------------------
1 | import { set, store } from "./store"
2 |
3 | let handler = null
4 |
5 | chrome.serial.on
6 | chrome.serial.onReceive.addListener(async ({ connectionId: id, data }) => {
7 | if (id === store.connection) {
8 | (new Uint8Array(data)).forEach((byte) => decodeMsg(byte))
9 | }
10 | })
11 | chrome.serial.onReceiveError.addListener(async ({ connectionId: id, error }) => {
12 | if (id === store.connection) {
13 | console.debug("serial closed:", error)
14 | await set({ connection: 0, connected: false })
15 | }
16 | })
17 |
18 | export async function onSerialMessage(cb) {
19 | handler = cb
20 | }
21 |
22 | export async function serialConnect(force = false) {
23 | await set({ connected: false })
24 | return new Promise(resolve => {
25 | if (!force && store.connection) {
26 | chrome.serial.getConnections(async (connections) => {
27 | const found = connections.find((conn) => conn.connectionId == store.connection)
28 | console.debug(connections)
29 | if (!found) {
30 | resolve(await serialConnect(true))
31 | } else {
32 | console.debug('serial resumed:', found.connectionId, store.port)
33 | await set({ connection: found.connectionId, connected: true })
34 | resolve(found.connectionId)
35 | }
36 | })
37 | } else {
38 | const options = {
39 | name: 'connection',
40 | bufferSize: 255,
41 | bitrate: 115200,
42 | receiveTimeout: 0
43 | }
44 | chrome.serial.connect(store.port, options, async (conn) => {
45 | if (conn) {
46 | console.debug('serial connected:', conn.connectionId, store.port)
47 | await set({ connection: conn.connectionId, connected: true })
48 | } else {
49 | console.debug('can not connect to serial port', store.port)
50 | await set({ connection: 0, connected: false })
51 | }
52 | resolve(conn)
53 | })
54 | }
55 | })
56 | }
57 |
58 | export async function serialDisconnect() {
59 | await set('connected', false)
60 | return new Promise(resolve => {
61 | chrome.serial.disconnect(store.connection, resolve)
62 | })
63 | }
64 |
65 | let responseHandler:Function;
66 |
67 | export async function sendRaw(data: ArrayBuffer, waitResponse = false) {
68 | // if (responseHandler) return
69 | return new Promise(async (resolve, reject) => {
70 | if (store.port && !store.connected) {
71 | await serialConnect()
72 | if (store.connected) {
73 | chrome.serial.send(store.connection, data, resolve)
74 | console.log('serial send>>>>>', data)
75 | } else resolve(null)
76 | }
77 | else if (store.connected) {
78 | chrome.serial.send(store.connection, data, resolve)
79 | // if (waitResponse) {
80 | // responseHandler = async (err = true) => {
81 | // if (err) {
82 | // reject()
83 | // } else {
84 | // resolve(null)
85 | // }
86 | // responseHandler = null
87 | // }
88 | // setTimeout(responseHandler, 100)
89 | // } else resolve(null)
90 | } else resolve(null)
91 | })
92 | }
93 |
94 | export async function sendCommand(id: string, head: string, ...bytes: number[]): Promise {
95 | // console.debug('send', id, head, ...bytes.map(c => c.toString(16).padStart(2, '0')))
96 | const i = id.split('').map(c => c.charCodeAt(0))
97 | const h = head.split('').map(c => c.charCodeAt(0))
98 | await sendRaw(encodeMsg([...i, 62, ...h, ...bytes]))
99 | }
100 |
101 | export function hexStr(arr: number[]) {
102 | return arr.map(n => n.toString(16).padStart(2, '0')).join(' ')
103 | }
104 |
105 | export function readStr(arr: number[], len = 1, pos = 0) {
106 | let output = ''
107 | for (let i = pos; i < pos + len; i++)
108 | output += String.fromCharCode(arr[i])
109 | return output
110 | }
111 |
112 | export function equals(a: number[], b: number[], size: number, offset: number = 0): boolean {
113 | for (let i = offset; i < offset + size; i++) {
114 | if (a[i] !== b[i]) return false
115 | }
116 | return true
117 | }
118 |
119 | const header = [36]
120 | const buffer: number[] = []
121 | let crc = 0;
122 | let length = 0;
123 | let synced = false;
124 |
125 | function isValidStart() {
126 | return synced = synced || equals(buffer, header, header.length);
127 | }
128 | function isValidSize() {
129 | return length >= header.length + buffer[1] + 2;
130 | }
131 | function checksum() {
132 | return buffer[length - 1] == crc % 256;
133 | }
134 | export function decodeMsg(byte: number) {
135 | buffer[length++] = byte;
136 | if (isValidStart() && isValidSize() && checksum()) {
137 | synced = false;
138 | let frame = buffer.slice(2, buffer[1] + 2)
139 | if (responseHandler && equals(frame, [35, 60, 79, 75], 4)) {
140 | responseHandler(false)
141 | } else if (handler) handler(frame)
142 | }
143 | if (!synced) {
144 | crc = 0;
145 | length = 0;
146 | } else {
147 | crc += byte;
148 | }
149 | }
150 | export function encodeMsg(input: number[]) {
151 | const output: number[] = []
152 | output[0] = 36 // header
153 | output[1] = input.length
154 | let crc = output[0] + output[1]
155 | for (let i = 0; i < input.length; i++) {
156 | output[i + 2] = input[i]
157 | crc += output[i + 2];
158 | }
159 | output.push(crc)
160 | return new Uint8Array(output)
161 | }
162 |
163 | export async function sendSync(time: number, show: number, ended: boolean, paused: boolean) {
164 | const data = new Uint8Array(8)
165 | const view = new DataView(data.buffer)
166 | view.setUint32(0, time, true)
167 | view.setUint8(4, show)
168 | view.setUint8(5, ended ? 1 : 0)
169 | view.setUint8(6, paused ? 1 : 0)
170 | view.setUint8(7, 0xFF)
171 | await sendCommand('#', 'SYNC', ...data);
172 | }
173 | export async function sendFile(file: File | Blob, path?: string, id: string = '#') {
174 | console.debug('begin uploading')
175 | let bytesSent = 0
176 | if (file instanceof File)
177 | file = new Blob([file])
178 | const bytesLength = file.size;
179 | const buffer = await file.arrayBuffer()
180 | await sendCommand(id, 'FBEGIN' + path)
181 | await delay(500);
182 | let count = 0
183 | while (bytesSent < bytesLength) {
184 | const start = bytesSent
185 | const end = Math.min(bytesSent + 16, bytesLength)
186 | await sendCommand(id, 'FWRITE', ...new Uint8Array(buffer, start, end - start))
187 | bytesSent = end
188 | if (count++) await delay(2);
189 | else await delay(500);
190 | }
191 | await sendCommand(id, 'FCLOSE')
192 | await delay(100);
193 | console.debug('file uploaded')
194 | }
195 |
196 | export async function delay(ms: number = 1000) {
197 | return new Promise(resolve => {
198 | setTimeout(resolve, ms)
199 | })
200 | }
201 |
--------------------------------------------------------------------------------
/app/ts/store.ts:
--------------------------------------------------------------------------------
1 | import { $player } from './view'
2 |
3 | Object.defineProperty(window, 'localStorage', { value: null })
4 |
5 | import JSZip from 'jszip'
6 | import unzip from 'unzip-js'
7 | import MusicTempo from 'music-tempo'
8 | import flattenDeep from 'lodash/flattenDeep'
9 | import { serialDisconnect } from './serial'
10 |
11 | export const cache: CacheData = {
12 | audio: null,
13 | show: null
14 | }
15 | export const DEFAULT_SHOW_DATA = {
16 | selected: 1,
17 | time: 0,
18 | duration: 0,
19 | markers: [],
20 | tracks: []
21 | }
22 | export const DEFAULT_AUDIO_DATA = {
23 | url: null,
24 | beats: [],
25 | duration: 0,
26 | waveform: []
27 | }
28 | export const DEFAULT_STORE_DATA: StoreData = {
29 |
30 | port: '',
31 | connection: 0,
32 | connected: false,
33 |
34 | sync: false,
35 | file: '',
36 | time: 0,
37 | slot: 1,
38 | channel: 'A',
39 | duration: 0,
40 | paused: false,
41 | ended: false,
42 |
43 | tempo: 120,
44 | division: 4,
45 |
46 | tracks: [],
47 | beats: [],
48 | waveform: [],
49 |
50 | show: null,
51 | audio: null
52 | }
53 | export const store: StoreData | any = Object.create(DEFAULT_STORE_DATA)
54 | window['store'] = store
55 |
56 | chrome.storage.onChanged.addListener(async (changes) => {
57 | for (let key in changes) {
58 | store[key] = changes[key].newValue
59 | }
60 | if (changes.port) {
61 | serialDisconnect()
62 | }
63 | // console.debug('changed', [changes])
64 | // await renderTracks()
65 | // await renderWaveform()
66 | // await renderBeats()
67 | })
68 |
69 | export async function init() {
70 | console.debug('init storage')
71 | return new Promise(resolve => {
72 | chrome.storage.local.get(async (res: any) => {
73 | // const { tracks, duration, solution, ...data } = res
74 | Object.assign(store, res)
75 | resolve(store)
76 | })
77 | })
78 | }
79 |
80 | export async function set(key: string | object, value?: any) {
81 | return new Promise(resolve => {
82 | if (typeof key === 'string') {
83 | chrome.storage.local.set({ [key]: value } as any, resolve)
84 | } else if (typeof key === 'object') {
85 | chrome.storage.local.set(key, resolve)
86 | } else {
87 | resolve(null)
88 | }
89 | })
90 | }
91 |
92 | export async function parseShowFile(file: File | Blob): Promise {
93 | console.debug('parse show file')
94 | return new Promise(async (resolve) => {
95 | if (file instanceof File) file = new Blob([file], { type: file.type })
96 | let result = null
97 | const body = await file.text()
98 | try {
99 | result = JSON.parse(body)
100 | } catch (e) {
101 | console.debug(e)
102 | console.debug(await file.text())
103 | }
104 | if (file.type !== 'lmp' && result) result = {
105 | tracks: parseShowTracks(result.tracks)
106 | }
107 | resolve(result)
108 | })
109 | }
110 |
111 | export async function parseAudioFile(file: File | Blob): Promise {
112 | console.debug('parse audio file')
113 | return {}
114 | // const { duration, waveform, beats, tempo }:any = await parseAudio(file)
115 | // return { duration: Math.max(duration, store.duration), waveform, beats, tempo }
116 | }
117 |
118 | export async function parseAudio(file: File | Blob | ArrayBuffer): Promise {
119 | return new Promise(async (resolve, reject) => {
120 | let name = ''
121 | let buffer = file
122 | if (buffer instanceof File) {
123 | name = buffer.name
124 | buffer = new Blob([buffer])
125 | }
126 | if (buffer instanceof Blob)
127 | buffer = await buffer.arrayBuffer()
128 | new AudioContext().decodeAudioData(buffer, res => {
129 | console.debug('decoded audio', [res])
130 | const data = res.getChannelData(0)
131 | if (res.numberOfChannels == 2) {
132 | const data2 = res.getChannelData(1)
133 | for (let i in data) {
134 | data[i] = (data[i] + data2[i]) / 2
135 | }
136 | }
137 | console.debug('getting music tempo')
138 | const { beats, tempo } = new MusicTempo(data) as { beats: Float32Array, tempo: number }
139 | resolve({
140 | name,
141 | tempo,
142 | beats: [...new Uint32Array(beats.map(v => v * 1000))],
143 | duration: Math.round(res.duration * 1000),
144 | waveform: parseWaveform(data, res.duration * 100)
145 | } as AudioData)
146 | }, reject)
147 | })
148 | }
149 |
150 | export function parseWaveform(audioData: Float32Array, width: number): WaveformData[] {
151 | const step = Math.round(audioData.length / width)
152 | const waveformData: WaveformData[] = []
153 | let x = 0,
154 | sumPositive = 0,
155 | sumNegative = 0,
156 | maxPositive = 0,
157 | maxNegative = 0,
158 | kNegative = 0,
159 | kPositive = 0,
160 | drawIdx = step
161 | for (let i = 0; i < audioData.length; i++) {
162 | if (i == drawIdx) {
163 | waveformData.push([
164 | Math.round(sumPositive / kPositive * 100) || 0,
165 | Math.round(sumNegative / kNegative * 100) || 0,
166 | Math.round(maxPositive * 100) || 0,
167 | Math.round(maxNegative * 100) || 0
168 | ])
169 | x++
170 | drawIdx += step
171 | sumPositive = 0
172 | sumNegative = 0
173 | maxPositive = 0
174 | maxNegative = 0
175 | kNegative = 0
176 | kPositive = 0
177 | } else {
178 | if (audioData[i] < 0) {
179 | sumNegative += audioData[i]
180 | kNegative++
181 | if (maxNegative > audioData[i]) maxNegative = audioData[i]
182 | } else {
183 | sumPositive += audioData[i]
184 | kPositive++
185 | if (maxPositive < audioData[i]) maxPositive = audioData[i]
186 | }
187 | }
188 | }
189 | return waveformData
190 | }
191 |
192 | export function parseShowTracks(tracks: any[]) {
193 | function rgb({ r, g, b }: { r: number, g: number, b: number }) {
194 | return [r, g, b].map(v => Math.round(v * 255))
195 | }
196 | function hex(num: number) {
197 | return num.toString(16).padStart(2, '0').toUpperCase()
198 | }
199 | function frame({ type, startTime: start, endTime: end, color, colorEnd, colorStart, period, spacing, ratio }: any) {
200 | const data: any = {
201 | type,
202 | start,
203 | duration: end - start,
204 | color: []
205 | }
206 | if (color) data.color[0] = '#' + rgb(color).map(hex).join('')
207 | if (colorStart) data.color[0] = '#' + rgb(colorStart).map(hex).join('')
208 | if (colorEnd) data.color[1] = '#' + rgb(colorEnd).map(hex).join('')
209 | if (typeof period !== 'undefined') data.period = period
210 | if (typeof ratio !== 'undefined') data.ratio = ratio
211 | if (typeof spacing !== 'undefined') data.spacing = spacing
212 | return data
213 | }
214 | return tracks.map(({ name, trackType: type, device, elements }: any) => {
215 | return {
216 | type,
217 | name,
218 | device,
219 | frames: (elements || []).map(frame)
220 | }
221 | })
222 | }
223 |
224 | export async function openShowEntry(entry: FileEntry) {
225 | return new Promise(async (resolve, reject) => {
226 | const upName = entry.name.toUpperCase()
227 | if (upName.endsWith('LMP') || upName.endsWith('LTP')) {
228 | await set('file', chrome['fileSystem'].retainEntry(entry))
229 | }
230 | entry.file(file => {
231 | unzip(file, (err: any, zip) => {
232 | if (err) reject(err)
233 | function callback(err: any, entries) {
234 | let ended = 0
235 | entries.forEach(entry => {
236 | const { name } = entry
237 | let show, audio
238 | zip.readEntryData(entry, false, (err, stream) => {
239 | if (err) return resolve(null)
240 | let content: Uint8Array[] = []
241 | stream.on('data', (data: Uint8Array) => {
242 | content.push(data)
243 | })
244 | stream.on('end', async () => {
245 | switch (name) {
246 | case 'project.lt3':
247 | cache.show = new Blob(content, { type: 'lt3' })
248 | show = await parseShowFile(cache.show)
249 | if (show) await set(show)
250 | break
251 | case 'project.json':
252 | cache.show = new Blob(content, { type: 'lmp' })
253 | show = await parseShowFile(cache.show)
254 | if (show) await set(show)
255 | break
256 | default:
257 | if (name.endsWith('.mp3')) {
258 | cache.audio = new Blob(content, { type: 'mp3' })
259 | audio = await parseAudioFile(cache.audio)
260 | if (audio) {
261 | await set(audio)
262 | $player.src = URL.createObjectURL(cache.audio)
263 | }
264 | }
265 | }
266 | if (++ended === entries.length) {
267 | resolve(null)
268 | }
269 | })
270 | })
271 | })
272 | }
273 | zip.readEntries(callback, reject)
274 | })
275 | })
276 | })
277 | }
278 |
279 | export async function saveShowEntry(entry: FileEntry) {
280 | if (!entry) return
281 | const upName = entry.name.toUpperCase()
282 | console.debug(`open file`, [entry.fullPath])
283 | if (upName.endsWith('LMP')) {
284 | try {
285 | await set('file', chrome['fileSystem'].retainEntry(entry))
286 | }
287 | catch (e) {
288 | console.debug(e)
289 | }
290 | }
291 | return new Promise((resolve, reject) => {
292 | chrome.storage.local.get(async ({ show, audio, ...data }) => {
293 | entry.createWriter(async (writer: FileWriter) => {
294 | const zip = new JSZip()
295 | zip.file('project.json', JSON.stringify(data))
296 | if (cache.audio) zip.file('audio.mp3', cache.audio)
297 | zip.generateAsync({ type: 'blob' }).then(content => {
298 | writer.write(content)
299 | writer.addEventListener('writeend', async () => {
300 | if (entry.fullPath.toUpperCase().endsWith('LMP')) {
301 | await set('file', chrome['fileSystem'].retainEntry(entry))
302 | } else {
303 | await set('file', '')
304 | }
305 | console.debug(`save LMP file`, [entry.fullPath])
306 | resolve(null)
307 | })
308 | }).catch(reject)
309 | })
310 | })
311 | })
312 | }
313 |
314 | interface Frame {
315 | type: number
316 | start: number
317 | duration?: number
318 | transition?: number
319 | r?: number
320 | g?: number
321 | b?: number
322 | }
323 |
324 | export function createBinaryFrame({ type, start, duration, transition, r, g, b }: Frame) {
325 | const data = new Uint8Array(16)
326 | const view = new DataView(data.buffer)
327 | view.setUint8(0, type || 1)
328 | view.setUint8(1, r || 0)
329 | view.setUint8(2, g || 0)
330 | view.setUint8(3, b || 0)
331 | view.setUint32(4, start || 0, true)
332 | view.setUint32(8, duration || 0, true)
333 | view.setUint32(12, transition || 0, true)
334 | return data
335 | }
336 |
337 | export function createColorFrame(start: number, duration: number, transition: number, r: number, g: number, b: number) {
338 | return createBinaryFrame({ type: 1, start, duration, transition, r, g, b })
339 | }
340 |
341 | export function createEndFrame(start: number) {
342 | return createBinaryFrame({ type: 2, start })
343 | }
344 |
345 | export function createLoopFrame(start: number, duration: number) {
346 | return createBinaryFrame({ type: 3, start, duration })
347 | }
348 |
349 | function hex2rgb(hex: string): [number, number, number] {
350 | return [
351 | parseInt(hex[1] + hex[2], 16),
352 | parseInt(hex[3] + hex[4], 16),
353 | parseInt(hex[5] + hex[6], 16)
354 | ]
355 | }
356 |
357 | export function convertFrame({ type, color, start, duration, ratio, spacing, period }) {
358 | let output
359 | switch (type) {
360 | case 2: // solid
361 | output = [
362 | createColorFrame(start, duration, 0, ...hex2rgb(color[0]))
363 | ]
364 | break
365 | case 3: // gradient
366 | output = [
367 | createColorFrame(start, 0, 0, ...hex2rgb(color[0])),
368 | createColorFrame(start, duration, duration, ...hex2rgb(color[1]))
369 | ]
370 | break
371 | case 4: // flash
372 | const dur = period * (ratio / 100)
373 | output = [
374 | createLoopFrame(start, duration),
375 | createColorFrame(0, dur, 0, ...hex2rgb(color[0])),
376 | createColorFrame(dur, period - dur, 0, ...hex2rgb(color[1])),
377 | createEndFrame(period)
378 | ]
379 | break
380 | case 5: // rainbow
381 | const seg = Math.floor(period * 10 / 7)
382 | console.log(seg)
383 | output = [
384 | createLoopFrame(start, duration),
385 | createColorFrame(seg * 0, seg, seg, 255, 0, 0),
386 | createColorFrame(seg * 1, seg, seg, 255, 165, 0),
387 | createColorFrame(seg * 2, seg, seg, 255, 255, 0),
388 | createColorFrame(seg * 3, seg, seg, 0, 128, 0),
389 | createColorFrame(seg * 4, seg, seg, 0, 255, 255),
390 | createColorFrame(seg * 5, seg, seg, 0, 0, 225),
391 | createColorFrame(seg * 6, seg, seg, 238, 130, 238),
392 | createEndFrame(seg * 7)
393 | ]
394 | break
395 | case 6: // dots
396 | output = [
397 | createLoopFrame(start, duration),
398 | createColorFrame(0, 1, 0, ...hex2rgb(color[0])),
399 | createColorFrame(1, spacing, 0, 0, 0, 0),
400 | createEndFrame(spacing + 1)
401 | ]
402 | break
403 | case 7: // pulse
404 | // period = period * 10
405 | if (period < 24) period = 24
406 | output = [
407 | createLoopFrame(start, duration),
408 | createColorFrame(0, 10, 0, 0, 0, 0),
409 | createColorFrame(10, 2, 0, 255, 255, 255),
410 | createColorFrame(12, 10, 0, 0, 0, 0),
411 | createColorFrame(22, period - 22, 0, ...hex2rgb(color[0])),
412 | createEndFrame(period)
413 | ]
414 | break
415 | default:
416 | output = []
417 | }
418 | return output
419 | }
420 |
421 | export function convertTracks(tracks) {
422 | const buf = []
423 | tracks.forEach((track: any, index: any) => {
424 | if (index === 0) {
425 | buf.push(...new Array(createColorFrame(0, track.start, 0, 0, 0, 0)))
426 | } else if (index > 0 && index < tracks.length) {
427 | const last = tracks[index - 1]
428 | const gap = track.start - (last.start + last.duration)
429 | if (gap > 0) buf.push(...new Array(createColorFrame(last.start + last.duration, gap, 0, 0, 0, 0)))
430 | }
431 | buf.push(...new Array(convertFrame(track)))
432 | if (index === tracks.length - 1) {
433 | buf.push(...new Array(createEndFrame(track.start + track.duration)))
434 | }
435 | })
436 | return flattenDeep(buf)
437 | }
438 |
439 | export function logHex(data) {
440 | console.debug(data.map((c: number) => c.toString(16).padStart(2, '0')).join(' '))
441 | }
442 |
443 |
444 | window['convert'] = convertTracks
445 |
--------------------------------------------------------------------------------
/app/ts/utils.ts:
--------------------------------------------------------------------------------
1 | const $txt = document.getElementById('txt') as HTMLOutputElement
2 | const $hex = document.getElementById('hex') as HTMLOutputElement
3 | const $dec = document.getElementById('dec') as HTMLOutputElement
4 | const $bin = document.getElementById('bin') as HTMLOutputElement
5 | const $int = document.getElementById('int') as HTMLOutputElement
6 | const $res = document.getElementById('res') as HTMLOutputElement
7 | const $array = document.getElementById('arr') as HTMLOutputElement
8 | const $message = document.getElementById('message') as HTMLOutputElement
9 | const $send = document.getElementById('send') as HTMLOutputElement
10 |
11 | chrome.storage.local.get('utils_txt', ({ utils_txt:codes }) => {
12 | if (!codes) return
13 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ')
14 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ')
15 | $dec.innerHTML = codes.map(c => c.toString()).join(' ')
16 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('')
17 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ')
18 | })
19 | let codes: number[] = []
20 | addEventListener('input', (e: any) => {
21 | let value: string = e.target.innerText.replace(/\s+/, '')
22 | switch (e.target.id) {
23 | case 'txt':
24 | codes = value ? value.split('').map(c => c.charCodeAt(0)) : []
25 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ')
26 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ')
27 | $dec.innerHTML = codes.map(c => c.toString()).join(' ')
28 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ')
29 | break
30 | case 'hex':
31 | value = e.target.innerText.replace(/[^0-9a-f ]/ig, '').trim()
32 | codes = value ? value.split(' ').map(c => parseInt(c, 16)) : []
33 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ')
34 | $dec.innerHTML = codes.map(c => c.toString()).join(' ')
35 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('')
36 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ')
37 | break
38 | case 'dec':
39 | value = e.target.innerText.replace(/[^0-9 ]/ig, '').trim()
40 | codes = value ? value.split(' ').map(c => parseInt(c, 10)) : []
41 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ')
42 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ')
43 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('')
44 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ')
45 | break
46 | case 'bin':
47 | value = e.target.innerText.replace(/[^01 ]/ig, '').trim()
48 | codes = value ? value.split(' ').map(c => parseInt(c, 2)) : []
49 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ')
50 | $dec.innerHTML = codes.map(c => c.toString()).join(' ')
51 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('')
52 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ')
53 | break
54 | case 'int':
55 | let res = '\n'
56 | value = e.target.innerText.replace(/[^0-9]/ig, '').trim()
57 | if (value) {
58 | const num = parseFloat(value)
59 | const buf = new Uint8Array(4)
60 | const view = new DataView(buf.buffer)
61 |
62 | view.setUint16(0, num)
63 | res += 'U16 BE = ' + toHEX(buf.slice(0, 2))
64 | view.setUint16(0, num, true)
65 | res += 'U16 LE = ' + toHEX(buf.slice(0, 2))
66 |
67 | view.setUint32(0, num)
68 | res += 'U32 BE = ' + toHEX(buf)
69 | view.setUint32(0, num, true)
70 | res += 'U32 LE = ' + toHEX(buf)
71 |
72 | view.setFloat32(0, num)
73 | res += 'F32 BE = ' + toHEX(buf)
74 | view.setFloat32(0, num, true)
75 | res += 'F32 LE = ' + toHEX(buf)
76 | }
77 | $res.innerHTML = res
78 | break
79 | default: return
80 | }
81 | chrome.storage.local.set({ 'utils_txt': codes })
82 | })
83 |
84 | function toHEX(bytes: Uint8Array): string {
85 | return [...bytes].map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') + '\n'
86 | }
87 |
88 | $send.addEventListener('click', e => {
89 | chrome['app'].window.get('app').contentWindow.postMessage(new Uint8Array([1, ...codes]))
90 | })
91 |
--------------------------------------------------------------------------------
/app/ts/view.ts:
--------------------------------------------------------------------------------
1 | import { store } from './store'
2 | import $ from 'jquery'
3 |
4 | export const $player = $('#player').get(0) as HTMLAudioElement
5 | export const $tempo = $('#tempo').get(0) as HTMLDivElement
6 | export const $waveform = $('#waveform').get(0) as HTMLDivElement
7 | export const $handle = $('#handle').get(0) as HTMLDivElement
8 | export const $main = $('#main').get(0) as HTMLDivElement
9 | export const $tracks = $('#tracks').get(0) as HTMLDivElement
10 | export const $timeline = $('#timeline').get(0) as HTMLDivElement
11 |
12 | export async function updateTime() {
13 | if (store.duration) {
14 | if ($player.duration && !$player.ended) {
15 | store.time = Math.round($player.currentTime * 1000)
16 | }
17 | const ratio = store.time / store.duration
18 | $main.scrollLeft = ($main.scrollWidth - $main.offsetWidth) * ratio
19 | $handle.style.left = ratio * 100 + '%'
20 | $handle.style.transform = `translateX(-${100 - Math.round(ratio * 100)}%)`
21 | }
22 | }
23 | export async function updateSize() {
24 | if (store.duration) {
25 | const width = store.duration / 10
26 | $tracks.style.width = width + 'px'
27 | $waveform.style.width = width + 'px'
28 | $tempo.style.width = width + 'px'
29 | }
30 | }
31 |
32 | let portsList = ''
33 | export async function renderSerial() {
34 | return new Promise(resolve => {
35 | chrome.serial.getDevices(devices => {
36 | // const list = devices.filter(dev => dev.path.startsWith('/dev/cu.')).map(dev => dev.path.replace('/dev/cu.', ''))
37 | // const list = devices
38 | // .filter(dev => {
39 | // return dev.path.startsWith('/dev/cu.') || dev.path.matches(/COM[0-9]+/)
40 | // }).map(dev => {
41 | // return dev.path.replace('/dev/cu.', '').replace(/^.*(COM[0-9]+)$/, '$1')
42 | // })
43 | const list = devices.map(dev => dev.path)
44 | if (list.join() != portsList) {
45 | portsList = list.join()
46 | console.debug('serial devices', list)
47 | $('[data-key="port"]')
48 | .html('... ')
49 | .append(list.map(path => $(``).val(path).html(path.replace('/dev/cu.', '').replace(/^.*(COM[0-9]+)$/, '$1'))))
50 | .val(store.port)
51 | resolve(null)
52 | }
53 | })
54 | })
55 | }
56 |
57 | export async function renderBeats() {
58 | if (!$tempo) return
59 | updateSize()
60 | if (store.beats && store.beats.length) {
61 | console.debug('render beats')
62 | store.beats.reduce((start = 0, end: number, index: number) => {
63 | const width = end - start
64 | const counter = index ? `${(index - 1) % 4 + 1}` : `-`
65 | $('')
66 | .width(width / 10)
67 | .html(counter)
68 | .appendTo('#tempo')
69 | return end
70 | })
71 | }
72 | }
73 |
74 | export async function renderWaveform() {
75 | if (!$waveform) return
76 | $waveform.innerHTML = ''
77 | updateSize()
78 | if (store.waveform && store.waveform.length) {
79 | console.debug('render waveform')
80 | const height = $waveform.offsetHeight
81 | const halfHeight = height / 2
82 | const canvas = document.createElement('canvas')
83 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
84 | const size = 1
85 |
86 | canvas.width = store.duration / 10
87 | canvas.height = height
88 | ctx.lineWidth = 2
89 | store.waveform.forEach(([max, min, pos, neg], i) => {
90 | const x = i * size
91 | ctx.fillStyle = '#333333'
92 | const p1 = neg / height * halfHeight + halfHeight
93 | ctx.fillRect(x, p1, size, (pos / height * halfHeight + halfHeight) - p1)
94 |
95 | ctx.fillStyle = '#000000'
96 | const p2 = Math.round(min / height * halfHeight) + halfHeight
97 | ctx.fillRect(x, p2, size, Math.round(max / height * halfHeight + halfHeight) - p2)
98 | })
99 | $waveform.appendChild(canvas)
100 | }
101 | }
102 |
103 | function formatSeconds(num) {
104 | const min = Math.floor(num / 60) + ''
105 | const sec = num % 60 + ''
106 | return `${min.padStart(2, '0')}:${sec.padStart(2, '0')}`
107 | }
108 |
109 | export async function renderTimeline() {
110 | if (!$timeline) return
111 | if (store.duration) {
112 | const width = Math.round(store.duration / 10)
113 | const blocks = Math.ceil(width / 100)
114 | for (let i = 0; i < blocks; i++) {
115 | const $block = document.createElement('span')
116 | $block.style.width = '100px'
117 | $block.innerHTML = `${formatSeconds(i)}`
118 | $timeline.appendChild($block)
119 | }
120 | }
121 | }
122 |
123 | export async function renderTracks() {
124 | if (!$tracks) return
125 | updateSize()
126 | $tracks.innerHTML = ''
127 | if (store.tracks && store.tracks.length) {
128 | store.tracks.forEach((track: any, index: number) => {
129 | const $track = document.createElement('div')
130 | const { frames, ...params } = track
131 | Object.assign($track.dataset, params)
132 | $track.dataset.foo = `foo-${index}`
133 | $track.dataset.index = `${index}`
134 | $track.classList.add('track')
135 | $track.innerHTML = `${index+1} `
136 | $tracks.appendChild($track)
137 | renderLight($track, track)
138 | })
139 | }
140 | }
141 |
142 | export function renderLight(container: any, track: any) {
143 | if (!track.device || !track.frames) return
144 | switch (track.device) {
145 | case 1:
146 | case 2:
147 | track.frames.forEach(el => {
148 | const $el = document.createElement('span')
149 | const { color, ...params } = el
150 | const { start, duration, ratio, spacing, period } = params
151 | Object.assign($el.dataset, params)
152 | $el.style.left = `${start / 10}px`
153 | $el.style.width = `${duration / 10}px`
154 | $el.style.backgroundRepeat = `repeat`
155 | switch (params.type) {
156 | case 2: // solid
157 | $el.style.backgroundColor = color[0]
158 | break
159 | case 3: // gradient
160 | $el.style.background = `linear-gradient(90deg, ${color[0]} 0%, ${color[1]} 100%)`
161 | break
162 | case 4: // flash
163 | $el.style.backgroundImage = `url(${drawFlash(color[0], color[1], period, ratio)})`
164 | break
165 | case 5: // rainbow
166 | $el.style.backgroundImage = `url(${drawRainbow(period)})`
167 | $el.style.backgroundSize = `${period}px`
168 | break
169 | case 6: // dots
170 | $el.style.backgroundImage = `url(${drawDots(color[0], spacing)})`
171 | // $el.style.backgroundSize = `${spacing}px`
172 | break
173 | case 7: // pulse
174 | $el.style.backgroundImage = `url(${drawPulse(color[0], period)})`
175 | // $el.style.backgroundSize = `${period + 3}px`
176 | break
177 | default:
178 | console.debug('unhandled light type', [params.type])
179 | }
180 | container.appendChild($el)
181 | })
182 | break
183 | default:
184 | return console.debug('unsupported device type', [track.device])
185 | }
186 | }
187 | const tmpCanvas = document.createElement('canvas')
188 | function drawFlash(color1, color2, period, ratio) {
189 | tmpCanvas.width = period / 10
190 | tmpCanvas.height = 1
191 | const ctx = tmpCanvas.getContext('2d')
192 | if (ctx) {
193 | const len1 = period * (ratio / 100) / 10
194 | const len2 = period * ((100 - ratio) / 100) / 10
195 | ctx.fillStyle = color1
196 | ctx.fillRect(0, 0, len1, 1)
197 | ctx.fillStyle = color2
198 | ctx.fillRect(len1, 0, len2, 1)
199 | }
200 | return tmpCanvas.toDataURL()
201 | }
202 | function drawDots(color, spacing) {
203 | tmpCanvas.width = spacing + 1
204 | tmpCanvas.height = 1
205 | const ctx = tmpCanvas.getContext('2d')
206 | if (ctx) {
207 | ctx.fillStyle = color
208 | ctx.fillRect(0, 0, 1, 1)
209 | ctx.fillStyle = 'black'
210 | ctx.fillRect(1, 0, spacing, 1)
211 | }
212 | return tmpCanvas.toDataURL()
213 | }
214 | function drawPulse(color, period) {
215 | tmpCanvas.width = period + 3
216 | tmpCanvas.height = 1
217 | const ctx = tmpCanvas.getContext('2d')
218 | if (ctx) {
219 | ctx.fillStyle = 'black'
220 | ctx.fillRect(0, 0, 1, 1)
221 | ctx.fillStyle = 'white'
222 | ctx.fillRect(1, 0, 1, 1)
223 | ctx.fillStyle = 'black'
224 | ctx.fillRect(2, 0, 1, 1)
225 | ctx.fillStyle = color
226 | // ctx.fillRect(3, 0, 4, 1)
227 | ctx.fillRect(3, 0, period, 1)
228 | }
229 | return tmpCanvas.toDataURL()
230 | }
231 | function drawRainbow(period) {
232 | tmpCanvas.width = period
233 | tmpCanvas.height = 1
234 | const ctx = tmpCanvas.getContext('2d')
235 | if (ctx) {
236 | var gradient = ctx.createLinearGradient(0, 0, period, 0)
237 | gradient.addColorStop(1 / 7 * 0, 'red')
238 | gradient.addColorStop(1 / 7 * 1, 'orange')
239 | gradient.addColorStop(1 / 7 * 2, 'yellow')
240 | gradient.addColorStop(1 / 7 * 3, 'green')
241 | gradient.addColorStop(1 / 7 * 4, 'cyan')
242 | gradient.addColorStop(1 / 7 * 5, 'blue')
243 | gradient.addColorStop(1 / 7 * 6, 'violet')
244 | ctx.fillStyle = gradient
245 | ctx.fillRect(0, 0, period, 1)
246 | }
247 | return tmpCanvas.toDataURL()
248 | }
249 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "ESNext",
8 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
9 | "module": "none",
10 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
11 | "lib": ["DOM", "DOM.Iterable", "ESNext"], /* Specify library files to be included in the compilation. */
12 | "allowJs": true, /* Allow javascript files to be compiled. */
13 | "checkJs": true, /* Report errors in .js files. */
14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
17 | "sourceMap": true, /* Generates corresponding '.map' file. */
18 | // "outFile": "./", /* Concatenate and emit output to single file. */
19 | "outDir": "js", /* Redirect output structure to the directory. */
20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
21 | // "composite": true, /* Enable project compilation */
22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
23 | "removeComments": true, /* Do not emit comments to output. */
24 | // "noEmit": true, /* Do not emit outputs. */
25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
26 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
28 |
29 | /* Strict Type-Checking Options */
30 | "strict": false,
31 | /* Enable all strict type-checking options. */
32 | "noImplicitAny": false,
33 | /* Raise error on expressions and declarations with an implied 'any' type. */
34 | "strictNullChecks": false,
35 | /* Enable strict null checks. */
36 | "strictFunctionTypes": false,
37 | /* Enable strict checking of function types. */
38 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
39 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
40 | "noImplicitThis": true,
41 | /* Raise error on 'this' expressions with an implied 'any' type. */
42 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
43 |
44 | /* Additional Checks */
45 | // "noUnusedLocals": true, /* Report errors on unused locals. */
46 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
47 | "noImplicitReturns": false, /* Report error when not all code paths in function return a value. */
48 | "noFallthroughCasesInSwitch": false, /* Report errors for fallthrough cases in switch statement. */
49 |
50 | /* Module Resolution Options */
51 | "moduleResolution": "node",
52 | /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
53 | "baseUrl": "./ts", /* Base directory to resolve non-absolute module names. */
54 | // "paths": {
55 | //
56 | // }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
57 | "rootDirs": [ "app/ts"], /* List of root folders whose combined content represents the structure of the project at runtime. */
58 | // "typeRoots": ["app"], /* List of folders to include type definitions from. */
59 | // "types": [], /* Type declaration files to be included in compilation. */
60 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
61 | "esModuleInterop": true,
62 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
63 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
64 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
65 |
66 | /* Source Map Options */
67 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
68 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
69 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
70 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
71 |
72 | /* Experimental Options */
73 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
74 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
75 |
76 | /* Advanced Options */
77 | "skipLibCheck": true,
78 | /* Skip type checking of declaration files. */
79 | "forceConsistentCasingInFileNames": true
80 | /* Disallow inconsistently-cased references to the same file. */
81 | },
82 | "include": [
83 | "ts/**/*.ts"
84 | ],
85 | "exclude": [
86 | "node_modules"
87 | ],
88 | "compileOnSave": true
89 | }
90 |
--------------------------------------------------------------------------------
/app/utils.html:
--------------------------------------------------------------------------------
1 |
2 | Utils
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
50 |
51 |
52 | Serial Buffer Tool
53 |
54 |
55 |
56 |
57 |
58 | SEND
59 |
60 | Integer Tool
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | devtool: 'inline-source-map',
5 |
6 | mode: 'development',
7 | entry: {
8 | app: './ts/app.ts',
9 | utils: './ts/utils.ts',
10 | remote: './ts/remote.ts',
11 | background: './ts/background.ts'
12 | },
13 | output: {
14 | publicPath: './js/',
15 | filename: '[name].js',
16 | path: __dirname + '/js'
17 | },
18 | resolve: {
19 | extensions: ['.js', '.ts', '.json'],
20 | alias: {
21 | css: path.resolve(__dirname, 'css/'),
22 | fonts: path.resolve(__dirname, 'fonts/'),
23 | ts: path.resolve(__dirname, 'ts/')
24 | }
25 | },
26 | module: {
27 | rules: [
28 | { test: /\.css$/, use: 'css-loader' },
29 | { test: /\.ts$/, use: 'ts-loader' },
30 | { test: /\.js$/, use: 'babel-loader' },
31 | { test: /\.(woff|woff2|ttf|eot)$/, use: 'file-loader' }
32 | ]
33 | },
34 | target: 'webworker'
35 | }
36 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Must use Node 16. Node 17 and greater will throw errors and build will fail
4 |
5 | cd app
6 | yarn install
7 | npx webpack
8 |
9 | # cp -R node_modules/@mdi/font/css/* ./css
10 | # cp -R node_modules/@mdi/font/fonts/* ./fonts
11 |
12 | zip app.zip dist/* css/* fonts/* img/* js/* app.html remote.html utils.html manifest.json
13 | cd ..
14 |
15 | mkdir -p build
16 | rm -Rf build/*
17 | mv app/app.zip build/
18 |
19 | pio run -e rx_uart
20 | pio run -e tx_uart
21 |
22 | cp .pio/build/rx_uart/firmware.bin build/rx.bin
23 | cp .pio/build/tx_uart/firmware.bin build/tx.bin
24 |
25 |
--------------------------------------------------------------------------------
/flash.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | while true; do
4 | echo ""
5 | echo ""
6 | echo "--------------------------------------------------------------------------------"
7 | echo ""
8 | read -p ' Plug in your device and press enter to continue...'
9 | echo ""
10 | echo ""
11 | esptool.py --baud 460800 write_flash 0x00 build/rx.bin
12 | sleep 1
13 | done
14 |
--------------------------------------------------------------------------------
/inc/api.h:
--------------------------------------------------------------------------------
1 | #include "app.h"
2 | #include "def.h"
3 | #include "led.h"
4 | #include "net.h"
5 |
6 | #ifndef __API_H__
7 | #define __API_H__
8 |
9 | namespace Api {
10 |
11 | ESP8266WebServer server(API_PORT);
12 |
13 | String unsupportedFiles = String();
14 | File uploadFile;
15 | String transferTarget;
16 |
17 | static const char TEXT_PLAIN[] PROGMEM = "text/plain";
18 | static const char FS_INIT_ERROR[] PROGMEM = "FS INIT ERROR";
19 | static const char FILE_NOT_FOUND[] PROGMEM = "FileNotFound";
20 | static const char DEFAULT_SHOW_JSON[] PROGMEM = "{}";
21 |
22 | boolean isIp(String str) {
23 | for (size_t i = 0; i < str.length(); i++) {
24 | int c = str.charAt(i);
25 | if (c != '.' && (c < '0' || c > '9')) {
26 | return false;
27 | }
28 | }
29 | return true;
30 | }
31 |
32 | String toStringIp(IPAddress ip) {
33 | String res = "";
34 | for (int i = 0; i < 3; i++) {
35 | res += String((ip >> (8 * i)) & 0xFF) + ".";
36 | }
37 | res += String(((ip >> 8 * 3)) & 0xFF);
38 | return res;
39 | }
40 |
41 | void sendHeaders() {
42 | server.sendHeader("Access-Control-Allow-Origin", "*");
43 | server.sendHeader("Access-Control-Allow-Methods", "*");
44 | }
45 |
46 | void replyOK() {
47 | sendHeaders();
48 | server.send(200, FPSTR(TEXT_PLAIN), "");
49 | }
50 |
51 | void replyOKWithMsg(String msg) {
52 | sendHeaders();
53 | server.send(200, FPSTR(TEXT_PLAIN), msg);
54 | }
55 |
56 | void replyNotFound(String msg) {
57 | sendHeaders();
58 | server.send(404, FPSTR(TEXT_PLAIN), msg);
59 | }
60 |
61 | void replyBadRequest(String msg) {
62 | sendHeaders();
63 | server.send(400, FPSTR(TEXT_PLAIN), msg + "\r\n");
64 | }
65 |
66 | void replyServerError(String msg) {
67 | sendHeaders();
68 | server.send(500, FPSTR(TEXT_PLAIN), msg + "\r\n");
69 | }
70 |
71 | /* Read the given file from the filesystem and stream it back to the client */
72 | bool handleFileRead(String path) {
73 | LOGL(String("handleFileRead: ") + path);
74 | if (!App::fsOK) {
75 | replyServerError(FPSTR(FS_INIT_ERROR));
76 | return true;
77 | }
78 | if (path.endsWith("/")) {
79 | path += "index.html";
80 | }
81 | String contentType;
82 |
83 | if (server.hasArg("download")) {
84 | contentType = F("application/octet-stream");
85 | } else if (path.endsWith("mp3")) {
86 | contentType = F("audio/mp3");
87 | } else if (path.endsWith("lt3")) {
88 | contentType = F("application/json");
89 | } else {
90 | contentType = mime::getContentType(path);
91 | }
92 |
93 | if (!App::fs->exists(path)) {
94 | // File not found, try gzip version
95 | path = path + ".gz";
96 | }
97 | if (App::fs->exists(path)) {
98 | File file = App::fs->open(path, "r");
99 | if (server.streamFile(file, contentType) != file.size()) {
100 | LOGL("Sent less data than expected!");
101 | }
102 | file.close();
103 | return true;
104 | }
105 | return false;
106 | } // namespace Api
107 | String lastExistingParent(String path) {
108 | while (!path.isEmpty() && !App::fs->exists(path)) {
109 | if (path.lastIndexOf('/') > 0) {
110 | path = path.substring(0, path.lastIndexOf('/'));
111 | } else {
112 | path = String(); // No slash => the top folder does not exist
113 | }
114 | }
115 | LOGL(String("Last existing parent: ") + path);
116 | return path;
117 | }
118 |
119 | void deleteRecursive(String path) {
120 | File file = App::fs->open(path, "r");
121 | bool isDir = file.isDirectory();
122 | file.close();
123 |
124 | // If it's a plain file, delete it
125 | if (!isDir) {
126 | App::fs->remove(path);
127 | return;
128 | }
129 |
130 | // Otherwise delete its contents first
131 | Dir dir = App::fs->openDir(path);
132 |
133 | while (dir.next()) {
134 | deleteRecursive(path + '/' + dir.fileName());
135 | }
136 |
137 | // Then delete the folder itself
138 | App::fs->rmdir(path);
139 | }
140 |
141 | void setup(void) {
142 | // server.getServer().setServerKeyAndCert_P(rsakey, sizeof(rsakey), x509, sizeof(x509));
143 |
144 | server.on("/status", HTTP_GET, []() {
145 | LOGL("handleStatus");
146 | FSInfo fs_info;
147 | String json;
148 | json.reserve(128);
149 | json = "{\"type\":\"LittleFS\",\"show\":";
150 | json += String(App::data.show);
151 | json += ",\"channel\":";
152 | json += String(App::data.channel);
153 | json += ", \"isOk\":";
154 | if (App::fsOK) {
155 | App::fs->info(fs_info);
156 | json += F("\"true\", \"totalBytes\":\"");
157 | json += fs_info.totalBytes;
158 | json += F("\", \"usedBytes\":\"");
159 | json += fs_info.usedBytes;
160 | json += "\"";
161 | } else {
162 | json += "\"false\"";
163 | }
164 | json += F(",\"unsupportedFiles\":\"");
165 | json += unsupportedFiles;
166 | json += "\"}";
167 |
168 | server.send(200, "application/json", json);
169 | });
170 | server.on("/show", []() {
171 | if (!server.hasArg("id")) {
172 | replyBadRequest("Missing show ID");
173 | } else {
174 | u32 id = server.arg("id").toInt();
175 | if (App::data.show != id) {
176 | App::data.show = id;
177 | App::save();
178 | }
179 | if (!handleFileRead("/show/" + String(id) + ".json")) {
180 | replyNotFound("Show not found.");
181 | }
182 | }
183 | });
184 | server.on("/list", HTTP_GET, []() {
185 | if (!App::fsOK) {
186 | return replyServerError(FPSTR(FS_INIT_ERROR));
187 | }
188 |
189 | if (!server.hasArg("dir")) {
190 | return replyBadRequest(F("DIR ARG MISSING"));
191 | }
192 |
193 | String path = server.arg("dir");
194 | if (path != "/" && !App::fs->exists(path)) {
195 | return replyBadRequest("BAD PATH");
196 | }
197 |
198 | LOGL(String("handleFileList: ") + path);
199 | Dir dir = App::fs->openDir(path);
200 | path.clear();
201 |
202 | // use HTTP/1.1 Chunked response to avoid building a huge temporary string
203 | if (!server.chunkedResponseModeStart(200, "text/json")) {
204 | server.send(505, F("text/html"), F("HTTP1.1 required"));
205 | return;
206 | }
207 |
208 | // use the same string for every line
209 | String output;
210 | output.reserve(64);
211 | while (dir.next()) {
212 | if (output.length()) {
213 | // send string from previous iteration
214 | // as an HTTP chunk
215 | server.sendContent(output);
216 | output = ',';
217 | } else {
218 | output = '[';
219 | }
220 |
221 | output += "{\"type\":\"";
222 | if (dir.isDirectory()) {
223 | output += "dir";
224 | } else {
225 | output += F("file\",\"size\":\"");
226 | output += dir.fileSize();
227 | }
228 |
229 | output += F("\",\"name\":\"");
230 | // Always return names without leading "/"
231 | if (dir.fileName()[0] == '/') {
232 | output += &(dir.fileName()[1]);
233 | } else {
234 | output += dir.fileName();
235 | }
236 |
237 | output += "\"}";
238 | }
239 |
240 | // send last string
241 | output += "]";
242 | server.sendContent(output);
243 | server.chunkedResponseFinalize();
244 | });
245 |
246 | // Editor
247 | server.on("/edit", HTTP_GET, []() {
248 | if (!handleFileRead(F("/edit.html"))) {
249 | replyNotFound(FPSTR(FILE_NOT_FOUND));
250 | }
251 | });
252 |
253 | // Create file
254 | server.on("/edit", HTTP_PUT, []() {
255 | if (!App::fsOK) {
256 | return replyServerError(FPSTR(FS_INIT_ERROR));
257 | }
258 |
259 | String path = server.arg("path");
260 | if (path.isEmpty()) {
261 | return replyBadRequest(F("PATH ARG MISSING"));
262 | }
263 | if (path == "/") {
264 | return replyBadRequest("BAD PATH");
265 | }
266 | if (App::fs->exists(path)) {
267 | return replyBadRequest(F("PATH FILE EXISTS"));
268 | }
269 |
270 | String src = server.arg("src");
271 | if (src.isEmpty()) {
272 | // No source specified: creation
273 | LOGL(String("handleFileCreate: ") + path);
274 | if (path.endsWith("/")) {
275 | // Create a folder
276 | path.remove(path.length() - 1);
277 | if (!App::fs->mkdir(path)) {
278 | return replyServerError(F("MKDIR FAILED"));
279 | }
280 | } else {
281 | // Create a file
282 | File file = App::fs->open(path, "w");
283 | if (file) {
284 | file.write((const char*)0);
285 | file.close();
286 | } else {
287 | return replyServerError(F("CREATE FAILED"));
288 | }
289 | }
290 | if (path.lastIndexOf('/') > -1) {
291 | path = path.substring(0, path.lastIndexOf('/'));
292 | }
293 | replyOKWithMsg(path);
294 | } else {
295 | // Source specified: rename
296 | if (src == "/") {
297 | return replyBadRequest(F("BAD SRC"));
298 | }
299 | if (!App::fs->exists(src)) {
300 | return replyBadRequest(F("SRC FILE NOT FOUND"));
301 | }
302 |
303 | LOGL(String("handleFileCreate: ") + path + " from " + src);
304 |
305 | if (path.endsWith("/")) {
306 | path.remove(path.length() - 1);
307 | }
308 | if (src.endsWith("/")) {
309 | src.remove(src.length() - 1);
310 | }
311 | if (!App::fs->rename(src, path)) {
312 | return replyServerError(F("RENAME FAILED"));
313 | }
314 | replyOKWithMsg(lastExistingParent(src));
315 | }
316 | });
317 |
318 | // Delete file
319 | server.on("/edit", HTTP_DELETE, []() {
320 | if (!App::fsOK) {
321 | return replyServerError(FPSTR(FS_INIT_ERROR));
322 | }
323 |
324 | String path = server.arg(0);
325 | if (path.isEmpty() || path == "/") {
326 | return replyBadRequest("BAD PATH");
327 | }
328 |
329 | LOGL(String("handleFileDelete: ") + path);
330 | if (!App::fs->exists(path)) {
331 | return replyNotFound(FPSTR(FILE_NOT_FOUND));
332 | }
333 | deleteRecursive(path);
334 |
335 | replyOKWithMsg(lastExistingParent(path));
336 | });
337 |
338 | // Upload file
339 | server.on("/edit", HTTP_POST, replyOK, []() {
340 | if (!App::fsOK) {
341 | return replyServerError(FPSTR(FS_INIT_ERROR));
342 | }
343 | if (server.uri() != "/edit") {
344 | return;
345 | }
346 | HTTPUpload& upload = server.upload();
347 | String filename;
348 | if (upload.status == UPLOAD_FILE_START) {
349 | if (server.hasArg("path")) {
350 | filename = server.arg("path");
351 | } else {
352 | filename = upload.filename;
353 | }
354 | // Make sure paths always start with "/"
355 | if (!filename.startsWith("/")) {
356 | filename = "/" + filename;
357 | }
358 | LOGL(String("handleFileUpload Name: ") + filename);
359 | uploadFile = App::fs->open(filename, "w");
360 | if (!uploadFile) {
361 | return replyServerError(F("CREATE FAILED"));
362 | }
363 | LOGL(String("Upload: START, filename: ") + filename);
364 | } else if (upload.status == UPLOAD_FILE_WRITE) {
365 | if (uploadFile) {
366 | size_t bytesWritten = uploadFile.write(upload.buf, upload.currentSize);
367 | if (bytesWritten != upload.currentSize) {
368 | return replyServerError(F("WRITE FAILED"));
369 | }
370 | }
371 | LOGL(String("Upload: WRITE, Bytes: ") + upload.currentSize);
372 | } else if (upload.status == UPLOAD_FILE_END) {
373 | if (uploadFile) {
374 | uploadFile.close();
375 | }
376 | LOGL(String("Upload: END, Size: ") + upload.totalSize);
377 | }
378 | });
379 |
380 | server.onNotFound([]() {
381 | if (!App::fsOK) {
382 | return replyServerError(FPSTR(FS_INIT_ERROR));
383 | }
384 |
385 | String uri = ESP8266WebServer::urlDecode(server.uri()); // required to read paths with blanks
386 |
387 | if (handleFileRead(uri)) {
388 | return;
389 | }
390 |
391 | // Dump debug data
392 | String message;
393 | message.reserve(100);
394 | message = F("Error: File not found\n\nURI: ");
395 | message += uri;
396 | message += F("\nMethod: ");
397 | message += (server.method() == HTTP_GET) ? "GET" : "POST";
398 | message += F("\nArguments: ");
399 | message += server.args();
400 | message += '\n';
401 | for (uint8_t i = 0; i < server.args(); i++) {
402 | message += F(" NAME:");
403 | message += server.argName(i);
404 | message += F("\n VALUE:");
405 | message += server.arg(i);
406 | message += '\n';
407 | }
408 | message += "path=";
409 | message += uri;
410 | message += '\n';
411 | LOG(message);
412 |
413 | return replyNotFound(message);
414 | });
415 |
416 | server.begin();
417 | LOGL("HTTP server started");
418 | }
419 |
420 | void loop(void) {
421 | server.handleClient();
422 | }
423 | }; // namespace Api
424 |
425 | #endif
426 |
--------------------------------------------------------------------------------
/inc/app.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include "espnow.h"
12 | #include "Button.h"
13 | #include "MeshRC.h"
14 |
15 | #include "def.h"
16 |
17 | #ifndef __APP_H__
18 | #define __APP_H__
19 |
20 | #define LOG(x...) Serial.print(x)
21 | #define LOGD(x...) Serial.printf(x)
22 | #define LOGL(x...) Serial.println(x)
23 |
24 | namespace App {
25 |
26 | u8 mode;
27 | SaveData data;
28 | Ticker saveTimer;
29 |
30 | FS* fs = &LittleFS;
31 | bool fsOK;
32 |
33 | bool isPaired() {
34 | return !equals(data.master, MeshRC::broadcast, 6);
35 | }
36 | bool isPairing() {
37 | return mode == MODE_BIND;
38 | }
39 | void saveData() {
40 | static SaveData tmp;
41 | static char buf[sizeof(tmp)];
42 | uint8_t crc = 255;
43 | size_t pos = 0;
44 | EEPROM.begin(512);
45 | memcpy(buf, &data, sizeof(data));
46 | LOG(F("Saving config ... [ "));
47 | while (pos < sizeof(data)) {
48 | crc += buf[pos];
49 | EEPROM.write(pos, buf[pos]);
50 | LOGD("%02X ", buf[pos]);
51 | pos++;
52 | }
53 | EEPROM.write(pos, crc);
54 | if (EEPROM.commit())
55 | LOGL(F("] - OK"));
56 | else
57 | LOGL(F("] - FAIL"));
58 | EEPROM.end();
59 | }
60 | void loadData() {
61 | static SaveData tmp;
62 | static char buf[sizeof(tmp)];
63 | uint8_t pos = 0;
64 | size_t crc = 255;
65 | EEPROM.begin(512);
66 | LOG(F("Loading config ... [ "));
67 | while (pos < sizeof(tmp)) {
68 | buf[pos] = EEPROM.read(pos);
69 | crc += buf[pos];
70 | LOGD("%02X ", buf[pos]);
71 | pos++;
72 | }
73 | EEPROM.end();
74 | memcpy(&tmp, buf, sizeof(tmp));
75 | if (tmp.header != HEADER || tmp.version != VERSION) {
76 | LOGL(F("] - INVALID"));
77 | saveData();
78 | } else {
79 | memcpy(&data, &tmp, sizeof(tmp));
80 | LOGL(F("] - OK"));
81 | }
82 | if (isPaired()) {
83 | MeshRC::setMaster(data.master);
84 | }
85 | }
86 | void save() {
87 | saveTimer.once(1, saveData);
88 | }
89 | void setMode(u8 newMode) {
90 | mode = newMode;
91 | }
92 | void setShow(u8 show) {
93 | data.show = show;
94 | }
95 | void setChannel(u8 channel) {
96 | data.channel = channel;
97 | }
98 | void setMaster(u8* addr) {
99 | if (addr == NULL) {
100 | memcpy(data.master, MeshRC::broadcast, 6);
101 | } else {
102 | memcpy(data.master, addr, 6);
103 | }
104 | if (isPaired()) {
105 | MeshRC::setMaster(data.master);
106 | }
107 | }
108 | void setup() {
109 | loadData();
110 | fsOK = fs->begin();
111 | LOGL(fsOK ? F("Filesystem initialized.") : F("Filesystem init failed!"));
112 | if (!fs->exists("/show")) fs->mkdir("/show");
113 | if (!fs->exists("/tmp")) fs->mkdir("/tmp");
114 | }
115 | } // namespace App
116 |
117 | #endif
118 |
--------------------------------------------------------------------------------
/inc/btn.h:
--------------------------------------------------------------------------------
1 | #include "Button.h"
2 | #include "MeshRC.h"
3 | #include "app.h"
4 | #include "net.h"
5 |
6 | Button btn(BTN_PIN);
7 |
8 | Button::callback_t buttonPressHandler = [](u8 repeats) {
9 | if (repeats == 1) LOGD("Button pressed %u time.\n", repeats);
10 | else LOGD("Button pressed %u times.\n", repeats);
11 | switch (repeats) {
12 | case 0:
13 | Net::sendPing();
14 | break;
15 | }
16 | };
17 |
18 | Button::callback_t buttonPressHoldHandler = [](u8 repeats) {
19 | if (!repeats) LOGD("Long pressed hold.\n");
20 | else LOGD("%u short presses then hold.\n", repeats);
21 | switch (repeats) {
22 | case 0:
23 | if (!App::isPairing()) {
24 | LED::end();
25 | App::mode = MODE_BIND;
26 | MeshRC::master = NULL;
27 | startBlink(200);
28 | } else {
29 | App::mode = MODE_SHOW;
30 | MeshRC::master = App::data.master;
31 | stopBlink();
32 | }
33 | LOGD("mode = %i\n", App::mode);
34 | break;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/inc/def.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #ifndef __DEF_H__
4 | #define __DEF_H__
5 |
6 | #define API_PORT (u16)11111
7 |
8 | #define HEADER (u8)0x36 // character '$'
9 | #define VERSION (u8)0x07
10 |
11 | #define RGB_FRAME (u8)0x01
12 | #define END_FRAME (u8)0x02
13 | #define LOOP_FRAME (u8)0x03
14 |
15 | #define MODE_SHOW (u8)0x00
16 | #define MODE_BIND (u8)0x01
17 |
18 | IPAddress apAddr = {10, 1, 1, 1};
19 | IPAddress apMask = {255, 255, 255, 0};
20 |
21 | enum MSG_ID {
22 |
23 | PING_MSG = 0x01,
24 | PAIR_MSG = 0x02,
25 | NODE_MSG = 0x03,
26 | NAME_MSG = 0x04,
27 | INFO_MSG = 0x05,
28 |
29 | SET_RGB_MSG = 0x10,
30 | SET_DIM_MSG = 0x11,
31 |
32 | FILE_BEGIN_MSG = 0x20,
33 | FILE_WRITE_MSG = 0x21,
34 | FILE_CLOSE_MSG = 0x22,
35 |
36 | WIFI_ON_MSG = 0x70,
37 | WIFI_OFF_MSG = 0x71
38 | };
39 |
40 | struct RGB {
41 | u8 r;
42 | u8 g;
43 | u8 b;
44 | void set(u32 c) {
45 | r = (c & 0xFF0000) >> 16;
46 | g = (c & 0x00FF00) >> 8;
47 | b = (c & 0x0000FF) >> 0;
48 | }
49 | void set(u8* d) {
50 | r = d[0];
51 | g = d[1];
52 | b = d[2];
53 | }
54 | void set(RGB* c) {
55 | r = c->r;
56 | g = c->g;
57 | b = c->b;
58 | }
59 | void set(u8 red, u8 green, u8 blue) {
60 | r = red;
61 | g = green;
62 | b = blue;
63 | }
64 | };
65 | struct FrameData {
66 | u8 type;
67 | u8 r;
68 | u8 g;
69 | u8 b;
70 | u32 start;
71 | u32 duration;
72 | u32 transition;
73 | };
74 | struct SyncData {
75 | u32 time;
76 | u8 show;
77 | u8 ended;
78 | u8 paused;
79 | };
80 | struct PingData {
81 | char id[6];
82 | u8 type;
83 | u16 vbat;
84 | char name[20];
85 | };
86 | struct SaveData {
87 | u8 header = HEADER;
88 | u8 version = VERSION;
89 | u8 master[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
90 | u8 brightness = 255;
91 | u8 channel = 0;
92 | u8 show = 1;
93 | char name[20];
94 | };
95 |
96 | bool equals(u8* a, u8* b, u8 size, u8 offset = 0) {
97 | for (auto i = offset; i < (offset + size); i++)
98 | if (a[i] != b[i])
99 | return false;
100 | return true;
101 | }
102 | bool equals(const char* a, const char* b, u8 size, u8 offset = 0) {
103 | return equals((u8*)a, (u8*)b, size, offset);
104 | }
105 | bool equals(u8* a, const char* b, u8 size, u8 offset = 0) {
106 | return equals(a, (u8*)b, size, offset);
107 | }
108 | bool equals(const char* a, u8* b, u8 size, u8 offset = 0) {
109 | return equals((u8*)a, b, size, offset);
110 | }
111 | u32 readUint32(u8* buffer) {
112 | return (u32)(buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3]);
113 | }
114 | u16 readUint16(u8* buffer) {
115 | return (u16)buffer[0] << 8 | buffer[1];
116 | }
117 | u8* setUint16(u8* buffer, u16 value, size_t offset = 0) {
118 | buffer[offset + 1] = value & 0xff;
119 | buffer[offset + 0] = (value >> 8);
120 | return &buffer[offset + 2];
121 | }
122 | u8* setUint32(u8* buffer, u16 value, size_t offset = 0) {
123 | buffer[offset + 3] = (value & 0x000000ff);
124 | buffer[offset + 2] = (value & 0x0000ff00) >> 8;
125 | buffer[offset + 1] = (value & 0x00ff0000) >> 16;
126 | buffer[offset + 0] = (value & 0xff000000) >> 24;
127 | return &buffer[offset + 4];
128 | }
129 | #ifndef LED_OFF
130 | #define LED_OFF LED_HIGH
131 | #endif
132 | #ifndef LED_ON
133 | #define LED_ON LED_LOW
134 | #endif
135 |
136 | Ticker blinkTimer;
137 | void LED_HIGH() {
138 | digitalWrite(LED_PIN, HIGH);
139 | }
140 | void LED_LOW() {
141 | digitalWrite(LED_PIN, LOW);
142 | }
143 | void LED_BLINK() {
144 | digitalWrite(LED_PIN, !digitalRead(LED_PIN));
145 | }
146 | void LED_SETUP() {
147 | pinMode(LED_PIN, OUTPUT);
148 | LED_HIGH();
149 | }
150 |
151 | bool isBlinking;
152 | void stopBlink() {
153 | if (isBlinking) {
154 | LED_HIGH();
155 | if (blinkTimer.active()) blinkTimer.detach();
156 | }
157 | isBlinking = false;
158 | }
159 | void startBlink(u32 time = 1000) {
160 | LED_LOW();
161 | blinkTimer.attach_ms(time, LED_BLINK);
162 | isBlinking = true;
163 | }
164 |
165 | String get_mac_address(bool short_addr = true) {
166 | char tmp[20];
167 | uint8_t mac[6];
168 | #ifdef ARDUINO_ARCH_ESP32
169 | esp_efuse_mac_get_default(mac);
170 | #endif
171 | #ifdef ARDUINO_ARCH_ESP8266
172 | WiFi.macAddress(mac);
173 | #endif
174 | sprintf(tmp, "%02X%02X%02X", mac[3], mac[4], mac[5]);
175 | return String(tmp).substring(0, 6);
176 | };
177 |
178 | #endif
179 |
--------------------------------------------------------------------------------
/inc/led.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "def.h"
4 |
5 | #ifndef __LED_H__
6 | #define __LED_H__
7 |
8 | namespace LED {
9 |
10 | #define START_TIMER u32 start_time = micros()
11 | #define CHECK_TIMEOUT (running && micros() - start_time > 900)
12 |
13 | class Show {
14 | protected:
15 | const char id;
16 | u8 r_pin;
17 | u8 g_pin;
18 | u8 b_pin;
19 |
20 | RGB color;
21 | RGB lastColor;
22 |
23 | Ticker tmr;
24 | File file;
25 |
26 | FrameData frame; // Active frame
27 | u32 currentTime; // Current playing time
28 |
29 | FrameData loopFrame;
30 | u32 loopTime;
31 | u32 loopStart;
32 | u32 loopEnd;
33 |
34 | u32 tickInterval = 2;
35 |
36 | float ratio;
37 | bool busy = false;
38 |
39 | public:
40 | bool running = false;
41 | bool paused = false;
42 | bool repeat = false;
43 |
44 | Show(const char id, u8 r_pin, u8 g_pin, u8 b_pin)
45 | : id(id), r_pin(r_pin), g_pin(g_pin), b_pin(b_pin) {
46 | }
47 |
48 | u32 readUint32(unsigned char* buffer) {
49 | return (u32)(buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3]);
50 | }
51 | FrameData f;
52 | u8 b[16];
53 | FrameData next() {
54 | if (file.available() >= 16) {
55 | file.readBytes((char*)b, 16);
56 | memcpy(&f, b, 16);
57 | } else {
58 | end();
59 | }
60 | // LOGD("- %u: %02x %02x %02x - %u %u %u\n", f.type, f.r, f.g, f.b, f.start, f.duration, frame.transition);
61 | return f;
62 | }
63 | bool isInLoop;
64 | FrameData prev() {
65 | isInLoop = 0;
66 | size_t pos = file.position();
67 | if (pos >= 32) {
68 | file.seek(pos - 32);
69 | file.readBytes((char*)b, 16);
70 | memcpy(&f, b, 16);
71 | }
72 | if (!isInLoop && f.type == END_FRAME) {
73 | isInLoop = true;
74 | while (isInLoop) prev();
75 | }
76 | if (isInLoop && f.type == LOOP_FRAME) {
77 | isInLoop = false;
78 | }
79 | // LOGD("- %u: %02x %02x %02x - %u %u %u\n", f.type, f.r, f.g, f.b, f.start, f.duration, frame.transition);
80 | return f;
81 | }
82 |
83 | void
84 | setTransition(FrameData* frame, u32 lapsed) {
85 | if (frame->transition && lapsed <= frame->transition) {
86 | // Compute color value during transition
87 | ratio = (float)lapsed / frame->transition;
88 |
89 | if (frame->r > lastColor.r)
90 | color.r = lastColor.r + (frame->r - lastColor.r) * ratio;
91 | else
92 | color.r = lastColor.r - (lastColor.r - frame->r) * ratio;
93 |
94 | if (frame->g > lastColor.g)
95 | color.g = lastColor.g + (frame->g - lastColor.g) * ratio;
96 | else
97 | color.g = lastColor.g - (lastColor.g - frame->g) * ratio;
98 |
99 | if (frame->b > lastColor.b)
100 | color.b = lastColor.b + (frame->b - lastColor.b) * ratio;
101 | else
102 | color.b = lastColor.b - (lastColor.b - frame->b) * ratio;
103 |
104 | // LOGD("%f %u %u %u\n", ratio, color.r, color.g, color.b);
105 |
106 | } else {
107 | color.set(frame->r, frame->g, frame->b);
108 | }
109 | setRGB(color.r, color.g, color.b);
110 | }
111 |
112 | void end() {
113 | if (file) file.close();
114 | tmr.detach();
115 | setRGB(0, 0, 0);
116 | currentTime = 0;
117 | loopTime = 0;
118 | running = false;
119 | paused = false;
120 | }
121 |
122 | void pause() {
123 | paused = true;
124 | }
125 |
126 | void resume() {
127 | paused = false;
128 | }
129 |
130 | u32 getTime() {
131 | return currentTime;
132 | }
133 | void setTime(u32 time) {
134 | busy = true;
135 | int offset = currentTime - time;
136 | #ifdef SYNC_LOGS
137 | LOGD("Time offset: %c %i\n", id, offset);
138 | #endif
139 | if (!paused) {
140 | if (offset > 16) {
141 | while (running && frame.start >= time) {
142 | currentTime = frame.start;
143 | frame = prev();
144 | }
145 | while (running && currentTime < time) {
146 | if (!tick(false)) break;
147 | }
148 | } else if (offset < -16) {
149 | while (running && frame.start + frame.duration < time) {
150 | frame = next();
151 | currentTime = frame.start;
152 | }
153 | while (running && currentTime < time) {
154 | if (!tick(false)) break;
155 | }
156 | }
157 | }
158 | busy = false;
159 | }
160 | // true = playing
161 | // false = ended
162 | bool tick(bool shouldUpdate = true) {
163 | switch (frame.type) {
164 | case RGB_FRAME:
165 | if (shouldUpdate)
166 | setTransition(&frame, currentTime - frame.start);
167 | break;
168 | case LOOP_FRAME:
169 | if (loopTime == 0) {
170 | // LOGD("\n * ");
171 | loopStart = file.position();
172 | loopFrame = next();
173 | }
174 | if (shouldUpdate && loopFrame.type == RGB_FRAME) {
175 | setTransition(&loopFrame, loopTime - loopFrame.start);
176 | }
177 | loopTime += tickInterval;
178 | if (loopTime >= loopFrame.start + loopFrame.duration) {
179 | loopTime = loopFrame.start + loopFrame.duration;
180 | // LOGD(" * ");
181 | if (loopFrame.type == RGB_FRAME) {
182 | lastColor.set(loopFrame.r, loopFrame.g, loopFrame.b);
183 | }
184 | loopFrame = next();
185 | if (loopFrame.type == END_FRAME) {
186 | loopTime = 0;
187 | loopEnd = file.position();
188 | file.seek(loopStart);
189 | }
190 | }
191 | break;
192 | case END_FRAME:
193 | // LOGL("ended");
194 | repeat ? begin() : end();
195 | break;
196 | }
197 | currentTime += tickInterval;
198 | if (currentTime >= frame.start + frame.duration) {
199 | currentTime = frame.start + frame.duration;
200 | // LOGD("\nframe");
201 | if (frame.type == LOOP_FRAME) {
202 | loopTime = 0;
203 | file.seek(loopEnd);
204 | }
205 | if (frame.type == RGB_FRAME) {
206 | lastColor.set(frame.r, frame.g, frame.b);
207 | }
208 | frame = next();
209 | }
210 | return true;
211 | }
212 |
213 | void setRGB(u8 r, u8 g, u8 b) {
214 | analogWrite(r_pin, map(r, 0, 255, 0, App::data.brightness * 1.0));
215 | analogWrite(g_pin, map(g, 0, 255, 0, App::data.brightness * 0.66));
216 | analogWrite(b_pin, map(b, 0, 255, 0, App::data.brightness * 0.66));
217 | }
218 |
219 | void setup() {
220 | pinMode(r_pin, OUTPUT_OPEN_DRAIN);
221 | pinMode(g_pin, OUTPUT_OPEN_DRAIN);
222 | pinMode(b_pin, OUTPUT_OPEN_DRAIN);
223 | }
224 |
225 | void begin() {
226 | end();
227 | String path = "/show/" + String(App::data.show) + (id) + ".lsb";
228 | if (!App::fs->exists(path)) {
229 | LOGD("Show not found: %s\n", path.c_str());
230 | return;
231 | }
232 | LOGD("Playing show: %s\n", path.c_str());
233 | running = 1;
234 | paused = 0;
235 | file = App::fs->open(path, "r");
236 | file.setTimeout(0);
237 | frame = next();
238 | tmr.attach_ms_scheduled_accurate(tickInterval, [this]() {
239 | if (running && !paused && !busy) tick(true);
240 | });
241 | }
242 | };
243 |
244 | Show A('A', R1_PIN, G1_PIN, B1_PIN);
245 | Show B('B', R2_PIN, G2_PIN, B2_PIN);
246 |
247 | void setup() {
248 | analogWriteFreq(10000);
249 | analogWriteRange(255);
250 | A.setup();
251 | B.setup();
252 | }
253 | void end() {
254 | A.end();
255 | B.end();
256 | }
257 | void begin() {
258 | A.begin();
259 | B.begin();
260 | }
261 | void pause() {
262 | A.pause();
263 | B.pause();
264 | }
265 | void resume() {
266 | A.resume();
267 | B.resume();
268 | }
269 | void setTime(u32 time) {
270 | A.setTime(time);
271 | B.setTime(time);
272 | }
273 | bool isRunning() {
274 | return A.running || B.running;
275 | }
276 | bool isPaused() {
277 | return A.paused && B.paused;
278 | }
279 | void setRGB(u8* buf, u8 len) {
280 | u8 r = buf[0];
281 | u8 g = buf[1];
282 | u8 b = buf[2];
283 | A.setRGB(r, g, b);
284 | B.setRGB(r, g, b);
285 | }
286 |
287 | } // namespace LED
288 |
289 | #endif
290 |
--------------------------------------------------------------------------------
/inc/net.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "app.h"
5 | #include "def.h"
6 | #include "led.h"
7 |
8 | #ifndef __NET_H__
9 | #define __NET_H__
10 |
11 | #define MSG_PING "#>PING"
12 | #define MSG_SYNC "#>SYNC"
13 | #define MSG_PAIR "#>PAIR"
14 | #define MSG_NAME "#>NAME"
15 | #define MSG_RGB "#>RGB"
16 | #define MSG_SET "#>SET"
17 | #define MSG_DIM "#>DIM"
18 | #define MSG_RESET "#>RESET"
19 | #define MSG_RESTART "#>RESTART"
20 | #define MSG_WIFI_ON "#>WIFI"
21 | #define MSG_WIFI_OFF "#>WIFI"
22 | #define MSG_FBEGIN "#>FBEGIN"
23 | #define MSG_FWRITE "#>FWRITE"
24 | #define MSG_FCLOSE "#>FCLOSE"
25 |
26 | namespace Net {
27 |
28 | void setSync(u8* data, u8 size) {
29 | LED_ON();
30 | SyncData state;
31 | memcpy(&state, data, size);
32 | LOGD("received sync: %u %u %u %u\n", state.show, state.ended, state.paused, state.time);
33 | if (state.show == 0 && App::data.show != 0) {
34 | LED::end();
35 | LED::begin();
36 | }
37 | App::data.show = state.show;
38 | if (state.show > 0) {
39 | if (LED::isRunning() && state.ended) {
40 | LED::end();
41 | } else if (!LED::isRunning() && !state.ended) {
42 | LED::begin();
43 | } else if (state.paused && !LED::isPaused()) {
44 | LED::pause();
45 | } else if (!state.paused && LED::isPaused()) {
46 | LED::resume();
47 | }
48 | if (!state.ended && LED::isRunning()) {
49 | LED::setTime(state.time);
50 | }
51 | }
52 | LED_OFF();
53 | }
54 | void sendPing() {
55 | LED_ON();
56 | size_t size = sizeof(App::data.name) + 3;
57 | u8 data[size];
58 | data[0] = 2;
59 | setUint16(&data[1], ESP.getVcc());
60 | memcpy(&data[3], App::data.name, sizeof(App::data.name));
61 | MeshRC::send(get_mac_address(false) + " 6 && equals(data, (u8*)get_mac_address(false).c_str(), 6) && (data[6] == '>')) {
103 | u8* newData = &data[5];
104 | newData[0] = '#';
105 | MeshRC::recvHandler(MeshRC::sender, newData, size - 5);
106 | MeshRC::send("# 0) {
115 | // wifi_softap_get_station_info();
116 | // if (lastCount == 0) startBlink(500);
117 | // } else {
118 | // stopBlink();
119 | // }
120 | // lastCount = cnt <= 0 ? 0 : cnt;
121 | }
122 | } // namespace Net
123 |
124 | #endif
125 |
--------------------------------------------------------------------------------
/lib/RGBLed/RGBLed.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #ifndef __RGBLED_H__
4 | #define __RGBLED_H__
5 |
6 | class RGBLed {
7 | protected:
8 | uint8_t r_pin;
9 | uint8_t g_pin;
10 | uint8_t b_pin;
11 |
12 | bool inverted;
13 |
14 | public:
15 | RGBLed(uint8_t r, uint8_t g, uint8_t b, bool common_anode = false)
16 | : r_pin(r), g_pin(g), b_pin(b), inverted(common_anode) {
17 | pinMode(r_pin, OUTPUT_OPEN_DRAIN);
18 | pinMode(g_pin, OUTPUT_OPEN_DRAIN);
19 | pinMode(b_pin, OUTPUT_OPEN_DRAIN);
20 | analogWriteFreq(10000);
21 | analogWriteRange(255);
22 | }
23 |
24 | void set(uint8_t r, uint8_t g, uint8_t b) {
25 | analogWrite(r_pin, inverted ? 255 - r : r);
26 | analogWrite(g_pin, inverted ? 255 - g : g);
27 | analogWrite(b_pin, inverted ? 255 - b : b);
28 | }
29 | };
30 |
31 | #endif // __RGBLED_H__
32 |
--------------------------------------------------------------------------------
/lib/transport/transport.h:
--------------------------------------------------------------------------------
1 | #ifdef ESP32
2 | #include
3 | #else
4 | #include
5 | #include
6 | #include
7 | #include
8 | #endif
9 |
10 | #ifndef __PROTOCOL_H__
11 | #define __PROTOCOL_H__
12 |
13 | using namespace std;
14 |
15 | static uint8_t syncword[] = {0x24};
16 | static uint8_t broadcast[] = {0xff,0xff,0xff,0xff,0xff,0xff};
17 |
18 | class Transport {
19 | protected:
20 | Stream *_stream;
21 | function _receive;
22 |
23 | uint8_t buffer[64];
24 | uint8_t length = 0;
25 | uint8_t crc = 0;
26 | bool synced = false;
27 |
28 | bool equals(uint8_t *a, uint8_t *b, uint8_t size, uint8_t offset = 0) {
29 | for (auto i = offset; i < (offset + size); i++)
30 | if (a[i] != b[i])
31 | return false;
32 | return true;
33 | }
34 | bool isValidStart() {
35 | return synced = synced || equals(buffer, syncword, sizeof(syncword));
36 | }
37 | bool isValidSize() {
38 | return length >= (uint8_t)(sizeof(syncword) + buffer[1] + 2);
39 | }
40 | bool checksum() {
41 | return buffer[length - 1] == crc;
42 | }
43 | bool isValidFrame() {
44 | return isValidStart() && isValidSize() && checksum();
45 | }
46 |
47 | public:
48 | Transport();
49 | Transport(Stream *stream) : _stream(stream) {}
50 |
51 | void receive(function cb) { _receive = cb; }
52 |
53 | void parse(uint8_t data) {
54 | buffer[length++] = data;
55 | if (isValidFrame()) {
56 | synced = false;
57 | uint8_t size = buffer[1];
58 | uint8_t *buf = &buffer[2];
59 |
60 | if (_receive)
61 | _receive(broadcast, buf, size);
62 |
63 | // LOGD(":: receive %u bytes (0x%02X): ", size, crc);
64 | // for (auto i = 0; i < size; i++) {
65 | // LOGD("%02X ", buf[i]);
66 | // }
67 | // LOGL();
68 | }
69 | crc += data;
70 | if (!synced) {
71 | length = 0;
72 | crc = 0;
73 | }
74 | }
75 | void parse(uint8_t *data, uint8_t size) {
76 | for (auto i = 0; i < size; i++) parse(data[i]);
77 | }
78 | void send(uint8_t* addr, uint8_t *data, uint8_t size) {
79 | uint8_t sum = 0;
80 | uint8_t len = size + sizeof(syncword) + 2;
81 | uint8_t payload[len];
82 | for (auto i = 0; i < (uint8_t)sizeof(syncword); i++) {
83 | payload[i] = syncword[i];
84 | sum += payload[i];
85 | }
86 | payload[sizeof(syncword)] = size;
87 | sum += size;
88 | for (auto i = 0; i < size; i++) {
89 | payload[sizeof(syncword) + 1 + i] = data[i];
90 | sum += data[i];
91 | }
92 | payload[len - 1] = sum;
93 |
94 | _stream->write(payload, len);
95 |
96 | // LOGD(":: sending %u bytes (0x%02X): ", size, sum);
97 | // for (auto i = 0; i < size; i++) {
98 | // LOGD("%02X ", data[i]);
99 | // }
100 | // LOGL();
101 | // parse(payload, len);
102 | }
103 | void send(uint8_t* addr, const char *data, size_t size) {
104 | send(addr, (uint8_t *)data, (uint8_t)size);
105 | }
106 | void send(uint8_t* addr, string data) {
107 | send(addr, data.c_str(), data.length());
108 | }
109 | void send(uint8_t * data, uint8_t size) {
110 | send(broadcast, data, size);
111 | }
112 | void send(const char * data, size_t size) {
113 | send(broadcast, data, size);
114 | }
115 | void send(string data) {
116 | send(broadcast, data);
117 | }
118 | void loop() {
119 | if (_stream && _stream->available()) {
120 | while (_stream->available()) {
121 | parse(_stream->read());
122 | }
123 | }
124 | }
125 | };
126 |
127 | #endif
128 |
--------------------------------------------------------------------------------
/platformio.ini:
--------------------------------------------------------------------------------
1 | ; PlatformIO Project Configuration File
2 | ;
3 | ; Build options: build flags, source filter
4 | ; Upload options: custom upload port, speed and extra flags
5 | ; Library options: dependencies, extra library storages
6 | ; Advanced options: extra scripting
7 | ;
8 | ; Please visit documentation for the other options and examples
9 | ; https://docs.platformio.org/page/projectconf.html
10 |
11 | [platformio]
12 | include_dir = inc
13 |
14 | [env]
15 | board = esp12e
16 | framework = arduino
17 | platform = espressif8266
18 | board_build.f_cpu = 160000000L
19 | board_build.f_flash = 80000000L
20 | board_build.filesystem = littlefs
21 | board_build.ldscript = eagle.flash.4m3m.ld
22 |
23 | [tx]
24 | build_flags =
25 | -D TRANSMITTER
26 | -D BTN_PIN=0
27 | -D LED_PIN=2
28 |
29 | [rx]
30 | build_flags =
31 | -D RECEIVER
32 | -D BTN_PIN=0
33 | -D LED_PIN=2
34 |
35 | -D G1_PIN=12
36 | -D R1_PIN=13
37 | -D B1_PIN=14
38 |
39 | -D G2_PIN=15
40 | -D R2_PIN=5
41 | -D B2_PIN=4
42 |
43 | [env:tx_uart]
44 | monitor_speed = 115200
45 | upload_speed = 460800
46 | build_flags = ${tx.build_flags}
47 | lib_deps = chris--a/Keypad@^3.1.1
48 |
49 | [env:rx_uart]
50 | monitor_speed = 115200
51 | upload_speed = 460800
52 | build_flags = ${rx.build_flags}
53 | lib_deps = chris--a/Keypad@^3.1.1
54 |
55 | [env:tx_ota]
56 | monitor_speed = 115200
57 | upload_protocol = espota
58 | upload_port = 10.1.1.1
59 | build_flags = ${tx.build_flags}
60 | lib_deps = chris--a/Keypad@^3.1.1
61 |
62 | [env:rx_ota]
63 | monitor_speed = 115200
64 | upload_protocol = espota
65 | upload_port = 10.1.1.1
66 | build_flags = ${rx.build_flags}
67 | lib_deps = chris--a/Keypad@^3.1.1
68 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | #ifdef RECEIVER
2 | #include "receiver.h"
3 | #endif
4 |
5 | #ifdef TRANSMITTER
6 | #include "transmitter.h"
7 | #endif
8 |
--------------------------------------------------------------------------------
/src/receiver.h:
--------------------------------------------------------------------------------
1 |
2 | #include "api.h"
3 | #include "app.h"
4 | #include "btn.h"
5 | #include "def.h"
6 | #include "led.h"
7 | #include "net.h"
8 |
9 | // ADC_MODE(ADC_VCC);
10 |
11 | void setup() {
12 | LED_SETUP();
13 | #ifdef ENABLE_DEBUG_LOGS
14 | Serial.begin(115200);
15 | #endif
16 |
17 | WiFi.mode(WIFI_AP_STA);
18 | WiFi.setSleepMode(WIFI_NONE_SLEEP);
19 | WiFi.setPhyMode(WIFI_PHY_MODE_11G);
20 | WiFi.disconnect();
21 | WiFi.softAPConfig(apAddr, apAddr, apMask);
22 | WiFi.softAP("SDC_" + get_mac_address(false).substring(0, 6), "");
23 |
24 | btn.begin();
25 | btn.onPress(buttonPressHandler);
26 | btn.onPressHold(buttonPressHoldHandler);
27 |
28 | // ArduinoOTA.onProgress([](int percent, int total) { if (LED::isRunning()) LED::end(); });
29 | ArduinoOTA.onProgress([](int percent, int total) { LED_BLINK(); });
30 | ArduinoOTA.onEnd([]() { LED_LOW(); });
31 | ArduinoOTA.begin();
32 |
33 | App::setup();
34 | LED::setup();
35 | Net::setup();
36 | Api::setup();
37 | }
38 |
39 | void loop() {
40 | ArduinoOTA.handle();
41 | Api::loop();
42 | Net::loop();
43 | }
44 |
--------------------------------------------------------------------------------
/src/transmitter.h:
--------------------------------------------------------------------------------
1 | #ifdef ESP8266
2 | #include
3 | #include
4 | #endif
5 | #ifdef ESP32
6 | #include
7 | #include
8 | #endif
9 |
10 | #include
11 | #include
12 | #include
13 |
14 | // SETUP MATRIX KEYPAD
15 |
16 | const uint8_t ROWS = 4;
17 | const uint8_t COLS = 4;
18 |
19 | char hexaKeys[ROWS][COLS] = {
20 | {'1', '2', '3', 'A'},
21 | {'4', '5', '6', 'B'},
22 | {'7', '8', '9', 'C'},
23 | {'*', '0', '#', 'D'}};
24 |
25 | uint8_t rowPins[ROWS] = {D0, D1, D2, D3};
26 | uint8_t colPins[COLS] = {D5, D6, D7, D8};
27 |
28 | Keypad keypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);
29 |
30 | // END SETUP MATRIX KEYPAD
31 |
32 | u8 psk[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f'};
33 |
34 | Transport bridge(&Serial);
35 |
36 | String get_mac_address(bool short_addr = true) {
37 | char tmp[20];
38 | uint8_t mac[6];
39 | #ifdef ARDUINO_ARCH_ESP32
40 | esp_efuse_mac_get_default(mac);
41 | #endif
42 | #ifdef ARDUINO_ARCH_ESP8266
43 | WiFi.macAddress(mac);
44 | #endif
45 | sprintf(tmp, "%02X%02X%02X", mac[3], mac[4], mac[5]);
46 | if (short_addr) {
47 | return String(tmp).substring(0, 6);
48 | } else {
49 | return String(tmp).substring(0, 6);
50 | }
51 | };
52 |
53 | void setup() {
54 | Serial.begin(115200);
55 |
56 | IPAddress apAddr = {10, 1, 1, 1};
57 | IPAddress apMask = {255, 255, 255, 0};
58 | String apSSID = "SDC_" + get_mac_address() + "_TX";
59 | String apPSK = "";
60 | WiFi.mode(WIFI_AP_STA);
61 | WiFi.setPhyMode(WIFI_PHY_MODE_11G);
62 | WiFi.softAPConfig(apAddr, apAddr, apMask);
63 | WiFi.softAP(apSSID, apPSK);
64 | WiFi.disconnect();
65 | // WiFi.softAPdisconnect();
66 | ArduinoOTA.begin();
67 |
68 | bridge.receive([](uint8_t* addr, uint8_t* data, size_t size) {
69 | esp_now_send(addr, data, size);
70 | });
71 | esp_now_init();
72 | #ifdef ESP8266
73 | // esp_now_set_kok(psk, sizeof(psk));
74 | esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
75 | esp_now_add_peer(broadcast, ESP_NOW_ROLE_COMBO, 1, 0, 0);
76 | esp_now_register_recv_cb([](uint8_t* addr, uint8_t* data, uint8_t size) {
77 | bridge.send(addr, data, size);
78 | });
79 | #endif
80 | #ifdef ESP32
81 | esp_now_peer_info_t peerInfo;
82 | memcpy(peerInfo.peer_addr, broadcast, 6);
83 | peerInfo.channel = 1;
84 | peerInfo.encrypt = false;
85 | esp_now_register_recv_cb([](const uint8_t* addr, const uint8_t* data, int size) {
86 | bridge.send((uint8_t*)addr, (uint8_t*)data, (uint8_t)size);
87 | });
88 | #endif
89 | }
90 |
91 | uint8_t WHITE[] = {0x16, 0x3E, 0x53, 0x45, 0x54, 0xFF, 0xFF, 0xFF};
92 | uint8_t BLACK[] = {0x16, 0x3E, 0x53, 0x45, 0x54, 0x00, 0x00, 0x00};
93 |
94 | void keypadLoop() {
95 | char key = keypad.getKey();
96 | switch (key) {
97 | case '1': esp_now_send(broadcast, WHITE, sizeof(WHITE)); break;
98 | case '2': esp_now_send(broadcast, BLACK, sizeof(BLACK)); break;
99 | }
100 | }
101 |
102 | void loop() {
103 | bridge.loop();
104 | keypadLoop();
105 | }
106 |
--------------------------------------------------------------------------------