discstation 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,7 +114,6 @@ Support by OS:
114
114
  ./install.sh
115
115
 
116
116
  # Arduino (ESP32 DevKit remote)
117
- arduino-cli lib install QRCode
118
117
  arduino-cli compile --upload -b esp32:esp32:esp32 -p /dev/ttyUSB0 arduino/DiscStation
119
118
 
120
119
  # macOS host
@@ -8,7 +8,7 @@
8
8
  #include <ESPmDNS.h>
9
9
  #include <Preferences.h>
10
10
  #include <ArduinoOTA.h>
11
- #include <QRCode.h>
11
+ #include "qrcode_lite.h"
12
12
 
13
13
  // Optional build-time Wi-Fi override for power users: copy secrets.h.example
14
14
  // to secrets.h (git-ignored) and set WIFI_SSID / WIFI_PASS / OTA_PASSWORD.
@@ -221,6 +221,8 @@ String discLine = "Disc: none";
221
221
  String line1 = "Status: READY";
222
222
  String line2 = "";
223
223
  String line3 = "";
224
+ bool trayOpen = false; // host says the tray is open (eject wait / STANDBY:Tray open) - EJECT button then closes it
225
+ bool waitingIsTray = false; // the WAITING screen is the eject "close tray" wait, not the pre-burn confirm
224
226
  String waitingLine1 = "";
225
227
  String waitingLine2 = "";
226
228
  int progressPercent = -1;
@@ -456,7 +458,7 @@ void drawWaiting() {
456
458
  printUpper(waitingLine2);
457
459
 
458
460
  display.setCursor(2, 49);
459
- display.print("SHORT // GO");
461
+ display.print(waitingIsTray ? "EJECT BTN // CLOSE" : "SHORT // GO");
460
462
 
461
463
  display.display();
462
464
  }
