donutsorelse
Published © LGPL

Making Subnautica Scarier with Real-Life Effects

By monitoring the health, oxygen, and depth in Subnautica, let's make the game scarier and more immersive with real life effects!

IntermediateFull instructions provided10 hours26
Making Subnautica Scarier with Real-Life Effects

Things used in this project

Hardware components

ESP32S
Espressif ESP32S
×1
NeoPixel Ring: WS2812 5050 RGB LED
Adafruit NeoPixel Ring: WS2812 5050 RGB LED
×1

Software apps and online services

Arduino IDE
Arduino IDE
visual studio code
For python code

Story

Read more

Code

immersive_subnautica.py

Python
This is the main program that captures your data as you play Subnautica and orchestrates having immersive effects happen.
import cv2
import pytesseract
import pyautogui
import socket
import time
import re
import numpy as np
import os
import pygame

###############################################################################
# Configuration
###############################################################################

# Turn logs on/off
DEBUG_LOG = False  # set True to see debug-level logs
INFO_LOG = True    # set True to see info-level logs

# UDP broadcast for the ESP32
UDP_BROADCAST_IP = "255.255.255.255"
UDP_PORT = 12345

# Health bounding box and color thresholds
HEALTH_REGION = (96, 808, 60, 60)
HEALTH_LOWER_COLOR = np.array([0, 100, 100])
HEALTH_UPPER_COLOR = np.array([10, 255, 255])
HEALTH_RAW_MIN = 4.7
HEALTH_RAW_MAX = 28.88

# O2 bounding boxes
O2_REGION = (176, 779, 125, 125)
O2_LABEL_REGION = (220, 810, 37, 26)
O2_MASK_FILE = "oxygen_icon_mask.png"
O2_RAW_MIN = 5.05
O2_RAW_MAX = 97.3

LOW_O2_THRESHOLD = 15.0

# MP3 path for the Sonic alarm (provide your own file name here).
LOW_O2_AUDIO = "sonic_underwater.mp3"

# Depth bounding box
DEPTH_REGION = (896, 37, 125, 59)
DEPTH_CONFIG = r'--psm 7 -c tessedit_char_whitelist=0123456789m'
VALID_DEPTH_RANGE = (0, 3000)

###############################################################################
# Logging Helpers
###############################################################################

def log_debug(message):
    if DEBUG_LOG:
        print("[DEBUG]", message)

def log_info(message):
    if INFO_LOG:
        print("[INFO]", message)

###############################################################################
# Pygame Audio Helpers
###############################################################################

def init_audio_system():
    """
    Initialize pygame.mixer so we can load and play MP3 files.
    """
    pygame.mixer.init()
    log_debug("Pygame mixer initialized.")

def load_sound(file_path):
    """
    Load a sound (MP3 or WAV) into pygame.
    Returns a pygame.mixer.Sound object or None if load fails.
    """
    try:
        sound = pygame.mixer.Sound(file_path)
        log_debug(f"Audio file loaded: {file_path}")
        return sound
    except pygame.error as e:
        print(f"Failed to load audio file '{file_path}': {e}")
        return None

def play_alarm(sound):
    """
    Start playing the alarm in a loop if it's not already playing.
    """
    if not pygame.mixer.get_busy():
        log_info("O2 < 15% => Starting alarm.")
        sound.play(loops=-1)
    else:
        log_debug("Alarm already playing, not restarting.")

def stop_alarm(sound):
    """
    Stop the alarm if it's currently playing.
    """
    if pygame.mixer.get_busy():
        log_info("O2 recovered or icon gone => Stopping alarm.")
        sound.stop()
    else:
        log_debug("No alarm playing to stop.")

###############################################################################
# Image Capture and OCR
###############################################################################

def capture_screenshot():
    screenshot = pyautogui.screenshot()
    screenshot.save("temp_screenshot.png")
    return cv2.imread("temp_screenshot.png")

def safe_crop(img, region):
    if img is None:
        return None
    x, y, w, h = map(int, region)
    H, W = img.shape[:2]
    if x < 0: x = 0
    if y < 0: y = 0
    if x + w > W: w = max(0, W - x)
    if y + h > H: h = max(0, H - y)
    if w <= 0 or h <= 0:
        return None
    return img[y:y+h, x:x+w]

