ComputerscareLaundrySoup.cpp (20714B)
1 #include <string> 2 #include <sstream> 3 #include <iomanip> 4 5 #include "Computerscare.hpp" 6 #include "dtpulse.hpp" 7 8 9 struct ComputerscareLaundrySoup; 10 struct LaundryTextField; 11 struct LaundryTF2; 12 struct LaundrySmallDisplay; 13 struct ComputerscareLaundrySoupWidget; 14 15 const int numFields = 6; 16 17 std::string randomFormula() { 18 std::string mainlookup = "111111111111111111111111112222333333344444444444444445556667778888888888888999"; 19 std::string string = ""; 20 std::string randchar = ""; 21 std::vector<std::string> atLookup = {"4", "8", "12", "16", "24", "32", "36", "48", "60", "64", "120", "128"}; 22 int length = 0; 23 24 length = floor(random::uniform() * 12) + 1; 25 string = ""; 26 for (int j = 0; j < length; j++) { 27 randchar = mainlookup[floor(random::uniform() * mainlookup.size())]; 28 string = string + randchar; 29 if (random::uniform() < 0.2) { 30 string += "?"; 31 } 32 } 33 if (random::uniform() < 0.5) { 34 if (random::uniform() < 0.8) { 35 string = "[" + string.insert(floor(random::uniform() * (string.size() - 1) + 1), ",") + "]"; 36 } 37 string += "@" + atLookup[floor(random::uniform() * atLookup.size())]; 38 } 39 return string; 40 } 41 42 struct ComputerscareLaundrySoup : Module { 43 enum ParamIds { 44 MANUAL_CLOCK_PARAM, 45 MANUAL_RESET_PARAM, 46 INDIVIDUAL_RESET_PARAM, 47 NUM_PARAMS = INDIVIDUAL_RESET_PARAM + numFields 48 }; 49 enum InputIds { 50 GLOBAL_CLOCK_INPUT, 51 GLOBAL_RESET_INPUT, 52 CLOCK_INPUT, 53 RESET_INPUT = CLOCK_INPUT + numFields, 54 NUM_INPUTS = RESET_INPUT + numFields 55 }; 56 enum OutputIds { 57 TRG_OUTPUT, 58 FIRST_STEP_OUTPUT = TRG_OUTPUT + numFields, 59 NUM_OUTPUTS = FIRST_STEP_OUTPUT + numFields 60 }; 61 enum LightIds { 62 SWITCH_LIGHTS, 63 NUM_LIGHTS 64 }; 65 66 rack::dsp::SchmittTrigger globalClockTrigger; 67 rack::dsp::SchmittTrigger globalResetTriggerInput; 68 69 rack::dsp::SchmittTrigger globalManualClockTrigger; 70 rack::dsp::SchmittTrigger globalManualResetTrigger; 71 72 rack::dsp::SchmittTrigger clockTriggers[numFields]; 73 rack::dsp::SchmittTrigger resetTriggers[numFields]; 74 75 LaundryTF2* textFields[numFields]; 76 LaundrySmallDisplay* smallLetterDisplays[numFields]; 77 78 std::string currentFormula[numFields]; 79 std::string currentTextFieldValue[numFields]; 80 std::string upcomingFormula[numFields]; 81 82 rack::dsp::SchmittTrigger manualResetTriggers[numFields]; 83 84 LaundryPoly laundryPoly[numFields]; 85 86 int checkCounter = 0; 87 int checkCounterLimit = 10000; 88 89 int channelCountEnum[numFields]; 90 int channelCount[numFields]; 91 92 bool activePolyStep[numFields][16] = {{false}}; 93 94 95 bool shouldChange[numFields] = {false}; 96 bool changeImminent[numFields] = {false}; 97 bool manualSet[numFields]; 98 bool inError[numFields]; 99 100 bool jsonLoaded = false; 101 bool laundryInitialized = false; 102 int sampleCounter = 0; 103 104 105 106 ComputerscareLaundrySoup() { 107 config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 108 for (int i = 0; i < numFields; i++) { 109 manualSet[i] = false; 110 inError[i] = false; 111 112 setNextAbsoluteSequence(i); 113 checkIfShouldChange(i); 114 resetOneOfThem(i); 115 116 std::string rowi = std::to_string(i + 1); 117 118 configButton(INDIVIDUAL_RESET_PARAM + i, "Reset Row " + rowi ); 119 120 configInput(CLOCK_INPUT + i, "Row " + rowi + " Clock"); 121 configInput(RESET_INPUT + i, "Row " + rowi + " Reset"); 122 123 configOutput(TRG_OUTPUT + i, "Row " + rowi + " Trigger"); 124 configOutput(FIRST_STEP_OUTPUT + i, "Row " + rowi + " End of Cycle"); 125 126 LaundryPoly lp = LaundryPoly(currentFormula[i]); 127 laundryPoly[i] = lp; 128 channelCountEnum[i] = -1; 129 channelCount[i] = 1; 130 } 131 132 configButton(MANUAL_CLOCK_PARAM, "Manual Clock Advance"); 133 configButton(MANUAL_RESET_PARAM, "Manual Reset"); 134 135 configInput(GLOBAL_CLOCK_INPUT, "Global Clock"); 136 configInput(GLOBAL_RESET_INPUT, "Global Reset"); 137 } 138 json_t *dataToJson() override { 139 json_t *rootJ = json_object(); 140 141 json_t *sequencesJ = json_array(); 142 json_t *channelCountJ = json_array(); 143 for (int i = 0; i < numFields; i++) { 144 json_t *sequenceJ = json_string(currentTextFieldValue[i].c_str()); 145 json_array_append_new(sequencesJ, sequenceJ); 146 json_t *channelJ = json_integer(channelCountEnum[i]); 147 json_array_append_new(channelCountJ, channelJ); 148 } 149 json_object_set_new(rootJ, "sequences", sequencesJ); 150 json_object_set_new(rootJ, "channelCount", channelCountJ); 151 152 return rootJ; 153 } 154 155 void dataFromJson(json_t *rootJ) override { 156 std::string val; 157 int count; 158 json_t *sequencesJ = json_object_get(rootJ, "sequences"); 159 if (sequencesJ) { 160 for (int i = 0; i < numFields; i++) { 161 162 json_t *sequenceJ = json_array_get(sequencesJ, i); 163 if (sequenceJ) 164 val = json_string_value(sequenceJ); 165 // currentFormula[i] = val; 166 //currentTextFieldValue[i] = val; 167 currentTextFieldValue[i] = val; 168 // upcomingFormula[i]=val; 169 manualSet[i] = true; 170 } 171 } 172 else { 173 json_t *textJLegacy = json_object_get(rootJ, "data"); 174 if (textJLegacy) { 175 json_t *seqJLegacy = json_object_get(textJLegacy, "sequences"); 176 177 if (seqJLegacy) { 178 for (int i = 0; i < numFields; i++) { 179 json_t *sequenceJ = json_array_get(seqJLegacy, i); 180 if (sequenceJ) 181 val = json_string_value(sequenceJ); 182 // currentFormula[i] = val; 183 //lastValue[i] = val; 184 currentTextFieldValue[i] = val; 185 //upcomingFormula[i]=val; 186 manualSet[i] = true; 187 188 } 189 } 190 } 191 } 192 json_t *channelCountEnumJ = json_object_get(rootJ, "channelCount"); 193 if (channelCountEnumJ) { 194 for (int i = 0; i < numFields; i++) { 195 json_t *countJ = json_array_get(channelCountEnumJ, i); 196 if (countJ) { 197 count = json_integer_value(countJ); 198 channelCountEnum[i] = count; 199 } 200 } 201 } 202 jsonLoaded = true; 203 204 } 205 void process(const ProcessArgs &args) override; 206 207 void onAdd() override { 208 } 209 210 void checkTextField(int channel) { 211 std::string textFieldValue = currentTextFieldValue[channel]; 212 213 if (textFieldValue != currentFormula[channel] && textFieldValue != upcomingFormula[channel]) { 214 215 LaundryPoly lp = LaundryPoly(textFieldValue); 216 if (!lp.inError && matchParens(textFieldValue)) { 217 upcomingFormula[channel] = textFieldValue; 218 setNextAbsoluteSequence(channel); 219 inError[channel] = false; 220 } 221 else { 222 DEBUG("Channel %i in error", channel); 223 inError[channel] = true; 224 } 225 } 226 227 } 228 229 void onRandomize() override { 230 randomizeAllFields(); 231 } 232 void randomizeAllFields() { 233 for (int i = 0; i < numFields; i++) { 234 randomizeAField(i); 235 } 236 } 237 238 void randomizeAField(int i) { 239 currentTextFieldValue[i] = randomFormula(); 240 manualSet[i] = true; 241 setNextAbsoluteSequence(i); 242 243 244 } 245 246 void setNextAbsoluteSequence(int index) { 247 shouldChange[index] = true; 248 } 249 void checkChannelCount(int index) { 250 if (channelCountEnum[index] == -1) { 251 if (currentFormula[index].find("#") != std::string::npos) { 252 channelCount[index] = 16; 253 } else { 254 size_t n = std::count(currentFormula[index].begin(), currentFormula[index].end(), ';'); 255 channelCount[index] = std::min((int)n + 1, 16); 256 } 257 } else { 258 channelCount[index] = channelCountEnum[index]; 259 } 260 } 261 void setAbsoluteSequenceFromQueue(int index) { 262 laundryPoly[index] = LaundryPoly(upcomingFormula[index]); 263 currentFormula[index] = upcomingFormula[index]; 264 checkChannelCount(index); 265 if (laundryPoly[index].inError) { 266 DEBUG("ERROR ch:%i", index); 267 } 268 } 269 void checkIfShouldChange(int index) { 270 if (shouldChange[index]) { 271 setAbsoluteSequenceFromQueue(index); 272 shouldChange[index] = false; 273 } 274 } 275 276 /* 277 lets say the sequence "332" is entered in the 0th (first) 278 numSteps[0] would then be 8 (3 + 3 + 2) 279 absoluteSequences[0] would be the vector (1,0,0,1,0,0,1,0) 280 absoluteStep[0] would run from 0 to 7 281 282 332-4 (332 offset by 4) 283 284 */ 285 void incrementInternalStep(int i) { 286 for (int ch = 0; ch < 16; ch++) { 287 laundryPoly[i].lss[ch].incrementAndCheck(); 288 if (laundryPoly[i].lss[laundryPoly[i].maxIndex].readHead == 0) { 289 this->setChangeImminent(i, false); 290 } 291 } 292 } 293 std::string getDisplayString(int index) { 294 LaundryPoly loopy = this->laundryPoly[index]; 295 296 std::string lhs = std::to_string(loopy.lss[loopy.maxIndex].readHead + 1); 297 std::string rhs = std::to_string(loopy.lss[loopy.maxIndex].numSteps); 298 299 padTo(lhs, 3, ' '); 300 padTo(rhs, 3, ' '); 301 302 std::string val = lhs + "\n" + rhs; 303 304 return val; 305 } 306 void setChangeImminent(int i, bool value) { 307 changeImminent[i] = value; 308 } 309 void resetOneOfThem(int i) { 310 for (int ch = 0; ch < 16; ch++) { 311 laundryPoly[i].lss[ch].readHead = -1; 312 } 313 } 314 }; 315 316 317 void ComputerscareLaundrySoup::process(const ProcessArgs &args) { 318 319 320 bool globalGateIn = globalClockTrigger.isHigh(); 321 bool atFirstStep = false; 322 bool atFirstStepPoly[16]; 323 bool atLastStepAfterIncrement = false; 324 bool clocked = globalClockTrigger.process(inputs[GLOBAL_CLOCK_INPUT].getVoltage()); 325 bool currentTriggerIsHigh = false; 326 bool currentTriggerClocked = false; 327 328 bool globalManualResetClicked = globalManualResetTrigger.process(params[MANUAL_RESET_PARAM].getValue()); 329 bool globalManualClockClicked = globalManualClockTrigger.process(params[MANUAL_CLOCK_PARAM].getValue()); 330 331 bool globalResetTriggered = globalResetTriggerInput.process(inputs[GLOBAL_RESET_INPUT].getVoltage() / 2.f); 332 bool currentResetActive = false; 333 bool currentResetTriggered = false; 334 bool currentManualResetClicked; 335 336 int numOutputChannels; 337 sampleCounter++; 338 if (sampleCounter > 10000) { 339 sampleCounter = 0; 340 } 341 342 343 if (checkCounter > checkCounterLimit) { 344 if (!jsonLoaded) { 345 for (int i = 0; i < numFields; i++) { 346 currentTextFieldValue[i] = i < numFields - 1 ? std::to_string(i + 1) : randomFormula(); 347 manualSet[i] = true; 348 } 349 for (int i = 0; i < numFields; i++) { 350 checkTextField(i); 351 checkChannelCount(i); 352 checkIfShouldChange(i); 353 } 354 jsonLoaded = true; 355 } 356 else { 357 for (int i = 0; i < numFields; i++) { 358 checkTextField(i); 359 checkChannelCount(i); 360 } 361 } 362 checkCounter = 0; 363 } 364 checkCounter++; 365 366 for (int i = 0; i < numFields; i++) { 367 numOutputChannels = channelCount[i]; 368 currentResetActive = inputs[RESET_INPUT + i].isConnected(); 369 currentResetTriggered = resetTriggers[i].process(inputs[RESET_INPUT + i].getVoltage() / 2.f); 370 currentTriggerIsHigh = clockTriggers[i].isHigh(); 371 currentTriggerClocked = clockTriggers[i].process(inputs[CLOCK_INPUT + i].getVoltage()); 372 373 currentManualResetClicked = manualResetTriggers[i].process(params[INDIVIDUAL_RESET_PARAM + i].getValue()); 374 375 if (this->laundryPoly[i].maxSteps > 0) { 376 if (inputs[CLOCK_INPUT + i].isConnected()) { 377 if (currentTriggerClocked || globalManualClockClicked) { 378 incrementInternalStep(i); 379 for (int ch = 0; ch < numOutputChannels; ch++) { 380 activePolyStep[i][ch] = (this->laundryPoly[i].lss[ch].peekWorkingStep() == 1); 381 } 382 atLastStepAfterIncrement = this->laundryPoly[i].maxChannelAtLastStep(); 383 if (atLastStepAfterIncrement) checkIfShouldChange(i); 384 } 385 } 386 else { 387 if ((inputs[GLOBAL_CLOCK_INPUT].isConnected() && clocked) || globalManualClockClicked) { 388 incrementInternalStep(i); 389 for (int ch = 0; ch < numOutputChannels; ch++) { 390 activePolyStep[i][ch] = (this->laundryPoly[i].lss[ch].peekWorkingStep() == 1); 391 } 392 atLastStepAfterIncrement = this->laundryPoly[i].maxChannelAtLastStep(); 393 if (atLastStepAfterIncrement) checkIfShouldChange(i); 394 } 395 } 396 397 for (int ch = 0; ch < numOutputChannels; ch++) { 398 399 atFirstStepPoly[ch] = (this->laundryPoly[i].lss[ch].readHead == 0); 400 } 401 402 atFirstStep = (this->laundryPoly[i].lss[this->laundryPoly[i].maxIndex].readHead == 0); 403 if (globalManualResetClicked || currentManualResetClicked) { 404 setChangeImminent(i, true); 405 checkIfShouldChange(i); 406 resetOneOfThem(i); 407 } 408 if ( (currentResetActive && currentResetTriggered) || (!currentResetActive && globalResetTriggered)) { 409 410 setChangeImminent(i, false); 411 checkIfShouldChange(i); 412 resetOneOfThem(i); 413 414 } 415 else { 416 if (atFirstStep && !currentResetActive && !inputs[GLOBAL_RESET_INPUT].isConnected()) { 417 //checkIfShouldChange(i); 418 //not sure why this was commented out but i think its important :) 419 } 420 } 421 } 422 //this always assumes 16 channel poly output. It is a waste if the user doesnt want poly 423 outputs[TRG_OUTPUT + i].setChannels(numOutputChannels); 424 outputs[FIRST_STEP_OUTPUT + i].setChannels(numOutputChannels); 425 426 if (inputs[CLOCK_INPUT + i].isConnected()) { 427 for (int ch = 0; ch < numOutputChannels; ch++) { 428 outputs[TRG_OUTPUT + i].setVoltage((currentTriggerIsHigh && activePolyStep[i][ch]) ? 10.0f : 0.0f, ch); 429 outputs[FIRST_STEP_OUTPUT + i].setVoltage((currentTriggerIsHigh && atFirstStepPoly[ch]) ? 10.f : 0.0f, ch); 430 } 431 } 432 else { 433 for (int ch = 0; ch < numOutputChannels; ch++) { 434 outputs[TRG_OUTPUT + i].setVoltage((globalGateIn && activePolyStep[i][ch]) ? 10.0f : 0.0f, ch); 435 outputs[FIRST_STEP_OUTPUT + i].setVoltage((globalGateIn && atFirstStepPoly[ch]) ? 10.f : 0.0f, ch); 436 } 437 } 438 } 439 } 440 441 struct LaundryTF2 : ComputerscareTextField 442 { 443 ComputerscareLaundrySoup *module; 444 //int fontSize = 16; 445 int rowIndex = 0; 446 447 LaundryTF2(int i) 448 { 449 rowIndex = i; 450 ComputerscareTextField(); 451 }; 452 453 454 void draw(const DrawArgs &args) override 455 { 456 if (module) 457 { 458 if (module->manualSet[rowIndex]) { 459 460 text = module->currentTextFieldValue[rowIndex]; 461 module->manualSet[rowIndex] = false; 462 } 463 std::string value = text.c_str(); 464 module->currentTextFieldValue[rowIndex] = value; 465 inError = module->inError[rowIndex]; 466 /* if (value != module->currentFormula[rowIndex] ) 467 { 468 module->currentTextFieldValue[rowIndex] = value; 469 inError = true; 470 } 471 else { 472 inError = false; 473 }*/ 474 } 475 else { 476 text = randomFormula(); 477 } 478 479 ComputerscareTextField::draw(args); 480 } 481 482 483 //void draw(const DrawArgs &args) override; 484 //int getTextPosition(math::Vec mousePos) override; 485 }; 486 487 struct LaundrySmallDisplay : SmallLetterDisplay 488 { 489 ComputerscareLaundrySoup *module; 490 int type; 491 int index; 492 LaundrySmallDisplay(int i) 493 { 494 index = i; 495 SmallLetterDisplay(); 496 }; 497 void draw(const DrawArgs &args) 498 { 499 //this->setNumDivisionsString(); 500 if (module) 501 { 502 value = module->getDisplayString(index); 503 blink = module->shouldChange[index]; 504 doubleblink = module->changeImminent[index]; 505 SmallLetterDisplay::draw(args); 506 } 507 } 508 509 }; 510 511 struct LaundryChannelItem : MenuItem { 512 ComputerscareLaundrySoup *module; 513 int channels; 514 int row; 515 void onAction(const event::Action &e) override { 516 if (row > -1) { 517 module->channelCountEnum[row] = channels; 518 } else { 519 for (int i = 0; i < numFields; i++) { 520 module->channelCountEnum[i] = channels; 521 } 522 } 523 } 524 }; 525 526 527 struct LaundryChannelsItem : MenuItem { 528 ComputerscareLaundrySoup *module; 529 int row; 530 Menu *createChildMenu() override { 531 Menu *menu = new Menu; 532 for (int channels = -1; channels <= 16; channels++) { 533 LaundryChannelItem *item = new LaundryChannelItem; 534 item->row = row; 535 if (channels < 0) 536 item->text = "Automatic"; 537 else 538 item->text = string::f("%d", channels); 539 if (row > -1) { 540 item->rightText = CHECKMARK(module->channelCountEnum[row] == channels); 541 } 542 item->module = module; 543 item->channels = channels; 544 menu->addChild(item); 545 } 546 return menu; 547 } 548 }; 549 550 551 struct ComputerscareLaundrySoupWidget : ModuleWidget { 552 553 double verticalSpacing = 18.4; 554 int verticalStart = 22; 555 ComputerscareLaundrySoupWidget(ComputerscareLaundrySoup *module) { 556 setModule(module); 557 setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ComputerscareLaundrySoupPanel.svg"))); 558 559 //global clock input 560 addInput(createInput<InPort>(mm2px(Vec(2 , 0)), module, ComputerscareLaundrySoup::GLOBAL_CLOCK_INPUT)); 561 562 //global reset input 563 addInput(createInput<InPort>(mm2px(Vec(12 , 0)), module, ComputerscareLaundrySoup::GLOBAL_RESET_INPUT)); 564 565 //momentary clock and reset buttons 566 addParam(createParam<ComputerscareClockButton>(mm2px(Vec(2 , 8)), module, ComputerscareLaundrySoup::MANUAL_CLOCK_PARAM)); 567 addParam(createParam<ComputerscareResetButton>(mm2px(Vec(12 , 8)), module, ComputerscareLaundrySoup::MANUAL_RESET_PARAM)); 568 569 for (int i = 0; i < numFields; i++) { 570 //first-step output 571 addOutput(createOutput<OutPort>(mm2px(Vec(42 , verticalStart + verticalSpacing * i - 11)), module, ComputerscareLaundrySoup::FIRST_STEP_OUTPUT + i)); 572 573 //individual output 574 addOutput(createOutput<OutPort>(mm2px(Vec(54 , verticalStart + verticalSpacing * i - 11)), module, ComputerscareLaundrySoup::TRG_OUTPUT + i)); 575 576 //individual clock input 577 addInput(createInput<InPort>(mm2px(Vec(2, verticalStart + verticalSpacing * i - 10)), module, ComputerscareLaundrySoup::CLOCK_INPUT + i)); 578 579 //individual reset input 580 addInput(createInput<InPort>(mm2px(Vec(12, verticalStart + verticalSpacing * i - 10)), module, ComputerscareLaundrySoup::RESET_INPUT + i)); 581 582 textFieldTemp = new LaundryTF2(i); 583 textFieldTemp->box.pos = mm2px(Vec(1, verticalStart + verticalSpacing * i)); 584 textFieldTemp->module = module; 585 textFieldTemp->box.size = mm2px(Vec(64, 7)); 586 textFieldTemp->multiline = false; 587 textFieldTemp->color = nvgRGB(0xC0, 0xE7, 0xDE); 588 textFieldTemp->text = ""; 589 590 laundryTextFields[i] = textFieldTemp; 591 addChild(textFieldTemp); 592 593 594 // active / total steps display 595 smallLetterDisplay = new LaundrySmallDisplay(i); 596 smallLetterDisplay->box.pos = mm2px(Vec(22, verticalStart - 9.2 + verticalSpacing * i)); 597 smallLetterDisplay->box.size = Vec(60, 30); 598 smallLetterDisplay->value = std::to_string(3); 599 smallLetterDisplay->baseColor = COLOR_COMPUTERSCARE_LIGHT_GREEN; 600 smallLetterDisplay->module = module; 601 addChild(smallLetterDisplay); 602 laundrySmallDisplays[i] = smallLetterDisplay; 603 604 addParam(createParam<ComputerscareInvisibleButton>(mm2px(Vec(20, verticalStart - 9.2 + verticalSpacing * i)), module, ComputerscareLaundrySoup::INDIVIDUAL_RESET_PARAM + i)); 605 606 } 607 laundry = module; 608 } 609 610 void appendContextMenu(Menu *menu) override { 611 ComputerscareLaundrySoup *module = dynamic_cast<ComputerscareLaundrySoup*>(this->laundry); 612 613 menu->addChild(new MenuEntry); 614 615 616 617 for (int i = -1; i < numFields; i++) { 618 LaundryChannelsItem *channelsItem = new LaundryChannelsItem; 619 channelsItem->text = i == -1 ? "Set All Channels Polyphony" : string::f("Channel %d Polyphony", i + 1);; 620 channelsItem->rightText = RIGHT_ARROW; 621 channelsItem->module = module; 622 channelsItem->row = i; 623 menu->addChild(channelsItem); 624 625 if (i == -1) { 626 MenuLabel *spacerLabel = new MenuLabel(); 627 menu->addChild(spacerLabel); 628 } 629 } 630 } 631 /*This is a deprecated method, but since I used ModuleWidget::toJson to save the custom sequences, 632 old patches have "sequences" at the root of the JSON serialization. Module::dataFromJSON does not provide 633 the root object, just the "data" key, so this is the only way to get the sequences from patches prior to v1.2 634 635 */ 636 /* void fromJson(json_t *rootJ) override 637 { 638 639 std::string val; 640 ModuleWidget::fromJson(rootJ); 641 642 json_t *seqJLegacy = json_object_get(rootJ, "sequences"); 643 if (seqJLegacy) { 644 for (int i = 0; i < numFields; i++) { 645 json_t *sequenceJ = json_array_get(seqJLegacy, i); 646 if (sequenceJ) { 647 val = json_string_value(sequenceJ); 648 laundry->currentTextFieldValue[i] = val; 649 laundry->manualSet[i] = true; 650 } 651 652 } 653 laundry->jsonLoaded = true; 654 } 655 656 657 }*/ 658 ComputerscareLaundrySoup *laundry; 659 660 LaundryTF2 *textFieldTemp; 661 LaundryTF2 *laundryTextFields[numFields]; 662 LaundrySmallDisplay* smallLetterDisplay; 663 LaundrySmallDisplay* laundrySmallDisplays[numFields]; 664 665 }; 666 667 Model *modelComputerscareLaundrySoup = createModel<ComputerscareLaundrySoup, ComputerscareLaundrySoupWidget>("computerscare-laundry-soup");