@@ -774,6 +776,7 @@ void parseMessage(String msg) {
774
776
  Out.println(msg);
775
777
 
776
778
  if (msg.startsWith("HOME:")) {
779
+ trayOpen = false;
777
780
  drawHome();
778
781
 
779
782
  } else if (msg.startsWith("DISC:")) {
@@ -802,6 +805,7 @@ void parseMessage(String msg) {
802
805
  if (uiState == UI_HOME) drawHome();
803
806
 
804
807
  } else if (msg.startsWith("STANDBY:")) {
808
+ trayOpen = msg.substring(8) == "Tray open";
805
809
  drawStandby();
806
810
 
807
811
  } else if (msg.startsWith("TITLE:")) {
@@ -855,8 +859,13 @@ void parseMessage(String msg) {
855
859
  } else if (msg.startsWith("WAITING:")) {
856
860
  returnToHomeAt = 0;
857
861
  String rest = msg.substring(8);
862
+ waitingIsTray = rest.indexOf("close tray") >= 0;
863
+ if (waitingIsTray) trayOpen = true;
858
864
  int slash = rest.indexOf('/');
859
- if (slash >= 0) {
865
+ if (waitingIsTray) {
866
+ waitingLine1 = "TRAY OPEN";
867
+ waitingLine2 = "";
868
+ } else if (slash >= 0) {
860
869
  waitingLine1 = "Data: " + rest.substring(0, slash);
861
870
  waitingLine1.trim();
862
871
  waitingLine2 = "Disc: " + rest.substring(slash + 1);
@@ -989,14 +998,6 @@ void handleSelectPress(bool longPress) {
989
998
  drawStatus();
990
999
  }
991
1000
 
992
- } else if (uiState == UI_STANDBY) {
993
- Out.println("EJECT");
994
- line1 = "Ejecting...";
995
- line2 = "";
996
- line3 = "";
997
- returnToHomeAt = millis() + 5000;
998
- drawStatus();
999
-
1000
1001
  } else if (uiState == UI_STATUS && line1 == "Ejecting...") {
1001
1002
  // Manual escape if stuck on eject screen with no app response
1002
1003
  drawStandby();
@@ -1011,7 +1012,7 @@ void handleSelectPress(bool longPress) {
1011
1012
  if (longPress) {
1012
1013
  Out.println("CANCEL");
1013
1014
  drawHome();
1014
- } else {
1015
+ } else if (!waitingIsTray) { // tray close moved to the EJECT button
1015
1016
  Out.println("CONFIRM");
1016
1017
  line1 = "Confirmed!";
1017
1018
  line2 = "";
@@ -1144,7 +1145,15 @@ void handleEncoderCCW() {
1144
1145
  void handleEjectButton() {
1145
1146
  if (wakeDisplay()) { lastInputTime = millis(); return; }
1146
1147
  lastInputTime = millis();
1147
- if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_DISCONNECTED || uiState == UI_PLAY) {
1148
+ if (trayOpen && (uiState == UI_STANDBY || uiState == UI_HOME || (uiState == UI_WAITING && waitingIsTray))) {
1149
+ Out.println("CONFIRM"); // host closes the tray
1150
+ trayOpen = false;
1151
+ line1 = "Closing tray...";
1152
+ line2 = "";
1153
+ line3 = "";
1154
+ returnToHomeAt = millis() + 5000;
1155
+ drawStatus();
1156
+ } else if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_DISCONNECTED || uiState == UI_PLAY) {
1148
1157
  Out.println("EJECT");
1149
1158
  line1 = "Ejecting...";
1150
1159
  line2 = "";
@@ -0,0 +1,872 @@
1
+ /**
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2017 Richard Moore
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ * THE SOFTWARE.
23
+ */
24
+
25
+ /**
26
+ * Special thanks to Nayuki (https://www.nayuki.io/) from which this library was
27
+ * heavily inspired and compared against.
28
+ *
29
+ * See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp
30
+ */
31
+
32
+ #include "qrcode_lite.h"
33
+
34
+ #include <stdlib.h>
35
+ #include <string.h>
36
+
37
+ #pragma mark - Error Correction Lookup tables
38
+
39
+ #if LOCK_VERSION == 0
40
+
41
+ static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = {
42
+ // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
43
+ { 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, // Medium
44
+ { 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, // Low
45
+ { 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, // High
46
+ { 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, // Quartile
47
+ };
48
+
49
+ static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = {
50
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
51
+ // 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
52
+ { 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium
53
+ { 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low
54
+ { 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High
55
+ { 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile
56
+ };
57
+
58
+ static const uint16_t NUM_RAW_DATA_MODULES[40] = {
59
+ // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
60
+ 208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523,
61
+ // 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
62
+ 7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587,
63
+ // 32, 33, 34, 35, 36, 37, 38, 39, 40
64
+ 19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648
65
+ };
66
+
67
+ // @TODO: Put other LOCK_VERSIONS here
68
+ #elif LOCK_VERSION == 3
69
+
70
+ static const int16_t NUM_ERROR_CORRECTION_CODEWORDS[4] = {
71
+ 26, 15, 44, 36
72
+ };
73
+
74
+ static const int8_t NUM_ERROR_CORRECTION_BLOCKS[4] = {
75
+ 1, 1, 2, 2
76
+ };
77
+
78
+ static const uint16_t NUM_RAW_DATA_MODULES = 567;
79
+
80
+ #else
81
+
82
+ #error Unsupported LOCK_VERSION (add it...)
83
+
84
+ #endif
85
+
86
+
87
+ static int max(int a, int b) {
88
+ if (a > b) { return a; }
89
+ return b;
90
+ }
91
+
92
+ /*
93
+ static int abs(int value) {
94
+ if (value < 0) { return -value; }
95
+ return value;
96
+ }
97
+ */
98
+
99
+
100
+ #pragma mark - Mode testing and conversion
101
+
102
+ static int8_t getAlphanumeric(char c) {
103
+
104
+ if (c >= '0' && c <= '9') { return (c - '0'); }
105
+ if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); }
106
+
107
+ switch (c) {
108
+ case ' ': return 36;
109
+ case '$': return 37;
110
+ case '%': return 38;
111
+ case '*': return 39;
112
+ case '+': return 40;
113
+ case '-': return 41;
114
+ case '.': return 42;
115
+ case '/': return 43;
116
+ case ':': return 44;
117
+ }
118
+
119
+ return -1;
120
+ }
121
+
122
+ static bool isAlphanumeric(const char *text, uint16_t length) {
123
+ while (length != 0) {
124
+ if (getAlphanumeric(text[--length]) == -1) { return false; }
125
+ }
126
+ return true;
127
+ }
128
+
129
+
130
+ static bool isNumeric(const char *text, uint16_t length) {
131
+ while (length != 0) {
132
+ char c = text[--length];
133
+ if (c < '0' || c > '9') { return false; }
134
+ }
135
+ return true;
136
+ }
137
+
138
+
139
+ #pragma mark - Counting
140
+
141
+ // We store the following tightly packed (less 8) in modeInfo
142
+ // <=9 <=26 <= 40
143
+ // NUMERIC ( 10, 12, 14);
144
+ // ALPHANUMERIC ( 9, 11, 13);
145
+ // BYTE ( 8, 16, 16);
146
+ static char getModeBits(uint8_t version, uint8_t mode) {
147
+ // Note: We use 15 instead of 16; since 15 doesn't exist and we cannot store 16 (8 + 8) in 3 bits
148
+ // hex(int("".join(reversed([('00' + bin(x - 8)[2:])[-3:] for x in [10, 9, 8, 12, 11, 15, 14, 13, 15]])), 2))
149
+ unsigned int modeInfo = 0x7bbb80a;
150
+
151
+ #if LOCK_VERSION == 0 || LOCK_VERSION > 9
152
+ if (version > 9) { modeInfo >>= 9; }
153
+ #endif
154
+
155
+ #if LOCK_VERSION == 0 || LOCK_VERSION > 26
156
+ if (version > 26) { modeInfo >>= 9; }
157
+ #endif
158
+
159
+ char result = 8 + ((modeInfo >> (3 * mode)) & 0x07);
160
+ if (result == 15) { result = 16; }
161
+
162
+ return result;
163
+ }
164
+
165
+
166
+ #pragma mark - BitBucket
167
+
168
+ typedef struct BitBucket {
169
+ uint32_t bitOffsetOrWidth;
170
+ uint16_t capacityBytes;
171
+ uint8_t *data;
172
+ } BitBucket;
173
+
174
+ /*
175
+ void bb_dump(BitBucket *bitBuffer) {
176
+ printf("Buffer: ");
177
+ for (uint32_t i = 0; i < bitBuffer->capacityBytes; i++) {
178
+ printf("%02x", bitBuffer->data[i]);
179
+ if ((i % 4) == 3) { printf(" "); }
180
+ }
181
+ printf("\n");
182
+ }
183
+ */
184
+
185
+ static uint16_t bb_getGridSizeBytes(uint8_t size) {
186
+ return (((size * size) + 7) / 8);
187
+ }
188
+
189
+ static uint16_t bb_getBufferSizeBytes(uint32_t bits) {
190
+ return ((bits + 7) / 8);
191
+ }
192
+
193
+ static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) {
194
+ bitBuffer->bitOffsetOrWidth = 0;
195
+ bitBuffer->capacityBytes = capacityBytes;
196
+ bitBuffer->data = data;
197
+
198
+ memset(data, 0, bitBuffer->capacityBytes);
199
+ }
200
+
201
+ static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) {
202
+ bitGrid->bitOffsetOrWidth = size;
203
+ bitGrid->capacityBytes = bb_getGridSizeBytes(size);
204
+ bitGrid->data = data;
205
+
206
+ memset(data, 0, bitGrid->capacityBytes);
207
+ }
208
+
209
+ static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) {
210
+ uint32_t offset = bitBuffer->bitOffsetOrWidth;
211
+ for (int8_t i = length - 1; i >= 0; i--, offset++) {
212
+ bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7));
213
+ }
214
+ bitBuffer->bitOffsetOrWidth = offset;
215
+ }
216
+ /*
217
+ void bb_setBits(BitBucket *bitBuffer, uint32_t val, int offset, uint8_t length) {
218
+ for (int8_t i = length - 1; i >= 0; i--, offset++) {
219
+ bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7));
220
+ }
221
+ }
222
+ */
223
+ static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) {
224
+ uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
225
+ uint8_t mask = 1 << (7 - (offset & 0x07));
226
+ if (on) {
227
+ bitGrid->data[offset >> 3] |= mask;
228
+ } else {
229
+ bitGrid->data[offset >> 3] &= ~mask;
230
+ }
231
+ }
232
+
233
+ static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) {
234
+ uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
235
+ uint8_t mask = 1 << (7 - (offset & 0x07));
236
+ bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0);
237
+ if (on ^ invert) {
238
+ bitGrid->data[offset >> 3] |= mask;
239
+ } else {
240
+ bitGrid->data[offset >> 3] &= ~mask;
241
+ }
242
+ }
243
+
244
+ static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) {
245
+ uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
246
+ return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
247
+ }
248
+
249
+
250
+ #pragma mark - Drawing Patterns
251
+
252
+ // XORs the data modules in this QR Code with the given mask pattern. Due to XOR's mathematical
253
+ // properties, calling applyMask(m) twice with the same value is equivalent to no change at all.
254
+ // This means it is possible to apply a mask, undo it, and try another mask. Note that a final
255
+ // well-formed QR Code symbol needs exactly one mask applied (not zero, not two, etc.).
256
+ static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) {
257
+ uint8_t size = modules->bitOffsetOrWidth;
258
+
259
+ for (uint8_t y = 0; y < size; y++) {
260
+ for (uint8_t x = 0; x < size; x++) {
261
+ if (bb_getBit(isFunction, x, y)) { continue; }
262
+
263
+ bool invert = 0;
264
+ switch (mask) {
265
+ case 0: invert = (x + y) % 2 == 0; break;
266
+ case 1: invert = y % 2 == 0; break;
267
+ case 2: invert = x % 3 == 0; break;
268
+ case 3: invert = (x + y) % 3 == 0; break;
269
+ case 4: invert = (x / 3 + y / 2) % 2 == 0; break;
270
+ case 5: invert = x * y % 2 + x * y % 3 == 0; break;
271
+ case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break;
272
+ case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break;
273
+ }
274
+ bb_invertBit(modules, x, y, invert);
275
+ }
276
+ }
277
+ }
278
+
279
+ static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) {
280
+ bb_setBit(modules, x, y, on);
281
+ bb_setBit(isFunction, x, y, true);
282
+ }
283
+
284
+ // Draws a 9*9 finder pattern including the border separator, with the center module at (x, y).
285
+ static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
286
+ uint8_t size = modules->bitOffsetOrWidth;
287
+
288
+ for (int8_t i = -4; i <= 4; i++) {
289
+ for (int8_t j = -4; j <= 4; j++) {
290
+ uint8_t dist = max(abs(i), abs(j)); // Chebyshev/infinity norm
291
+ int16_t xx = x + j, yy = y + i;
292
+ if (0 <= xx && xx < size && 0 <= yy && yy < size) {
293
+ setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4);
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // Draws a 5*5 alignment pattern, with the center module at (x, y).
300
+ static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
301
+ for (int8_t i = -2; i <= 2; i++) {
302
+ for (int8_t j = -2; j <= 2; j++) {
303
+ setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1);
304
+ }
305
+ }
306
+ }
307
+
308
+ // Draws two copies of the format bits (with its own error correction code)
309
+ // based on the given mask and this object's error correction level field.
310
+ static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) {
311
+
312
+ uint8_t size = modules->bitOffsetOrWidth;
313
+
314
+ // Calculate error correction code and pack bits
315
+ uint32_t data = ecc << 3 | mask; // errCorrLvl is uint2, mask is uint3
316
+ uint32_t rem = data;
317
+ for (int i = 0; i < 10; i++) {
318
+ rem = (rem << 1) ^ ((rem >> 9) * 0x537);
319
+ }
320
+
321
+ data = data << 10 | rem;
322
+ data ^= 0x5412; // uint15
323
+
324
+ // Draw first copy
325
+ for (uint8_t i = 0; i <= 5; i++) {
326
+ setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0);
327
+ }
328
+
329
+ setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0);
330
+ setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0);
331
+ setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0);
332
+
333
+ for (int8_t i = 9; i < 15; i++) {
334
+ setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0);
335
+ }
336
+
337
+ // Draw second copy
338
+ for (int8_t i = 0; i <= 7; i++) {
339
+ setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0);
340
+ }
341
+
342
+ for (int8_t i = 8; i < 15; i++) {
343
+ setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0);
344
+ }
345
+
346
+ setFunctionModule(modules, isFunction, 8, size - 8, true);
347
+ }
348
+
349
+
350
+ // Draws two copies of the version bits (with its own error correction code),
351
+ // based on this object's version field (which only has an effect for 7 <= version <= 40).
352
+ static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) {
353
+
354
+ int8_t size = modules->bitOffsetOrWidth;
355
+
356
+ #if LOCK_VERSION != 0 && LOCK_VERSION < 7
357
+ return;
358
+
359
+ #else
360
+ if (version < 7) { return; }
361
+
362
+ // Calculate error correction code and pack bits
363
+ uint32_t rem = version; // version is uint6, in the range [7, 40]
364
+ for (uint8_t i = 0; i < 12; i++) {
365
+ rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
366
+ }
367
+
368
+ uint32_t data = version << 12 | rem; // uint18
369
+
370
+ // Draw two copies
371
+ for (uint8_t i = 0; i < 18; i++) {
372
+ bool bit = ((data >> i) & 1) != 0;
373
+ uint8_t a = size - 11 + i % 3, b = i / 3;
374
+ setFunctionModule(modules, isFunction, a, b, bit);
375
+ setFunctionModule(modules, isFunction, b, a, bit);
376
+ }
377
+
378
+ #endif
379
+ }
380
+
381
+ static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) {
382
+
383
+ uint8_t size = modules->bitOffsetOrWidth;
384
+
385
+ // Draw the horizontal and vertical timing patterns
386
+ for (uint8_t i = 0; i < size; i++) {
387
+ setFunctionModule(modules, isFunction, 6, i, i % 2 == 0);
388
+ setFunctionModule(modules, isFunction, i, 6, i % 2 == 0);
389
+ }
390
+
391
+ // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
392
+ drawFinderPattern(modules, isFunction, 3, 3);
393
+ drawFinderPattern(modules, isFunction, size - 4, 3);
394
+ drawFinderPattern(modules, isFunction, 3, size - 4);
395
+
396
+ #if LOCK_VERSION == 0 || LOCK_VERSION > 1
397
+
398
+ if (version > 1) {
399
+
400
+ // Draw the numerous alignment patterns
401
+
402
+ uint8_t alignCount = version / 7 + 2;
403
+ uint8_t step;
404
+ if (version != 32) {
405
+ step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; // ceil((size - 13) / (2*numAlign - 2)) * 2
406
+ } else { // C-C-C-Combo breaker!
407
+ step = 26;
408
+ }
409
+
410
+ uint8_t alignPositionIndex = alignCount - 1;
411
+ uint8_t alignPosition[alignCount];
412
+
413
+ alignPosition[0] = 6;
414
+
415
+ uint8_t size = version * 4 + 17;
416
+ for (uint8_t i = 0, pos = size - 7; i < alignCount - 1; i++, pos -= step) {
417
+ alignPosition[alignPositionIndex--] = pos;
418
+ }
419
+
420
+ for (uint8_t i = 0; i < alignCount; i++) {
421
+ for (uint8_t j = 0; j < alignCount; j++) {
422
+ if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) {
423
+ continue; // Skip the three finder corners
424
+ } else {
425
+ drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]);
426
+ }
427
+ }
428
+ }
429
+ }
430
+
431
+ #endif
432
+
433
+ // Draw configuration data
434
+ drawFormatBits(modules, isFunction, ecc, 0); // Dummy mask value; overwritten later in the constructor
435
+ drawVersion(modules, isFunction, version);
436
+ }
437
+
438
+
439
+ // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
440
+ // data area of this QR Code symbol. Function modules need to be marked off before this is called.
441
+ static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) {
442
+
443
+ uint32_t bitLength = codewords->bitOffsetOrWidth;
444
+ uint8_t *data = codewords->data;
445
+
446
+ uint8_t size = modules->bitOffsetOrWidth;
447
+
448
+ // Bit index into the data
449
+ uint32_t i = 0;
450
+
451
+ // Do the funny zigzag scan
452
+ for (int16_t right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair
453
+ if (right == 6) { right = 5; }
454
+
455
+ for (uint8_t vert = 0; vert < size; vert++) { // Vertical counter
456
+ for (int j = 0; j < 2; j++) {
457
+ uint8_t x = right - j; // Actual x coordinate
458
+ bool upwards = ((right & 2) == 0) ^ (x < 6);
459
+ uint8_t y = upwards ? size - 1 - vert : vert; // Actual y coordinate
460
+ if (!bb_getBit(isFunction, x, y) && i < bitLength) {
461
+ bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0);
462
+ i++;
463
+ }
464
+ // If there are any remainder bits (0 to 7), they are already
465
+ // set to 0/false/white when the grid of modules was initialized
466
+ }
467
+ }
468
+ }
469
+ }
470
+
471
+
472
+
473
+ #pragma mark - Penalty Calculation
474
+
475
+ #define PENALTY_N1 3
476
+ #define PENALTY_N2 3
477
+ #define PENALTY_N3 40
478
+ #define PENALTY_N4 10
479
+
480
+ // Calculates and returns the penalty score based on state of this QR Code's current modules.
481
+ // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
482
+ // @TODO: This can be optimized by working with the bytes instead of bits.
483
+ static uint32_t getPenaltyScore(BitBucket *modules) {
484
+ uint32_t result = 0;
485
+
486
+ uint8_t size = modules->bitOffsetOrWidth;
487
+
488
+ // Adjacent modules in row having same color
489
+ for (uint8_t y = 0; y < size; y++) {
490
+
491
+ bool colorX = bb_getBit(modules, 0, y);
492
+ for (uint8_t x = 1, runX = 1; x < size; x++) {
493
+ bool cx = bb_getBit(modules, x, y);
494
+ if (cx != colorX) {
495
+ colorX = cx;
496
+ runX = 1;
497
+
498
+ } else {
499
+ runX++;
500
+ if (runX == 5) {
501
+ result += PENALTY_N1;
502
+ } else if (runX > 5) {
503
+ result++;
504
+ }
505
+ }
506
+ }
507
+ }
508
+
509
+ // Adjacent modules in column having same color
510
+ for (uint8_t x = 0; x < size; x++) {
511
+ bool colorY = bb_getBit(modules, x, 0);
512
+ for (uint8_t y = 1, runY = 1; y < size; y++) {
513
+ bool cy = bb_getBit(modules, x, y);
514
+ if (cy != colorY) {
515
+ colorY = cy;
516
+ runY = 1;
517
+ } else {
518
+ runY++;
519
+ if (runY == 5) {
520
+ result += PENALTY_N1;
521
+ } else if (runY > 5) {
522
+ result++;
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ uint16_t black = 0;
529
+ for (uint8_t y = 0; y < size; y++) {
530
+ uint16_t bitsRow = 0, bitsCol = 0;
531
+ for (uint8_t x = 0; x < size; x++) {
532
+ bool color = bb_getBit(modules, x, y);
533
+
534
+ // 2*2 blocks of modules having same color
535
+ if (x > 0 && y > 0) {
536
+ bool colorUL = bb_getBit(modules, x - 1, y - 1);
537
+ bool colorUR = bb_getBit(modules, x, y - 1);
538
+ bool colorL = bb_getBit(modules, x - 1, y);
539
+ if (color == colorUL && color == colorUR && color == colorL) {
540
+ result += PENALTY_N2;
541
+ }
542
+ }
543
+
544
+ // Finder-like pattern in rows and columns
545
+ bitsRow = ((bitsRow << 1) & 0x7FF) | color;
546
+ bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x);
547
+
548
+ // Needs 11 bits accumulated
549
+ if (x >= 10) {
550
+ if (bitsRow == 0x05D || bitsRow == 0x5D0) {
551
+ result += PENALTY_N3;
552
+ }
553
+ if (bitsCol == 0x05D || bitsCol == 0x5D0) {
554
+ result += PENALTY_N3;
555
+ }
556
+ }
557
+
558
+ // Balance of black and white modules
559
+ if (color) { black++; }
560
+ }
561
+ }
562
+
563
+ // Find smallest k such that (45-5k)% <= dark/total <= (55+5k)%
564
+ uint16_t total = size * size;
565
+ for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) {
566
+ result += PENALTY_N4;
567
+ }
568
+
569
+ return result;
570
+ }
571
+
572
+
573
+ #pragma mark - Reed-Solomon Generator
574
+
575
+ static uint8_t rs_multiply(uint8_t x, uint8_t y) {
576
+ // Russian peasant multiplication
577
+ // See: https://en.wikipedia.org/wiki/Ancient_Egyptian_multiplication
578
+ uint16_t z = 0;
579
+ for (int8_t i = 7; i >= 0; i--) {
580
+ z = (z << 1) ^ ((z >> 7) * 0x11D);
581
+ z ^= ((y >> i) & 1) * x;
582
+ }
583
+ return z;
584
+ }
585
+
586
+ static void rs_init(uint8_t degree, uint8_t *coeff) {
587
+ memset(coeff, 0, degree);
588
+ coeff[degree - 1] = 1;
589
+
590
+ // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
591
+ // drop the highest term, and store the rest of the coefficients in order of descending powers.
592
+ // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
593
+ uint16_t root = 1;
594
+ for (uint8_t i = 0; i < degree; i++) {
595
+ // Multiply the current product by (x - r^i)
596
+ for (uint8_t j = 0; j < degree; j++) {
597
+ coeff[j] = rs_multiply(coeff[j], root);
598
+ if (j + 1 < degree) {
599
+ coeff[j] ^= coeff[j + 1];
600
+ }
601
+ }
602
+ root = (root << 1) ^ ((root >> 7) * 0x11D); // Multiply by 0x02 mod GF(2^8/0x11D)
603
+ }
604
+ }
605
+
606
+ static void rs_getRemainder(uint8_t degree, uint8_t *coeff, uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) {
607
+ // Compute the remainder by performing polynomial division
608
+
609
+ //for (uint8_t i = 0; i < degree; i++) { result[] = 0; }
610
+ //memset(result, 0, degree);
611
+
612
+ for (uint8_t i = 0; i < length; i++) {
613
+ uint8_t factor = data[i] ^ result[0];
614
+ for (uint8_t j = 1; j < degree; j++) {
615
+ result[(j - 1) * stride] = result[j * stride];
616
+ }
617
+ result[(degree - 1) * stride] = 0;
618
+
619
+ for (uint8_t j = 0; j < degree; j++) {
620
+ result[j * stride] ^= rs_multiply(coeff[j], factor);
621
+ }
622
+ }
623
+ }
624
+
625
+
626
+
627
+ #pragma mark - QrCode
628
+
629
+ static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) {
630
+ int8_t mode = MODE_BYTE;
631
+
632
+ if (isNumeric((char*)text, length)) {
633
+ mode = MODE_NUMERIC;
634
+ bb_appendBits(dataCodewords, 1 << MODE_NUMERIC, 4);
635
+ bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
636
+
637
+ uint16_t accumData = 0;
638
+ uint8_t accumCount = 0;
639
+ for (uint16_t i = 0; i < length; i++) {
640
+ accumData = accumData * 10 + ((char)(text[i]) - '0');
641
+ accumCount++;
642
+ if (accumCount == 3) {
643
+ bb_appendBits(dataCodewords, accumData, 10);
644
+ accumData = 0;
645
+ accumCount = 0;
646
+ }
647
+ }
648
+
649
+ // 1 or 2 digits remaining
650
+ if (accumCount > 0) {
651
+ bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1);
652
+ }
653
+
654
+ } else if (isAlphanumeric((char*)text, length)) {
655
+ mode = MODE_ALPHANUMERIC;
656
+ bb_appendBits(dataCodewords, 1 << MODE_ALPHANUMERIC, 4);
657
+ bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC));
658
+
659
+ uint16_t accumData = 0;
660
+ uint8_t accumCount = 0;
661
+ for (uint16_t i = 0; i < length; i++) {
662
+ accumData = accumData * 45 + getAlphanumeric((char)(text[i]));
663
+ accumCount++;
664
+ if (accumCount == 2) {
665
+ bb_appendBits(dataCodewords, accumData, 11);
666
+ accumData = 0;
667
+ accumCount = 0;
668
+ }
669
+ }
670
+
671
+ // 1 character remaining
672
+ if (accumCount > 0) {
673
+ bb_appendBits(dataCodewords, accumData, 6);
674
+ }
675
+
676
+ } else {
677
+ bb_appendBits(dataCodewords, 1 << MODE_BYTE, 4);
678
+ bb_appendBits(dataCodewords, length, getModeBits(version, MODE_BYTE));
679
+ for (uint16_t i = 0; i < length; i++) {
680
+ bb_appendBits(dataCodewords, (char)(text[i]), 8);
681
+ }
682
+ }
683
+
684
+ //bb_setBits(dataCodewords, length, 4, getModeBits(version, mode));
685
+
686
+ return mode;
687
+ }
688
+
689
+ static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) {
690
+
691
+ // See: http://www.thonky.com/qr-code-tutorial/structure-final-message
692
+
693
+ #if LOCK_VERSION == 0
694
+ uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1];
695
+ uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1];
696
+ uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
697
+ #else
698
+ uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc];
699
+ uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc];
700
+ uint16_t moduleCount = NUM_RAW_DATA_MODULES;
701
+ #endif
702
+
703
+ uint8_t blockEccLen = totalEcc / numBlocks;
704
+ uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks;
705
+ uint8_t shortBlockLen = moduleCount / 8 / numBlocks;
706
+
707
+ uint8_t shortDataBlockLen = shortBlockLen - blockEccLen;
708
+
709
+ uint8_t result[data->capacityBytes];
710
+ memset(result, 0, sizeof(result));
711
+
712
+ uint8_t coeff[blockEccLen];
713
+ rs_init(blockEccLen, coeff);
714
+
715
+ uint16_t offset = 0;
716
+ uint8_t *dataBytes = data->data;
717
+
718
+
719
+ // Interleave all short blocks
720
+ for (uint8_t i = 0; i < shortDataBlockLen; i++) {
721
+ uint16_t index = i;
722
+ uint8_t stride = shortDataBlockLen;
723
+ for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
724
+ result[offset++] = dataBytes[index];
725
+
726
+ #if LOCK_VERSION == 0 || LOCK_VERSION >= 5
727
+ if (blockNum == numShortBlocks) { stride++; }
728
+ #endif
729
+ index += stride;
730
+ }
731
+ }
732
+
733
+ // Version less than 5 only have short blocks
734
+ #if LOCK_VERSION == 0 || LOCK_VERSION >= 5
735
+ {
736
+ // Interleave long blocks
737
+ uint16_t index = shortDataBlockLen * (numShortBlocks + 1);
738
+ uint8_t stride = shortDataBlockLen;
739
+ for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) {
740
+ result[offset++] = dataBytes[index];
741
+
742
+ if (blockNum == 0) { stride++; }
743
+ index += stride;
744
+ }
745
+ }
746
+ #endif
747
+
748
+ // Add all ecc blocks, interleaved
749
+ uint8_t blockSize = shortDataBlockLen;
750
+ for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
751
+
752
+ #if LOCK_VERSION == 0 || LOCK_VERSION >= 5
753
+ if (blockNum == numShortBlocks) { blockSize++; }
754
+ #endif
755
+ rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks);
756
+ dataBytes += blockSize;
757
+ }
758
+
759
+ memcpy(data->data, result, data->capacityBytes);
760
+ data->bitOffsetOrWidth = moduleCount;
761
+ }
762
+
763
+ // We store the Format bits tightly packed into a single byte (each of the 4 modes is 2 bits)
764
+ // The format bits can be determined by ECC_FORMAT_BITS >> (2 * ecc)
765
+ static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0);
766
+
767
+
768
+ #pragma mark - Public QRCode functions
769
+
770
+ uint16_t qrcode_getBufferSize(uint8_t version) {
771
+ return bb_getGridSizeBytes(4 * version + 17);
772
+ }
773
+
774
+ // @TODO: Return error if data is too big.
775
+ int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) {
776
+ uint8_t size = version * 4 + 17;
777
+ qrcode->version = version;
778
+ qrcode->size = size;
779
+ qrcode->ecc = ecc;
780
+ qrcode->modules = modules;
781
+
782
+ uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03;
783
+
784
+ #if LOCK_VERSION == 0
785
+ uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
786
+ uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1];
787
+ #else
788
+ version = LOCK_VERSION;
789
+ uint16_t moduleCount = NUM_RAW_DATA_MODULES;
790
+ uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits];
791
+ #endif
792
+
793
+ struct BitBucket codewords;
794
+ uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)];
795
+ bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
796
+
797
+ // Place the data code words into the buffer
798
+ int8_t mode = encodeDataCodewords(&codewords, data, length, version);
799
+
800
+ if (mode < 0) { return -1; }
801
+ qrcode->mode = mode;
802
+
803
+ // Add terminator and pad up to a byte if applicable
804
+ uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth;
805
+ if (padding > 4) { padding = 4; }
806
+ bb_appendBits(&codewords, 0, padding);
807
+ bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8);
808
+
809
+ // Pad with alternate bytes until data capacity is reached
810
+ for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) {
811
+ bb_appendBits(&codewords, padByte, 8);
812
+ }
813
+
814
+ BitBucket modulesGrid;
815
+ bb_initGrid(&modulesGrid, modules, size);
816
+
817
+ BitBucket isFunctionGrid;
818
+ uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)];
819
+ bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size);
820
+
821
+ // Draw function patterns, draw all codewords, do masking
822
+ drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits);
823
+ performErrorCorrection(version, eccFormatBits, &codewords);
824
+ drawCodewords(&modulesGrid, &isFunctionGrid, &codewords);
825
+
826
+ // Find the best (lowest penalty) mask
827
+ uint8_t mask = 0;
828
+ int32_t minPenalty = INT32_MAX;
829
+ for (uint8_t i = 0; i < 8; i++) {
830
+ drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i);
831
+ applyMask(&modulesGrid, &isFunctionGrid, i);
832
+ int penalty = getPenaltyScore(&modulesGrid);
833
+ if (penalty < minPenalty) {
834
+ mask = i;
835
+ minPenalty = penalty;
836
+ }
837
+ applyMask(&modulesGrid, &isFunctionGrid, i); // Undoes the mask due to XOR
838
+ }
839
+
840
+ qrcode->mask = mask;
841
+
842
+ // Overwrite old format bits
843
+ drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask);
844
+
845
+ // Apply the final choice of mask
846
+ applyMask(&modulesGrid, &isFunctionGrid, mask);
847
+
848
+ return 0;
849
+ }
850
+
851
+ int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) {
852
+ return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data));
853
+ }
854
+
855
+ bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) {
856
+ if (x < 0 || x >= qrcode->size || y < 0 || y >= qrcode->size) {
857
+ return false;
858
+ }
859
+
860
+ uint32_t offset = y * qrcode->size + x;
861
+ return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
862
+ }
863
+
864
+ /*
865
+ uint8_t qrcode_getHexLength(QRCode *qrcode) {
866
+ return ((qrcode->size * qrcode->size) + 7) / 4;
867
+ }
868
+
869
+ void qrcode_getHex(QRCode *qrcode, char *result) {
870
+
871
+ }
872
+ */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2017 Richard Moore
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ * THE SOFTWARE.
23
+ */
24
+
25
+ /**
26
+ * Special thanks to Nayuki (https://www.nayuki.io/) from which this library was
27
+ * heavily inspired and compared against.
28
+ *
29
+ * See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp
30
+ */
31
+
32
+
33
+ #ifndef __QRCODE_H_
34
+ #define __QRCODE_H_
35
+
36
+ #ifndef __cplusplus
37
+ typedef unsigned char bool;
38
+ static const bool false = 0;
39
+ static const bool true = 1;
40
+ #endif
41
+
42
+ #include <stdint.h>
43
+
44
+
45
+ // QR Code Format Encoding
46
+ #define MODE_NUMERIC 0
47
+ #define MODE_ALPHANUMERIC 1
48
+ #define MODE_BYTE 2
49
+
50
+
51
+ // Error Correction Code Levels
52
+ #define ECC_LOW 0
53
+ #define ECC_MEDIUM 1
54
+ #define ECC_QUARTILE 2
55
+ #define ECC_HIGH 3
56
+
57
+
58
+ // If set to non-zero, this library can ONLY produce QR codes at that version
59
+ // This saves a lot of dynamic memory, as the codeword tables are skipped
60
+ #ifndef LOCK_VERSION
61
+ #define LOCK_VERSION 0
62
+ #endif
63
+
64
+
65
+ typedef struct QRCode {
66
+ uint8_t version;
67
+ uint8_t size;
68
+ uint8_t ecc;
69
+ uint8_t mode;
70
+ uint8_t mask;
71
+ uint8_t *modules;
72
+ } QRCode;
73
+
74
+
75
+ #ifdef __cplusplus
76
+ extern "C"{
77
+ #endif /* __cplusplus */
78
+
79
+
80
+
81
+ uint16_t qrcode_getBufferSize(uint8_t version);
82
+
83
+ int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data);
84
+ int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length);
85
+
86
+ bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y);
87
+
88
+
89
+
90
+ #ifdef __cplusplus
91
+ }
92
+ #endif /* __cplusplus */
93
+
94
+
95
+ #endif /* __QRCODE_H_ */
package/install.sh CHANGED
@@ -52,6 +52,10 @@ if [[ ! -f "$CONFIG_DIR/server.crt" || ! -f "$CONFIG_DIR/server.key" ]]; then
52
52
  fi