def read_text(roi, config=None):
    """
    Convert the region to grayscale, enlarge, threshold,
    then run Tesseract for text. Returns the text string.
    """
    if roi is None:
        return ""
    gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
    scale_factor = 3
    bigger = cv2.resize(gray, (gray.shape[1]*scale_factor, 
                               gray.shape[0]*scale_factor),
                        interpolation=cv2.INTER_CUBIC)
    _, thresh = cv2.threshold(bigger, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    thresh = cv2.bitwise_not(thresh)
    kernel = np.ones((2,2), np.uint8)
    cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    text = pytesseract.image_to_string(cleaned, config=config).strip()
    return text

###############################################################################
# Health / O2 / Depth Measurement
###############################################################################

def measure_health_fill(roi):
    """
    Returns scaled health % from 0..100, based on color threshold in HSV.
    """
    hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, HEALTH_LOWER_COLOR, HEALTH_UPPER_COLOR)
    total_px = roi.size // 3
    fill_px = np.count_nonzero(mask)
    raw_pct = (fill_px / total_px) * 100.0

    if raw_pct <= HEALTH_RAW_MIN:
        return 0.0
    elif raw_pct >= HEALTH_RAW_MAX:
        return 100.0
    else:
        return ((raw_pct - HEALTH_RAW_MIN) /
                (HEALTH_RAW_MAX - HEALTH_RAW_MIN)) * 100.0

def otsu_threshold(gray):
    _, th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    return th

def measure_o2_otsu_with_mask(roi, ring_mask):
    """
    Convert ROI to grayscale, Otsu threshold,
    then mask with ring_mask to isolate the ring's fill.
    """
    gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
    th = otsu_threshold(gray)
    H_roi, W_roi = roi.shape[:2]

    if ring_mask is None:
        return 0.0, th, None

    H_mask, W_mask = ring_mask.shape[:2]
    if (W_mask != W_roi) or (H_mask != H_roi):
        ring_mask_resized = cv2.resize(ring_mask, (W_roi, H_roi), 
                                       interpolation=cv2.INTER_NEAREST)
    else:
        ring_mask_resized = ring_mask

    masked = cv2.bitwise_and(th, th, mask=ring_mask_resized)
    ring_pixels = np.count_nonzero(ring_mask_resized)
    if ring_pixels == 0:
        return 0.0, th, masked

    white_in_ring = np.count_nonzero(masked)
    raw_pct = (white_in_ring / ring_pixels) * 100.0

    if raw_pct <= O2_RAW_MIN:
        scaled = 0.0
    elif raw_pct >= O2_RAW_MAX:
        scaled = 100.0
    else:
        scaled = ((raw_pct - O2_RAW_MIN) /
                  (O2_RAW_MAX - O2_RAW_MIN)) * 100.0
    return scaled, th, masked

def read_depth(roi):
    text = read_text(roi, config=DEPTH_CONFIG)
    match = re.search(r'(\d+)m', text)
    if match:
        return int(match.group(1))
    match2 = re.search(r'\d+', text)
    if match2:
        return int(match2.group(0))
    return None

###############################################################################
# Main
###############################################################################

