// ==================================================
// YAESU ROTATOR CONTROLLER – V3.10 (V3.8 + Watchdog Fix)
// Ethernet + USB (GS-232 compatible)
// ==================================================
//
// This program controls an antenna rotator compatible
// with the YAESU GS-232 protocol.
//
// Two control interfaces are supported:
//  - Ethernet (TCP server, port 2823)
//  - USB / Serial (same GS-232 protocol)
//
// The program is designed for long-term continuous operation.
// It does not use delay(); all timing is driven by millis().
//
// Key features:
//  - rotation state machine
//  - motor stall protection
//  - infinite rotation protection
//  - safe CW ↔ CCW direction change
//  - hard lockout on repeated faults
//
// ============================================================
// VERSION 3.9 – CHANGES FROM 3.8:
// ============================================================
// 1. BUGFIX: Watchdog timer now enabled BEFORE configuration
//    validation loops in setup().
//    - In V3.8 the watchdog was enabled after the validation
//      while(1) loops, so a configuration error would hang
//      the device indefinitely with no automatic recovery.
//    - Now wdt_enable(WDTO_8S) is called as the very first
//      action in setup(), guaranteeing an 8-second reset
//      even during config error blink loops.
// ============================================================
// VERSION 3.8 – CHANGES FROM 3.7:
// ============================================================
// 1. Added 250 ms "settling time" to eliminate oscillation near target
//    - Motor stops only after the position is stable within tolerance for 250 ms
//    - A new command immediately interrupts settling and starts movement
//    - Manual fine-tuning behaviour is unchanged
// ============================================================
#include <SPI.h>
#include <Ethernet.h>
#include <math.h>
#include <ctype.h>
#include <avr/wdt.h>  // V3.7: Watchdog timer for extra protection
///////////////////////////////////////////////////////////////////////////////
// 0. USER CONFIG – EVERYTHING A NEW USER MAY / MUST CONFIGURE
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// 0.1 ETHERNET SETTINGS
///////////////////////////////////////////////////////////////////////////////
//
// MAC address:
// - 0x02 at the start means "locally administered" MAC.
// - This is safe for a local network and does not conflict with real vendors.
//
// Safe MAC change:
// - keep the first byte as 0x02
// - you can change the last 3 bytes to distinguish multiple devices on the network
//
// Static IP:
// - set according to your LAN (watch out for conflicts with other devices)
// - DNS is required for correct initialisation of the Ethernet library
//
#define ETHERNET_MAC_ADDRESS  0x02,0x00,0x00,0x00,0x00,0x01
#define ETHERNET_IP_ADDRESS      192,168,1,182
#define ETHERNET_IP_GATEWAY      192,168,1,1
#define ETHERNET_IP_SUBNET_MASK  255,255,255,0
#define ETHERNET_IP_DNS          192,168,1,1
// TCP port – YAESU GS-232 standard uses 2823
#define ETHERNET_TCP_PORT_0 2823
// Timeout for incomplete commands over Ethernet (ms)
#define ETHERNET_MESSAGE_TIMEOUT_MS 5000UL
// Maximum command length
#define COMMAND_BUFFER_SIZE 50
///////////////////////////////////////////////////////////////////////////////
// 0.2 USB (SERIAL) SETTINGS
///////////////////////////////////////////////////////////////////////////////
//
// The program also supports GS-232 commands over USB Serial.
// The USB timeout is shorter so the buffer is cleared faster.
//
#define USB_BAUDRATE 9600
#define USB_COMMAND_BUFFER_SIZE 50
#define USB_MESSAGE_TIMEOUT_MS  1000UL
///////////////////////////////////////////////////////////////////////////////
// 0.3 HARDWARE PINS
///////////////////////////////////////////////////////////////////////////////
//
// rotate_cw / rotate_ccw:
//  - outputs that control the direction relay or driver
//
// rotator_analog_az:
//  - analogue input from the position sensor (potentiometer)
//
// LED_HARD_LOCKOUT:
//  - LED indicates hard lockout (multiple faults in a short time)
//
#define rotate_cw          6
#define rotate_ccw         7
#define rotator_analog_az  A0
#define LED_HARD_LOCKOUT   8
///////////////////////////////////////////////////////////////////////////////
// 0.4 ROTATOR CONFIGURATION (ADC ↔ degrees)
///////////////////////////////////////////////////////////////////////////////
//
// analog_az_full_ccw / analog_az_full_cw:
//  - ADC values for the mechanical end stops
//
// azimuth_rotation_capability:
//  - mechanical rotation range of the rotator (e.g. 360° or 450°)
//
struct config_t {
int  analog_az_full_ccw;
int  analog_az_full_cw;
int  azimuth_starting_point;
long azimuth_rotation_capability;
};
// !!! Set the rotator range here !!!
static const config_t configuration = {
0,   // ADC value for FULL CCW (mechanically all the way left)
826, // ADC value for FULL CW  (mechanically all the way right)
0,   // starting point / offset (reserved)
450  // mechanical rotation range in degrees
};
// Number of samples for ADC averaging (noise filtering)
#define AZ_ADC_AVG_SAMPLES 10
///////////////////////////////////////////////////////////////////////////////
// 1. TIMING AND SAFETY CONSTANTS (do not change without understanding)
///////////////////////////////////////////////////////////////////////////////
#define AZIMUTH_MEASUREMENT_FREQUENCY_MS 100
#define AZIMUTH_TOLERANCE                1.5f
#define MOTOR_STARTUP_GRACE_MS     1500UL
#define MOTION_EPS_DEG              0.7f
#define STALL_TIMEOUT_MS           4000UL
#define T1_MAX_DIR_TIME_MS       180000UL
#define T2_DIR_CHANGE_DELAY_MS     2000UL
#define T3_TOTAL_CYCLE_MS        225000UL
#define T3_IDLE_RESET_MS          15000UL
#define MOTOR_COOLDOWN_MS         15000UL
#define FAULT_WINDOW_MS           70000UL
#define FAULTS_FOR_EXT_COOLDOWN       3
#define EXTENDED_COOLDOWN_MS     300000UL
// V3.7: New constants
#define FAULT_DEBOUNCE_MS            500UL  // Min. time to register a fault
#define MOTOR_RELAY_SAFE_DELAY_US    100    // Safety pause between relay switching (microseconds)
#define REQUEST_DEBOUNCE_MS          100UL  // Min. interval between requests (race condition protection)
#define CONFIG_ERROR_BLINK_MS        100UL  // LED blink rate for configuration error
#define ADC_SPAN_MIN_VALUE             1.0f // Minimum valid ADC span value
// V3.8: NEW CONSTANT – Settling time for position stabilisation
#define SETTLING_TIME_MS             250UL
///////////////////////////////////////////////////////////////////////////////
// 2. ROTATOR STATES (state machine)
///////////////////////////////////////////////////////////////////////////////
enum AzState : byte {
IDLE = 0,
INIT_CW,
RUN_CW,
INIT_CCW,
RUN_CCW,
WAIT_DIR_CHANGE
};
///////////////////////////////////////////////////////////////////////////////
// 3. REQUEST QUEUE STATE
///////////////////////////////////////////////////////////////////////////////
enum RequestQueueState : byte {
NONE = 0,
IN_QUEUE,
IN_PROGRESS_TO_TARGET
};
///////////////////////////////////////////////////////////////////////////////
// 4. REQUEST TYPE
///////////////////////////////////////////////////////////////////////////////
enum RequestType : byte {
REQUEST_STOP = 0,
REQUEST_AZIMUTH,
REQUEST_RESET_LOCKOUT  // V3.7: New request type for lockout reset
};
///////////////////////////////////////////////////////////////////////////////
// 5. STATE VARIABLES
///////////////////////////////////////////////////////////////////////////////
static float az_raw = 0.0f;
static float az_deg = 0.0f;
static float az_target_raw = 0.0f;
static AzState az_state = IDLE;
static AzState pending_state = IDLE;
static RequestQueueState az_request_queue_state = NONE;
static byte  az_request = REQUEST_STOP;
static float az_request_parm = 0.0f;
static bool az_initialized = false;
// V3.8: NEW VARIABLE – tracking settling time
static unsigned long settling_start_ms = 0;
///////////////////////////////////////////////////////////////////////////////
// 6. TIMERS AND PROTECTIONS
///////////////////////////////////////////////////////////////////////////////
static unsigned long motor_start_ms = 0;
static unsigned long last_progress_ms = 0;
static float last_progress_raw = 0.0f;
static byte progress_hits = 0;
static unsigned long t1_dir_start_ms = 0;
static unsigned long t3_cycle_start_ms = 0;
static unsigned long t3_last_idle_ms = 0;
static unsigned long dir_change_until_ms = 0;
static unsigned long cooldown_until_ms = 0;
// V3.7: New protection against race condition
static unsigned long last_request_time_ms = 0;
///////////////////////////////////////////////////////////////////////////////
// 7. HARD LOCKOUT (repeated faults)
///////////////////////////////////////////////////////////////////////////////
static unsigned long fault_times[FAULTS_FOR_EXT_COOLDOWN] = {0,0,0};
static byte fault_times_wr = 0;
static bool hard_lockout_active = false;
///////////////////////////////////////////////////////////////////////////////
// 8. ETHERNET OBJECTS
///////////////////////////////////////////////////////////////////////////////
byte mac[] = { ETHERNET_MAC_ADDRESS };
IPAddress ip(ETHERNET_IP_ADDRESS);
IPAddress dns(ETHERNET_IP_DNS);
IPAddress gateway(ETHERNET_IP_GATEWAY);
IPAddress subnet(ETHERNET_IP_SUBNET_MASK);
EthernetServer server(ETHERNET_TCP_PORT_0);
///////////////////////////////////////////////////////////////////////////////
// 9. HELPER FUNCTIONS – MATH AND MAPPING
///////////////////////////////////////////////////////////////////////////////
static float clamp_0_359(float a) {
float r = fmodf(a, 360.0f);
if (r < 0.0f) r += 360.0f;
return r;
}
static float choose_best_target_raw(float target_deg, float current_raw) {
// Neorezávame na 0-359 – rotátor má rozsah 0-450°
float best = target_deg;
float best_dist = fabs(current_raw - best);
float cand = target_deg + 360.0f;
if (cand <= configuration.azimuth_rotation_capability) {
float d = fabs(current_raw - cand);
if (d < best_dist) best = cand;
}
return best;
}
///////////////////////////////////////////////////////////////////////////////
// 10. DRIVER – AZIMUTH READING (ADC → DEGREES)
///////////////////////////////////////////////////////////////////////////////
static void driver_read_azimuth(byte force) {
static unsigned long last = 0;
static float last_ok = 0.0f;
unsigned long now = millis();
if (!force && (now - last) < AZIMUTH_MEASUREMENT_FREQUENCY_MS)
return;
long sum = 0;
analogRead(rotator_analog_az);
for (byte i = 0; i < AZ_ADC_AVG_SAMPLES; i++)
sum += analogRead(rotator_analog_az);
float adc = (float)sum / AZ_ADC_AVG_SAMPLES;
float span = (float)(configuration.analog_az_full_cw -
configuration.analog_az_full_ccw);
// V3.7: Improved check – using symbolic constant
if (span < ADC_SPAN_MIN_VALUE) {
az_initialized = false;
az_raw = last_ok;
az_deg = clamp_0_359(az_raw);
last = now;
return;
}
float raw =
((adc - configuration.analog_az_full_ccw) *
configuration.azimuth_rotation_capability) /
span;
if (!isnan(raw) && !isinf(raw)) {
if (raw < 0.0f)
  raw = 0.0f;
else if (raw > configuration.azimuth_rotation_capability)
  raw = configuration.azimuth_rotation_capability;

last_ok = raw;
az_initialized = true;
}
az_raw = last_ok;
az_deg = clamp_0_359(az_raw);
last = now;
}
///////////////////////////////////////////////////////////////////////////////
// 11. MOTOR – ON / OFF
///////////////////////////////////////////////////////////////////////////////
static void motor_off() {
digitalWrite(rotate_cw, LOW);
digitalWrite(rotate_ccw, LOW);
}
// V3.7: CRITICAL FIX – safe relay switching
static void motor_on(bool cw) {
// SAFETY PROTOCOL:
// 1. FIRST turn off both relays
digitalWrite(rotate_cw, LOW);
digitalWrite(rotate_ccw, LOW);
// 2. Short pause for safety (100 µs is sufficient for relay stabilisation)
delayMicroseconds(MOTOR_RELAY_SAFE_DELAY_US);
// 3. THEN activate the requested direction
if (cw)
digitalWrite(rotate_cw, HIGH);
else
digitalWrite(rotate_ccw, HIGH);
}
///////////////////////////////////////////////////////////////////////////////
// 12. FAULT LOGIC – FAULT RESPONSE
///////////////////////////////////////////////////////////////////////////////
static void motor_fault() {
unsigned long now = millis();
motor_off();
az_state = IDLE;
pending_state = IDLE;
az_request_queue_state = NONE;
dir_change_until_ms = 0;
// V3.7: Using symbolic constant instead of a magic number
if ((now - motor_start_ms) < FAULT_DEBOUNCE_MS) {
cooldown_until_ms = now + MOTOR_COOLDOWN_MS;
t3_last_idle_ms = now;
return;
}
fault_times[fault_times_wr] = now;
fault_times_wr = (fault_times_wr + 1) % FAULTS_FOR_EXT_COOLDOWN;
byte recent = 0;
for (byte i = 0; i < FAULTS_FOR_EXT_COOLDOWN; i++)
if (fault_times[i] && (now - fault_times[i]) <= FAULT_WINDOW_MS)
recent++;
if (recent >= FAULTS_FOR_EXT_COOLDOWN) {
cooldown_until_ms = now + EXTENDED_COOLDOWN_MS;
hard_lockout_active = true;
digitalWrite(LED_HARD_LOCKOUT, HIGH);
} else {
cooldown_until_ms = now + MOTOR_COOLDOWN_MS;
}
t3_last_idle_ms = now;
}
///////////////////////////////////////////////////////////////////////////////
// 13. REQUEST SUBMISSION (ETHERNET / USB)
///////////////////////////////////////////////////////////////////////////////
void driver_submit_request(byte r, float p) {
unsigned long now = millis();
// V3.7: NEW FUNCTION – Manual hard lockout reset
if (r == REQUEST_RESET_LOCKOUT) {
if (hard_lockout_active) {
hard_lockout_active = false;
digitalWrite(LED_HARD_LOCKOUT, LOW);
  for (byte i = 0; i < FAULTS_FOR_EXT_COOLDOWN; i++)
    fault_times[i] = 0;
  
  fault_times_wr = 0;
  cooldown_until_ms = 0;  // Immediately allow movement
}
return;
}
// V3.7: RACE CONDITION PROTECTION
// Ignore requests that arrive too quickly in succession (except STOP)
if (r != REQUEST_STOP) {
if ((now - last_request_time_ms) < REQUEST_DEBOUNCE_MS) {
return;  // Too soon – ignore
}
}
last_request_time_ms = now;
if (r == REQUEST_STOP) {
az_request = r;
az_request_queue_state = IN_QUEUE;
settling_start_ms = 0;  // V3.8: Reset settling on STOP
return;
}
if (!az_initialized) return;
// V3.7: FIXED – correct time comparison for millis() overflow
if ((long)(now - cooldown_until_ms) < 0) return;  // Correct comparison after overflow
az_request = r;
az_request_parm = p;
az_request_queue_state = IN_QUEUE;
settling_start_ms = 0;  // V3.8: CRITICAL – reset settling on new command (allows immediate fine adjustment)
}
///////////////////////////////////////////////////////////////////////////////
// 14. REQUEST PROCESSING
///////////////////////////////////////////////////////////////////////////////
static void driver_service_requests() {
if (az_request_queue_state != IN_QUEUE) return;
unsigned long now = millis();
if (az_request == REQUEST_STOP) {
motor_off();
az_state = IDLE;
pending_state = IDLE;
az_request_queue_state = NONE;
t3_last_idle_ms = now;
settling_start_ms = 0;  // V3.8: Reset settling
return;
}
az_target_raw = choose_best_target_raw(az_request_parm, az_raw);
if (fabs(az_raw - az_target_raw) < AZIMUTH_TOLERANCE) {
motor_off();
az_state = IDLE;
az_request_queue_state = NONE;
t3_last_idle_ms = now;
settling_start_ms = 0;  // V3.8: Reset settling
return;
}
bool want_cw = az_target_raw > az_raw;
AzState wanted = want_cw ? INIT_CW : INIT_CCW;
if (az_state == WAIT_DIR_CHANGE) {
pending_state = wanted;
az_request_queue_state = IN_PROGRESS_TO_TARGET;
return;
}
if ((az_state == RUN_CW  && wanted == INIT_CCW) ||
(az_state == RUN_CCW && wanted == INIT_CW)) {
motor_off();
az_state = WAIT_DIR_CHANGE;
pending_state = wanted;
dir_change_until_ms = now + T2_DIR_CHANGE_DELAY_MS;
settling_start_ms = 0;  // V3.8: Reset settling on direction change
} else {
az_state = wanted;
}
az_request_queue_state = IN_PROGRESS_TO_TARGET;
}
///////////////////////////////////////////////////////////////////////////////
// 15. ROTATION SERVICE (STATE MACHINE)
///////////////////////////////////////////////////////////////////////////////
static void driver_service_rotation() {
unsigned long now = millis();
if (az_state == IDLE && t3_cycle_start_ms &&
(now - t3_last_idle_ms) > T3_IDLE_RESET_MS)
t3_cycle_start_ms = 0;
if (az_state == WAIT_DIR_CHANGE) {
if (t3_cycle_start_ms &&
    (now - t3_cycle_start_ms) > T3_TOTAL_CYCLE_MS) {
  motor_fault();
  return;
}

// V3.7: FIXED – correct comparison for millis() overflow
if ((long)(now - dir_change_until_ms) >= 0)
  az_state = pending_state;

return;
}
if (az_state == INIT_CW || az_state == INIT_CCW) {
motor_on(az_state == INIT_CW);
az_state = (az_state == INIT_CW) ? RUN_CW : RUN_CCW;

motor_start_ms = now;
last_progress_ms = now;
last_progress_raw = az_raw;
progress_hits = 0;
t1_dir_start_ms = now;
settling_start_ms = 0;  // V3.8: Reset settling on motor start

if (!t3_cycle_start_ms)
  t3_cycle_start_ms = now;

return;
}
if (az_state == RUN_CW || az_state == RUN_CCW) {
if ((now - t1_dir_start_ms) > T1_MAX_DIR_TIME_MS) {
  motor_fault();
  return;
}

if (t3_cycle_start_ms &&
    (now - t3_cycle_start_ms) > T3_TOTAL_CYCLE_MS) {
  motor_fault();
  return;
}

float delta = (az_state == RUN_CW)
  ? az_raw - last_progress_raw
  : last_progress_raw - az_raw;

if (delta >= MOTION_EPS_DEG) {
  last_progress_raw = az_raw;
  if (progress_hits < 2) progress_hits++;
  if (progress_hits >= 2)
    last_progress_ms = now;
}

if ((now - motor_start_ms) > MOTOR_STARTUP_GRACE_MS &&
    (now - last_progress_ms) > STALL_TIMEOUT_MS) {
  motor_fault();
  return;
}

// === V3.8: NEW SETTLING TIME LOGIC (replaces the original immediate stop) ===
if (az_request_queue_state == IN_PROGRESS_TO_TARGET) {
  float error = fabs(az_raw - az_target_raw);
  
  if (error < AZIMUTH_TOLERANCE) {
    // Within tolerance – start/continue measuring stability
    if (settling_start_ms == 0) {
      settling_start_ms = now;  // Start measuring stability
    } else if ((now - settling_start_ms) >= SETTLING_TIME_MS) {
      // Position stable for >250 ms → stop permanently
      motor_off();
      az_state = IDLE;
      az_request_queue_state = NONE;
      t3_last_idle_ms = now;
      settling_start_ms = 0;  // Reset timer
    }
  } else {
    // Outside tolerance – reset stability measurement
    settling_start_ms = 0;
  }
}
// === END OF NEW LOGIC ===
}
}
///////////////////////////////////////////////////////////////////////////////
// 16. DRIVER – MAIN SERVICE FUNCTION
///////////////////////////////////////////////////////////////////////////////
void driver_service() {
driver_read_azimuth(0);
// V3.7: FIXED – correct time comparison
unsigned long now = millis();
if (hard_lockout_active && (long)(now - cooldown_until_ms) >= 0) {
hard_lockout_active = false;
digitalWrite(LED_HARD_LOCKOUT, LOW);
for (byte i = 0; i < FAULTS_FOR_EXT_COOLDOWN; i++)
  fault_times[i] = 0;

fault_times_wr = 0;
}
driver_service_requests();
driver_service_rotation();
}
///////////////////////////////////////////////////////////////////////////////
// 17. GS-232 – RESPONSE FORMAT "C"
///////////////////////////////////////////////////////////////////////////////
static void gs232_format_C(char *out, size_t sz) {
int v = (int)lroundf(az_deg);
snprintf(out, sz, "+0%03d", v % 360);
}
///////////////////////////////////////////////////////////////////////////////
// 18. GS-232 – STREAM PROCESSING
///////////////////////////////////////////////////////////////////////////////
static void gs232_process_stream(
Stream &in,
Stream &out,
byte *buf,
int &idx,
unsigned long &last_rx,
size_t buf_size,
unsigned long timeout_ms
) {
while (in.available()) {
byte b = (byte)in.read();
last_rx = millis();

if (b >= 'a' && b <= 'z') b -= 32;

if (b != 10 && b != 13 && idx < (int)buf_size)
  buf[idx++] = b;

if ((b == 13 || idx >= (int)buf_size) && idx) {

  if (buf[0] == 'C') {
    char r[16];
    gs232_format_C(r, sizeof(r));
    out.println(r);
  }
  else if (buf[0] == 'S') {
    driver_submit_request(REQUEST_STOP, 0);
  }
  // V3.7: NEW COMMAND – 'R' = RESET HARD LOCKOUT
  else if (buf[0] == 'R') {
    driver_submit_request(REQUEST_RESET_LOCKOUT, 0);
    out.println("LOCKOUT RESET");
  }
  else if (buf[0] == 'M' && idx >= 4) {

    if (isdigit(buf[1]) && isdigit(buf[2]) && isdigit(buf[3])) {

      int v = (buf[1] - '0') * 100 +
              (buf[2] - '0') * 10  +
              (buf[3] - '0');

      if (v >= 0 && v <= 450)
        driver_submit_request(REQUEST_AZIMUTH, (float)v);
    }
  }

  idx = 0;
}
}
if (idx && (millis() - last_rx > timeout_ms))
idx = 0;
}
///////////////////////////////////////////////////////////////////////////////
// 19. ETHERNET – PROTOCOL SERVICE
///////////////////////////////////////////////////////////////////////////////
static void protocol_service() {
static byte buf[COMMAND_BUFFER_SIZE];
static int idx = 0;
static unsigned long last_rx = 0;
static unsigned long connection_start = 0;  // NEW
EthernetClient c = server.available();
if (!c) return;
// Record connection start time
if (connection_start == 0) {
connection_start = millis();
}
gs232_process_stream(
c, c,
buf, idx, last_rx,
COMMAND_BUFFER_SIZE,
ETHERNET_MESSAGE_TIMEOUT_MS
);
// Close only if the connection has been inactive for more than 30 seconds
unsigned long now = millis();
if ((now - last_rx) > 30000UL && last_rx > 0) {
c.stop();
connection_start = 0;
}
}
///////////////////////////////////////////////////////////////////////////////
// 20. USB (SERIAL) – PROTOCOL SERVICE
///////////////////////////////////////////////////////////////////////////////
static void usb_protocol_service() {
static byte buf[USB_COMMAND_BUFFER_SIZE];
static int idx = 0;
static unsigned long last_rx = 0;
gs232_process_stream(
Serial, Serial,
buf, idx, last_rx,
USB_COMMAND_BUFFER_SIZE,
USB_MESSAGE_TIMEOUT_MS
);
}
///////////////////////////////////////////////////////////////////////////////
// 21. SETUP – INITIALISATION
///////////////////////////////////////////////////////////////////////////////
void setup() {
// V3.10: BUGFIX – Watchdog enabled FIRST, before any validation loops.
// This guarantees automatic 8-second reset even if a config error
// traps execution in the while(1) blink loops below.
wdt_enable(WDTO_8S);

pinMode(rotate_cw, OUTPUT);
pinMode(rotate_ccw, OUTPUT);
pinMode(rotator_analog_az, INPUT);
pinMode(LED_HARD_LOCKOUT, OUTPUT);
// SPI CS pins – prevent bus conflicts (Ethernet shield)
pinMode(10, OUTPUT);
digitalWrite(10, HIGH);
pinMode(4, OUTPUT);
digitalWrite(4, HIGH);
digitalWrite(LED_HARD_LOCKOUT, LOW);
motor_off();
// V3.7: NEW FUNCTION – Configuration validation at startup
// V3.10: Watchdog is already running here, so the loops below are safe –
//       the device will auto-reset after 8 s if it gets stuck.
float span = (float)(configuration.analog_az_full_cw -
configuration.analog_az_full_ccw);
if (span < ADC_SPAN_MIN_VALUE) {
// CRITICAL ERROR – invalid ADC calibration
// Blink LED rapidly; watchdog will reset the device after 8 seconds
while(1) {
digitalWrite(LED_HARD_LOCKOUT, HIGH);
delay(CONFIG_ERROR_BLINK_MS);
digitalWrite(LED_HARD_LOCKOUT, LOW);
delay(CONFIG_ERROR_BLINK_MS);
}
}
if (configuration.azimuth_rotation_capability < 1 ||
configuration.azimuth_rotation_capability > 450) {
// ERROR – invalid rotator range
// Blink LED slowly; watchdog will reset the device after 8 seconds
while(1) {
digitalWrite(LED_HARD_LOCKOUT, HIGH);
delay(CONFIG_ERROR_BLINK_MS * 2);
digitalWrite(LED_HARD_LOCKOUT, LOW);
delay(CONFIG_ERROR_BLINK_MS * 2);
}
}
// First azimuth reading after startup
driver_read_azimuth(1);
Serial.begin(USB_BAUDRATE);
// Version info via Serial
Serial.println("YAESU Rotator Controller V3.10 (Watchdog Fix)");
Serial.print("Config OK - Range: ");
Serial.print(configuration.azimuth_rotation_capability);
Serial.println(" deg");
// Ethernet – correct signature with DNS
Ethernet.begin(mac, ip, dns, gateway, subnet);
server.begin();
}
///////////////////////////////////////////////////////////////////////////////
// 22. LOOP – MAIN LOOP
///////////////////////////////////////////////////////////////////////////////
void loop() {
// Reset watchdog timer at the start of each cycle
wdt_reset();
driver_service();
protocol_service();
usb_protocol_service();
}