53
53
 
54
54
  sudo usermod -aG dialout,cdrom "$USER" || true
55
+ # Don't let the kernel re-close an ejected tray whenever some process (a desktop
56
+ # volume monitor, a disc probe) opens the drive.
57
+ echo 'dev.cdrom.autoclose = 0' | sudo tee /etc/sysctl.d/90-discstation.conf >/dev/null \
58
+ && sudo sysctl -q -w dev.cdrom.autoclose=0 || true
55
59
  mkdir -p "$HOME/.config/systemd/user"
56
60
  cp "$ROOT_DIR/systemd/discstation.service" "$HOME/.config/systemd/user/discstation.service"
57
61
  systemctl --user daemon-reload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discstation",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs",
@@ -1002,30 +1002,41 @@ def eject_disc(ser, device):
1002
1002
  safe_send(ser, "ERROR:Eject failed")
1003
1003
  safe_send(ser, "STANDBY:Insert disc")
1004
1004
  return ok
1005
- subprocess.run(["sync"], timeout=5)
1005
+ # The kernel closes the tray again (dev.cdrom.autoclose) when anything opens
1006
+ # the drive while it's out, and every disc probe (blkid/lsdvd/wodim/mediainfo,
1007
+ # driven by the background poll and by the web/phone /disc-info requests)
1008
+ # does exactly that. So: mark the tray open BEFORE the eject command runs (new
1009
+ # probes now bail out) and hold _detect_lock so any probe already in flight
1010
+ # finishes first - otherwise a probe landing inside the few seconds the eject
1011
+ # takes shuts the tray right after it opens.
1012
+ with _detect_lock:
1013
+ _tray_open = True
1014
+ _tray_open_since = time.monotonic()
1015
+ subprocess.run(["sync"], timeout=5)
1006
1016
 