def main():
    log_debug("Starting main function.")

    # Setup network
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

    # Init pygame audio
    pygame.mixer.init()
    log_debug("pygame.mixer initialized.")

    # Load ring mask
    ring_mask = cv2.imread(O2_MASK_FILE, cv2.IMREAD_GRAYSCALE)
    if ring_mask is None:
        print(f"Failed to load ring mask: {O2_MASK_FILE}")
        return

    # Load alarm sound
    try:
        alarm_sound = pygame.mixer.Sound(LOW_O2_AUDIO)
        log_debug(f"Loaded alarm sound: {LOW_O2_AUDIO}")
    except pygame.error as e:
        print(f"Could not load alarm sound: {e}")
        return

    prev_health = None
    o2_alarm_playing = False

    while True:
        img = capture_screenshot()
        if img is None:
            log_debug("No screenshot captured, sleeping.")
            time.sleep(1)
            continue

        # --- Health ---
        health_roi = safe_crop(img, HEALTH_REGION)
        if health_roi is not None:
            health_val = measure_health_fill(health_roi)
            log_info(f"Health: {health_val:.1f}%")
            if prev_health is not None and health_val > 0 and health_val < prev_health:
                log_info("Health decreased => broadcast BUZZ")
                sock.sendto(b"BUZZ\n", (UDP_BROADCAST_IP, UDP_PORT))
            prev_health = health_val
        else:
            log_debug("Health ROI is None.")

        # --- O2 ---
        label_roi = safe_crop(img, O2_LABEL_REGION)
        o2_icon_text = ""
        if label_roi is not None:
            o2_icon_text = read_text(label_roi).upper()
        log_info(f"O2 icon text => '{o2_icon_text}'")

        # We'll consider multiple potential OCR outputs that represent O2
        is_o2 = any(sub in o2_icon_text for sub in ["O2","O.","OS","OS."])
        if is_o2:
            o2_roi = safe_crop(img, O2_REGION)
            if o2_roi is not None:
                o2_fill, _, _ = measure_o2_otsu_with_mask(o2_roi, ring_mask)
                log_info(f"O2 fill: {o2_fill:.1f}%")

                if o2_fill < LOW_O2_THRESHOLD:
                    if not o2_alarm_playing:
                        log_info("O2 < 15 => starting alarm with pygame")
                        alarm_sound.play(loops=-1)
                        o2_alarm_playing = True
                else:
                    if o2_alarm_playing:
                        log_info("O2 recovered => stopping alarm")
                        alarm_sound.stop()
                        o2_alarm_playing = False
            else:
                log_debug("O2 ROI is None.")
        else:
            log_debug("Not O2 => skip O2 fill.")
            # If not recognized as O2, also stop if playing
            if o2_alarm_playing:
                log_info("No O2 circle => stopping alarm")
                alarm_sound.stop()
                o2_alarm_playing = False

        # --- Depth ---
        depth_roi = safe_crop(img, DEPTH_REGION)
        if depth_roi is not None:
            depth_val = read_depth(depth_roi)
            if depth_val is not None and VALID_DEPTH_RANGE[0] <= depth_val <= VALID_DEPTH_RANGE[1]:
                log_debug(f"Depth: {depth_val}m")
                msg = f"DEPTH:{depth_val}\n".encode('utf-8')
                sock.sendto(msg, (UDP_BROADCAST_IP, UDP_PORT))
            else:
                log_debug(f"Depth invalid: {depth_val}")
        else:
            log_debug("Depth ROI is None.")

        # Check if user pressed 'q'
        key = cv2.waitKey(1000) & 0xFF
        if key == ord('q'):
            log_debug("'q' pressed => break loop")
            break

    # On exit
    if o2_alarm_playing:
        log_info("Stopping alarm on exit.")
        alarm_sound.stop()

    cv2.destroyAllWindows()
    sock.close()
    pygame.quit()  # optional to close the mixer
    log_debug("main() ended, script done.")

if __name__ == "__main__":
    main()

calibration.py

Python
A simple calibration program that makes it easier to get the right location and sizing for the different parts we want to capture for the full immersive Subnautica program.
import cv2
import pyautogui
import numpy as np

SCALE = 0.1

MAX_RANGE_XY = 10800 
MAX_RANGE_WH = 10800 

# Initial bounding boxes, in actual pixels. Adjust if you already know approximate coords.
depth_vals = [600, 50, 100, 40]     # (x, y, width, height)
health_vals = [50, 850, 60, 60]
oxygen_vals = [200, 850, 60, 60]
temperature_vals = [350, 850, 60, 60]

# Window where we show the main screenshot preview
MAIN_WINDOW = "Calibration Preview"
cv2.namedWindow(MAIN_WINDOW, cv2.WINDOW_NORMAL)

# Window for trackbar controls
TRACKBAR_WINDOW = "Adjust Regions"
cv2.namedWindow(TRACKBAR_WINDOW, cv2.WINDOW_NORMAL)

def val_to_trackbar(pixel_value):
    """Convert an actual pixel value to trackbar units."""
    return int(pixel_value / SCALE)

def trackbar_to_val(trackbar_value):
    """Convert from trackbar units back to float pixel values."""
    return trackbar_value * SCALE

