   Serial.println(opcClient.ipAddress);
 }
 #endif
+
+#if ENCODER
+void checkEncoder() {
+  // Get "raw" encoder value
+  int32_t val = encoder.read();
+  // First, check if we're in a long press, which overrides any
+  // encoder movement.
+  if (encoderPressed &&
+      !encoderMovedDuringPress &&
+      millis() > encoderPressTime + ENCODER_LONG_PRESS) {
+    // We're in the middle of a long press; reduce brightness accordingly.
+    settings.brightness = max(
+      0,
+      (int)encoderPressInitialBrightness -
+        (int)(millis() - encoderPressTime - ENCODER_LONG_PRESS) /
+        ENCODER_MS_PER_BRIGHTNESS);
+#if DEBUG
+  Serial.print("NewBright! ");
+  Serial.print(encoderPressInitialBrightness);
+  Serial.print(" => ");
+  Serial.print(settings.brightness);
+  Serial.print("   ");
+  Serial.print(millis());
+  Serial.print(',');
+  Serial.println(encoderPressTime);
+#endif
+  // We're not in a long press; did the encoder move?
+  } else if (val != lastEncoder) {
+    // If brightness is zero, probably due to a long press,
+    // turn back on given this new interaction.
+    if (settings.brightness == 0) {
+      settings.brightness = 255;
+    }
+    if (encoderPressed) {
+      encoderMovedDuringPress = true;
+      encoderSecondary += val - lastEncoder;
+      settings.saturation += val - lastEncoder;
+    } else {
+      encoderPrimary += val - lastEncoder;
+      settings.hue += val - lastEncoder;
+    }
+    encoderNewSettingsPending = true;
+    lastEncoder = val;
+#if DEBUG
+    Serial.print("Encoder change: ");
+    Serial.print(encoderPrimary);
+    Serial.print(',');
+    Serial.print(encoderSecondary);
+    Serial.print(',');
+    Serial.println(encoderPressed);
+#endif
+  }
+}
+void encoderSwitchHandler() {
+  portENTER_CRITICAL_ISR(&encoderMux);
+#if DEBUG
+  Serial.print("Switch! ");
+  Serial.println(!digitalRead(ENC_SW));
+#endif
+  if (!digitalRead(ENC_SW) != encoderPressed) {
+    encoderPressed = !encoderPressed;
+    long ms = millis();
+    if (encoderPressed) {
+      // If brightness is zero, probably due to a long press,
+      // turn back on given this new interaction.
+      if (settings.brightness == 0) {
+        settings.brightness = 255;
+        // Also, make sure this interaction will never register
+        // as a click, though it could register as other stuff.
+        encoderClickDisabled = true;
+      } else {
+        encoderClickDisabled = false;
+      }
+      encoderMovedDuringPress = false;
+      encoderPressTime = ms;
+      encoderPressInitialBrightness = settings.brightness;
+    } else if (!encoderMovedDuringPress &&
+               !encoderClickDisabled &&
+               ms > encoderPressTime + ENCODER_DEBOUNCE_MS &&
+               ms <= encoderPressTime + ENCODER_LONG_PRESS) {
+#if DEBUG
+      Serial.print("Click! New mode: ");
+      Serial.println(nextEncoderMode);
+#endif
+      settings = encoderModes[nextEncoderMode];
+      nextEncoderMode = (nextEncoderMode + 1) %
+        (sizeof(encoderModes) / sizeof(SavedSettings));
+      encoderNewSettingsPending = true;
+    }
+  }
+  checkEncoder();
+  portEXIT_CRITICAL_ISR(&encoderMux);
+}
+#endif // ENCODER

  Serial.print(',');
  Serial.println(settings.c2);
#endif
  
  FastLED.setBrightness(settings.brightness);
  if (settings.mode == 'C') {

// Stored frames of POV/light-painting image, used by mode 'P'
// Each pixel is in 8-bit RRRGGGBB format
byte ledFrames[NUM_LEDS * MAX_FRAMES];

#if ENCODER
#include <Encoder.h>
Encoder encoder(ENC_P1, ENC_P2);
// Is the encoder's swith currently pressed?
volatile bool encoderPressed = false;
// true when the main run loop should restoreSettings().
// We can't call it directly; it crashes, probably due to
// concurrency issues.
volatile bool encoderNewSettingsPending = false;
// Used when switch was pressed to turn on.
volatile bool encoderClickDisabled = false;
// Has the encoder knob moved during the current press?
volatile bool encoderMovedDuringPress = false;
// millis() when the encoder switch was pressed
volatile long encoderPressTime = 0;
// settings.brightness when the encoder switch was pressed
volatile byte encoderPressInitialBrightness = 255;
// Un-pressed encoder position
volatile int32_t encoderPrimary = 0;
// Pressed encoder position
volatile int32_t encoderSecondary = 0;
// Last raw encoder value, regardless of press state
volatile int32_t lastEncoder = 0;
portMUX_TYPE encoderMux = portMUX_INITIALIZER_UNLOCKED;
#define ENCODER_DEBOUNCE_MS 100
// How many milliseconds before a pending click becomes a
// long-held press to change brightness
#define ENCODER_LONG_PRESS 1000
// After ENCODER_LONG_PRESS milliseconds, this additional time
// reduces brightess by one level.
#define ENCODER_MS_PER_BRIGHTNESS 10
SavedSettings encoderModes[] = {
  // Standard color mode with a warm yellow
  { 255, 'C', 60, 255, 0, 0 },
  // Small slow blobs
  { 255, 'B', 14, 45, 45, 45 },
  // Fire
  { 255, 'Z', 16, 255, 139, 118 },
  // Blue blobs
  { 255, 'B', 38, 147, 43, 255 },
  // Pulsing red
  { 255, 'M', 0, 255, 47, 149 },
  // Twinkle
  { 255, 'Z', 0, 0, 124, 154 },
  // Chasing blue
  { 255, 'M', 168, 255, 51, 98 },
  // Noise
  { 255, 'Z', 0, 255, 201, 201 },
  // Chasing rainbow
  { 255, 'M', 168, 255, 149, 100 }
};
byte nextEncoderMode = 0;
#endif // ENCODER


#if TEXTMODE
#include "Picopixel.h"