1007
- subprocess.run(["sg_raw", device, "1e", "00", "00", "00", "00", "00"],
1008
- timeout=5, capture_output=True)
1017
+ subprocess.run(["sg_raw", device, "1e", "00", "00", "00", "00", "00"],
1018
+ timeout=5, capture_output=True)
1009
1019
 
1010
- ok = False
1011
- for cmd in (["eject", device], ["sg_raw", device, "1b", "00", "00", "00", "02", "00"]):
1012
- if ok:
1013
- break
1014
- try:
1015
- r = subprocess.run(cmd, timeout=10, capture_output=True)
1016
- ok = r.returncode == 0
1020
+ ok = False
1021
+ for cmd in (["eject", device], ["sg_raw", device, "1b", "00", "00", "00", "02", "00"]):
1017
1022
  if ok:
1018
- print(f"{cmd[0]} eject ok")
1019
- else:
1020
- err = (r.stderr or r.stdout or b"failed").decode(errors="ignore").strip()[:40]
1021
- print(f"{cmd[0]} eject failed: {err}")
1022
- except Exception as e:
1023
- print(f"{cmd[0]} eject error: {e}")
1023
+ break
1024
+ try:
1025
+ r = subprocess.run(cmd, timeout=10, capture_output=True)
1026
+ ok = r.returncode == 0
1027
+ if ok:
1028
+ print(f"{cmd[0]} eject ok")
1029
+ else:
1030
+ err = (r.stderr or r.stdout or b"failed").decode(errors="ignore").strip()[:40]
1031
+ print(f"{cmd[0]} eject failed: {err}")
1032
+ except Exception as e:
1033
+ print(f"{cmd[0]} eject error: {e}")
1034
+ if not ok:
1035
+ _tray_open = False
1024
1036
 
