Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include <Arduino.h>
- #include <ESP8266WiFi.h>
- #include <WiFiUdp.h>
- #include <stdlib.h>
- #include <DNSServer.h>
- #include <ESP8266WebServer.h>
- #include <WiFiManager.h>
- // The GPIO pin numbers for the three colors. Make sure these match your wiring/schematic.
- #define GPIO_RED 13
- #define GPIO_GREEN 12
- #define GPIO_BLUE 15
- // The target ID of this device on the network if ID_SWITCHED is false
- // Commands sent to target ID 0 (zero) will be executed by ALL targets
- #define TARGET_ID_FIXED 1
- // If a 4-position dip switch is present and tied to 4 GPIO pins, set ID_SWITCHED to true and put the pin numbers in the ID_BIT_? settings
- #define ID_SWITCHED true
- #define ID_BIT_0 16
- #define ID_BIT_1 14
- #define ID_BIT_2 4
- #define ID_BIT_3 5
- // Used to turn debug serial output on/off
- #define DEBUG false
- // UDP Command values
- #define CMD_OFF 0x00
- #define CMD_SETLEVELS 0x01
- #define CMD_AUTOPATTERN 0x02
- #define CMD_AUTODISABLE 0x03
- // Auto-cycler state conditions
- #define AUTO_DISABLED 0x00
- #define AUTO_ACTIVE 0x01
- // Maximum analog output level
- #define MAX_ANALOG 1023
- // ID Numbers for the UDP Packet filter
- #define PACKET_FILTER_1 4039196302
- #define PACKET_FILTER_2 3194769291
- // Set up this reference so we can put the huge array definition at the end
- extern const uint16_t gamma10[];
- // Variables for tracking blue LED blink state during initialization
- volatile bool blueInitState = false;
- volatile unsigned long blueFlashCount;
- // Struct for storing color value sets
- // Colors are obvious. restDuration is how long to rest on this color.
- struct colorTriplet {
- short red;
- short green;
- short blue;
- unsigned long restDuration;
- colorTriplet() {
- red = 0;
- green = 0;
- blue = 0;
- restDuration = 0;
- }
- };
- // This gets dynamically reset in setup() from the DIP switch or the #define at the top of the sketch so make sure this is set to 0 (zero)
- byte myTargetID = 0;
- // Variables for keeping the state of automatic color switching
- byte autoMode = AUTO_DISABLED;
- bool autoActive = false;
- // For storing automatic color values
- #define MAX_AUTO_COLORS 35
- unsigned int rampDuration;
- byte numAutoColors = 0;
- struct colorTriplet autoColors[MAX_AUTO_COLORS];
- byte autoColorTargetIndex = 0;
- short redStatic = 0;
- short greenStatic = 0;
- short blueStatic = 0;
- bool resting = false;
- // Debugging crap
- String rampValues[200] = {""};
- int rampValuesIndex = 0;
- // We're going to listen on port 6565
- WiFiUDP listener;
- // Variables to track message IDs so we don't process duplicate messages
- unsigned int lastMessageID = 0;
- unsigned int curMessageID = 0;
- // Reset function
- void(* resetFunc) (void) = 0;
- void inline blueBlinkISR(void) {
- blueInitState = !blueInitState;
- if ( blueInitState ) {
- analogWrite(GPIO_BLUE, 0);
- blueFlashCount++;
- if ( blueFlashCount >= 15 ) {
- delay(60000);
- resetFunc();
- }
- } else {
- analogWrite(GPIO_BLUE, MAX_ANALOG);
- }
- timer0_write(ESP.getCycleCount() + 40000000);
- }
- // Set up our initial states and WiFi
- void setup() {
- // Set the three color PWM pins to output
- pinMode(GPIO_RED, OUTPUT);
- pinMode(GPIO_GREEN, OUTPUT);
- pinMode(GPIO_BLUE, OUTPUT);
- // If ID_SWITCHED is true then set myTargetID from the dip switch, otherwise set it from TARGET_ID_FIXED
- if ( ID_SWITCHED ) {
- pinMode(ID_BIT_0, INPUT_PULLUP);
- pinMode(ID_BIT_1, INPUT_PULLUP);
- pinMode(ID_BIT_2, INPUT_PULLUP);
- pinMode(ID_BIT_3, INPUT_PULLUP);
- bitWrite(myTargetID, 0, ((digitalRead(ID_BIT_0) == HIGH) ? 1 : 0));
- bitWrite(myTargetID, 1, ((digitalRead(ID_BIT_1) == HIGH) ? 1 : 0));
- bitWrite(myTargetID, 2, ((digitalRead(ID_BIT_2) == HIGH) ? 1 : 0));
- bitWrite(myTargetID, 3, ((digitalRead(ID_BIT_3) == HIGH) ? 1 : 0));
- //pinMode(ID_BIT_1, INPUT);
- } else {
- myTargetID = TARGET_ID_FIXED;
- }
- // Start up the serial console output
- Serial.begin(115200);
- Serial.println("--- Serial Output Started ---");
- delay(10);
- WiFiManager wifiManager;
- //wifiManager.resetSettings();
- noInterrupts();
- timer0_isr_init();
- timer0_attachInterrupt(blueBlinkISR);
- timer0_write(ESP.getCycleCount() + 1000);
- interrupts();
- wifiManager.autoConnect("JaJLEDController");
- // Show our ID!
- Serial.println();
- Serial.print("My target ID: ");
- Serial.println(myTargetID);
- // Turn off blue LED and disable interrupt
- timer0_detachInterrupt();
- analogWrite(GPIO_BLUE, 0);
- Serial.println("");
- Serial.println("WiFi connected");
- Serial.println("IP address: ");
- Serial.println(WiFi.localIP());
- // Start listening for packets on port 6565 (yes, this should probably be defined at the top)
- listener.begin(6565);
- }
- bool processMessage() {
- struct colorTriplet colorBlank;
- byte command;
- uint32 messageTargetID = 0;
- // Read in the packet data to buff (up to 1024 bytes which is way more than we would ever need)
- char buff[1024];
- listener.read(buff, 1024);
- listener.flush();
- // Check for the Packet Filter ID values and skip packet if not matched
- unsigned int filter1, filter2;
- memcpy(&filter1, (char*)buff, 4);
- memcpy(&filter2, (char*)buff + 4, 4);
- if ( (filter1 != PACKET_FILTER_1) || (filter2 != PACKET_FILTER_2) ) {
- if ( DEBUG ) {
- Serial.println("Filter Fail: " + String(filter1) + ", " + String(filter2));
- }
- return false;
- }
- // Parse out the messageID field
- memcpy(&curMessageID, (char*)buff + 8, 4);
- // If we've seen the message before, skip it, otherwise update lastMessageID
- if ( curMessageID == lastMessageID ) return false;
- lastMessageID = curMessageID;
- // Parse out the command and target ID fields
- memcpy(&command, (char*)buff + 12, 1);
- memcpy(&messageTargetID, (char*)buff + 13, 4);
- // If this packet isn't destined for ID=0 (all targets) or ID=TARGET_ID (our ID) then stop processing
- if ( (myTargetID != 0 ) && (messageTargetID != 0) && (messageTargetID != myTargetID) ) {
- if ( DEBUG ) {
- Serial.println("Got packet for different ID! Mine=" + String(myTargetID) + " Target=" + String(messageTargetID));
- }
- return false;
- }
- if ( DEBUG ) Serial.println("Got UDP packet!");
- // Clear out the global autoColors[] array
- memset(autoColors, 0, sizeof(autoColors));
- // If we get a CMD_SETLEVELS then disable autoMode, set the new static values and ramp to that color
- if ( command == CMD_SETLEVELS ) {
- autoMode = AUTO_DISABLED;
- numAutoColors = 1;
- autoColorTargetIndex = 0;
- memcpy(&rampDuration, (char*)buff + 17, 4);
- memcpy(&autoColors[0].red, (char*)buff + 21, 1);
- memcpy(&autoColors[0].green, (char*)buff + 22, 1);
- memcpy(&autoColors[0].blue, (char*)buff + 23, 1);
- autoColors[0].red = map(autoColors[0].red, 0, 255, 0, 1023);
- autoColors[0].green = map(autoColors[0].green, 0, 255, 0, 1023);
- autoColors[0].blue = map(autoColors[0].blue, 0, 255, 0, 1023);
- redStatic = autoColors[0].red;
- greenStatic = autoColors[0].green;
- blueStatic = autoColors[0].blue;
- if ( DEBUG ) {
- String debugOutput = "Setting Levels: ";
- debugOutput += String(redStatic) + ", ";
- debugOutput += String(greenStatic) + ", ";
- debugOutput += String(blueStatic) + ", ";
- debugOutput += String(rampDuration);
- Serial.println(debugOutput);
- }
- return true;
- }
- // If we get a CMD_AUTOPATTERN then build up the autoColors array and set autoMode to AUTO_ACTIVE
- if ( command == CMD_AUTOPATTERN ) {
- String debugOutput = "";
- autoMode = AUTO_ACTIVE;
- autoColorTargetIndex = 0;
- memcpy(&rampDuration, (char*)buff + 17, 4);
- memcpy(&numAutoColors, (char*)buff + 21, 1);
- if ( numAutoColors > MAX_AUTO_COLORS ) numAutoColors = MAX_AUTO_COLORS;
- if ( DEBUG ) debugOutput += "New AutoPattern " + String(numAutoColors) + ": ";
- for ( int i=0; i<numAutoColors; i++ ) {
- memcpy(&autoColors[i].red, (char*)buff + 22 + (i*7), 1);
- memcpy(&autoColors[i].green, (char*)buff + 23 + (i*7), 1);
- memcpy(&autoColors[i].blue, (char*)buff + 24 + (i*7), 1);
- memcpy(&autoColors[i].restDuration, (char*)buff + 25 + (i*7), 4);
- autoColors[i].red = map(autoColors[i].red, 0, 255, 0, 1023);
- autoColors[i].green = map(autoColors[i].green, 0, 255, 0, 1023);
- autoColors[i].blue = map(autoColors[i].blue, 0, 255, 0, 1023);
- if ( DEBUG ) {
- debugOutput += String(i) + "=[";
- debugOutput += String(autoColors[i].red) + ", ";
- debugOutput += String(autoColors[i].green) + ", ";
- debugOutput += String(autoColors[i].blue) + ", ";
- debugOutput += String(autoColors[i].restDuration) + "], ";
- }
- }
- if ( DEBUG ) {
- debugOutput += "Ramp=" + String(rampDuration);
- Serial.println(debugOutput);
- }
- return true;
- }
- // If we get a CMD_AUTODISABLE then disable autoMode and ramp back to the last static color levels
- if ( command == CMD_AUTODISABLE ) {
- autoMode = AUTO_DISABLED;
- numAutoColors = 1;
- autoColorTargetIndex = 0;
- autoColors[0].red = redStatic;
- autoColors[0].green = greenStatic;
- autoColors[0].blue = blueStatic;
- rampDuration = 1000;
- if ( DEBUG ) {
- String debugOutput = "Resetting Levels to static: ";
- debugOutput += String(redStatic) + ", ";
- debugOutput += String(greenStatic) + ", ";
- debugOutput += String(blueStatic) + ", ";
- debugOutput += "Ramp=" + String(rampDuration);
- Serial.println(debugOutput);
- }
- return true;
- }
- // If we get a CMD_OFF then set the levels to zero and ensure autoMode is disabled
- // The rampDuration is set to the same value as the step value during ramping (yes, this should probably be a constant or a #define)
- if ( command == CMD_OFF ) {
- autoMode = AUTO_DISABLED;
- numAutoColors = 1;
- autoColorTargetIndex = 0;
- rampDuration = 10;
- autoColors[0].restDuration = 10000;
- redStatic = 0;
- greenStatic = 0;
- blueStatic = 0;
- if ( DEBUG ) Serial.println("Shutting off LEDs");
- return true;
- }
- return false;
- }
- void loop() {
- static short redTarget = 0, greenTarget = 0, blueTarget = 0;
- static short redInitial = 0, greenInitial = 0, blueInitial = 0;
- static short redPrevious = 0, greenPrevious = 0, bluePrevious = 0;
- static short redLevel = 0, greenLevel = 0, blueLevel = 0;
- static bool newColor = false;
- static unsigned long nextDIPSample = 0;
- unsigned long nowMillis = millis();
- static unsigned long nextRampMillis;
- static unsigned long rampStartMillis;
- static unsigned long restingEndMillis;
- static unsigned long reportMillis = 0;
- static unsigned long repCount = 0;
- if ( nowMillis >= reportMillis ) {
- repCount++;
- //Serial.print("Still alive... ("); Serial.print(repCount); Serial.println(")");
- reportMillis = nowMillis + 1000;
- }
- // Check to see if we have a new packet waiting and parse it out if we do
- int packetSize = listener.parsePacket();
- if ( packetSize ) {
- if ( processMessage() ) {
- // Always reset newColor and resting states when we get a new UDP color command
- newColor = true;
- resting = false;
- }
- }
- if ( newColor ) {
- // Reset rampColors Debugging
- for ( int i=0; i<200; i++ ) { rampValues[i] = ""; };
- rampValuesIndex = 0;
- // If the incoming auto-color pattern (from a CMD_AUTOPATTERN) has a single all zero color,
- // and autoMode is AUTO_ACTIVE then generate colors randomly instead of from the pattern
- if ( (autoMode == AUTO_ACTIVE) && (numAutoColors == 1) && (autoColors[0].red == 0) && (autoColors[0].green == 0) && (autoColors[0].blue == 0) ) {
- //redTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
- //greenTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
- //blueTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
- redTarget = gamma10[random(0, MAX_ANALOG)];
- greenTarget = gamma10[random(0, MAX_ANALOG)];
- blueTarget = gamma10[random(0, MAX_ANALOG)];
- } else {
- //redTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].red]);
- //greenTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].green]);
- //blueTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].blue]);
- redTarget = gamma10[autoColors[autoColorTargetIndex].red];
- greenTarget = gamma10[autoColors[autoColorTargetIndex].green];
- blueTarget = gamma10[autoColors[autoColorTargetIndex].blue];
- }
- redInitial = redLevel;
- greenInitial = greenLevel;
- blueInitial = blueLevel;
- newColor = false;
- rampStartMillis = nowMillis;
- nextRampMillis = rampStartMillis;
- if ( DEBUG ) {
- String debugOutput = "Switching to new color: C=";
- debugOutput += String(redInitial) + "," + String(greenInitial) + "," + String(blueInitial) + " ";
- debugOutput += "T=" + String(redTarget) + "," + String(greenTarget) + "," + String(blueTarget) + " ";
- debugOutput += "Index=" + String(autoColorTargetIndex) + " Ramp=" + String(rampDuration);
- Serial.println(debugOutput);
- }
- }
- if ( (redLevel != redTarget) || (greenLevel != greenTarget) || (blueLevel != blueTarget) ) {
- // Calculate the current rampInterval (how many milliseconds between start of ramp and now)
- unsigned long rampInterval = nowMillis - rampStartMillis;
- // If nowMillis wrapped around the max INT32 we will just go straight to the last iteration of the ramp
- if ( rampInterval > rampDuration ) rampInterval = rampDuration;
- // If we aren't at the target values yet, map the current number of milliseconds
- // into the ramp (rampInterval) to the (color)Initial -> (color)Target values
- if ( redLevel != redTarget ) {
- redLevel = map(rampInterval, 0, rampDuration, redInitial, redTarget);
- }
- if ( greenLevel != greenTarget ) {
- greenLevel = map(rampInterval, 0, rampDuration, greenInitial, greenTarget);
- }
- if ( blueLevel != blueTarget ) {
- blueLevel = map(rampInterval, 0, rampDuration, blueInitial, blueTarget);
- }
- if ( (nowMillis >= nextRampMillis) || ((redLevel == redTarget) && (greenLevel == greenTarget) && (blueLevel == blueTarget)) ) {
- if ( redLevel != redPrevious ) {
- analogWrite(GPIO_RED, redLevel);
- redPrevious = redLevel;
- }
- if ( greenLevel != greenPrevious ) {
- analogWrite(GPIO_GREEN, greenLevel);
- greenPrevious = greenLevel;
- }
- if ( blueLevel != bluePrevious ) {
- analogWrite(GPIO_BLUE, blueLevel);
- bluePrevious = blueLevel;
- }
- //rampValues[rampValuesIndex] = (String)"(" + (nowMillis - nextRampMillis) + "," + rampValuesIndex + ")" + "(" + redLevel + "," + greenLevel + "," + blueLevel + "),";
- //rampValuesIndex++;
- nextRampMillis = nowMillis + 15;
- }
- // If all the colors are at their targets and we aren't resting, then start resting
- } else if ( !resting ) {
- resting = true;
- restingEndMillis = nowMillis + autoColors[autoColorTargetIndex].restDuration;
- if ( DEBUG ) {
- String debugOutput = "Now resting for: ";
- debugOutput += String(autoColors[autoColorTargetIndex].restDuration) + " on ";
- debugOutput += String(autoColorTargetIndex);
- Serial.println(debugOutput);
- //Serial.print("rampValues=[");
- //for ( int i=0; i<rampValuesIndex; i++ ) Serial.print(rampValues[i]);
- //Serial.println("]");
- }
- // If we are resting and the restingEndMillis passes then it is time to ramp to
- // the next color IF AND ONLY IF we are in a pattern (i.e. don't keep ramping if we're on a static color)
- } else if ( (nowMillis >= restingEndMillis) && (autoMode == AUTO_ACTIVE) ) {
- resting = false;
- newColor = true;
- autoColorTargetIndex = (autoColorTargetIndex < (numAutoColors - 1)) ? autoColorTargetIndex + 1 : 0;
- }
- // If the DIP switches change, go ahead and adjust myTargetID accordingly. Saves having to restart.
- // In order to save clock cycles this is only done every 10 seconds.
- if ( ID_SWITCHED && (nowMillis >= nextDIPSample) ) {
- byte newVal = 0;
- bitWrite(newVal, 0, ((digitalRead(ID_BIT_0) == HIGH) ? 1 : 0));
- bitWrite(newVal, 1, ((digitalRead(ID_BIT_1) == HIGH) ? 1 : 0));
- bitWrite(newVal, 2, ((digitalRead(ID_BIT_2) == HIGH) ? 1 : 0));
- bitWrite(newVal, 3, ((digitalRead(ID_BIT_3) == HIGH) ? 1 : 0));
- if ( myTargetID != newVal ) {
- myTargetID = newVal;
- if ( DEBUG ) {
- Serial.println("BIT_0: " + String(digitalRead(ID_BIT_0)));
- Serial.println("BIT_1: " + String(digitalRead(ID_BIT_1)));
- Serial.println("BIT_2: " + String(digitalRead(ID_BIT_2)));
- Serial.println("BIT_3: " + String(digitalRead(ID_BIT_3)));
- Serial.println("New Target ID Detected: " + String(myTargetID));
- }
- }
- nextDIPSample = nowMillis + 10000; // Sample the DIP switches every 10 seconds
- }
- yield();
- }
- //const uint16_t PROGMEM gamma10[] {
- const uint16_t gamma10[] {
- 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
- 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
- 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
- 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
- 0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,
- 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
- 1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,
- 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
- 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
- 4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,
- 5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,
- 6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,
- 8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,
- 10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,
- 12,12,12,13,13,13,13,13,13,14,14,14,14,14,15,
- 15,15,15,15,15,16,16,16,16,16,17,17,17,17,17,
- 18,18,18,18,18,19,19,19,19,20,20,20,20,20,21,
- 21,21,21,22,22,22,22,23,23,23,23,24,24,24,24,
- 25,25,25,25,26,26,26,26,27,27,27,27,28,28,28,
- 29,29,29,29,30,30,30,31,31,31,31,32,32,32,33,
- 33,33,34,34,34,35,35,35,35,36,36,36,37,37,37,
- 38,38,38,39,39,40,40,40,41,41,41,42,42,42,43,
- 43,43,44,44,45,45,45,46,46,46,47,47,48,48,48,
- 49,49,50,50,50,51,51,52,52,52,53,53,54,54,55,
- 55,55,56,56,57,57,58,58,58,59,59,60,60,61,61,
- 62,62,63,63,63,64,64,65,65,66,66,67,67,68,68,
- 69,69,70,70,71,71,72,72,73,73,74,74,75,75,76,
- 76,77,77,78,79,79,80,80,81,81,82,82,83,83,84,
- 85,85,86,86,87,87,88,89,89,90,90,91,92,92,93,
- 93,94,95,95,96,96,97,98,98,99,99,100,101,101,102,
- 103,103,104,105,105,106,106,107,108,108,109,110,110,111,112,
- 112,113,114,115,115,116,117,117,118,119,119,120,121,122,122,
- 123,124,124,125,126,127,127,128,129,130,130,131,132,132,133,
- 134,135,136,136,137,138,139,139,140,141,142,143,143,144,145,
- 146,146,147,148,149,150,151,151,152,153,154,155,155,156,157,
- 158,159,160,161,161,162,163,164,165,166,167,167,168,169,170,
- 171,172,173,174,175,175,176,177,178,179,180,181,182,183,184,
- 185,186,186,187,188,189,190,191,192,193,194,195,196,197,198,
- 199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,
- 214,215,216,217,218,219,220,221,222,223,224,225,226,228,229,
- 230,231,232,233,234,235,236,237,238,239,241,242,243,244,245,
- 246,247,248,249,251,252,253,254,255,256,257,259,260,261,262,
- 263,264,266,267,268,269,270,272,273,274,275,276,278,279,280,
- 281,282,284,285,286,287,289,290,291,292,294,295,296,297,299,
- 300,301,302,304,305,306,308,309,310,311,313,314,315,317,318,
- 319,321,322,323,325,326,327,329,330,331,333,334,336,337,338,
- 340,341,342,344,345,347,348,349,351,352,354,355,356,358,359,
- 361,362,364,365,366,368,369,371,372,374,375,377,378,380,381,
- 383,384,386,387,389,390,392,393,395,396,398,399,401,402,404,
- 405,407,408,410,412,413,415,416,418,419,421,423,424,426,427,
- 429,431,432,434,435,437,439,440,442,444,445,447,448,450,452,
- 453,455,457,458,460,462,463,465,467,468,470,472,474,475,477,
- 479,480,482,484,486,487,489,491,493,494,496,498,500,501,503,
- 505,507,509,510,512,514,516,518,519,521,523,525,527,528,530,
- 532,534,536,538,539,541,543,545,547,549,551,553,554,556,558,
- 560,562,564,566,568,570,572,574,575,577,579,581,583,585,587,
- 589,591,593,595,597,599,601,603,605,607,609,611,613,615,617,
- 619,621,623,625,627,629,631,633,635,637,640,642,644,646,648,
- 650,652,654,656,658,660,663,665,667,669,671,673,675,678,680,
- 682,684,686,688,690,693,695,697,699,701,704,706,708,710,712,
- 715,717,719,721,724,726,728,730,733,735,737,739,742,744,746,
- 749,751,753,755,758,760,762,765,767,769,772,774,776,779,781,
- 783,786,788,790,793,795,798,800,802,805,807,810,812,814,817,
- 819,822,824,827,829,831,834,836,839,841,844,846,849,851,854,
- 856,859,861,864,866,869,871,874,876,879,881,884,887,889,892,
- 894,897,899,902,905,907,910,912,915,918,920,923,925,928,931,
- 933,936,939,941,944,947,949,952,955,957,960,963,965,968,971,
- 973,976,979,982,984,987,990,992,995,998,1001,1004,1006,1009,1012,
- 1015,1017,1020,1023 };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement