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 +0 -1
- package/arduino/DiscStation/DiscStation.ino +22 -13
- package/arduino/DiscStation/qrcode_lite.c +872 -0
- package/arduino/DiscStation/qrcode_lite.h +95 -0
- package/install.sh +4 -0
- package/package.json +1 -1
- package/src/discstation.py +38 -22
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#include <ESPmDNS.h>
|
|
9
9
|
#include <Preferences.h>
|
|
10
10
|
#include <ArduinoOTA.h>
|
|
11
|
-
#include
|
|
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 (
|
|
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 (
|
|
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
package/src/discstation.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1017
|
+
subprocess.run(["sg_raw", device, "1e", "00", "00", "00", "00", "00"],
|
|
1018
|
+
timeout=5, capture_output=True)
|
|
1009
1019
|
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
|
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
|
|
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()
|