├── .gitignore ├── ArduManFX.pro ├── LICENSE ├── README.md ├── build.compiler ├── compile.sh ├── recovered └── recovered.ino ├── src ├── .gitignore ├── ArduManFX.cpp ├── ArduManFX.h ├── arduboy-pixelartV4-white.png ├── arduboyrecovery.cpp ├── arduboyrecovery.h ├── arduboyrecovery.ui ├── flashbuilder.cpp ├── flashbuilder.h ├── fx.png ├── game.cpp ├── game.h ├── game.ui ├── icon.ico ├── icon.png ├── icon.rc ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── progress.cpp ├── progress.h ├── progress.ui ├── recovered.hex ├── resources.qrc ├── src.pro ├── stars_back.png ├── stars_front.png ├── tester.cpp ├── tester.h └── tester.ui └── title.png /.gitignore: -------------------------------------------------------------------------------- 1 | /erwinRepo/ 2 | /example-flashcarts/ 3 | /jsScripts/ 4 | /pyScripts/ 5 | /asScripts/ 6 | /src/angelscript/ 7 | *.o 8 | Makefile 9 | moc_* 10 | qrc_* 11 | /Screenshot_2020-02-20_01-44-42.png 12 | /.qmake.stash 13 | *.bin 14 | *.etag 15 | *.dll 16 | /ArduManFX 17 | /ArduboyAssistant.ino.hex 18 | /Inversion.ino.hex 19 | -------------------------------------------------------------------------------- /ArduManFX.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE=subdirs 2 | SUBDIRS=src 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tuxinator2009 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArduManFX - A Library Manager for Arduboy's with the FX mod 2 | 3 | ### Features 4 | 5 | - Local, cached, copy of Eried's Unofficial Arduboy Repo (https://arduboy.ried.cl/) 6 | - Upload HEX files from the repository directly to a standard Arduboy 7 | - Create flash cartridge images via a drag-and-drop interface from the list of available programs 8 | - Read/Write flash cart images to modified/homebrew Arduboy's that have the RAM expansion mod (NOT IMPLEMENTED YET) 9 | - Load/Save EEPROM to/from a file on your computer 10 | - Works on Windows, Linux, and Mac 11 | - Completely portable, no installation necessary. Repository is cached in the same folder as the executable. 12 | 13 | This is still very much a work in progress and as of right now it only supports downloading the repository of programs and uploading them to the device. 14 | 15 | Credit for the RAM expansion mod goes to @MrBlinky 16 | Credit for the Arduboy image goes to @huard_olivier https://twitter.com/huard_olivier 17 | Credit for the Arduboy device goes to @bateske 18 | Credit for the unofficial Arduboy repo goes to @eried 19 | 20 | ## Installation 21 | No installation necessary. Simply download the binary for your system, extract it and run ArduManFX 22 | 23 | NOTE: On MacOS you will need to extract the App from the DMG file. A shortcut to the Applications 24 | folder is provided for your convenience, but it can be extracted and run anywhere and is entirely self contained. 25 | -------------------------------------------------------------------------------- /build.compiler: -------------------------------------------------------------------------------- 1 | { 2 | "Global Environment":{ 3 | "PATH":"C:\\Qt\\5.12.0\\mingw73_64\\bin;C:\\Qt\\Tools\\mingw730_64\\bin;%PATH%" 4 | }, 5 | "Processes":[ 6 | { 7 | "Title":"Build", 8 | "Description":"Compile the project.", 9 | "Icon":":/icons/hammer-wrench-icon.png", 10 | "Commands":[ 11 | { 12 | "Command":"qmake", 13 | "Arguments":["CONFIG += debug", "PROJECT = %PROJECT%"] 14 | }, 15 | { 16 | "Command":"mingw32-make", 17 | "Arguments":[] 18 | } 19 | ] 20 | }, 21 | { 22 | "Title":"Clean", 23 | "Description":"Cleanup compiled code.", 24 | "Icon":":/icons/clean.png", 25 | "Commands":[ 26 | { 27 | "Command":"mingw32-make", 28 | "Arguments":["clean"] 29 | } 30 | ] 31 | }, 32 | { 33 | "Title":"Dist Clean", 34 | "Description":"Completely remove all compiled code.", 35 | "Icon":":/icons/full-clean.png", 36 | "Commands":[ 37 | { 38 | "Command":"mingw32-make", 39 | "Arguments":["distclean"] 40 | } 41 | ] 42 | }, 43 | { 44 | "Title":"Run", 45 | "Description":"Launch the program.", 46 | "Icon":":/icons/play.png", 47 | "Commands":[ 48 | { 49 | "Command":"./%PROJECT%", 50 | "Arguments":[] 51 | } 52 | ] 53 | }, 54 | { 55 | "Title":"Debug", 56 | "Description":"Run program with the gdb debuger.", 57 | "Icon":":/icons/debug.png", 58 | "Commands":[ 59 | { 60 | "Command":"gdb", 61 | "Arguments":["./%PROJECT%"] 62 | } 63 | ] 64 | }, 65 | { 66 | "Title":"Release", 67 | "Description":"Build a release version of the project.", 68 | "Icon":":/icons/package.png", 69 | "Commands":[ 70 | { 71 | "Command":"cmd", 72 | "Arguments":["/q/c", "if", "exist", ".\\%PROJECT%.exe", "mingw32-make", "distclean"] 73 | }, 74 | { 75 | "Command":"qmake", 76 | "Arguments":["CONFIG += release", "PROJECT = %PROJECT%"] 77 | }, 78 | { 79 | "Command":"mingw32-make", 80 | "Arguments":["-j5"] 81 | }, 82 | { 83 | "Command":"cmd", 84 | "Arguments":["/q/c", "if", "not", "exist", "C:\\Qt\\Released\\%PROJECT%.exe", "mkdir", "C:\\Qt\\Released\\%PROJECT%"] 85 | }, 86 | { 87 | "Command":"robocopy", 88 | "Arguments":[ 89 | "C:\\Qt\\Projects\\%PROJECT%", 90 | "C:\\Qt\\Released\\%PROJECT%", 91 | "/MIR", 92 | "/XF", 93 | ".qmake.stash", 94 | "Makefile", 95 | "*.compiler", 96 | "*.pro", 97 | "*.bat", 98 | "/XD", 99 | "src", 100 | "/NS", 101 | "/NC", 102 | "/NP", 103 | "/NJS" 104 | ], 105 | "Ignore Codes":[1, 2, 3, 4, 5, 6, 7] 106 | }, 107 | { 108 | "Command":"windeployqt", 109 | "Arguments":["C:\\Qt\\Released\\%PROJECT%\\%PROJECT%.exe"] 110 | }, 111 | { 112 | "Command":"cmd", 113 | "Arguments":["/q/c", "start", "C:\\Qt\\Released\\%PROJECT%"] 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | qmake 3 | make 4 | -------------------------------------------------------------------------------- /recovered/recovered.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | Arduboy2 arduboy; 4 | 5 | void setup() { 6 | arduboy.begin(); 7 | arduboy.setFrameRate(15); 8 | } 9 | 10 | void loop() { 11 | if (!(arduboy.nextFrame())) 12 | return; 13 | arduboy.clear(); 14 | arduboy.setCursor(0, 0); 15 | arduboy.println(F(" ARDUBOY RESTORED")); 16 | arduboy.println(F("---------------------")); 17 | arduboy.println(F("Your Arduboy has been")); 18 | arduboy.println(F("successfully restored")); 19 | arduboy.println(F("---------------------")); 20 | arduboy.println(F("You may now upload a")); 21 | arduboy.println(F("new game!")); 22 | arduboy.println(F("---------------------")); 23 | arduboy.display(); 24 | } 25 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | ui_* 2 | -------------------------------------------------------------------------------- /src/ArduManFX.cpp: -------------------------------------------------------------------------------- 1 | //The following code is translated from Mr.Blinky's Arduboy-Python-Utilities 2 | //https://github.com/MrBlinky/Arduboy-Python-Utilities 3 | 4 | #include 5 | #include 6 | #include 7 | #include "ArduManFX.h" 8 | #include "mainwindow.h" 9 | 10 | const quint16 ArduManFX::compatibleDevices[] = 11 | { 12 | //Arduboy Leonardo 13 | 0x2341,0x0036, 0x2341,0x8036, 14 | 0x2A03,0x0036, 0x2A03,0x8036, 15 | //Arduboy Micro 16 | 0x2341,0x0037, 0x2341,0x8037, 17 | 0x2A03,0x0037, 0x2A03,0x8037, 18 | //Genuino Micro 19 | 0x2341,0x0237, 0x2341,0x8237, 20 | //Sparkfun Pro Micro 5V 21 | 0x1B4F,0x9205, 0x1B4F,0x9206, 22 | //Adafruit ItsyBitsy 5V 23 | 0x239A,0x000E, 0x239A,0x800E, 24 | }; 25 | 26 | const quint8 ArduManFX::manufacturerIDs[] = {0x01,0x14,0x1C,0x1F,0x20,0x37,0x9D,0xC2,0xC8,0xBF,0xEF}; 27 | const char *ArduManFX::manufacturerNames[] 28 | { 29 | "Spansion", 30 | "Cypress", 31 | "EON", 32 | "Adesto(Atmel)", 33 | "Micron", 34 | "AMIC", 35 | "ISSI", 36 | "General Plus", 37 | "Giga Device", 38 | "Microchip", 39 | "Winbond" 40 | }; 41 | 42 | const char *ArduManFX::lcdBootProgram = "\xD5\xF0\x8D\x14\xA1\xC8\x81\xCF\xD9\xF1\xAF\x20\x00"; 43 | 44 | QString ArduManFX::errorString = ""; 45 | 46 | ArduManFX::ArduManFX() 47 | { 48 | arduboy.setReadBufferSize(0); 49 | bootloaderActive = false; 50 | } 51 | 52 | ArduManFX::~ArduManFX() 53 | { 54 | if (arduboy.isOpen()) 55 | { 56 | arduboy.write("E", 1); 57 | waitWrite(); 58 | waitRead(); 59 | arduboy.read(1); 60 | arduboy.close(); 61 | } 62 | } 63 | 64 | bool ArduManFX::connect() 65 | { 66 | if (isConnected()) 67 | return true; 68 | arduboyPort = getComPort(); 69 | bootloaderActive = false; 70 | if (arduboyPort.isNull()) 71 | { 72 | errorString = "Failed to find an Arduboy."; 73 | return false; 74 | } 75 | arduboy.setPort(arduboyPort); 76 | arduboy.setBaudRate(1200); 77 | if (!arduboy.open(QIODevice::ReadWrite)) 78 | { 79 | errorString = QString("Failed to open Arduboy port %1.\nReason: %2").arg(arduboyPort.portName()).arg(arduboy.errorString()); 80 | return false; 81 | } 82 | return true; 83 | } 84 | 85 | void ArduManFX::disconnect() 86 | { 87 | if (isConnected()) 88 | { 89 | if (bootloaderActive) 90 | exitBootloader(); 91 | arduboy.close(); 92 | } 93 | } 94 | 95 | bool ArduManFX::startBootloader() 96 | { 97 | int attempt = 0; 98 | bootloaderActive = false; 99 | if (!isConnected()) 100 | { 101 | if (!connect()) 102 | return false; 103 | } 104 | if (!bootloaderActive) 105 | { 106 | arduboy.setDataTerminalReady(false); 107 | QThread::msleep(500); 108 | arduboy.close(); 109 | //wait for disconnect and reconnect in bootloader mode 110 | while (samePort(getComPort(), arduboyPort) && !bootloaderActive) 111 | { 112 | ++attempt; 113 | if (attempt > 50) 114 | { 115 | errorString = QString("Failed to restart Arduboy on port %1.").arg(arduboyPort.portName()); 116 | return false; 117 | } 118 | QThread::msleep(100); 119 | } 120 | arduboyPort = QSerialPortInfo(); 121 | attempt = 0; 122 | while (arduboyPort.isNull()) 123 | { 124 | arduboyPort = getComPort(); 125 | ++attempt; 126 | if (attempt > 50) 127 | { 128 | errorString = QString("Failed to connect to Arduboy on port %1.").arg(arduboyPort.portName()); 129 | return false; 130 | } 131 | QThread::msleep(100); 132 | } 133 | QThread::msleep(100); 134 | arduboy.setPort(arduboyPort); 135 | arduboy.setBaudRate(57600); 136 | if (!arduboy.open(QIODevice::ReadWrite)) 137 | { 138 | errorString = QString("Failed to open Arduboy port %1.\nReason: %2").arg(arduboyPort.portName()).arg(arduboy.errorString()); 139 | return false; 140 | } 141 | QThread::msleep(500); 142 | bootloaderActive = true; 143 | } 144 | return true; 145 | } 146 | 147 | void ArduManFX::exitBootloader() 148 | { 149 | if (isConnected() && bootloaderActive) 150 | { 151 | arduboy.write("E", 1); 152 | waitWrite(); 153 | waitRead(); 154 | arduboy.read(1); 155 | bootloaderActive = false; 156 | } 157 | } 158 | 159 | bool ArduManFX::isConnected() 160 | { 161 | if (arduboy.isOpen()) 162 | return true; 163 | return false; 164 | } 165 | 166 | QByteArray ArduManFX::readEEPROM() 167 | { 168 | QByteArray eeprom; 169 | if (!startBootloader()) 170 | return QByteArray(); 171 | arduboy.write("A\x00\x00", 3); 172 | if (!waitWrite()) 173 | return QByteArray(); 174 | if (!waitRead()) 175 | return QByteArray(); 176 | arduboy.read(1); 177 | arduboy.write("g\x04\x00\x45", 4); 178 | if (!waitWrite()) 179 | return QByteArray(); 180 | if (!waitRead()) 181 | return QByteArray(); 182 | eeprom = arduboy.read(1024); 183 | return eeprom; 184 | } 185 | 186 | bool ArduManFX::writeEEPROM(QByteArray data, bool verify) 187 | { 188 | if (data.length() != 1024) 189 | { 190 | errorString = "Error: EEPROM data must be exactly 1024 bytes."; 191 | return false; 192 | } 193 | if (!startBootloader()) 194 | return false; 195 | arduboy.write("A\x00\x00", 3); 196 | if (!waitWrite()) 197 | return false; 198 | if (!waitRead()) 199 | return false; 200 | arduboy.read(1); 201 | arduboy.write("B\x04\x00\x45", 4); 202 | if (!waitWrite()) 203 | return false; 204 | if (!waitRead()) 205 | return false; 206 | arduboy.write(data); 207 | if (!waitWrite()) 208 | return false; 209 | if (!waitRead()) 210 | return false; 211 | arduboy.read(1); 212 | if (verify) 213 | { 214 | QByteArray eeprom = readEEPROM(); 215 | if (eeprom.length() == 0) 216 | return false; 217 | else if (eeprom != data) 218 | { 219 | errorString = "Failed to upload EEPROM.\nValues read back don't match values written."; 220 | return false; 221 | } 222 | } 223 | return true; 224 | } 225 | 226 | QByteArray ArduManFX::readFlashCart(QProgressBar *progress) 227 | { 228 | QByteArray flashCart, jedecID; 229 | unsigned long capacity, blocks; 230 | if (!startBootloader()) 231 | return QByteArray(); 232 | if (getVersion() < 13) 233 | { 234 | errorString = "Bootloader has no flash cart support"; 235 | return QByteArray(); 236 | } 237 | jedecID = getJedecID(); 238 | capacity = 1 << jedecID[2]; 239 | blocks = capacity / BLOCKSIZE; 240 | if (progress != nullptr) 241 | { 242 | progress->setMaximum(blocks); 243 | progress->setValue(0); 244 | } 245 | QCoreApplication::processEvents(); 246 | for (unsigned long block = 0; block < blocks; ++block) 247 | { 248 | QByteArray data(3, 0xFF); 249 | unsigned long blockaddr = block * BLOCKSIZE / PAGESIZE; 250 | if (block & 1) 251 | arduboy.write("x\xC0", 2); 252 | else 253 | arduboy.write("x\xC1", 2); 254 | if (!waitWrite()) 255 | return QByteArray(); 256 | if (!waitRead()) 257 | return QByteArray(); 258 | arduboy.read(1); 259 | data[0] = 'A'; 260 | data[1] = blockaddr >> 8; 261 | data[2] = blockaddr & 0xFF; 262 | arduboy.write(data); 263 | if (!waitWrite()) 264 | return QByteArray(); 265 | if (!waitRead()) 266 | return QByteArray(); 267 | arduboy.read(1); 268 | arduboy.write("g\x00\x00\x43", 4); 269 | if (!waitWrite()) 270 | return QByteArray(); 271 | if (!waitRead()) 272 | return QByteArray(); 273 | flashCart += arduboy.read(BLOCKSIZE); 274 | if (progress != nullptr) 275 | progress->setValue(block + 1); 276 | QCoreApplication::processEvents(); 277 | } 278 | arduboy.write("x\x44", 2); 279 | if (!waitWrite()) 280 | return QByteArray(); 281 | if (!waitRead()) 282 | return QByteArray(); 283 | arduboy.read(1); 284 | return flashCart; 285 | } 286 | 287 | bool ArduManFX::writeFlashCart(QByteArray data, bool verify, QProgressBar *progress) 288 | { 289 | QByteArray jedecID; 290 | long blocks; 291 | long capacity = 1l; 292 | if (!startBootloader()) 293 | return false; 294 | if (data.length() % PAGESIZE != 0) 295 | data += QByteArray(PAGESIZE - (data.length() % PAGESIZE), 0xFF); 296 | if (getVersion() < 13) 297 | { 298 | errorString = "Bootloader has no flash cart support."; 299 | return false; 300 | } 301 | jedecID = getJedecID(); 302 | if (jedecID.isNull()) 303 | return false; 304 | capacity <<= jedecID[2]; 305 | printf("JEDEC ID: %02X %02X %02X\n", (unsigned char)jedecID.at(0), (unsigned char)jedecID.at(1), (unsigned char)jedecID.at(2)); 306 | if (data.length() > capacity) 307 | { 308 | errorString = QString("Flash data too large (%1 %2 %3).\nFlash Data Size: %4 bytes\nFlash Ram Capacity: %5 bytes").arg(jedecID.at(0), 2, 16, QChar('0')).arg(jedecID.at(1), 2, 16, QChar('0')).arg(jedecID.at(2), 2, 16, QChar('0')).arg(data.length()).arg(capacity); 309 | return false; 310 | } 311 | blocks = data.length() / BLOCKSIZE; 312 | if (progress != nullptr) 313 | { 314 | progress->setMaximum(blocks); 315 | progress->setValue(0); 316 | } 317 | QCoreApplication::processEvents(); 318 | for (long block = 0; block < blocks; ++block) 319 | { 320 | QByteArray cmd(3, 0xFF); 321 | long blockaddr = block * BLOCKSIZE / PAGESIZE; 322 | long blocklen = BLOCKSIZE; 323 | if (block & 1) 324 | arduboy.write("x\xC0", 2); 325 | else 326 | arduboy.write("x\xC2", 2); 327 | if (!waitWrite()) 328 | return false; 329 | if (!waitRead()) 330 | return false; 331 | arduboy.read(1); 332 | cmd[0] = 'A'; 333 | cmd[1] = blockaddr >> 8; 334 | cmd[2] = blockaddr & 0xFF; 335 | arduboy.write(cmd); 336 | if (!waitWrite()) 337 | return false; 338 | if (!waitRead()) 339 | return false; 340 | arduboy.read(1); 341 | cmd[0] = 'B'; 342 | cmd[1] = (blocklen >> 8) & 0xFF; 343 | cmd[2] = blocklen & 0xFF; 344 | cmd += 'C'; 345 | arduboy.write(cmd); 346 | arduboy.write(data.mid(block * BLOCKSIZE, blocklen)); 347 | if (!waitWrite()) 348 | return false; 349 | if (!waitRead()) 350 | return false; 351 | arduboy.read(1); 352 | if (verify) 353 | { 354 | cmd = QByteArray(3, 0xFF); 355 | cmd[0] = 'A'; 356 | cmd[1] = blockaddr >> 8; 357 | cmd[2] = blockaddr & 0xFF; 358 | arduboy.write(cmd); 359 | if (!waitWrite()) 360 | return false; 361 | if (!waitRead()) 362 | return false; 363 | arduboy.read(1); 364 | cmd[0] = 'g'; 365 | cmd[1] = (blocklen >> 8) & 0xFF; 366 | cmd[2] = blocklen & 0xFF; 367 | cmd += 'C'; 368 | arduboy.write(cmd); 369 | if (!waitWrite()) 370 | return false; 371 | if (!waitRead()) 372 | return false; 373 | if (arduboy.read(blocklen) != data.mid(block * BLOCKSIZE, blocklen)) 374 | { 375 | errorString = "Verify failed!"; 376 | return false; 377 | } 378 | } 379 | if (progress != nullptr) 380 | progress->setValue(block + 1); 381 | QCoreApplication::processEvents(); 382 | } 383 | arduboy.write("x\x44", 2); 384 | if (!waitWrite()) 385 | return false; 386 | if (!waitRead()) 387 | return false; 388 | arduboy.read(1); 389 | return true; 390 | } 391 | 392 | QByteArray ArduManFX::readSketch() 393 | { 394 | QByteArray sketch; 395 | if (!startBootloader()) 396 | return QByteArray(); 397 | if (!arduboy.isOpen()) 398 | { 399 | errorString = "Arduboy not connected."; 400 | return QByteArray(); 401 | } 402 | arduboy.write("A\x00\x00", 3); 403 | if (!waitWrite()) 404 | return QByteArray(); 405 | if (!waitRead()) 406 | return QByteArray(); 407 | arduboy.read(1); 408 | arduboy.write("g\x70\x00\x46", 4); 409 | if (!waitWrite()) 410 | return QByteArray(); 411 | while (sketch.length() < 0x7000) 412 | { 413 | if (!waitRead()) 414 | return QByteArray(); 415 | sketch += arduboy.readAll(); 416 | } 417 | return sketch; 418 | } 419 | 420 | bool ArduManFX::writeSketch(QString hexData, bool verify, bool fix1309, bool fixMicro, QProgressBar *progress) 421 | { 422 | QVector flashPageUsed(256, false); 423 | bool overwritesCatarina = false; 424 | QByteArray data = encodeHexFile(hexData, flashPageUsed, &overwritesCatarina); 425 | if (data.isNull()) 426 | return false; 427 | return writeSketch(data, flashPageUsed, verify, fix1309, fixMicro, progress, overwritesCatarina); 428 | } 429 | 430 | bool ArduManFX::writeSketch(QByteArray data, const QVector &flashPageUsed, bool verify, bool fix1309, bool fixMicro, QProgressBar *progress, bool overwritesCatarina) 431 | { 432 | if (fix1309) 433 | applyLcd1309Patch(data); 434 | if (fixMicro) 435 | applyMicroPolarityPatch(data); 436 | if (!startBootloader()) 437 | return false; 438 | if (getVersion() == 10) 439 | { 440 | arduboy.write("r", 1); 441 | if (!waitWrite()) 442 | return false; 443 | if (!waitRead()) 444 | return false; 445 | if ((arduboy.read(1)[0] & 0x10) != 0 && overwritesCatarina) 446 | { 447 | errorString = "This upload will most likely corrupt the bootloader."; 448 | return false; 449 | } 450 | } 451 | if (progress != nullptr) 452 | { 453 | progress->setMaximum(data.length() / 128); 454 | progress->setValue(0); 455 | } 456 | QCoreApplication::processEvents(); 457 | for (int i = 0; i < data.length() / 128; ++i) 458 | { 459 | if (flashPageUsed[i]) 460 | { 461 | QByteArray cmd(3, 0xFF); 462 | QByteArray page = data.mid(i * 128, 128); 463 | QByteArray data; 464 | cmd[0] = 'A'; 465 | cmd[1] = i >> 2; 466 | cmd[2] = (i & 3) << 6; 467 | arduboy.write(cmd); 468 | if (!waitWrite()) 469 | return false; 470 | if (!waitRead()) 471 | return false; 472 | arduboy.read(1); 473 | arduboy.write("B\x00\x80\x46", 4); 474 | arduboy.write(page); 475 | if (!waitWrite()) 476 | return false; 477 | if (!waitRead()) 478 | return false; 479 | arduboy.read(1); 480 | } 481 | if (progress != nullptr) 482 | progress->setValue(i + 1); 483 | QCoreApplication::processEvents(); 484 | } 485 | if (verify) 486 | { 487 | QByteArray sketch = readSketch(); 488 | for (int i = 0; i < data.length() / 128; ++i) 489 | { 490 | if (flashPageUsed[i]) 491 | { 492 | QByteArray pageWritten = data.mid(i * 128, 128); 493 | QByteArray pageRead = sketch.mid(i * 128, 128); 494 | if (pageWritten != pageRead) 495 | { 496 | errorString = QString("Verify failed at address %1.\nUpload Failed.").arg(i * 128); 497 | return false; 498 | } 499 | } 500 | } 501 | } 502 | return true; 503 | } 504 | 505 | QByteArray ArduManFX::createFlashCart(QList flashData, bool fix1309, bool fixMicro, QProgressBar *progress) 506 | { 507 | QVector flashPageUsed(256, false); 508 | QByteArray flashCart; 509 | unsigned short previousPage = 0xFFFF; 510 | unsigned short currentPage = 0; 511 | unsigned short nextPage = 0; 512 | if (progress != nullptr) 513 | { 514 | progress->setMaximum(flashData.count()); 515 | progress->setValue(0); 516 | } 517 | QCoreApplication::processEvents(); 518 | for (int i = 0; i < flashData.count(); ++i) 519 | { 520 | QByteArray header = defaultHeader(); 521 | QByteArray title = convertImage(flashData[i].titleImage); 522 | QByteArray program = encodeHexFile(flashData[i].hexData, flashPageUsed); 523 | QByteArray gameData = flashData[i].gameData + QByteArray((256 - flashData[i].gameData.length()) % 256, 0xFF); 524 | int programSize = program.length(); 525 | int dataSize = gameData.length(); 526 | int slotSize = ((programSize + dataSize) >> 8) + 5; 527 | int programPage = currentPage + 5; 528 | int dataPage = programPage + (programSize >> 8); 529 | if (fix1309) 530 | applyLcd1309Patch(program); 531 | if (fixMicro) 532 | applyMicroPolarityPatch(program); 533 | nextPage += slotSize; 534 | header[7] = flashData[i].listID; 535 | header[8] = previousPage >> 8; 536 | header[9] = previousPage & 0xFF; 537 | header[10] = nextPage >> 8; 538 | header[11] = nextPage & 0xFF; 539 | header[12] = slotSize >> 8; 540 | header[13] = slotSize & 0xFF; 541 | header[14] = programSize >> 7; 542 | if (programSize > 0) 543 | { 544 | header[15] = programPage >> 8; 545 | header[16] = programPage & 0xFF; 546 | if (dataSize > 0) 547 | { 548 | program[0x14] = 0x18; 549 | program[0x15] = 0x95; 550 | program[0x16] = dataPage >> 8; 551 | program[0x17] = dataPage & 0xFF; 552 | } 553 | } 554 | if (dataSize > 0) 555 | { 556 | header[17] = dataPage >> 8; 557 | header[18] = dataPage & 0xFF; 558 | } 559 | flashCart += header; 560 | flashCart += title; 561 | flashCart += program; 562 | flashCart += gameData; 563 | previousPage = currentPage; 564 | currentPage = nextPage; 565 | if (progress != nullptr) 566 | progress->setValue(i + 1); 567 | QCoreApplication::processEvents(); 568 | } 569 | return flashCart; 570 | } 571 | 572 | QByteArray ArduManFX::convertImage(QImage image, bool encodeSize, int numFrames) 573 | { 574 | QByteArray data; 575 | QRgb pixel; 576 | int frameWidth = image.width(); 577 | int frameHeight = image.height() / numFrames; 578 | if (encodeSize) 579 | { 580 | data += frameWidth; 581 | data += frameHeight; 582 | } 583 | for (int y = 0; y < image.height(); y += 8) 584 | { 585 | for (int x = 0; x < image.width(); ++x) 586 | { 587 | char byte = 0x00; 588 | for (int bit = 0; bit < 8; ++bit) 589 | { 590 | pixel = image.pixel(x, y + bit); 591 | if (qRed(pixel) != 0 || qGreen(pixel) != 0 || qBlue(pixel) != 0) 592 | byte += 1 << bit; 593 | } 594 | data += byte; 595 | } 596 | } 597 | return data; 598 | } 599 | 600 | QList ArduManFX::parseCSV(QString csvLocation) 601 | { 602 | QList flashData; 603 | QFile file(csvLocation); 604 | QTextStream stream(&file); 605 | QStringList rows; 606 | QString basePath = QFileInfo(csvLocation).absolutePath(); 607 | if (!file.open(QFile::ReadOnly|QFile::Text)) 608 | { 609 | errorString = file.errorString(); 610 | return flashData; 611 | } 612 | rows = stream.readAll().split('\n', QString::SkipEmptyParts); 613 | file.close(); 614 | for (int row = 1; row < rows.count(); ++row) 615 | { 616 | QStringList rowData = rows[row].split(';'); 617 | FlashData data; 618 | while (rowData.count() < 6) 619 | rowData += ""; 620 | data.listID = rowData[0].toInt(); 621 | data.description = rowData[1]; 622 | fflush(stdout); 623 | if (rowData[2].isEmpty()) 624 | { 625 | data.titleImage = QImage(128, 64, QImage::Format_RGB32); 626 | data.titleImage.fill(qRgb(0, 0, 0)); 627 | } 628 | else 629 | data.titleImage = QImage(basePath + "/" + rowData[2]).convertToFormat(QImage::Format_RGB32); 630 | if (!rowData[3].isEmpty()) 631 | { 632 | file.setFileName(basePath + "/" + rowData[3]); 633 | if (file.open(QFile::ReadOnly|QFile::Text)) 634 | { 635 | data.hexData = stream.readAll(); 636 | file.close(); 637 | } 638 | } 639 | if (!rowData[4].isEmpty()) 640 | { 641 | file.setFileName(basePath + "/" + rowData[4]); 642 | if (file.open(QFile::ReadOnly)) 643 | { 644 | data.gameData = file.readAll(); 645 | file.close(); 646 | } 647 | } 648 | if (!rowData[5].isEmpty()) 649 | { 650 | file.setFileName(basePath + "/" + rowData[5]); 651 | if (file.open(QFile::ReadOnly)) 652 | { 653 | data.saveData = file.readAll(); 654 | file.close(); 655 | } 656 | } 657 | flashData += data; 658 | } 659 | errorString = "Empty/Invalid CSV file"; 660 | return flashData; 661 | } 662 | 663 | QByteArray ArduManFX::defaultHeader() 664 | { 665 | QByteArray header("ARDUBOY"); 666 | header.append(249, 0xFF); 667 | return header; 668 | } 669 | 670 | QByteArray ArduManFX::encodeHexFile(const QString &hexData, QVector &flashPageUsed, bool *overwritesCatarina) 671 | { 672 | QStringList records = hexData.split('\n', QString::SkipEmptyParts); 673 | QByteArray data(32768, 0xFF); 674 | int flashEnd = 0; 675 | for (int i = 0; i < records.count(); ++i) 676 | { 677 | QString rcd = records[i]; 678 | if (rcd == ":00000001FF") 679 | break; 680 | if (rcd[0] == ':') 681 | { 682 | int rcd_len = rcd.mid(1, 2).toInt(nullptr, 16); 683 | int rcd_typ = rcd.mid(7, 2).toInt(nullptr, 16); 684 | int rcd_addr = rcd.mid(3, 4).toInt(nullptr, 16); 685 | int rcd_sum = rcd.mid(9+rcd_len*2, 2).toInt(nullptr, 16); 686 | if (rcd_typ == 0 && rcd_len > 0) 687 | { 688 | int flash_addr = rcd_addr; 689 | int checksum = rcd_sum; 690 | flashPageUsed[rcd_addr / 128] = true; 691 | flashPageUsed[(rcd_addr + rcd_len - 1) / 128] = true; 692 | for (int j = 1; j < 9 + rcd_len * 2; j += 2) 693 | { 694 | int byte = rcd.mid(j, 2).toInt(nullptr, 16); 695 | checksum = (checksum + byte) & 0xFF; 696 | if (j >= 9) 697 | { 698 | data[flash_addr] = byte; 699 | ++flash_addr; 700 | if (flash_addr > flashEnd) 701 | flashEnd = flash_addr; 702 | } 703 | } 704 | if (checksum != 0) 705 | { 706 | errorString = "Error: Hex data contains errors."; 707 | return QByteArray(); 708 | } 709 | } 710 | } 711 | } 712 | if (overwritesCatarina != nullptr) 713 | { 714 | int flashPageCount = 0; 715 | *overwritesCatarina = false; 716 | for (int i = 0; i < 256; ++i) 717 | { 718 | if (flashPageUsed[i]) 719 | { 720 | ++flashPageCount; 721 | if (i >= 224) 722 | *overwritesCatarina = true; 723 | } 724 | } 725 | } 726 | flashEnd = ((flashEnd + 255) / 256) * 256; 727 | return data.left(flashEnd); 728 | } 729 | 730 | bool ArduManFX::samePort(QSerialPortInfo port1, QSerialPortInfo port2) 731 | { 732 | if (port1.vendorIdentifier() != port2.vendorIdentifier()) 733 | return false; 734 | if (port1.productIdentifier() != port2.productIdentifier()) 735 | return false; 736 | if (port1.portName() != port2.portName()) 737 | return false; 738 | return true; 739 | } 740 | 741 | void ArduManFX::applyLcd1309Patch(QByteArray &data) 742 | { 743 | int lcdBootProgram_addr = 0; 744 | while (lcdBootProgram_addr >= 0) 745 | { 746 | lcdBootProgram_addr = data.indexOf(lcdBootProgram, lcdBootProgram_addr); 747 | if (lcdBootProgram_addr >= 0) 748 | { 749 | data[lcdBootProgram_addr+2] = 0xE3; 750 | data[lcdBootProgram_addr+3] = 0xE3; 751 | } 752 | } 753 | } 754 | 755 | void ArduManFX::applyMicroPolarityPatch(QByteArray &data) 756 | { 757 | for (int i = 0; i < data.length(); i += 2) 758 | { 759 | if (data[i] == '\x28' && data[i+1] == '\x98') 760 | data[i+1] = '\x9a'; 761 | else if (data[i] == '\x28' && data[i+1] == '\x9a') 762 | data[i+1] = '\x98'; 763 | else if (data[i] == '\x5d' && data[i+1] == '\x98') 764 | data[i+1] = '\x9a'; 765 | else if (data[i] == '\x5d' && data[i+1] == '\x9a') 766 | data[i+1] = '\x98'; 767 | else if (data[i] == '\x81' && data[i+1] == '\xef' && data[i+2] == '\x85' && data[i+3] == '\xb9') 768 | data[i] = '\x80'; 769 | else if (data[i] == '\x84' && data[i+1] == '\xe2' && data[i+2] == '\x8b' && data[i+3] == '\xb9') 770 | data[i+1] = '\xe0'; 771 | } 772 | } 773 | 774 | bool ArduManFX::isCompatibleDevice(quint16 vid, quint16 pid) 775 | { 776 | for (size_t i = 0; i < sizeof(compatibleDevices) / sizeof(compatibleDevices[0]); i += 2) 777 | { 778 | if (compatibleDevices[i] == vid && compatibleDevices[i+1] == pid) 779 | { 780 | bootloaderActive = ((i / 2) & 1) == 0; 781 | return true; 782 | } 783 | } 784 | return false; 785 | } 786 | 787 | QSerialPortInfo ArduManFX::getComPort() 788 | { 789 | QList ports = QSerialPortInfo::availablePorts(); 790 | for (int i = 0; i < ports.count(); ++i) 791 | { 792 | if (ports[i].hasProductIdentifier() && ports[i].hasVendorIdentifier()) 793 | { 794 | if (isCompatibleDevice(ports[i].vendorIdentifier(), ports[i].productIdentifier())) 795 | return ports[i]; 796 | } 797 | } 798 | return QSerialPortInfo(); 799 | } 800 | 801 | int ArduManFX::getVersion() 802 | { 803 | if (!arduboy.isOpen()) 804 | { 805 | errorString = "Arduboy not connected"; 806 | return -1; 807 | } 808 | arduboy.write("V", 1); 809 | if (!waitWrite()) 810 | return -1; 811 | if (!waitRead()) 812 | return -1; 813 | return QString(arduboy.read(2)).toInt(); 814 | } 815 | 816 | QByteArray ArduManFX::getJedecID() 817 | { 818 | QByteArray jedec1, jedec2; 819 | if (!arduboy.isOpen()) 820 | { 821 | errorString = "Arduboy not connected"; 822 | return QByteArray(); 823 | } 824 | arduboy.write("j", 1); 825 | if (!waitWrite()) 826 | return QByteArray(); 827 | if (!waitRead()) 828 | return QByteArray(); 829 | jedec1 = arduboy.read(3); 830 | QThread::msleep(500); 831 | arduboy.write("j", 1); 832 | if (!waitWrite()) 833 | return QByteArray(); 834 | if (!waitRead()) 835 | return QByteArray(); 836 | jedec2 = arduboy.read(3); 837 | if (jedec1[0] != jedec2[0] || jedec1[1] != jedec2[1] || jedec1[2] != jedec2[2]) 838 | { 839 | errorString = "No flash cart detected."; 840 | return QByteArray(); 841 | } 842 | return jedec1; 843 | } 844 | 845 | bool ArduManFX::waitWrite(int msecs) 846 | { 847 | if (arduboy.waitForBytesWritten(msecs)) 848 | return true; 849 | errorString = QString("Failed to communicate with arduboy.\nReason: %1").arg(arduboy.errorString()); 850 | return false; 851 | } 852 | 853 | bool ArduManFX::waitRead(int msecs) 854 | { 855 | if (arduboy.waitForReadyRead(msecs)) 856 | return true; 857 | errorString = QString("Failed to communicate with arduboy.\nReason: %1").arg(arduboy.errorString()); 858 | return false; 859 | } 860 | -------------------------------------------------------------------------------- /src/ArduManFX.h: -------------------------------------------------------------------------------- 1 | #ifndef ARDUMANFX_H 2 | #define ARDUMANFX_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define PAGESIZE 256 10 | #define BLOCKSIZE 65536 11 | 12 | class ArduManFX 13 | { 14 | public: 15 | struct FlashData 16 | { 17 | QImage titleImage; 18 | QString description; 19 | QString hexData; 20 | QByteArray gameData; 21 | QByteArray saveData; 22 | int listID; 23 | }; 24 | ArduManFX(); 25 | ~ArduManFX(); 26 | bool connect(); 27 | void disconnect(); 28 | bool startBootloader(); 29 | void exitBootloader(); 30 | bool isConnected(); 31 | QByteArray readEEPROM(); 32 | bool writeEEPROM(QByteArray data, bool verify); 33 | QByteArray readFlashCart(QProgressBar *progress); 34 | bool writeFlashCart(QByteArray data, bool verify, QProgressBar *progress); 35 | QByteArray readSketch(); 36 | bool writeSketch(QString hexData, bool verify, bool fix1309, bool fixMicro, QProgressBar *progress); 37 | bool writeSketch(QByteArray data, const QVector &flashPageUsed, bool verify, bool fix1309, bool fixMicro, QProgressBar *progress, bool overwritesCatarina=false); 38 | QSerialPortInfo getPortInfo() {return arduboyPort;} 39 | static QByteArray createFlashCart(QList flashData, bool fix1309, bool fixMicro, QProgressBar *progress); 40 | static QByteArray convertImage(QImage image, bool encodeSize=false, int numFrames=1); 41 | static QString getErrorString() {return errorString;} 42 | static QList parseCSV(QString csv); 43 | private: 44 | static QByteArray defaultHeader(); 45 | static QByteArray encodeHexFile(const QString &hexData, QVector &flashPageUsed, bool *overwritesCatarina=nullptr); 46 | static bool samePort(QSerialPortInfo port1, QSerialPortInfo port2); 47 | static void applyLcd1309Patch(QByteArray &data); 48 | static void applyMicroPolarityPatch(QByteArray &data); 49 | bool isCompatibleDevice(quint16 vid, quint16 pid); 50 | QSerialPortInfo getComPort(); 51 | int getVersion(); 52 | QByteArray getJedecID(); 53 | bool waitWrite(int msecs = 3000); 54 | bool waitRead(int msecs = 3000); 55 | static const quint16 compatibleDevices[]; 56 | static const quint8 manufacturerIDs[]; 57 | static const char *manufacturerNames[]; 58 | static const char *lcdBootProgram; 59 | static QString errorString; 60 | QSerialPort arduboy; 61 | QSerialPortInfo arduboyPort; 62 | bool bootloaderActive; 63 | }; 64 | 65 | #endif //ARDUMANFX_H 66 | -------------------------------------------------------------------------------- /src/arduboy-pixelartV4-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/arduboy-pixelartV4-white.png -------------------------------------------------------------------------------- /src/arduboyrecovery.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "arduboyrecovery.h" 7 | #include "ArduManFX.h" 8 | #include "progress.h" 9 | 10 | ArduboyRecovery::ArduboyRecovery(QWidget *parent) : QDialog(parent) 11 | { 12 | QFile hexFile(":/recovered.hex"); 13 | QTextStream stream(&hexFile); 14 | setupUi(this); 15 | QObject::connect(&timer, SIGNAL(timeout()), this, SLOT(scanForArduboy())); 16 | timer.start(100); 17 | hexFile.open(QFile::ReadOnly|QFile::Text); 18 | hex = stream.readAll(); 19 | hexFile.close(); 20 | resize(sizeHint()); 21 | setMinimumSize(sizeHint()); 22 | setMaximumSize(sizeHint()); 23 | } 24 | 25 | ArduboyRecovery::~ArduboyRecovery() 26 | { 27 | } 28 | 29 | void ArduboyRecovery::scanForArduboy() 30 | { 31 | if (arduboy.connect()) 32 | { 33 | timer.stop(); 34 | if (!arduboy.writeSketch(hex, true, false, false, progressBar)) 35 | QMessageBox::critical(this, "Upload Failed", QString("Failed to upload recovery sketch to Arduboy\n\n%1").arg(arduboy.getErrorString())); 36 | arduboy.disconnect(); 37 | accept(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/arduboyrecovery.h: -------------------------------------------------------------------------------- 1 | #ifndef ARDUBOYRECOVERY_H 2 | #define ARDUBOYRECOVERY_H 3 | 4 | #include "ArduManFX.h" 5 | #include "ui_arduboyrecovery.h" 6 | 7 | class ArduboyRecovery : public QDialog, protected Ui::ArduboyRecovery 8 | { 9 | Q_OBJECT 10 | public: 11 | ArduboyRecovery(QWidget *parent=nullptr); 12 | ~ArduboyRecovery(); 13 | protected slots: 14 | void scanForArduboy(); 15 | private: 16 | ArduManFX arduboy; 17 | QTimer timer; 18 | QString hex; 19 | }; 20 | 21 | #endif //ARDUBOYRECOVERY_H 22 | -------------------------------------------------------------------------------- /src/arduboyrecovery.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ArduboyRecovery 4 | 5 | 6 | 7 | 0 8 | 0 9 | 382 10 | 152 11 | 12 | 13 | 14 | Arduboy Recovery 15 | 16 | 17 | 18 | :/images/icon.png:/images/icon.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 16 32 | 75 33 | true 34 | 35 | 36 | 37 | Scanning for any signs of life. 38 | Press the reset button at any time. 39 | 40 | 41 | Qt::AlignCenter 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 16 50 | 75 51 | true 52 | 53 | 54 | 55 | 0 56 | 57 | 58 | -1 59 | 60 | 61 | Qt::AlignCenter 62 | 63 | 64 | 65 | 66 | 67 | 68 | Cancel 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | btnCancel 80 | clicked() 81 | ArduboyRecovery 82 | reject() 83 | 84 | 85 | 328 86 | 118 87 | 88 | 89 | 381 90 | 113 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/flashbuilder.cpp: -------------------------------------------------------------------------------- 1 | #include "ArduManFX.h" 2 | #include "flashbuilder.h" 3 | 4 | FlashBuilder::FlashBuilder(QWidget *parent) : QTreeWidget(parent) 5 | { 6 | } 7 | 8 | FlashBuilder::~FlashBuilder() 9 | { 10 | } 11 | 12 | void FlashBuilder::addCategory(QImage image, QString description) 13 | { 14 | } 15 | 16 | void FlashBuilder::dragEnterEvent(QDragEnterEvent *event) 17 | { 18 | printf("drag enter event\n"); 19 | } 20 | 21 | void FlashBuilder::dragMoveEvent(QDragMoveEvent *event) 22 | { 23 | printf("drag move event\n"); 24 | } 25 | 26 | void FlashBuilder::dropEvent(QDropEvent *event) 27 | { 28 | printf("drop event\n"); 29 | } 30 | -------------------------------------------------------------------------------- /src/flashbuilder.h: -------------------------------------------------------------------------------- 1 | #ifndef FLASHBUILDER_H 2 | #define FLASHBUILDER_H 3 | 4 | #include 5 | #include "ArduManFX.h" 6 | 7 | class FlashBuilder : public QTreeWidget 8 | { 9 | Q_OBJECT 10 | public: 11 | FlashBuilder(QWidget *parent=nullptr); 12 | ~FlashBuilder(); 13 | void addCategory(QImage image, QString description); 14 | private: 15 | void dragEnterEvent(QDragEnterEvent *event); 16 | void dragMoveEvent(QDragMoveEvent *event); 17 | void dropEvent(QDropEvent *event); 18 | }; 19 | 20 | #endif //FLASHBUILDER_H 21 | -------------------------------------------------------------------------------- /src/fx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/fx.png -------------------------------------------------------------------------------- /src/game.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "ArduManFX.h" 4 | #include "game.h" 5 | 6 | Game::Game(const GameInfo &info, QWidget *parent) : QWidget(parent) 7 | { 8 | setupUi(this); 9 | lblTitle->setText(info.title); 10 | lblAuthor->setText(info.author); 11 | lblDescription->setText(info.description); 12 | lblLicense->setText(info.license); 13 | lblDate->setText(info.date); 14 | lblGenre->setText(info.genre); 15 | gameID = info.gameID; 16 | url = info.url; 17 | rating = info.rating; 18 | numVotes = info.numVotes; 19 | numScreenshots = info.numScreenshots; 20 | fx = info.fx; 21 | lblRating->setMinimumWidth((int)(rating / 5.0 * 80)); 22 | lblRating->setMaximumWidth((int)(rating / 5.0 * 80)); 23 | lblRating->setToolTip(QString("%1 / 5.0 - %2 %3").arg(rating, 0, 'g', 2).arg(numVotes).arg((numVotes == 1) ? "vote":"votes")); 24 | if (numVotes != -1) 25 | lblNotImplemented->setVisible(false); 26 | lblFX->setVisible(info.fx); 27 | if (info.fx) 28 | btnUpload->setText("UploadFX"); 29 | btnMoreInfo->setEnabled(url.isValid()); 30 | btnScreenshots->setEnabled(false); 31 | btnUpload->setEnabled(false); 32 | } 33 | 34 | Game::~Game() 35 | { 36 | } 37 | 38 | void Game::setHexLocation(QString location) 39 | { 40 | hexLocation = location; 41 | if (!fx) 42 | btnUpload->setEnabled(true); 43 | } 44 | 45 | void Game::setDataLocation(QString location) 46 | { 47 | dataLocation = location; 48 | btnUpload->setEnabled(true); 49 | } 50 | 51 | void Game::setTitleImage(QString imageLocation) 52 | { 53 | lblTitleImage->setPixmap(QPixmap(imageLocation).scaled(256, 128, Qt::KeepAspectRatio)); 54 | } 55 | 56 | void Game::addScreenshot(QString imageLocation) 57 | { 58 | screenshots += imageLocation; 59 | btnScreenshots->setEnabled(numScreenshots == screenshots.count()); 60 | } 61 | 62 | void Game::setRating(int total, int votes) 63 | { 64 | rating = (float)total / (float)votes; 65 | numVotes = votes; 66 | lblRating->setMinimumWidth((int)(rating / 5.0 * 80)); 67 | lblRating->setMaximumWidth((int)(rating / 5.0 * 80)); 68 | lblRating->setToolTip(QString("%1 / 5.0 - %2 %3").arg(rating, 0, 'g', 2).arg(numVotes).arg((numVotes == 1) ? "vote":"votes")); 69 | if (numVotes != -1) 70 | lblNotImplemented->setVisible(false); 71 | } 72 | 73 | void Game::on_btnMoreInfo_clicked() 74 | { 75 | QDesktopServices::openUrl(url); 76 | } 77 | 78 | void Game::on_btnScreenshots_clicked() 79 | { 80 | //TODO: Launch screenshot viewer with list of screenshots 81 | } 82 | 83 | void Game::on_btnUpload_clicked() 84 | { 85 | emit upload(lblTitle->text(), hexLocation, dataLocation); 86 | } 87 | 88 | void Game::mousePressEvent(QMouseEvent *event) 89 | { 90 | if (event->button() == Qt::LeftButton) 91 | dragStartPosition = event->pos(); 92 | } 93 | 94 | void Game::mouseMoveEvent(QMouseEvent *event) 95 | { 96 | if (!(event->buttons() & Qt::LeftButton)) 97 | return; 98 | if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) 99 | return; 100 | QDrag *drag = new QDrag(this); 101 | QMimeData *mimeData = new QMimeData; 102 | QLabel *label = new QLabel(lblTitle->text(), nullptr); 103 | QPixmap pixmap; 104 | QPoint hotSpot(2, 2); 105 | label->setAutoFillBackground(true); 106 | label->setFrameShape(QFrame::Panel); 107 | label->setFrameShadow(QFrame::Raised); 108 | pixmap = QPixmap(label->sizeHint()); 109 | label->render(&pixmap); 110 | label->deleteLater(); 111 | mimeData->setText(lblTitle->text()); 112 | mimeData->setImageData(lblTitleImage->pixmap()->toImage()); 113 | mimeData->setData("ardumanfx-title", lblTitle->text().toLocal8Bit()); 114 | mimeData->setData("ardumanfx-gameID", gameID.toLocal8Bit()); 115 | mimeData->setData("ardumanfx-hex", hexLocation.toLocal8Bit()); 116 | mimeData->setData("ardumanfx-bin", dataLocation.toLocal8Bit()); 117 | drag->setMimeData(mimeData); 118 | drag->setPixmap(pixmap); 119 | drag->setHotSpot(hotSpot); 120 | drag->exec(Qt::CopyAction); 121 | drag->deleteLater(); 122 | } 123 | -------------------------------------------------------------------------------- /src/game.h: -------------------------------------------------------------------------------- 1 | #ifndef GAME_H 2 | #define GAME_H 3 | 4 | #include 5 | #include 6 | #include "ui_game.h" 7 | 8 | class Game : public QWidget, protected Ui::Game 9 | { 10 | Q_OBJECT 11 | public: 12 | struct GameInfo 13 | { 14 | QString title; 15 | QString description; 16 | QString author; 17 | QString license; 18 | QString date; 19 | QString genre; 20 | QString gameID; 21 | QUrl url; 22 | float rating; 23 | int numVotes; 24 | int numScreenshots; 25 | bool fx; 26 | }; 27 | Game(const GameInfo &info, QWidget *parent=nullptr); 28 | ~Game(); 29 | void setHexLocation(QString location); 30 | void setDataLocation(QString location); 31 | void setTitleImage(QString imageLocation); 32 | void addScreenshot(QString imageLocation); 33 | void setRating(int total, int votes); 34 | QString getTitle() {return lblTitle->text();} 35 | float getRating() {return rating;} 36 | int getNumVotes() {return numVotes;} 37 | QString getDate() {return lblDate->text();} 38 | QString getGenre() {return lblGenre->text();} 39 | QString getGameID() {return gameID;} 40 | signals: 41 | void upload(QString title, QString hexLocation, QString dataLocation); 42 | protected slots: 43 | void on_btnMoreInfo_clicked(); 44 | void on_btnScreenshots_clicked(); 45 | void on_btnUpload_clicked(); 46 | private: 47 | void mousePressEvent(QMouseEvent *event); 48 | void mouseMoveEvent(QMouseEvent *event); 49 | QUrl url; 50 | QStringList screenshots; 51 | QString hexLocation; 52 | QString dataLocation; 53 | QString gameID; 54 | QPoint dragStartPosition; 55 | float rating; 56 | int numVotes; 57 | int numScreenshots; 58 | bool fx; 59 | }; 60 | 61 | #endif //GAME_H 62 | -------------------------------------------------------------------------------- /src/game.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Game 4 | 5 | 6 | 7 | 0 8 | 0 9 | 530 10 | 183 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | .QFrame[objectName="frame"] 33 | { 34 | border: 8px ridge rgb(128, 128, 128); 35 | background-color: rgb(192, 192, 192); 36 | } 37 | 38 | .QFrame 39 | { 40 | border: 1px solid black; 41 | background-color: rgb(192, 192, 192); 42 | } 43 | 44 | 45 | QFrame::StyledPanel 46 | 47 | 48 | QFrame::Raised 49 | 50 | 51 | 52 | 53 | 54 | 55 | 256 56 | 128 57 | 58 | 59 | 60 | 61 | 256 62 | 128 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 0 70 | 256 71 | 128 72 | 73 | 74 | 75 | 76 | 256 77 | 128 78 | 79 | 80 | 81 | 82 | 256 83 | 128 84 | 85 | 86 | 87 | 88 | 28 89 | 75 90 | true 91 | 92 | 93 | 94 | background-color: black; 95 | color: white; 96 | 97 | 98 | Loading 99 | Image 100 | 101 | 102 | Qt::AlignCenter 103 | 104 | 105 | 106 | 107 | 108 | 149 109 | 0 110 | 107 111 | 107 112 | 113 | 114 | 115 | 116 | 107 117 | 107 118 | 119 | 120 | 121 | 122 | 107 123 | 107 124 | 125 | 126 | 127 | 128 | 129 | 130 | :/images/fx.png 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 142 | 143 | 144 | 145 | 0 146 | 147 | 148 | 0 149 | 150 | 151 | 0 152 | 153 | 154 | 0 155 | 156 | 157 | 158 | 159 | 160 | 0 161 | 0 162 | 163 | 164 | 165 | 166 | 0 167 | 168 | 169 | 0 170 | 171 | 172 | 0 173 | 174 | 175 | 0 176 | 177 | 178 | 179 | 180 | 181 | 12 182 | 75 183 | true 184 | 185 | 186 | 187 | GAME TITLE 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 0 196 | 0 197 | 198 | 199 | 200 | 201 | 10 202 | 75 203 | true 204 | 205 | 206 | 207 | by 208 | 209 | 210 | Qt::AlignCenter 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 0 219 | 0 220 | 221 | 222 | 223 | 224 | 10 225 | 75 226 | true 227 | 228 | 229 | 230 | AUTHOR 231 | 232 | 233 | Qt::AlignCenter 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 0 245 | 0 246 | 247 | 248 | 249 | 250 | 0 251 | 252 | 253 | 0 254 | 255 | 256 | 0 257 | 258 | 259 | 0 260 | 261 | 262 | 263 | 264 | 265 | 6 266 | 267 | 268 | 0 269 | 270 | 271 | 0 272 | 273 | 274 | 0 275 | 276 | 277 | 0 278 | 279 | 280 | 281 | 282 | 283 | 10 284 | 50 285 | false 286 | 287 | 288 | 289 | Description 290 | 291 | 292 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 293 | 294 | 295 | true 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 0 304 | 0 305 | 306 | 307 | 308 | .QFrame>QLabel 309 | { 310 | background-color: palette(text); 311 | color: palette(window); 312 | } 313 | 314 | 315 | 316 | 3 317 | 318 | 319 | 0 320 | 321 | 322 | 0 323 | 324 | 325 | 0 326 | 327 | 328 | 0 329 | 330 | 331 | 332 | 333 | QFrame::StyledPanel 334 | 335 | 336 | QFrame::Raised 337 | 338 | 339 | 340 | 0 341 | 342 | 343 | 0 344 | 345 | 346 | 0 347 | 348 | 349 | 0 350 | 351 | 352 | 0 353 | 354 | 355 | 356 | 357 | 358 | 0 359 | 0 360 | 361 | 362 | 363 | LICENSE 364 | 365 | 366 | Qt::AlignCenter 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 3 375 | 376 | 377 | 3 378 | 379 | 380 | 3 381 | 382 | 383 | 3 384 | 385 | 386 | 387 | 388 | LICENSE 389 | 390 | 391 | Qt::AlignCenter 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | QFrame::StyledPanel 405 | 406 | 407 | QFrame::Raised 408 | 409 | 410 | 411 | 0 412 | 413 | 414 | 0 415 | 416 | 417 | 0 418 | 419 | 420 | 0 421 | 422 | 423 | 0 424 | 425 | 426 | 427 | 428 | 429 | 0 430 | 0 431 | 432 | 433 | 434 | DATE 435 | 436 | 437 | Qt::AlignCenter 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 3 446 | 447 | 448 | 3 449 | 450 | 451 | 3 452 | 453 | 454 | 3 455 | 456 | 457 | 458 | 459 | DATE 460 | 461 | 462 | Qt::AlignCenter 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | QFrame::StyledPanel 476 | 477 | 478 | QFrame::Raised 479 | 480 | 481 | 482 | 0 483 | 484 | 485 | 0 486 | 487 | 488 | 0 489 | 490 | 491 | 0 492 | 493 | 494 | 0 495 | 496 | 497 | 498 | 499 | 500 | 0 501 | 0 502 | 503 | 504 | 505 | GENRE 506 | 507 | 508 | Qt::AlignCenter 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 3 517 | 518 | 519 | 3 520 | 521 | 522 | 3 523 | 524 | 525 | 3 526 | 527 | 528 | 529 | 530 | GENRE 531 | 532 | 533 | Qt::AlignCenter 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 0 554 | 0 555 | 556 | 557 | 558 | 559 | 0 560 | 561 | 562 | 0 563 | 564 | 565 | 0 566 | 567 | 568 | 0 569 | 570 | 571 | 572 | 573 | 574 | 80 575 | 16 576 | 577 | 578 | 579 | 580 | 80 581 | 16 582 | 583 | 584 | 585 | .QWidget 586 | { 587 | background-image: url(:/images/stars_back.png); 588 | } 589 | 590 | 591 | 592 | 593 | 0 594 | 0 595 | 80 596 | 16 597 | 598 | 599 | 600 | 601 | 80 602 | 16 603 | 604 | 605 | 606 | 607 | 80 608 | 16 609 | 610 | 611 | 612 | 613 | 614 | 615 | :/images/stars_front.png 616 | 617 | 618 | 619 | 620 | 621 | 0 622 | 0 623 | 80 624 | 16 625 | 626 | 627 | 628 | 629 | 80 630 | 16 631 | 632 | 633 | 634 | 635 | 80 636 | 16 637 | 638 | 639 | 640 | Ratings not yet implemented 641 | 642 | 643 | background-color: rgba(192, 192, 192, 192); 644 | color: rgb(255, 0, 0); 645 | 646 | 647 | Not Implemented 648 | 649 | 650 | Qt::AlignCenter 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 10 660 | 661 | 662 | 663 | More Info 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 10 672 | 673 | 674 | 675 | Screenshots 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 10 684 | 685 | 686 | 687 | Upload 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/icon.ico -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/icon.png -------------------------------------------------------------------------------- /src/icon.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "icon.ico" 2 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "mainwindow.h" 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | QApplication a(argc, argv); 9 | MainWindow w; 10 | w.show(); 11 | return a.exec(); 12 | } 13 | -------------------------------------------------------------------------------- /src/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "arduboyrecovery.h" 13 | #include "ArduManFX.h" 14 | #include "game.h" 15 | #include "mainwindow.h" 16 | #include "progress.h" 17 | #include "tester.h" 18 | 19 | /*const char *MainWindow::categories[] = 20 | { 21 | "ALL", 22 | "Shooter", 23 | "Puzzle", 24 | "Arcade", 25 | "Application", 26 | "Misc", 27 | "Racing", 28 | "Sports", 29 | "Action", 30 | "RPG", 31 | "Platformer", 32 | "Demo" 33 | }; 34 | const int MainWindow::CATEGORY_ALL = 0; 35 | const int MainWindow::CATEGORY_SHOOTER = 1; 36 | const int MainWindow::CATEGORY_PUZZLE = 2; 37 | const int MainWindow::CATEGORY_ARCADE = 3; 38 | const int MainWindow::CATEGORY_APPLICATION = 4; 39 | const int MainWindow::CATEGORY_MISC = 5; 40 | const int MainWindow::CATEGORY_RACING = 6; 41 | const int MainWindow::CATEGORY_SPORTS = 7; 42 | const int MainWindow::CATEGORY_ACTION = 8; 43 | const int MainWindow::CATEGORY_RPG = 9; 44 | const int MainWindow::CATEGORY_PLATFORMER = 10; 45 | const int MainWindow::CATEGORY_DEMO = 11;*/ 46 | const int MainWindow::SORT_TITLE_AZ = 0; 47 | const int MainWindow::SORT_TITLE_ZA = 1; 48 | const int MainWindow::SORT_RATING_HIGHLOW = 2; 49 | const int MainWindow::SORT_RATING_LOWHIGH = 3; 50 | const int MainWindow::SORT_DATE_NEWOLD = 4; 51 | const int MainWindow::SORT_DATE_OLDNEW = 5; 52 | const int MainWindow::SORT_RANDOM = 6; 53 | 54 | MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) 55 | { 56 | QNetworkRequest request(QUrl("https://arduboy.ried.cl/repo.json")); 57 | QString fileLocation = QCoreApplication::applicationDirPath() + "/erwinRepo.json"; 58 | if (QFile::exists(fileLocation)) 59 | request.setHeader(QNetworkRequest::IfNoneMatchHeader, getETag(fileLocation)); 60 | setupUi(this); 61 | treeFlashCartBuilder->invisibleRootItem()->setFlags(Qt::ItemIsEnabled); 62 | net = new QNetworkAccessManager(this); 63 | reply = net->get(request); 64 | connect(reply, SIGNAL(readyRead()), this, SLOT(downloadRead())); 65 | connect(reply, SIGNAL(finished()), this, SLOT(loadErwinRepo())); 66 | Ui_MainWindow::statusBar->showMessage("Downloading Erwin's \"Unofficial\" Repo from https://arduboy.ried.cl/repo.json"); 67 | this->setEnabled(false); 68 | //Uncomment these two lines for a release build 69 | btnRunTests->setVisible(false); 70 | wFlashCartBuilder->setEnabled(false); 71 | wFilter->setVisible(false); 72 | btnManageLocalRepo->setVisible(false); 73 | lblRepo->setVisible(false); 74 | cboxRepo->setVisible(false); 75 | //TODO: Load local repo 76 | } 77 | 78 | MainWindow::~MainWindow() 79 | { 80 | } 81 | 82 | void MainWindow::on_cboxCategory_currentIndexChanged(int index) 83 | { 84 | for (int i = 0; i < gameWidgets.count(); ++i) 85 | { 86 | if (index == 0) 87 | gameWidgets[i]->setVisible(true); 88 | else 89 | gameWidgets[i]->setVisible(gameWidgets[i]->getGenre() == categories[index-1]); 90 | } 91 | } 92 | 93 | void MainWindow::on_cboxSort_currentIndexChanged(int index) 94 | { 95 | QVBoxLayout *gameLayout = dynamic_cast(wGameList->layout()); 96 | std::random_device rd; 97 | 98 | std::mt19937 mt(rd()); 99 | 100 | std::uniform_int_distribution dist(0, 1); 101 | std::sort(gameWidgets.begin(), gameWidgets.end(), [index,&dist,&mt](Game *a, Game *b) 102 | { 103 | if (index == SORT_TITLE_AZ) 104 | return a->getTitle() < b->getTitle(); 105 | else if (index == SORT_TITLE_ZA) 106 | return a->getTitle() >= b->getTitle(); 107 | else if (index == SORT_RATING_HIGHLOW) 108 | { 109 | if (a->getRating() > b->getRating()) 110 | return true; 111 | else if (a->getRating() == b->getRating()) 112 | return a->getNumVotes() > b->getNumVotes(); 113 | return false; 114 | } 115 | else if (index == SORT_RATING_LOWHIGH) 116 | { 117 | if (a->getRating() < b->getRating()) 118 | return true; 119 | else if (a->getRating() == b->getRating()) 120 | return a->getNumVotes() < b->getNumVotes(); 121 | return false; 122 | } 123 | else if (index == SORT_DATE_NEWOLD) 124 | return a->getDate() >= b->getDate(); 125 | else if (index == SORT_DATE_OLDNEW) 126 | return a->getDate() < b->getDate(); 127 | return dist(mt) == 1; 128 | }); 129 | for (int i = 0; i < gameWidgets.count(); ++i) 130 | { 131 | gameLayout->removeWidget(gameWidgets[i]); 132 | gameLayout->insertWidget(i, gameWidgets[i]); 133 | } 134 | } 135 | 136 | void MainWindow::loadErwinRepo() 137 | { 138 | QDir erwinRepo(QCoreApplication::applicationDirPath()); 139 | QJsonParseError parseError; 140 | QJsonDocument doc; 141 | QJsonArray items; 142 | QString ratingURLString = "https://app.widgetpack.com/widget/rating/bootstrap?id=14914&chan="; 143 | Ui_MainWindow::statusBar->showMessage("Parsing Erwin's \"Unofficial\" Repo from https://arduboy.ried.cl/repo.json"); 144 | QCoreApplication::processEvents(); 145 | currentDownload += reply->readAll(); 146 | saveETag(QCoreApplication::applicationDirPath() + "/erwinRepo.json", reply->header(QNetworkRequest::ETagHeader).toString()); 147 | doc = QJsonDocument::fromJson(currentDownload, &parseError); 148 | items = doc["items"].toArray(); 149 | if (!erwinRepo.exists("erwinRepo")) 150 | erwinRepo.mkdir("erwinRepo"); 151 | erwinRepo.cd("erwinRepo"); 152 | for (int i = 0; i < items.count(); ++i) 153 | { 154 | Game *gameWidget; 155 | DownloadFile download; 156 | Game::GameInfo info; 157 | QJsonObject game = items[i].toObject(); 158 | QJsonObject binary; 159 | QJsonArray binaries = game["binaries"].toArray(); 160 | QJsonArray screenshotArray = game["screenshots"].toArray(); 161 | QString hexLocation; 162 | QString dataLocation; 163 | QStringList screenshots; 164 | info.title = game["title"].toString(); 165 | info.author = game["author"].toString(); 166 | info.description = game["description"].toString(); 167 | info.date = game["date"].toString(); 168 | info.url = QUrl(game["url"].toString(), QUrl::StrictMode); 169 | info.genre = game["genre"].toString(); 170 | info.license = game["license"].toString(); 171 | info.gameID = game["id"].toString(); 172 | info.rating = 0.0; 173 | info.numVotes = -1; 174 | info.fx = false; 175 | ratingURLString += info.gameID; 176 | if (i < items.count() - 1) 177 | ratingURLString += "%2C"; 178 | if (!categories.contains(info.genre)) 179 | categories += info.genre; 180 | //Search for Arduboy sketch 181 | for (int i = 0; i < binaries.count() && hexLocation.isEmpty(); ++i) 182 | { 183 | binary = binaries[i].toObject(); 184 | if (binary["device"].toString().contains("Arduboy", Qt::CaseInsensitive)) 185 | hexLocation = binary["filename"].toString(); 186 | } 187 | //Search for ArduboyFX data 188 | for (int i = 0; i < binaries.count() && dataLocation.isEmpty(); ++i) 189 | { 190 | binary = binaries[i].toObject(); 191 | if (binary["device"].toString().contains("ArduboyFX", Qt::CaseInsensitive)) 192 | { 193 | dataLocation = binary["filename"].toString(); 194 | info.fx = true; 195 | } 196 | } 197 | info.numScreenshots = screenshotArray.count(); 198 | for (int i = 0; i < screenshotArray.count(); ++i) 199 | screenshots += screenshotArray[i].toString(); 200 | if (!erwinRepo.exists(info.gameID)) 201 | erwinRepo.mkdir(info.gameID); 202 | erwinRepo.cd(info.gameID); 203 | gameWidget = new Game(info); 204 | connect(gameWidget, SIGNAL(upload(QString, QString, QString)), this, SLOT(uploadGame(QString, QString, QString))); 205 | wGameList->layout()->addWidget(gameWidget); 206 | gameWidgets += gameWidget; 207 | download.game = gameWidget; 208 | gamesByID.insert(info.gameID, gameWidget); 209 | download.url = QUrl(game["banner"].toString(), QUrl::StrictMode); 210 | download.fileLocation = QString("%1/title.%2").arg(erwinRepo.path()).arg(download.url.fileName().section(".", -1)); 211 | download.fileType = "title"; 212 | if (!download.url.isValid()) 213 | gameWidget->setTitleImage(""); 214 | else 215 | downloadQueue.enqueue(download); 216 | if (QFile::exists(download.fileLocation)) 217 | gameWidget->setTitleImage(download.fileLocation); 218 | download.url = QUrl(hexLocation, QUrl::StrictMode); 219 | download.fileLocation = erwinRepo.path() + "/sketch.hex"; 220 | download.fileType = "sketch"; 221 | if (QFile::exists(download.fileLocation)) 222 | gameWidget->setHexLocation(download.fileLocation); 223 | downloadQueue.enqueue(download); 224 | if (info.fx) 225 | { 226 | download.url = QUrl(dataLocation, QUrl::StrictMode); 227 | download.fileLocation = erwinRepo.path() + "/data.bin"; 228 | download.fileType = "data"; 229 | if (QFile::exists(download.fileLocation)) 230 | gameWidget->setDataLocation(download.fileLocation); 231 | downloadQueue.enqueue(download); 232 | } 233 | for (int i = 0; i < screenshots.count(); ++i) 234 | { 235 | download.url = QUrl(screenshots[i], QUrl::StrictMode); 236 | download.fileLocation = QString("%1/screenshot%2.%3").arg(erwinRepo.path()).arg(i).arg(download.url.fileName().section(".", -1)); 237 | download.fileType = "screenshot"; 238 | if (QFile::exists(download.fileLocation)) 239 | gameWidget->addScreenshot(download.fileLocation); 240 | downloadQueue.enqueue(download); 241 | } 242 | erwinRepo.cdUp(); 243 | } 244 | std::sort(categories.begin(), categories.end()); 245 | for (int i = 0; i < categories.count(); ++i) 246 | cboxCategory->addItem(categories[i]); 247 | reply->close(); 248 | reply->deleteLater(); 249 | currentDownload.clear(); 250 | reply = net->get(QNetworkRequest(QUrl(ratingURLString))); 251 | connect(reply, SIGNAL(readyRead()), this, SLOT(downloadRead())); 252 | connect(reply, SIGNAL(finished()), this, SLOT(loadErwinRatings())); 253 | Ui_MainWindow::statusBar->showMessage("Downloading Ratings from Erwin's \"Unofficial\" Repo"); 254 | } 255 | 256 | void MainWindow::loadErwinRatings() 257 | { 258 | QJsonParseError parseError; 259 | QJsonDocument doc; 260 | QJsonArray scores; 261 | Ui_MainWindow::statusBar->showMessage("Parsing Ratings from Erwin's \"Unofficial\" Repo"); 262 | QCoreApplication::processEvents(); 263 | currentDownload += reply->readAll(); 264 | doc = QJsonDocument::fromJson(currentDownload, &parseError); 265 | scores = doc["score"].toArray(); 266 | for (int i = 0; i < scores.count(); ++i) 267 | { 268 | QJsonObject score = scores[i].toObject(); 269 | gameWidgets[i]->setRating(score["sum"].toInt(), score["count"].toInt()); 270 | } 271 | if (downloadQueue.count() > 0) 272 | nextDownload(); 273 | else 274 | Ui_MainWindow::statusBar->clearMessage(); 275 | this->setEnabled(true); 276 | } 277 | 278 | void MainWindow::nextDownload() 279 | { 280 | DownloadFile download = downloadQueue.head(); 281 | QNetworkRequest request(download.url); 282 | currentDownload.clear(); 283 | Ui_MainWindow::statusBar->showMessage(QString("Downloading %1").arg(download.url.fileName())); 284 | if (QFile::exists(download.fileLocation)) 285 | request.setHeader(QNetworkRequest::IfNoneMatchHeader, getETag(download.fileLocation)); 286 | reply = net->get(request); 287 | connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); 288 | connect(reply, SIGNAL(readyRead()), this, SLOT(downloadRead())); 289 | connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished())); 290 | } 291 | 292 | void MainWindow::downloadError(QNetworkReply::NetworkError code) 293 | { 294 | Ui_MainWindow::statusBar->showMessage(QString("Network Error (%1): %2").arg(code).arg(reply->errorString())); 295 | reply->abort(); 296 | } 297 | 298 | void MainWindow::downloadRead() 299 | { 300 | currentDownload += reply->readAll(); 301 | } 302 | 303 | void MainWindow::downloadFinished() 304 | { 305 | DownloadFile download = downloadQueue.dequeue(); 306 | QFile file(download.fileLocation); 307 | QTextStream stream(&file); 308 | QIODevice::OpenMode mode = QIODevice::WriteOnly; 309 | saveETag(download.fileLocation, reply->header(QNetworkRequest::ETagHeader).toString()); 310 | if (reply->error()) 311 | { 312 | fprintf(stdout, "Download error %d: %s\n", reply->error(), download.url.toString().toLocal8Bit().data()); 313 | fflush(stdout); 314 | return; 315 | } 316 | if (!currentDownload.isEmpty()) 317 | { 318 | if (download.fileType == "sketch") 319 | mode |= QIODevice::Text; 320 | if (file.open(mode)) 321 | { 322 | if (download.fileType == "sketch") 323 | stream << currentDownload; 324 | else 325 | file.write(currentDownload); 326 | file.close(); 327 | } 328 | else 329 | QMessageBox::critical(this, "Save Failed", QString("Failed to save file: %1\nReason: %2").arg(download.fileLocation).arg(file.errorString())); 330 | if (QFile::exists(download.fileLocation)) 331 | { 332 | if (download.fileType == "title") 333 | download.game->setTitleImage(download.fileLocation); 334 | else if (download.fileType == "sketch") 335 | download.game->setHexLocation(download.fileLocation); 336 | else if (download.fileType == "data") 337 | download.game->setDataLocation(download.fileLocation); 338 | else if (download.fileType == "screenshot") 339 | download.game->addScreenshot(download.fileLocation); 340 | } 341 | } 342 | reply->close(); 343 | reply->deleteLater(); 344 | if (downloadQueue.count() > 0) 345 | nextDownload(); 346 | else 347 | Ui_MainWindow::statusBar->clearMessage(); 348 | } 349 | 350 | void MainWindow::on_btnRunTests_clicked() 351 | { 352 | Tester *tester = new Tester(this); 353 | tester->exec(); 354 | tester->deleteLater(); 355 | } 356 | 357 | void MainWindow::on_btnRecover_clicked() 358 | { 359 | ArduboyRecovery *recovery = new ArduboyRecovery(this); 360 | recovery->exec(); 361 | recovery->deleteLater(); 362 | } 363 | 364 | void MainWindow::uploadGame(QString title, QString hexLocation, QString dataLocation) 365 | { 366 | Progress *progress; 367 | QFile file; 368 | QTextStream stream(&file); 369 | QString hex; 370 | QByteArray data; 371 | if (!dataLocation.isEmpty()) 372 | { 373 | QList flashCart; 374 | ArduManFX::FlashData flashData; 375 | flashData.listID = 0; 376 | flashData.titleImage = QImage(128,64, QImage::Format_RGB32); 377 | flashData.titleImage.fill(qRgb(0, 0, 0)); 378 | flashData.description = title; 379 | file.setFileName(hexLocation); 380 | if (!file.open(QFile::ReadOnly|QFile::Text)) 381 | { 382 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open \"%1\" in read-only mode.\nReason: %2").arg(hexLocation).arg(file.errorString())); 383 | return; 384 | } 385 | flashData.hexData = stream.readAll(); 386 | file.close(); 387 | file.setFileName(dataLocation); 388 | if (!file.open(QFile::ReadOnly|QFile::Text)) 389 | { 390 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open \"%1\" in read-only mode.\nReason: %2").arg(dataLocation).arg(file.errorString())); 391 | return; 392 | } 393 | flashData.gameData = file.readAll(); 394 | file.close(); 395 | flashCart += flashData; 396 | data = ArduManFX::createFlashCart(flashCart, chkFix1309->isChecked(), chkFixMicro->isChecked(), nullptr); 397 | if (!arduboy.connect()) 398 | { 399 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 400 | return; 401 | } 402 | progress = new Progress(QString("Uploading %1").arg(title), arduboy.getPortInfo(), this); 403 | progress->show(); 404 | if (!arduboy.writeFlashCart(data, chkVerify->isChecked(), progress->getProgressBar())) 405 | QMessageBox::critical(this, "Upload Error", QString("Failed to write data to device.\nReason: %1").arg(ArduManFX::getErrorString())); 406 | arduboy.disconnect(); 407 | progress->close(); 408 | progress->deleteLater(); 409 | } 410 | else 411 | { 412 | file.setFileName(hexLocation); 413 | if (!file.open(QFile::ReadOnly|QFile::Text)) 414 | { 415 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open \"%1\" in read-only mode.\nReason: %2").arg(hexLocation).arg(file.errorString())); 416 | return; 417 | } 418 | hex = stream.readAll(); 419 | file.close(); 420 | if (!arduboy.connect()) 421 | { 422 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 423 | return; 424 | } 425 | progress = new Progress(QString("Uploading %1").arg(title), arduboy.getPortInfo(), this); 426 | progress->show(); 427 | if (!arduboy.writeSketch(hex, chkVerify->isChecked(), chkFix1309->isChecked(), chkFixMicro->isChecked(), progress->getProgressBar())) 428 | QMessageBox::critical(this, "Upload Error", QString("Failed to write sketch to device.\nReason: %1").arg(ArduManFX::getErrorString())); 429 | arduboy.disconnect(); 430 | progress->close(); 431 | progress->deleteLater(); 432 | } 433 | } 434 | 435 | QString MainWindow::getETag(QString fileLocation) 436 | { 437 | QFile file(fileLocation + ".etag"); 438 | QTextStream stream(&file); 439 | QString etag; 440 | if (file.open(QFile::ReadOnly|QFile::Text)) 441 | { 442 | etag = stream.readAll(); 443 | file.close(); 444 | } 445 | return etag; 446 | } 447 | 448 | void MainWindow::saveETag(QString fileLocation, QString etag) 449 | { 450 | QFile file(fileLocation + ".etag"); 451 | QTextStream stream(&file); 452 | if (file.open(QFile::WriteOnly|QFile::Text)) 453 | { 454 | stream << etag; 455 | file.close(); 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | //#include 10 | #include "ui_mainwindow.h" 11 | #include "ArduManFX.h" 12 | #include "game.h" 13 | 14 | #define DEBUG_MESSAGE "%s: %d\n", __FILE__, __LINE__ 15 | #define PRINT_DEBUG_MESSAGE() fprintf(stderr, DEBUG_MESSAGE);fflush(stderr); 16 | 17 | class MainWindow : public QMainWindow, protected Ui::MainWindow 18 | { 19 | Q_OBJECT 20 | public: 21 | MainWindow(QWidget *parent=nullptr); 22 | virtual ~MainWindow(); 23 | protected slots: 24 | void on_cboxCategory_currentIndexChanged(int index); 25 | void on_cboxSort_currentIndexChanged(int index); 26 | void loadErwinRepo(); 27 | void loadErwinRatings(); 28 | void nextDownload(); 29 | void downloadError(QNetworkReply::NetworkError code); 30 | void downloadRead(); 31 | void downloadFinished(); 32 | void on_btnRunTests_clicked(); 33 | void on_btnRecover_clicked(); 34 | void uploadGame(QString title, QString hexLocation, QString dataLocation); 35 | private: 36 | struct DownloadFile 37 | { 38 | QUrl url; 39 | QString fileLocation; 40 | QString fileType; 41 | Game *game; 42 | }; 43 | QString getETag(QString fileLocation); 44 | void saveETag(QString fileLocation, QString etag); 45 | QNetworkAccessManager *net; 46 | QNetworkReply *reply; 47 | QByteArray currentDownload; 48 | QQueue downloadQueue; 49 | QList gameWidgets; 50 | QList flashCart; 51 | QMap gamesByID; 52 | QStringList categories; 53 | ArduManFX arduboy; 54 | //static const char *categories[]; 55 | static const int CATEGORY_ALL; 56 | static const int CATEGORY_SHOOTER; 57 | static const int CATEGORY_PUZZLE; 58 | static const int CATEGORY_ARCADE; 59 | static const int CATEGORY_APPLICATION; 60 | static const int CATEGORY_MISC; 61 | static const int CATEGORY_RACING; 62 | static const int CATEGORY_SPORTS; 63 | static const int CATEGORY_ACTION; 64 | static const int CATEGORY_RPG; 65 | static const int CATEGORY_PLATFORMER; 66 | static const int CATEGORY_DEMO; 67 | static const int SORT_TITLE_AZ; 68 | static const int SORT_TITLE_ZA; 69 | static const int SORT_RATING_HIGHLOW; 70 | static const int SORT_RATING_LOWHIGH; 71 | static const int SORT_DATE_NEWOLD; 72 | static const int SORT_DATE_OLDNEW; 73 | static const int SORT_RANDOM; 74 | }; 75 | 76 | #endif //MAINWINDOW_H 77 | -------------------------------------------------------------------------------- /src/progress.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "progress.h" 5 | 6 | Progress::Progress(QString info, QSerialPortInfo portInfo, QWidget *parent) : QDialog(parent) 7 | { 8 | setupUi(this); 9 | lblInfo->setText(info); 10 | lblPort->setText(portInfo.portName()); 11 | lblVID->setText(QString("%1").arg(portInfo.vendorIdentifier(), 2, 16, QChar('0'))); 12 | lblPID->setText(QString("%1").arg(portInfo.productIdentifier(), 2, 16, QChar('0'))); 13 | lblManufacturer->setText(portInfo.manufacturer()); 14 | lblDescription->setText(portInfo.description()); 15 | resize(sizeHint()); 16 | setMinimumSize(sizeHint()); 17 | setMaximumSize(sizeHint()); 18 | } 19 | 20 | Progress::~Progress() 21 | { 22 | } 23 | 24 | QProgressBar *Progress::getProgressBar() 25 | { 26 | return progressBar; 27 | } 28 | -------------------------------------------------------------------------------- /src/progress.h: -------------------------------------------------------------------------------- 1 | #ifndef PROGRESS_H 2 | #define PROGRESS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "ui_progress.h" 10 | 11 | class Progress : public QDialog, protected Ui::Progress 12 | { 13 | Q_OBJECT 14 | public: 15 | Progress(QString info, QSerialPortInfo portInfo, QWidget *parent=nullptr); 16 | ~Progress(); 17 | QProgressBar *getProgressBar(); 18 | }; 19 | 20 | #endif //PROGRESS_H 21 | -------------------------------------------------------------------------------- /src/progress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Progress 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 307 13 | 154 14 | 15 | 16 | 17 | Progress 18 | 19 | 20 | 21 | :/images/icon.png:/images/icon.png 22 | 23 | 24 | .QFrame 25 | { 26 | border: 1px solid palette(text); 27 | } 28 | 29 | .QFrame>QLabel 30 | { 31 | background-color: palette(text); 32 | color: palette(window); 33 | padding: 4px; 34 | } 35 | 36 | 37 | 38 | 39 | 40 | 41 | 18 42 | 75 43 | true 44 | 45 | 46 | 47 | Upload Info 48 | 49 | 50 | Qt::AlignCenter 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 16 59 | 75 60 | true 61 | 62 | 63 | 64 | 24 65 | 66 | 67 | Qt::AlignCenter 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 0 76 | 0 77 | 78 | 79 | 80 | 81 | 0 82 | 83 | 84 | 0 85 | 86 | 87 | 0 88 | 89 | 90 | 0 91 | 92 | 93 | 94 | 95 | 96 | 0 97 | 0 98 | 99 | 100 | 101 | QFrame::StyledPanel 102 | 103 | 104 | QFrame::Raised 105 | 106 | 107 | 108 | 0 109 | 110 | 111 | 0 112 | 113 | 114 | 0 115 | 116 | 117 | 0 118 | 119 | 120 | 0 121 | 122 | 123 | 124 | 125 | 126 | 0 127 | 0 128 | 129 | 130 | 131 | Port 132 | 133 | 134 | Qt::AlignCenter 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 4 143 | 144 | 145 | 4 146 | 147 | 148 | 4 149 | 150 | 151 | 4 152 | 153 | 154 | 155 | 156 | PORT 157 | 158 | 159 | Qt::AlignCenter 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 0 174 | 0 175 | 176 | 177 | 178 | QFrame::StyledPanel 179 | 180 | 181 | QFrame::Raised 182 | 183 | 184 | 185 | 0 186 | 187 | 188 | 0 189 | 190 | 191 | 0 192 | 193 | 194 | 0 195 | 196 | 197 | 0 198 | 199 | 200 | 201 | 202 | 203 | 0 204 | 0 205 | 206 | 207 | 208 | VID 209 | 210 | 211 | Qt::AlignCenter 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 4 220 | 221 | 222 | 4 223 | 224 | 225 | 4 226 | 227 | 228 | 4 229 | 230 | 231 | 232 | 233 | VID 234 | 235 | 236 | Qt::AlignCenter 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 0 251 | 0 252 | 253 | 254 | 255 | QFrame::StyledPanel 256 | 257 | 258 | QFrame::Raised 259 | 260 | 261 | 262 | 0 263 | 264 | 265 | 0 266 | 267 | 268 | 0 269 | 270 | 271 | 0 272 | 273 | 274 | 0 275 | 276 | 277 | 278 | 279 | 280 | 0 281 | 0 282 | 283 | 284 | 285 | PID 286 | 287 | 288 | Qt::AlignCenter 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 4 297 | 298 | 299 | 4 300 | 301 | 302 | 4 303 | 304 | 305 | 4 306 | 307 | 308 | 309 | 310 | PID 311 | 312 | 313 | Qt::AlignCenter 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 0 328 | 0 329 | 330 | 331 | 332 | QFrame::StyledPanel 333 | 334 | 335 | QFrame::Raised 336 | 337 | 338 | 339 | 0 340 | 341 | 342 | 0 343 | 344 | 345 | 0 346 | 347 | 348 | 0 349 | 350 | 351 | 0 352 | 353 | 354 | 355 | 356 | 357 | 0 358 | 0 359 | 360 | 361 | 362 | Manufacturer 363 | 364 | 365 | Qt::AlignCenter 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 4 374 | 375 | 376 | 4 377 | 378 | 379 | 4 380 | 381 | 382 | 4 383 | 384 | 385 | 386 | 387 | MAN 388 | 389 | 390 | Qt::AlignCenter 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 0 405 | 0 406 | 407 | 408 | 409 | QFrame::StyledPanel 410 | 411 | 412 | QFrame::Raised 413 | 414 | 415 | 416 | 0 417 | 418 | 419 | 0 420 | 421 | 422 | 0 423 | 424 | 425 | 0 426 | 427 | 428 | 0 429 | 430 | 431 | 432 | 433 | 434 | 0 435 | 0 436 | 437 | 438 | 439 | Description 440 | 441 | 442 | Qt::AlignCenter 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 4 451 | 452 | 453 | 4 454 | 455 | 456 | 4 457 | 458 | 459 | 4 460 | 461 | 462 | 463 | 464 | DES 465 | 466 | 467 | Qt::AlignCenter 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | -------------------------------------------------------------------------------- /src/recovered.hex: -------------------------------------------------------------------------------- 1 | :100000000C94BC030C94E4030C94E4030C94E403FC 2 | :100010000C94E4030C94E4030C94E4030C94E403C4 3 | :100020000C94E4030C94E4030C9468070C94D70735 4 | :100030000C94E4030C94E4030C94E4030C94E403A4 5 | :100040000C94E4030C94E4030C94E4030C94E40394 6 | :100050000C94E4030C94E4030C94E4030C94C8099A 7 | :100060000C94E4030C94E4030C94E4030C94E40374 8 | :100070000C94E4030C94E4030C94E4030C94E40364 9 | :100080000C94E4030C94E4030C94E4030C94E40354 10 | :100090000C94E4030C94E4030C94E4030C94E40344 11 | :1000A0000C94E4030C94E4030C94E40300000000BB 12 | :1000B000003E5B4F5B3E3E6B4F6B3E1C3E7C3E1CEE 13 | :1000C000183C7E3C181C577D571C1C5E7F5E1C0034 14 | :1000D000183C1800FFE7C3E7FF0018241800FFE7EB 15 | :1000E000DBE7FF30483A060E2629792926407F05AE 16 | :1000F0000507407F05253F5A3CE73C5A7F3E1C1CC4 17 | :1001000008081C1C3E7F14227F22145F5F005F5F83 18 | :1001100006097F017F006689956A6060606060946F 19 | :10012000A2FFA29408047E040810207E2010080874 20 | :100130002A1C08081C2A08081E101010100C1E0C7F 21 | :100140001E0C30383E3830060E3E0E060000000011 22 | :100150000000005F00000007000700147F147F14F8 23 | :10016000242A7F2A1223130864623649562050003D 24 | :1001700008070300001C2241000041221C002A1C29 25 | :100180007F1C2A08083E0808008070300008080814 26 | :100190000808000060600020100804023E51494534 27 | :1001A0003E00427F400072494949462141494D3352 28 | :1001B0001814127F1027454545393C4A49493141B9 29 | :1001C000211109073649494936464949291E000087 30 | :1001D00014000000403400000008142241141414DC 31 | :1001E0001414004122140802015909063E415D59C8 32 | :1001F0004E7C1211127C7F494949363E41414122D1 33 | :100200007F4141413E7F494949417F090909013EFA 34 | :10021000414151737F0808087F00417F4100204021 35 | :10022000413F017F081422417F404040407F021C33 36 | :10023000027F7F0408107F3E4141413E7F0909094A 37 | :10024000063E4151215E7F09192946264949493216 38 | :1002500003017F01033F4040403F1F2040201F3FDC 39 | :100260004038403F63140814630304780403615961 40 | :10027000494D43007F4141410204081020004141A3 41 | :10028000417F04020102044040404040000307084F 42 | :100290000020545478407F2844443838444444284B 43 | :1002A000384444287F385454541800087E090218F2 44 | :1002B000A4A49C787F0804047800447D400020407A 45 | :1002C000403D007F1028440000417F40007C0478BE 46 | :1002D00004787C080404783844444438FC18242406 47 | :1002E0001818242418FC7C08040408485454542486 48 | :1002F00004043F44243C4040207C1C2040201C3C03 49 | :100300004030403C44281028444C9090907C4464F9 50 | :10031000544C44000836410000007700000041368C 51 | :10032000080002010204023C2623263C1EA1A16112 52 | :10033000123A4040207A3854545559215555794144 53 | :100340002154547841215554784020545579400C1B 54 | :100350001E527212395555555939545454593955FC 55 | :100360005454580000457C410002457D420001453F 56 | :100370007C40F0292429F0F0282528F07C545545AC 57 | :10038000002054547C547C0A097F4932494949323F 58 | :100390003248484832324A4848303A4141217A3A54 59 | :1003A00042402078009DA0A07D39444444393D401E 60 | :1003B00040403D3C24FF2424487E4943662B2FFCCB 61 | :1003C0002F2BFF0929F620C0887E09032054547979 62 | :1003D000410000447D413048484A32384040227A4A 63 | :1003E000007A0A0A727D0D19317D2629292F2826C7 64 | :1003F0002929292630484D402038080808080808CF 65 | :100400000808382F10C8ACBA2F102834FA00007B27 66 | :10041000000008142A142222142A1408950022002D 67 | :1004200095AA005500AAAA55AA55AA000000FF00E7 68 | :10043000101010FF00141414FF001010FF00FF1024 69 | :1004400010F010F0141414FC001414F700FF000056 70 | :10045000FF00FF1414F404FC141417101F10101FD5 71 | :10046000101F1414141F00101010F0000000001FC3 72 | :10047000101010101F10101010F010000000FF10CE 73 | :100480001010101010101010FF10000000FF1400CA 74 | :1004900000FF00FF00001F10170000FC04F41414FC 75 | :1004A0001710171414F404F40000FF00F7141414C8 76 | :1004B00014141414F700F7141414171410101F1048 77 | :1004C0001F141414F4141010F010F000001F101F6B 78 | :1004D0000000001F14000000FC140000F010F010D9 79 | :1004E00010FF10FF141414FF141010101F00000050 80 | :1004F00000F010FFFFFFFFFFF0F0F0F0F0FFFFFF54 81 | :100500000000000000FFFF0F0F0F0F0F38444438AA 82 | :10051000447C2A2A3E147E02020606027E027E02E5 83 | :1005200063554941633844443C04407E201E200604 84 | :10053000027E020299A5E7A5991C2A492A1C4C7241 85 | :1005400001724C304A4D4D303048784830BC625AC8 86 | :10055000463D3E494949007E0101017E2A2A2A2A58 87 | :100560002A44445F444440514A444040444A514034 88 | :100570000000FF0103E080FF000008086B6B0836F5 89 | :1005800012362436060F090F060000181800000066 90 | :100590001010003040FF0101001F01011E00191D55 91 | :1005A0001712003C3C3C3C0000000000F0F89C8E20 92 | :1005B0008783878E9CF8F00000FEFF03030303038C 93 | :1005C000070EFCF80000FEFF0303030303070EFC05 94 | :1005D000F80000FFFF00000000000000FFFF000027 95 | :1005E000FEFF8383838383C7EE7C380000F8FC0E14 96 | :1005F00007030303070EFCF800003F7FE0C0808084 97 | :10060000C0E07F3FFFFF01010101010101FFFF0089 98 | :1006100000FFFF0C0C0C0C1C3E77E3C100007FFFB9 99 | :10062000C0C0C0C0C0E0703F1F00001F3F70E0C0EE 100 | :10063000C0C0E0703F1F00007FFFC1C1C1C1C1E366 101 | :10064000773E1C00001F3F70E0C0C0C0E0703F1F3D 102 | :10065000000000000001FFFF01000000D5F08D1434 103 | :10066000A1C881CFD9F1AF20002D2D2D2D2D2D2DFD 104 | :100670002D2D2D2D2D2D2D2D2D2D2D2D2D2D006E96 105 | :1006800065772067616D652100596F75206D61790F 106 | :10069000206E6F772075706C6F61642061002D2D66 107 | :1006A0002D2D2D2D2D2D2D2D2D2D2D2D2D2D2D2D7A 108 | :1006B0002D2D2D007375636365737366756C6C798E 109 | :1006C00020726573746F72656400596F7572204192 110 | :1006D000726475626F7920686173206265656E006F 111 | :1006E0002D2D2D2D2D2D2D2D2D2D2D2D2D2D2D2D3A 112 | :1006F0002D2D2D2D2D00202041524455424F5920A3 113 | :10070000524553544F52454400080B000202020167 114 | :100710000009040000010202000005240010010588 115 | :1007200024010101042402060524060001070581B5 116 | :100730000310004009040100020A0000000705023E 117 | :100740000240000007058302400000040309041270 118 | :10075000010002EF02014041233680000101020343 119 | :100760000141726475696E6F204C4C4300417264A4 120 | :1007700075626F790000A50A11241FBECFEFDAE081 121 | :10078000DEBFCDBF11E0A0E0B1E0E2EDF9E102C0D3 122 | :1007900005900D92AC33B107D9F725E0ACE3B1E099 123 | :1007A00001C01D92A53CB207E1F713E0CCEBD3E00A 124 | :1007B00004C02197FE010E94E10CCB3BD107C9F791 125 | :1007C0000E94D60A0C94E70C0C940000EEE5F1E0D0 126 | :1007D000A0E0B8E000800EBC8111012C1197A0FDB3 127 | :1007E000FDCF0192B9F70DB40895FC0180915D0130 128 | :1007F000882311F13FB7F89482E08093E9002091BB 129 | :10080000F200822F90E01816190614F481E090E0AF 130 | :10081000882339F0289844E640935C014091F10028 131 | :100820004083222339F02091F200211103C02BE6EE 132 | :100830002093E8003FBF08958FEF9FEF08952FB7F3 133 | :10084000F89483E08093E9009091E800892F80720A 134 | :1008500095FF04C09091F20080E4891B2FBF08959A 135 | :100860008091540181110DC082E08093500184E099 136 | :1008700080935101109253011092520181E08093B4 137 | :10088000540180E591E00895CF93DF931F92CDB797 138 | :10089000DEB76983DC01ED91FC910280F381E02DEC 139 | :1008A00041E050E0BE016F5F7F4F09950F90DF91EF 140 | :1008B000CF91089583E08093E9008091F20088232E 141 | :1008C00019F08AE38093E80008950E941F0490E0E5 142 | :1008D0000895CF93DF931F92CDB7DEB7FC018485D7 143 | :1008E000958597FD08C02FEF3FEF358724870F9040 144 | :1008F000DF91CF910895CE0101960E94F5030197F3 145 | :1009000019F4898190E0F3CF8FEF9FEFF0CF0F9331 146 | :100910001F93CF93DF931F92CDB7DEB78C01FC01FD 147 | :100920008485958597FF0BC0CE0101960E94F50343 148 | :10093000019771F4898190E0F80195878487F80127 149 | :10094000848595850F90DF91CF911F910F91089528 150 | :100950008FEF9FEFF1CFFC018485958597FD0BC04C 151 | :100960009FB7F89482E08093E9008091F2009FBFE6 152 | :1009700090E0019608959FB7F89482E08093E90093 153 | :100980008091F2009FBF90E0089540914A015091FC 154 | :100990004B01209148013091490142175307B4F4AB 155 | :1009A0009091E8009570E1F39091E80092FD19C0F4 156 | :1009B0008093F10080914A0190914B0101968F73D1 157 | :1009C0009927892B19F48EEF8093E80080914A01D2 158 | :1009D00090914B01019690934B0180934A0181E0E5 159 | :1009E000089580E00895DF92EF92FF920F931F9396 160 | :1009F000CF93DF93D82E8A01EB017B01E40EF51E25 161 | :100A0000CE15DF0559F0D7FE12C0FE0184910E9479 162 | :100A1000C50421968111F4CF0FEF1FEFC801DF91BC 163 | :100A2000CF911F910F91FF90EF90DF9008958881F3 164 | :100A3000EECF0F931F93CF93DF931F92CDB7DEB707 165 | :100A400082E0898342E450E069E077E080E80E9438 166 | :100A5000F3040E943004DC0112960D911C910115E3 167 | :100A6000110589F0D801ED91FC910280F381E02D10 168 | :100A7000BE016F5F7F4FC801099597FD04C0F80163 169 | :100A800000851185ECCF89810F90DF91CF911F9167 170 | :100A90000F910895615030F02091F100FC01208306 171 | :100AA0000196F8CF289884E680935C010895AF9270 172 | :100AB000BF92CF92DF92EF92FF920F931F93CF934B 173 | :100AC000DF936C017B018B01040F151FEB015E01AD 174 | :100AD000AE18BF08C017D10759F06991D601ED9142 175 | :100AE000FC910190F081E02DC6010995892B79F7E1 176 | :100AF000C501DF91CF911F910F91FF90EF90DF9093 177 | :100B0000CF90BF90AF900895FC010E941B063197D3 178 | :100B100028F068517C4F8F4F9F4FF9CFAB01FC01FC 179 | :100B20000E941B06641775078E079F07C8F3089578 180 | :100B30008F929F92AF92BF92CF92DF92EF92FF92ED 181 | :100B40000F931F93CF93DF936C017B018A018091F8 182 | :100B50000B01882309F45AC080915D01882309F4B0 183 | :100B600055C080914F0180FF05C08091E0008260F8 184 | :100B70008093E000E801B12C8AEFA82E93E0892E43 185 | :100B80002AE3922E209711F4BB20C9F10E941F0482 186 | :100B9000811108C0AA94AA20C9F181E090E00E94C6 187 | :100BA0008405F0CF8C171D0611F00CF08C2F9FB729 188 | :100BB000F8948092E9002091E80025FD02C09FBFD3 189 | :100BC000E1CF282F30E0C21BD30BF701815020F07A 190 | :100BD00041914093F100FACFE20EF31EBB2021F0C9 191 | :100BE0009092E800B12CEBCF8091E80085FDE7CF33 192 | :100BF0009092E800BB24B394209709F3F3CF5D985B 193 | :100C000084E680934E01101611063CF081E090E0DE 194 | :100C1000F6019383828310E000E0C801DF91CF9159 195 | :100C20001F910F91FF90EF90DF90CF90BF90AF900A 196 | :100C30009F908F900895A0E4B1E02FB7F8947D9134 197 | :100C40008D919C9166B52FBFA89B05C06F3F18F48E 198 | :100C50007F5F8F4F9F4F22E0660F771F881F991F7E 199 | :100C60002A95D1F708952D9A08950C9484052F9212 200 | :100C70003F924F925F926F927F928F929F92AF922C 201 | :100C8000BF92CF92DF92EF92FF920F931F93CF9379 202 | :100C9000DF9300D000D01F92CDB7DEB79C838B834B 203 | :100CA0006A3059F5DC0155968C91B8E08B9F900124 204 | :100CB0001124EB81FC8181899289820F931F928B91 205 | :100CC000818B108A178681E090E00F900F900F9033 206 | :100CD0000F900F90DF91CF911F910F91FF90EF90A8 207 | :100CE000DF90CF90BF90AF909F908F907F906F904C 208 | :100CF0005F904F903F902F9008956D3021F3AB811E 209 | :100D0000BC811F968D919C915097803891050CF075 210 | :100D100087C05196CD90DC90B0E4CB16D1040CF096 211 | :100D20007FC0EB81FC819588E5E09C019E9E200DB3 212 | :100D3000311D1124121613060CF072C0E92CF12C8F 213 | :100D40009701B3E0220F331FBA95E1F72C0D3D1D3B 214 | :100D5000121613060CF064C0AB81BC8154965C90F3 215 | :100D6000549753964C906E9FF0011124E455FF4F19 216 | :100D700070E02491753009F420E05601A8E06A2E55 217 | :100D8000222E312C642D20FF652D611103C05414D7 218 | :100D900009F437C0AC0161706A834983242F281B92 219 | :100DA000291578F510E000E0091540F540385105A7 220 | :100DB00010F598012A0D3B1D20343105E0F460E167 221 | :100DC000762E898061E021FD64E020FD660F22FD22 222 | :100DD0006295287F729ED0011124A80D6D83A25ABE 223 | :100DE000BE4F2C913A81862E822A311103C08D800C 224 | :100DF000809482228C920F5F1F4FD6CF4F5F5F4F40 225 | :100E0000CCCF9101359527956A94AE0CBF1C61102B 226 | :100E1000B7CF7F5F31968E0D9F1D763009F0A9CF39 227 | :100E2000AB81BC8155962C915597422F50E01F966F 228 | :100E30008D919C91B6E02B9F800D911D1124EB812B 229 | :100E4000FC81908B87872689222309F43CCF6AEFA7 230 | :100E500064039001659F300D112420583F4F2817DF 231 | :100E600039070CF030CFCF010190F081E02D0190D7 232 | :100E7000F081E02D6AE0099526CF0F931F93CF9361 233 | :100E8000DF938C01D0E0C0E0F801EC0FFD1F64910E 234 | :100E9000662341F08EEA95E00E943706892B11F017 235 | :100EA0002196F2CF42E050E069E371E08EEA95E0EE 236 | :100EB0000E9457058C0F9D1FDF91CF911F910F91BD 237 | :100EC00008950895089590E080E008950C94CB0C67 238 | :100ED0001F920F920FB60F9211248F939F938091C0 239 | :100EE000E1009091E100937F9093E10083FF0FC0B8 240 | :100EF0001092E90091E09093EB001092EC0092E3E5 241 | :100F00009093ED0010925D0198E09093F00082FFC5 242 | :100F100022C093E09093E9009091F200992319F098 243 | :100F20009AE39093E80090914E01992341F09091BB 244 | :100F30004E01915090934E01911101C05D9A909194 245 | :100F40005C01992341F090915C01915090935C0178 246 | :100F5000911101C0289A84FF18C08091E2008E7E12 247 | :100F600081608093E2008091E1008F7E8093E100B8 248 | :100F700080914F018E7E806180934F019F918F9170 249 | :100F80000F900FBE0F901F90189580FFF7CF8091A4 250 | :100F9000E2008E7E80618093E2008091E1008E7E8F 251 | :100FA0008093E10080914F018E7E8160E5CF1F929A 252 | :100FB0000F920FB60F921124CF92DF92EF92FF9211 253 | :100FC0000F931F932F933F934F935F936F937F9351 254 | :100FD0008F939F93AF93BF93EF93FF93CF93DF9341 255 | :100FE000CDB7DEB76C97DEBFCDBF1092E900809120 256 | :100FF000E80083FF25C068E0CE0145960E944A05BF 257 | :1010000082EF8093E8008D8987FF39C09091E800D6 258 | :1010100090FFFCCF982F907609F034C19E894F89BC 259 | :10102000588D2F89F88C911131C0803861F58091ED 260 | :101030004D018093F1001092F1008EEF8093E80053 261 | :101040006C960FB6F894DEBF0FBECDBFDF91CF9187 262 | :10105000FF91EF91BF91AF919F918F917F916F9190 263 | :101060005F914F913F912F911F910F91FF90EF90C2 264 | :10107000DF90CF900F900FBE0F901F9018959EEFAE 265 | :101080009093E800C7CF1092F100D5CF913059F47A 266 | :101090008111D3CF4130510581F680914D018D7F73 267 | :1010A00080934D01CACF933049F48111C6CF4130AE 268 | :1010B000510519F680914D018260F2CF953041F4CF 269 | :1010C0008091E80080FFFCCF20682093E300B5CF3B 270 | :1010D000963009F0A9C00B8D1C8D22E01092E9001A 271 | :1010E00010924B0110924A01F2122EC01092490147 272 | :1010F000109248010E9419051F8299E09983FA8293 273 | :1011000091E09E8390EA98879AEF998720914A010F 274 | :1011100030914B01275F3F4F3C832B838D8310928F 275 | :10112000E90010924B0110924A011093490100937B 276 | :10113000480149E050E0BE016F5F7F4F80E00E94B0 277 | :10114000F3040E94190579CF1093490100934801D7 278 | :101150000E943004DC0112960D911C9101151105BD 279 | :1011600009F410C1D801ED91FC910480F581E02DC6 280 | :10117000BE016B5E7F4FC8010995009709F0FDC065 281 | :10118000F80100851185EACFF3E0FF120EC08F89C8 282 | :10119000882309F440C0823061F440E867E08DE6BE 283 | :1011A00097E00E942D0A811148CF81E28093EB00E5 284 | :1011B00047CF813029F440E86BE081E697E0F1CF3A 285 | :1011C000833099F70E943004DC011296ED90FC9078 286 | :1011D0008E010F5F1F4F6801E114F10479F0D70110 287 | :1011E000ED91FC910680F781E02DB801C7010995CA 288 | :1011F000080F111DF701E084F184EECFD8011C9295 289 | :10120000F60101900020E9F73197BF016C197D09C3 290 | :1012100040E0C601C6CF6BE477E0FB01449150E0AB 291 | :1012200080E80E94F30409CF973009F4BECF9830CC 292 | :1012300021F481E08093F10000CF993009F0FDCED8 293 | :10124000837009F0B2CFEDE0F1E081E031E096E3A8 294 | :101250002191222371F08093E9003093EB00DF01AC 295 | :1012600011972C912093EC009093ED008F5F8730C5 296 | :1012700079F78EE78093EA001092EA008F898093D5 297 | :101280005D01DBCE8B8D9C8D1092E90010924B019D 298 | :1012900010924A019093490180934801898D8111F0 299 | :1012A00051C08E899D89913A49F4813209F07DCFF0 300 | :1012B00047E050E064E071E080E0B3CF913209F0A4 301 | :1012C00074CF833269F48F89988DB0E0A0E0809369 302 | :1012D000000190930101A0930201B0930301ADCEF0 303 | :1012E000803269F48091E80082FFFCCF67E084E0FF 304 | :1012F00091E00E944A058BEF8093E8009ECE8232F7 305 | :1013000009F09BCE8F8980930B018091040190910D 306 | :101310000501A0910601B0910701803B9440A10511 307 | :10132000B10569F480910B0180FD09C088E78093C5 308 | :101330004C01809164008F7D809364007ECE10927A 309 | :101340004C017BCE0E943004DC0112960D911C9161 310 | :101350000115110509F429CFD801ED91FC910190F7 311 | :10136000F081E02DBE016B5E7F4FC80109958111B0 312 | :1013700064CEF80100851185EBCF181619060CF420 313 | :101380005CCE13CFF1E0FF12FFCE6FE477E045CFE4 314 | :101390000F930FB70F931F9211248F939F93EF9387 315 | :1013A000FF93E4E4F1E080913F0191E08D5F8D37A0 316 | :1013B00010F08D57939580933F018081890F808332 317 | :1013C0009181911D91830281011D02838381811D81 318 | :1013D000838309270F700927029500933E0135D0BA 319 | :1013E000811100933C01803989F400913D01901BEB 320 | :1013F000963070F087E7809300088093010888E1B9 321 | :1014000098E08093600090936000FFCF90933D013F 322 | :1014100080914C01815018F080934C0159F3E0E425 323 | :10142000F1E0808191810196808391838281938113 324 | :10143000811D911D82839383FF91EF919F918F91E5 325 | :101440001F900F910FBF0F9118958FB18095807FDE 326 | :10145000669B88601C9B84600895EF92FF920F93B7 327 | :101460001F93CF93DF93F82E192FE62E042F81E0E0 328 | :10147000860F880F0E94C50483E00E94C504CF2D0B 329 | :10148000D12FEC0EFD2EF11CCE15DF05B9F007FFB4 330 | :1014900013C0FE0184910E94C504182F80E00E94B1 331 | :1014A000C504812321968111EFCFDF91CF911F9148 332 | :1014B0000F91FF90EF9008958881EDCF81E0F5CFF7 333 | :1014C0000F931F93CF93DF938C0181E090E00E94F4 334 | :1014D000660780FF32C08AE090E00E9466079FEFB7 335 | :1014E000980F9E3F50F522E330E0D80150963C9390 336 | :1014F0002E931F9728E330E052963C932E935197FA 337 | :10150000CBE0D0E0D801ED91FC910190F081E02D8D 338 | :10151000682FC8010995CE010E9466072196C13146 339 | :10152000D10581F780E00E94E60388EE93E0DF9129 340 | :10153000CF911F910F910C943506DF91CF911F91A0 341 | :101540000F91089504970C94600AEEE5F5E013827C 342 | :10155000128288EE93E0A0E0B0E084839583A683B6 343 | :10156000B78387E191E0918380838FEF9FEF958729 344 | :101570008487EEEAF5E0138212821186128680E1FA 345 | :10158000838710861782158689E291E09183808394 346 | :1015900087E391E095838483108A1786128A118AE3 347 | :1015A00081E0838B148A858B168A0895789483E072 348 | :1015B00084BD83E085BD81E080936E00E1E8F0E0CA 349 | :1015C00083E08083E0E881E08083E1E983E0808359 350 | :1015D000E0E981E08083E1EC87E08083E3EC81E077 351 | :1015E0008083E0EC82E08083E2EC81E08083EAE7C4 352 | :1015F00087E080838068808310925D0110924D01A6 353 | :1016000010924F018091D70081608093D70080EACB 354 | :101610008093D80089B5806189BD89B5826089BD14 355 | :1016200009B400FEFDCF81E090E00E948405809126 356 | :10163000D8008F7C80618093D8008091E000807F0B 357 | :101640008093E0008091E1008E7E8093E1008DE048 358 | :101650008093E200559A209A81EC80937C0081EF80 359 | :1016600085B987EE84B984E38BB984EF8AB9769A19 360 | :101670006E9880EF81BB10BA80E58CBD81E08DBD96 361 | :1016800085E090E00E9435065F9902C05F9AF8CF2E 362 | :101690005C98ECE5F6E09DE085918EBD00000DB410 363 | :1016A00007FEFDCF8EB59A95B9F75C9A81E88093D5 364 | :1016B000640081E0809365000E94E6030E94250A91 365 | :1016C00087FF11C05C9885EA8EBD00000DB407FE4F 366 | :1016D000FDCF8EB55C9A2E982F982D9881E083BF10 367 | :1016E000889513BEFCCF0E94250A82FF48C02D9822 368 | :1016F0000E94250A84788438E1F40E94330688EC3D 369 | :1017000090E00E9435062F9882E090E00E94CB0C7A 370 | :101710008F3F29F06FEF82E090E00E94D30C84EFBE 371 | :1017200091E00E9435062F9A0E94250A8478843819 372 | :10173000D9F30E94250A84718431E1F40E943306B2 373 | :1017400088EC90E00E9435062E9882E090E00E949E 374 | :10175000CB0C882329F060E082E090E00E94D30C5B 375 | :1017600084EF91E00E9435062E9A0E94250A84712A 376 | :101770008431D9F388EC90E00E943506B4CF0E9402 377 | :10178000330682E090E00E946607882309F43EC099 378 | :101790003E9A3F9A81E090E00E946607182F1470ED 379 | :1017A00081FF0CC011112E98C0EFDFEF0E94250AB7 380 | :1017B00086FF2FC02E9A2F9A0E94330682E390E074 381 | :1017C0000E9435060E94250A8111F8CF82E4809399 382 | :1017D000B905FF24F3940EE511E0C0E0D0E0EE245B 383 | :1017E000E394209144018091BA05F22FF81B8F2FCA 384 | :1017F0009091BB05992309F47EC0F093BC0510922B 385 | :10180000BB05209771F30E940000EBCF3E983F98F4 386 | :10181000C1CF112329F0C430D10511F42E9A2F988D 387 | :1018200081E00E94E6030C2F0770A02EAE0183E03A 388 | :10183000559547958A95E1F7CA019695982F88277F 389 | :1018400097958795825A9E4F70E060E04E3F2FEF4C 390 | :10185000520769F130E020E0DB01A455BA4F7C016A 391 | :10186000E0E8EE0EF11CFD01E20FF31FE491F0E061 392 | :101870004F010A2C02C0880C991C0A94E2F74F3FD2 393 | :10188000540739F06C01C20ED31EF601B488B82893 394 | :10189000B48AAA2039F06701C20ED31EF60104896A 395 | :1018A0000929048B2F5F3F4F28353105E1F64F5F43 396 | :1018B0005F4F80589F4F685A7F4F603B710531F6EC 397 | :1018C00080E00E94E6038FE090E00E9435062196BA 398 | :1018D000C931D10509F06ACF112311F02F9A2D9843 399 | :1018E00080E991E00E9435060E9433068EEA95E079 400 | :1018F0000E94600A63CF3091B905F31740F48F5FFF 401 | :10190000831708F07ECFF3BE889513BE7ACFE0929E 402 | :10191000BB052093BA052091B5053091B6052F5F20 403 | :101920003F4F3093B6052093B505F80191119FEF15 404 | :101930008FEF91939193919391938150D0F710925F 405 | :10194000C0051092BF051092BE051092BD0586EF2E 406 | :1019500096E00E943D0780EE96E00E943D078AECEB 407 | :1019600096E00E943D0784EB96E00E943D078EE9D9 408 | :1019700096E00E943D0789E896E00E943D078FE7C8 409 | :1019800096E00E943D0789E696E00E943D0780E0D0 410 | :101990000E94E60336CFF999FECF92BD81BDF89A39 411 | :1019A000992780B50895262FF999FECF1FBA92BDC9 412 | :1019B00081BD20BD0FB6F894FA9AF99A0FBE019630 413 | :1019C0000895EE0FFF1F0590F491E02D0994F8940F 414 | :0219D000FFCF47 415 | :1019D200FFFFFFFF00E100000000000000C1808166 416 | :1019E200000000000000004404980565045A04AB9E 417 | :1019F2000469048704000000003706570563076284 418 | :0C1A020007600AFCFF0000A20A0D0A00A9 419 | :00000001FF 420 | -------------------------------------------------------------------------------- /src/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | stars_back.png 4 | stars_front.png 5 | fx.png 6 | icon.png 7 | arduboy-pixelartV4-white.png 8 | 9 | 10 | recovered.hex 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/src.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | TARGET = ArduManFX 3 | DESTDIR = .. 4 | 5 | DEFINES += QT_DEPRECATED_WARNINGS 6 | #LIBS += -langelscript 7 | 8 | QT += core gui serialport network 9 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 | CONFIG += c++11 11 | CONFIG += debug 12 | 13 | SOURCES += \ 14 | arduboyrecovery.cpp \ 15 | ArduManFX.cpp \ 16 | game.cpp \ 17 | main.cpp \ 18 | mainwindow.cpp \ 19 | progress.cpp \ 20 | tester.cpp 21 | #angelscript/scriptbuilder.cpp \ 22 | #angelscript/scriptstdstring.cpp 23 | 24 | HEADERS += \ 25 | arduboyrecovery.h \ 26 | ArduManFX.h \ 27 | game.h \ 28 | mainwindow.h \ 29 | progress.h \ 30 | tester.h 31 | #angelscript/scriptbuilder.h \ 32 | #angelscript/scriptstdstring.h 33 | 34 | FORMS += \ 35 | arduboyrecovery.ui \ 36 | game.ui \ 37 | mainwindow.ui \ 38 | progress.ui \ 39 | tester.ui 40 | 41 | RESOURCES += resources.qrc 42 | #RC_FILE = icon.rc 43 | -------------------------------------------------------------------------------- /src/stars_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/stars_back.png -------------------------------------------------------------------------------- /src/stars_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/src/stars_front.png -------------------------------------------------------------------------------- /src/tester.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "ArduManFX.h" 6 | #include "tester.h" 7 | 8 | Tester::Tester(QWidget *parent) : QDialog(parent) 9 | { 10 | setupUi(this); 11 | } 12 | 13 | Tester::~Tester() 14 | { 15 | } 16 | 17 | void Tester::on_btnPortScan_clicked() 18 | { 19 | QList ports = QSerialPortInfo::availablePorts(); 20 | tblPorts->clearContents(); 21 | tblPorts->setRowCount(ports.count()); 22 | for (int i = 0; i < ports.count(); ++i) 23 | { 24 | tblPorts->setItem(i, 0, new QTableWidgetItem(ports[i].portName())); 25 | tblPorts->setItem(i, 1, new QTableWidgetItem(QString("%1").arg(ports[i].vendorIdentifier(), 4, 16, QChar('0')))); 26 | tblPorts->setItem(i, 2, new QTableWidgetItem(QString("%1").arg(ports[i].productIdentifier(), 4, 16, QChar('0')))); 27 | tblPorts->setItem(i, 3, new QTableWidgetItem(ports[i].manufacturer())); 28 | tblPorts->setItem(i, 4, new QTableWidgetItem(ports[i].description())); 29 | } 30 | } 31 | 32 | void Tester::on_btnCreateFX_clicked() 33 | { 34 | QFile file; 35 | QList flashData; 36 | QByteArray flashCart; 37 | QString inLocation, outLocation; 38 | inLocation = QFileDialog::getOpenFileName(this, "Choose Flash Cart Index File", QCoreApplication::applicationDirPath(), "CSV Files (*.csv)"); 39 | if (inLocation.isNull()) 40 | return; 41 | outLocation = QFileDialog::getSaveFileName(this, "Choose Flash Cart Image File", QCoreApplication::applicationDirPath(), "Image Files (*.bin)"); 42 | if (outLocation.isNull()) 43 | return; 44 | this->setEnabled(false); 45 | flashData = ArduManFX::parseCSV(inLocation); 46 | if (flashData.count() == 0) 47 | { 48 | QMessageBox::critical(this, "CSV Parse Error", QString("Failed to load the csv file.\nReason: %1").arg(ArduManFX::getErrorString())); 49 | this->setEnabled(true); 50 | return; 51 | } 52 | flashCart = ArduManFX::createFlashCart(flashData, chkFix1309->isChecked(), chkFixMicro->isChecked(), progressBar); 53 | if (flashCart.isNull()) 54 | { 55 | QMessageBox::critical(this, "Cart Failed", QString("Failed to create a flash cart.\nReason: %1").arg(ArduManFX::getErrorString())); 56 | this->setEnabled(true); 57 | return; 58 | } 59 | file.setFileName(outLocation); 60 | if (file.open(QFile::WriteOnly)) 61 | { 62 | file.write(flashCart); 63 | file.close(); 64 | } 65 | else 66 | QMessageBox::critical(this, "Save Failed", QString("Failed to open output file for writing.\nReason: %1").arg(file.errorString())); 67 | this->setEnabled(true); 68 | } 69 | 70 | void Tester::on_btnReadFX_clicked() 71 | { 72 | QFile file; 73 | QByteArray flashCart; 74 | QString outLocation; 75 | QString vid, pid; 76 | QSerialPortInfo port; 77 | on_btnPortScan_clicked(); 78 | outLocation = QFileDialog::getSaveFileName(this, "Choose Flash Cart Image File", QCoreApplication::applicationDirPath(), "Image Files (*.bin)"); 79 | if (outLocation.isNull()) 80 | return; 81 | if (!arduboy.connect()) 82 | { 83 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 84 | return; 85 | } 86 | port = arduboy.getPortInfo(); 87 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 88 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 89 | for (int i = 0; i < tblPorts->rowCount(); ++i) 90 | { 91 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 92 | tblPorts->selectRow(i); 93 | } 94 | this->setEnabled(false); 95 | flashCart = arduboy.readFlashCart(progressBar); 96 | arduboy.disconnect(); 97 | if (flashCart.isNull()) 98 | { 99 | QMessageBox::critical(this, "Read Failed", QString("Failed to read flash cart.\nReason: %1").arg(ArduManFX::getErrorString())); 100 | this->setEnabled(true); 101 | return; 102 | } 103 | file.setFileName(outLocation); 104 | if (file.open(QFile::WriteOnly)) 105 | { 106 | file.write(flashCart); 107 | file.close(); 108 | } 109 | else 110 | QMessageBox::critical(this, "Save Failed", QString("Failed to save flash cart to file.\nReason: %1").arg(file.errorString())); 111 | this->setEnabled(true); 112 | } 113 | 114 | void Tester::on_btnWriteFX_clicked() 115 | { 116 | QFile file; 117 | QByteArray flashCart; 118 | QString inLocation; 119 | QString vid, pid; 120 | QSerialPortInfo port; 121 | on_btnPortScan_clicked(); 122 | inLocation = QFileDialog::getOpenFileName(this, "Choose Flash Cart File", QCoreApplication::applicationDirPath(), "Supported Flash Carts (*.csv *.bin)"); 123 | if (inLocation.isNull()) 124 | return; 125 | if (!arduboy.connect()) 126 | { 127 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 128 | return; 129 | } 130 | port = arduboy.getPortInfo(); 131 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 132 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 133 | for (int i = 0; i < tblPorts->rowCount(); ++i) 134 | { 135 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 136 | tblPorts->selectRow(i); 137 | } 138 | if (inLocation.endsWith(".csv", Qt::CaseInsensitive)) 139 | { 140 | QList flashData = ArduManFX::parseCSV(inLocation); 141 | if (flashData.count() == 0) 142 | { 143 | QMessageBox::critical(this, "CSV Parse Error", QString("Failed to load the csv file.\nReason: %1").arg(ArduManFX::getErrorString())); 144 | this->setEnabled(true); 145 | return; 146 | } 147 | flashCart = ArduManFX::createFlashCart(flashData, chkFix1309->isChecked(), chkFixMicro->isChecked(), progressBar); 148 | if (flashCart.isNull()) 149 | { 150 | QMessageBox::critical(this, "Cart Failed", QString("Failed to create a flash cart.\nReason: %1").arg(ArduManFX::getErrorString())); 151 | this->setEnabled(true); 152 | return; 153 | } 154 | } 155 | else 156 | { 157 | file.setFileName(inLocation); 158 | if (!file.open(QFile::ReadOnly)) 159 | { 160 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open file in read-only mode.\nReason: %1").arg(file.errorString())); 161 | return; 162 | } 163 | flashCart = file.readAll(); 164 | } 165 | file.close(); 166 | this->setEnabled(false); 167 | if (!arduboy.writeFlashCart(flashCart, chkVerify->isChecked(), progressBar)) 168 | { 169 | this->setEnabled(true); 170 | QMessageBox::critical(this, "Upload Error", QString("Failed to write flash cart to device.\nReason: %1").arg(ArduManFX::getErrorString())); 171 | } 172 | arduboy.disconnect(); 173 | this->setEnabled(true); 174 | } 175 | 176 | void Tester::on_btnReadEEPROM_clicked() 177 | { 178 | QFile file; 179 | QByteArray eeprom; 180 | QString outLocation; 181 | QString vid, pid; 182 | QSerialPortInfo port; 183 | on_btnPortScan_clicked(); 184 | outLocation = QFileDialog::getSaveFileName(this, "Choose EEPROM File", QCoreApplication::applicationDirPath(), "EEPROM Files (*.eeprom)"); 185 | if (outLocation.isNull()) 186 | return; 187 | if (!arduboy.connect()) 188 | { 189 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 190 | return; 191 | } 192 | port = arduboy.getPortInfo(); 193 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 194 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 195 | for (int i = 0; i < tblPorts->rowCount(); ++i) 196 | { 197 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 198 | tblPorts->selectRow(i); 199 | } 200 | this->setEnabled(false); 201 | eeprom = arduboy.readEEPROM(); 202 | arduboy.disconnect(); 203 | if (eeprom.isNull()) 204 | { 205 | QMessageBox::critical(this, "Read Failed", QString("Failed to read EEPROM.\nReason: %1").arg(ArduManFX::getErrorString())); 206 | this->setEnabled(true); 207 | return; 208 | } 209 | file.setFileName(outLocation); 210 | if (file.open(QFile::WriteOnly)) 211 | { 212 | file.write(eeprom); 213 | file.close(); 214 | } 215 | else 216 | QMessageBox::critical(this, "Save Failed", QString("Failed to save EEPROM to file.\nReason: %1").arg(file.errorString())); 217 | this->setEnabled(true); 218 | } 219 | 220 | void Tester::on_btnWriteEEPROM_clicked() 221 | { 222 | QFile file; 223 | QByteArray eeprom; 224 | QString inLocation; 225 | QString vid, pid; 226 | QSerialPortInfo port; 227 | on_btnPortScan_clicked(); 228 | inLocation = QFileDialog::getOpenFileName(this, "Choose EEPROM File", QCoreApplication::applicationDirPath(), "EEPROM Files (*.eeprom)"); 229 | if (inLocation.isNull()) 230 | return; 231 | if (!arduboy.connect()) 232 | { 233 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 234 | return; 235 | } 236 | port = arduboy.getPortInfo(); 237 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 238 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 239 | for (int i = 0; i < tblPorts->rowCount(); ++i) 240 | { 241 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 242 | tblPorts->selectRow(i); 243 | } 244 | file.setFileName(inLocation); 245 | if (!file.open(QFile::ReadOnly)) 246 | { 247 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open file in read-only mode.\nReason: %1").arg(file.errorString())); 248 | return; 249 | } 250 | eeprom = file.readAll(); 251 | file.close(); 252 | this->setEnabled(false); 253 | if (!arduboy.writeEEPROM(eeprom, chkVerify->isChecked())) 254 | QMessageBox::critical(this, "Upload Error", QString("Failed to write EEPROM to device.\nReason: %1").arg(ArduManFX::getErrorString())); 255 | arduboy.disconnect(); 256 | this->setEnabled(true); 257 | } 258 | 259 | void Tester::on_btnReadSketch_clicked() 260 | { 261 | QFile file; 262 | QByteArray sketch; 263 | QString outLocation; 264 | QString vid, pid; 265 | QSerialPortInfo port; 266 | on_btnPortScan_clicked(); 267 | outLocation = QFileDialog::getSaveFileName(this, "Choose Sketch File", QCoreApplication::applicationDirPath(), "Sketch Binaries (*.bin)"); 268 | if (outLocation.isNull()) 269 | return; 270 | this->setEnabled(false); 271 | if (!arduboy.connect()) 272 | { 273 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 274 | return; 275 | } 276 | port = arduboy.getPortInfo(); 277 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 278 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 279 | for (int i = 0; i < tblPorts->rowCount(); ++i) 280 | { 281 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 282 | tblPorts->selectRow(i); 283 | } 284 | sketch = arduboy.readSketch(); 285 | arduboy.disconnect(); 286 | if (sketch.isNull()) 287 | { 288 | QMessageBox::critical(this, "Read Failed", QString("Failed to read sketch.\nReason: %1").arg(ArduManFX::getErrorString())); 289 | this->setEnabled(true); 290 | return; 291 | } 292 | file.setFileName(outLocation); 293 | if (file.open(QFile::WriteOnly)) 294 | { 295 | file.write(sketch); 296 | file.close(); 297 | } 298 | else 299 | QMessageBox::critical(this, "Save Failed", QString("Failed to save sketch to file.\nReason: %1").arg(file.errorString())); 300 | QMessageBox::information(this, "Save Finished", QString("Sketch size %1\n").arg(sketch.length())); 301 | this->setEnabled(true); 302 | } 303 | 304 | void Tester::on_btnWriteSketch_clicked() 305 | { 306 | QFile file; 307 | QTextStream stream(&file); 308 | QString hex; 309 | QByteArray sketch; 310 | QString inLocation; 311 | QString vid, pid; 312 | QSerialPortInfo port; 313 | on_btnPortScan_clicked(); 314 | inLocation = QFileDialog::getOpenFileName(this, "Choose Sketch File", QCoreApplication::applicationDirPath(), "Sketch Files (*.hex *.bin)"); 315 | if (inLocation.isNull()) 316 | return; 317 | if (!arduboy.connect()) 318 | { 319 | QMessageBox::critical(this, "Connection Failed", ArduManFX::getErrorString()); 320 | return; 321 | } 322 | port = arduboy.getPortInfo(); 323 | vid = QString("%1").arg(port.vendorIdentifier(), 4, 16, QChar('0')); 324 | pid = QString("%1").arg(port.productIdentifier(), 4, 16, QChar('0')); 325 | for (int i = 0; i < tblPorts->rowCount(); ++i) 326 | { 327 | if (port.portName() == tblPorts->item(i, 0)->text() && vid == tblPorts->item(i, 1)->text() && pid == tblPorts->item(i, 2)->text()) 328 | tblPorts->selectRow(i); 329 | } 330 | file.setFileName(inLocation); 331 | if (inLocation.endsWith(".hex", Qt::CaseInsensitive)) 332 | { 333 | if (!file.open(QFile::ReadOnly|QFile::Text)) 334 | { 335 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open file in read-only text mode.\nReason: %1").arg(file.errorString())); 336 | return; 337 | } 338 | hex = stream.readAll(); 339 | file.close(); 340 | this->setEnabled(false); 341 | if (!arduboy.writeSketch(hex, chkVerify->isChecked(), chkFix1309->isChecked(), chkFixMicro->isChecked(), progressBar)) 342 | QMessageBox::critical(this, "Upload Error", QString("Failed to write sketch to device.\nReason: %1").arg(ArduManFX::getErrorString())); 343 | arduboy.disconnect(); 344 | this->setEnabled(true); 345 | } 346 | else 347 | { 348 | if (!file.open(QFile::ReadOnly)) 349 | { 350 | QMessageBox::critical(this, "File I/O Error", QString("Failed to open file in read-only mode.\nReason: %1").arg(file.errorString())); 351 | return; 352 | } 353 | sketch = file.readAll(); 354 | file.close(); 355 | this->setEnabled(false); 356 | if (!arduboy.writeSketch(sketch, chkVerify->isChecked(), chkFix1309->isChecked(), chkFixMicro->isChecked(), progressBar)) 357 | QMessageBox::critical(this, "Upload Error", QString("Failed to write sketch to device.\nReason: %1").arg(ArduManFX::getErrorString())); 358 | arduboy.disconnect(); 359 | this->setEnabled(true); 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/tester.h: -------------------------------------------------------------------------------- 1 | #ifndef TESTER_H 2 | #define TESTER_H 3 | 4 | #include "ArduManFX.h" 5 | #include "ui_tester.h" 6 | 7 | class Tester : public QDialog, protected Ui::Tester 8 | { 9 | Q_OBJECT 10 | public: 11 | Tester(QWidget *parent=nullptr); 12 | ~Tester(); 13 | protected slots: 14 | void on_btnPortScan_clicked(); 15 | void on_btnCreateFX_clicked(); 16 | void on_btnReadFX_clicked(); 17 | void on_btnWriteFX_clicked(); 18 | void on_btnReadEEPROM_clicked(); 19 | void on_btnWriteEEPROM_clicked(); 20 | void on_btnReadSketch_clicked(); 21 | void on_btnWriteSketch_clicked(); 22 | private: 23 | ArduManFX arduboy; 24 | }; 25 | 26 | #endif //TESTER_H 27 | -------------------------------------------------------------------------------- /src/tester.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tester 4 | 5 | 6 | 7 | 0 8 | 0 9 | 645 10 | 423 11 | 12 | 13 | 14 | ArduManFX Tester 15 | 16 | 17 | 18 | :/images/icon.png:/images/icon.png 19 | 20 | 21 | .QFrame 22 | { 23 | border: 1px solid palette(text); 24 | } 25 | 26 | .QFrame>QLabel 27 | { 28 | background-color: palette(text); 29 | color: palette(window); 30 | padding: 4px; 31 | } 32 | 33 | 34 | 35 | 36 | 37 | QFrame::StyledPanel 38 | 39 | 40 | QFrame::Raised 41 | 42 | 43 | 44 | 0 45 | 46 | 47 | 0 48 | 49 | 50 | 0 51 | 52 | 53 | 0 54 | 55 | 56 | 0 57 | 58 | 59 | 60 | 61 | 62 | 0 63 | 0 64 | 65 | 66 | 67 | 68 | 12 69 | 75 70 | true 71 | 72 | 73 | 74 | Run Tests 75 | 76 | 77 | Qt::AlignCenter 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 87 | 88 | 0 89 | 90 | 91 | 0 92 | 93 | 94 | 0 95 | 96 | 97 | 0 98 | 99 | 100 | 101 | 102 | 103 | 0 104 | 105 | 106 | 0 107 | 108 | 109 | 0 110 | 111 | 112 | 0 113 | 114 | 115 | 0 116 | 117 | 118 | 119 | 120 | 121 | 10 122 | 123 | 124 | 125 | Create FX 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 10 134 | 135 | 136 | 137 | Write Sketch 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 10 146 | 147 | 148 | 149 | Write EEPROM 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 10 158 | 159 | 160 | 161 | Read EEPROM 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 10 170 | 171 | 172 | 173 | Write FX 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 10 182 | 183 | 184 | 185 | Read Sketch 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 10 194 | 195 | 196 | 197 | Read FX 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 10 206 | 207 | 208 | 209 | Port Scan 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 3 221 | 222 | 223 | 3 224 | 225 | 226 | 3 227 | 228 | 229 | 3 230 | 231 | 232 | 233 | 234 | 235 | 0 236 | 0 237 | 238 | 239 | 240 | 241 | 10 242 | 243 | 244 | 245 | Verify 246 | 247 | 248 | true 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 0 257 | 0 258 | 259 | 260 | 261 | 262 | 10 263 | 264 | 265 | 266 | Fix 1309 Display 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 0 275 | 0 276 | 277 | 278 | 279 | 280 | 10 281 | 282 | 283 | 284 | Fix Micro 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 12 296 | 75 297 | true 298 | 299 | 300 | 301 | 0 302 | 303 | 304 | Qt::AlignCenter 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 10 313 | 314 | 315 | 316 | QTableWidget::item:selected 317 | { 318 | background-color: palette(highlight); 319 | color: palette(highlighted-text); 320 | } 321 | 322 | 323 | QFrame::NoFrame 324 | 325 | 326 | QAbstractItemView::NoEditTriggers 327 | 328 | 329 | QAbstractItemView::SingleSelection 330 | 331 | 332 | QAbstractItemView::SelectRows 333 | 334 | 335 | QAbstractItemView::ScrollPerPixel 336 | 337 | 338 | QAbstractItemView::ScrollPerPixel 339 | 340 | 341 | true 342 | 343 | 344 | false 345 | 346 | 347 | 348 | Port Name 349 | 350 | 351 | 352 | 353 | VID 354 | 355 | 356 | 357 | 358 | PID 359 | 360 | 361 | 362 | 363 | Manufacturer 364 | 365 | 366 | 367 | 368 | Description 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | Qt::Horizontal 383 | 384 | 385 | QDialogButtonBox::Close 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | buttonBox 397 | accepted() 398 | Tester 399 | accept() 400 | 401 | 402 | 248 403 | 254 404 | 405 | 406 | 157 407 | 274 408 | 409 | 410 | 411 | 412 | buttonBox 413 | rejected() 414 | Tester 415 | reject() 416 | 417 | 418 | 316 419 | 260 420 | 421 | 422 | 286 423 | 274 424 | 425 | 426 | 427 | 428 | 429 | -------------------------------------------------------------------------------- /title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxinator2009/ArduManFX/9714e36b11a21ff77194885061444c1928d11087/title.png --------------------------------------------------------------------------------