1025
1037
  if ok:
1026
- _tray_open = True
1027
1038
  _tray_open_since = time.monotonic()
1028
- safe_send(ser, "WAITING:Press SELECT/to close tray")
1039
+ safe_send(ser, "WAITING:Press EJECT/to close tray")
1029
1040
  # WAITING: isn't in _record_web_status()'s prefix whitelist, so the
1030
1041
  # send above never reaches the web page - _tray_open has to be
1031
1042
  # published explicitly here, same as DISC: gets its own dedicated
@@ -1064,7 +1075,7 @@ def eject_disc(ser, device):
1064
1075
  continue
1065
1076
  if line == "PONG":
1066
1077
  continue
1067
- if line == "CONFIRM":
1078
+ if line in ("CONFIRM", "EJECT"): # EJECT button doubles as close while the tray is out
1068
1079
  print("Closing tray...")
1069
1080
  safe_send(ser, "STATUS:Closing tray...")
1070
1081
  for close_cmd in (
@@ -1265,7 +1276,7 @@ def _refresh_udev(device):
1265
1276
  def udev_cdrom_properties(device, refresh=False):
1266
1277
  if discstation_host.system_name() != "linux":
1267
1278
  return discstation_host.media_properties(device)
1268
- overlay = _refresh_udev(device) if refresh else {}
1279
+ overlay = _refresh_udev(device) if refresh and not _tray_open else {}
1269
1280
  properties = _udev_props_via_pyudev(device)
1270
1281
  if properties is None:
1271
1282
  # pyudev unavailable — fall back to parsing `udevadm info` output.
@@ -1306,7 +1317,7 @@ def _device_present(device):
1306
1317
 
1307
1318
 
1308
1319
  def is_blank_disc(device):
1309
- if not device:
1320
+ if not device or _tray_open: # probing an open tray re-closes it
1310
1321
  return False
1311
1322
  if not _device_present(device):
1312
1323
  return False
@@ -1357,7 +1368,7 @@ def is_blank_disc(device):
1357
1368
 
1358
1369
 
1359
1370
  def is_rewritable_disc(device):
1360
- if not device:
1371
+ if not device or _tray_open: # probing an open tray re-closes it
1361
1372
  return False
1362
1373
  """Return whether the inserted medium can be overwritten."""
1363
1374
  if not _device_present(device):
@@ -1714,6 +1725,8 @@ def _maybe_reset_stuck_drive(device, failed_probes):
1714
1725
 
1715
1726
 
1716
1727
  def _detect_disc_locked(device, settle, budget):
1728
+ if _tray_open: # an eject started while we waited for the lock
1729
+ return _disc_info(False, "none")
1717
1730
  deadline = time.monotonic() + (DISC_DETECT_BUDGET if budget is None else budget)
1718
1731
  props = udev_cdrom_properties(device, refresh=True)
1719
1732
 
@@ -4138,6 +4151,9 @@ def station_loop(ser):
4138
4151
  print(f"Menu: {line.split(':', 1)[1]}")
4139
4152
  continue
4140
4153
 
4154
+ if line == "EJECT" and _tray_open:
4155
+ line = "CONFIRM" # EJECT is a toggle: with the tray already out, close it
4156
+
4141
4157
  if line == "EJECT":
4142
4158
  try:
4143
4159
  device = discstation_burn.disc_device()