# trackbars for Depth region
cv2.createTrackbar("Depth X", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth Y", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth W", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Depth H", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Health region
cv2.createTrackbar("Health X", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health Y", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health W", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Health H", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Oxygen region
cv2.createTrackbar("O2 X", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 Y", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 W", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("O2 H", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Temperature region
cv2.createTrackbar("Temp X", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp Y", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp W", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Temp H", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[3]), MAX_RANGE_WH, lambda x: None)

# Windows to display each roi
cv2.namedWindow("Depth ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Health ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Oxygen ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Temperature ROI", cv2.WINDOW_NORMAL)

def safe_crop(img, x, y, w, h):
    """Crop (x, y, w, h) safely from an image, avoiding out-of-bounds errors."""
    h_img, w_img = img.shape[:2]
    x, y, w, h = map(int, [x, y, w, h])
    if x < 0: x = 0
    if y < 0: y = 0
    if x + w > w_img: w = max(0, w_img - x)
    if y + h > h_img: h = max(0, h_img - y)
    if w <= 0 or h <= 0:
        return None
    return img[y:y+h, x:x+w]

while True:
    # Capture current screen
    screenshot = pyautogui.screenshot()
    full_frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    # Read trackbar positions, convert to float pixel values
    dx = trackbar_to_val(cv2.getTrackbarPos("Depth X", TRACKBAR_WINDOW))
    dy = trackbar_to_val(cv2.getTrackbarPos("Depth Y", TRACKBAR_WINDOW))
    dw = trackbar_to_val(cv2.getTrackbarPos("Depth W", TRACKBAR_WINDOW))
    dh = trackbar_to_val(cv2.getTrackbarPos("Depth H", TRACKBAR_WINDOW))

    hx = trackbar_to_val(cv2.getTrackbarPos("Health X", TRACKBAR_WINDOW))
    hy = trackbar_to_val(cv2.getTrackbarPos("Health Y", TRACKBAR_WINDOW))
    hw = trackbar_to_val(cv2.getTrackbarPos("Health W", TRACKBAR_WINDOW))
    hh = trackbar_to_val(cv2.getTrackbarPos("Health H", TRACKBAR_WINDOW))

    ox = trackbar_to_val(cv2.getTrackbarPos("O2 X", TRACKBAR_WINDOW))
    oy = trackbar_to_val(cv2.getTrackbarPos("O2 Y", TRACKBAR_WINDOW))
    ow = trackbar_to_val(cv2.getTrackbarPos("O2 W", TRACKBAR_WINDOW))
    oh = trackbar_to_val(cv2.getTrackbarPos("O2 H", TRACKBAR_WINDOW))

    tx = trackbar_to_val(cv2.getTrackbarPos("Temp X", TRACKBAR_WINDOW))
    ty = trackbar_to_val(cv2.getTrackbarPos("Temp Y", TRACKBAR_WINDOW))
    tw = trackbar_to_val(cv2.getTrackbarPos("Temp W", TRACKBAR_WINDOW))
    th = trackbar_to_val(cv2.getTrackbarPos("Temp H", TRACKBAR_WINDOW))

    # Draw rectangles on the main frame to visualize each region
    cv2.rectangle(full_frame, (int(dx), int(dy)), (int(dx + dw), int(dy + dh)), (255, 0, 0), 2)
    cv2.rectangle(full_frame, (int(hx), int(hy)), (int(hx + hw), int(hy + hh)), (0, 255, 0), 2)
    cv2.rectangle(full_frame, (int(ox), int(oy)), (int(ox + ow), int(oy + oh)), (0, 0, 255), 2)
    cv2.rectangle(full_frame, (int(tx), int(ty)), (int(tx + tw), int(ty + th)), (0, 255, 255), 2)

    # Crop each ROI and show in separate windows
    depth_roi = safe_crop(full_frame, dx, dy, dw, dh)
    health_roi = safe_crop(full_frame, hx, hy, hw, hh)
    oxygen_roi = safe_crop(full_frame, ox, oy, ow, oh)
    temp_roi   = safe_crop(full_frame, tx, ty, tw, th)

    cv2.imshow("Depth ROI", depth_roi if depth_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Health ROI", health_roi if health_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Oxygen ROI", oxygen_roi if oxygen_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Temperature ROI", temp_roi if temp_roi is not None else np.zeros((10,10,3), dtype=np.uint8))

    # Show the main preview
    cv2.imshow(MAIN_WINDOW, full_frame)

    # Press 'q' to quit
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

arduino_subnautica

Arduino
This is to be run on an esp32 (or tweak it as needed to run on your arduino of choice). It handles listening for depth information as well as when we get damaged to update led display and buzz us when relevant.
#include <WiFi.h>
#include <WiFiUdp.h>
#include <FastLED.h>

// Wi-Fi credentials
const char* WIFI_SSID     = "your wifi here";
const char* WIFI_PASSWORD = "your wifi pw here";

const int UDP_PORT = 12345;
WiFiUDP udp;

// for your buzzers
const int MOTOR_PIN1 = 14; 
const int MOTOR_PIN2 = 2;

#define LED_PIN 5
#define NUM_LEDS 120
CRGB leds[NUM_LEDS];

// Short motor activation
unsigned long BUZZ_TIME_MS = 60;
bool buzzing = false;
unsigned long buzzStartTime = 0;

// We define multiple color stops for different depths
// Each entry is { depth in meters, CRGB color }.
struct DepthColor {
  int depth;
  CRGB color;
};

DepthColor depthStops[] = {
  {   0, CRGB::Green   }, // 0 m
  { 300, CRGB::Teal    }, // 300 m
  { 700, CRGB::Blue    }, // 700 m
  {1200, CRGB::Purple  }, // 1200 m
  {1700, CRGB::Red     }  // 1700 m
};
const int NUM_STOPS = sizeof(depthStops)/sizeof(depthStops[0]);

void setup() {
  Serial.begin(115200);
  Serial.println("Subnautica Depth + Multi-Color Gradient + Buzz (FastLED).");

  pinMode(MOTOR_PIN1, OUTPUT);
  pinMode(MOTOR_PIN2, OUTPUT);
  digitalWrite(MOTOR_PIN1, LOW);
  digitalWrite(MOTOR_PIN2, LOW);

  // Setup FastLED
  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(255);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();

  // Connect Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi connected.");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());

  // UDP
  udp.begin(UDP_PORT);
  Serial.print("Listening on port ");
  Serial.println(UDP_PORT);
}

// Helper to blend between two CRGB colors
CRGB blendCRGB(const CRGB &c1, const CRGB &c2, float fraction) {
  // clamp fraction to [0..1]
  if (fraction < 0) fraction = 0;
  if (fraction > 1) fraction = 1;

  uint8_t r = c1.r + (uint8_t)((c2.r - c1.r) * fraction);
  uint8_t g = c1.g + (uint8_t)((c2.g - c1.g) * fraction);
  uint8_t b = c1.b + (uint8_t)((c2.b - c1.b) * fraction);
  return CRGB(r, g, b);
}

// We find which two stops the depth is between, then interpolate
CRGB getColorForDepth(int depthVal) {
  // If below first stop, clamp to first color
  if (depthVal <= depthStops[0].depth) {
    return depthStops[0].color;
  }
  // If above last stop, clamp to last color
  if (depthVal >= depthStops[NUM_STOPS - 1].depth) {
    return depthStops[NUM_STOPS - 1].color;
  }

  // Otherwise, find the segment of the two stops we fall between
  for (int i = 0; i < NUM_STOPS - 1; i++) {
    int d1 = depthStops[i].depth;
    int d2 = depthStops[i+1].depth;
    if (depthVal >= d1 && depthVal <= d2) {
      // fraction from d1..d2
      float fraction = (float)(depthVal - d1) / (float)(d2 - d1);
      // blend between color[i]..color[i+1]
      return blendCRGB(depthStops[i].color, depthStops[i+1].color, fraction);
    }
  }
  // Fallback (shouldn’t happen)
  return depthStops[NUM_STOPS - 1].color;
}

// Set entire strip to the color for a given depth
void setStripColorForDepth(int depthVal) {
  // get interpolated color
  CRGB c = getColorForDepth(depthVal);
  // Serial.print("Color for depth: ");
  // Serial.println(c);
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = c;
  }
  FastLED.show();
}

void loop() {
  // Check incoming UDP
  int packetSize = udp.parsePacket();
  if (packetSize > 0) {
    static char incomingPacket[128]; 
    int len = udp.read(incomingPacket, 127);
    if (len > 0) {
      incomingPacket[len] = 0;
    }
    String data = String(incomingPacket);
    data.trim();

    Serial.print("Received: ");
    Serial.println(data);

    if (data.startsWith("BUZZ")) {
      Serial.println("Buzz => motors on");
      buzzing = true;
      buzzStartTime = millis();
      digitalWrite(MOTOR_PIN1, HIGH);
      digitalWrite(MOTOR_PIN2, HIGH);
    } 
    else if (data.startsWith("DEPTH:")) {
      String valStr = data.substring(6);
      valStr.trim();
      int depthVal = valStr.toInt();
      Serial.print("Depth: ");
      Serial.println(depthVal);
      setStripColorForDepth(depthVal);
    }
  }

  // turn off motors if time’s up
  if (buzzing) {
    if (millis() - buzzStartTime >= BUZZ_TIME_MS) {
      buzzing = false;
      digitalWrite(MOTOR_PIN1, LOW);
      digitalWrite(MOTOR_PIN2, LOW);
      Serial.println("Motors off");
    }
  }

  delay(5);
}

Credits

donutsorelse
19 projects • 18 followers
I make different stuff every week of all kinds. Usually I make funny yet useful inventions.

Comments