#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "livox_lidar_api.h" #include "livox_lidar_def.h" // Global flag for signal handling volatile std::sig_atomic_t g_running = 1; void signalHandler(int signum) { (void)signum; g_running = 0; } // Modbus RTU CRC16 calculation uint16_t calcCRC16(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFF; for (size_t i = 0; i < len; ++i) { crc ^= data[i]; for (int j = 0; j < 8; ++j) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; } class SerialPort { private: int fd_; std::string port_name_; public: SerialPort() : fd_(-1) {} ~SerialPort() { closePort(); } bool openPort(const std::string &port_name, int baudrate = 115200) { port_name_ = port_name; fd_ = open(port_name.c_str(), O_RDWR | O_NOCTTY | O_NDELAY); if (fd_ < 0) { std::cerr << "[오류] C++ 포트 열기 실패: " << port_name << std::endl; return false; } fcntl(fd_, F_SETFL, 0); struct termios options; tcgetattr(fd_, &options); speed_t speed = B115200; switch (baudrate) { case 9600: speed = B9600; break; case 19200: speed = B19200; break; case 38400: speed = B38400; break; case 57600: speed = B57600; break; default: speed = B115200; break; } cfsetispeed(&options, speed); cfsetospeed(&options, speed); options.c_cflag &= ~PARENB; // No parity options.c_cflag &= ~CSTOPB; // 1 stop bit options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; // 8 data bits options.c_cflag &= ~CRTSCTS; // No hardware flow control options.c_cflag |= CREAD | CLOCAL; options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_iflag &= ~(IXON | IXOFF | IXANY | IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); options.c_oflag &= ~OPOST; options.c_cc[VMIN] = 0; options.c_cc[VTIME] = 1; // 0.1s timeout tcsetattr(fd_, TCSANOW, &options); tcflush(fd_, TCIOFLUSH); return true; } void closePort() { if (fd_ >= 0) { close(fd_); fd_ = -1; } } bool writeReg(uint8_t slave, uint16_t reg, uint16_t val) { uint8_t pkt[8]; pkt[0] = slave; pkt[1] = 0x06; pkt[2] = (reg >> 8) & 0xFF; pkt[3] = reg & 0xFF; pkt[4] = (val >> 8) & 0xFF; pkt[5] = val & 0xFF; uint16_t crc = calcCRC16(pkt, 6); pkt[6] = crc & 0xFF; pkt[7] = (crc >> 8) & 0xFF; tcflush(fd_, TCIOFLUSH); ssize_t written = write(fd_, pkt, 8); if (written != 8) return false; if (slave == 0) return true; // Broadcast uint8_t rx_buf[8]; ssize_t rx_bytes = 0; auto start = std::chrono::steady_clock::now(); while (rx_bytes < 8) { ssize_t res = read(fd_, rx_buf + rx_bytes, 8 - rx_bytes); if (res > 0) rx_bytes += res; auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - start) .count() > 50) break; std::this_thread::sleep_for(std::chrono::microseconds(500)); } if (rx_bytes != 8) return false; // CRC 검증: 손상된 응답을 정상으로 오인하지 않도록 확인 uint16_t resp_crc = calcCRC16(rx_buf, 6); uint16_t recv_crc = static_cast(rx_buf[6]) | (static_cast(rx_buf[7]) << 8); return resp_crc == recv_crc && rx_buf[0] == slave; } bool writeRegs(uint8_t slave, uint16_t reg, const std::vector &vals) { size_t count = vals.size(); size_t pkt_len = 7 + count * 2 + 2; std::vector pkt(pkt_len); pkt[0] = slave; pkt[1] = 0x10; pkt[2] = (reg >> 8) & 0xFF; pkt[3] = reg & 0xFF; pkt[4] = (count >> 8) & 0xFF; pkt[5] = count & 0xFF; pkt[6] = static_cast(count * 2); for (size_t i = 0; i < count; ++i) { pkt[7 + i * 2] = (vals[i] >> 8) & 0xFF; pkt[8 + i * 2] = vals[i] & 0xFF; } uint16_t crc = calcCRC16(pkt.data(), pkt_len - 2); pkt[pkt_len - 2] = crc & 0xFF; pkt[pkt_len - 1] = (crc >> 8) & 0xFF; tcflush(fd_, TCIOFLUSH); ssize_t written = write(fd_, pkt.data(), pkt_len); if (written != static_cast(pkt_len)) return false; if (slave == 0) return true; uint8_t rx_buf[8]; ssize_t rx_bytes = 0; auto start = std::chrono::steady_clock::now(); while (rx_bytes < 8) { ssize_t res = read(fd_, rx_buf + rx_bytes, 8 - rx_bytes); if (res > 0) rx_bytes += res; auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - start) .count() > 50) break; std::this_thread::sleep_for(std::chrono::microseconds(500)); } if (rx_bytes != 8) return false; uint16_t resp_crc = calcCRC16(rx_buf, 6); uint16_t recv_crc = static_cast(rx_buf[6]) | (static_cast(rx_buf[7]) << 8); return resp_crc == recv_crc && rx_buf[0] == slave; } bool readRegs(uint8_t slave, uint16_t reg, uint16_t count, std::vector &out_vals) { uint8_t pkt[8]; pkt[0] = slave; pkt[1] = 0x03; pkt[2] = (reg >> 8) & 0xFF; pkt[3] = reg & 0xFF; pkt[4] = (count >> 8) & 0xFF; pkt[5] = count & 0xFF; uint16_t crc = calcCRC16(pkt, 6); pkt[6] = crc & 0xFF; pkt[7] = (crc >> 8) & 0xFF; tcflush(fd_, TCIOFLUSH); if (write(fd_, pkt, 8) != 8) return false; size_t expected_bytes = 5 + count * 2; std::vector rx_buf(expected_bytes); size_t rx_bytes = 0; auto start = std::chrono::steady_clock::now(); while (rx_bytes < expected_bytes) { ssize_t res = read(fd_, rx_buf.data() + rx_bytes, expected_bytes - rx_bytes); if (res > 0) rx_bytes += res; auto now = std::chrono::steady_clock::now(); // 15ms -> 25ms: 100Hz(10ms 주기) 루프 내에서 응답을 안정적으로 받기 위한 // 여유 확보 if (std::chrono::duration_cast(now - start) .count() > 25) break; std::this_thread::sleep_for(std::chrono::microseconds(100)); } if (rx_bytes == expected_bytes && rx_buf[0] == slave && rx_buf[1] == 0x03) { // CRC 검증 추가: 노이즈로 손상된 응답을 정상 데이터로 오인하지 않도록 // 방지 uint16_t resp_crc = calcCRC16(rx_buf.data(), expected_bytes - 2); uint16_t recv_crc = static_cast(rx_buf[expected_bytes - 2]) | (static_cast(rx_buf[expected_bytes - 1]) << 8); if (resp_crc != recv_crc) return false; out_vals.resize(count); for (uint16_t i = 0; i < count; ++i) { out_vals[i] = (static_cast(rx_buf[3 + i * 2]) << 8) | rx_buf[4 + i * 2]; } return true; } return false; } }; class MotorDriver { private: SerialPort *port_; uint8_t slave_id_; public: MotorDriver(SerialPort *port, uint8_t slave_id) : port_(port), slave_id_(slave_id) {} bool initDriver(uint16_t acl_ms = 500, uint16_t dcl_ms = 500) { port_->writeReg(slave_id_, 0x200E, 0x0006); // Alarm clear std::this_thread::sleep_for(std::chrono::milliseconds(30)); port_->writeReg(slave_id_, 0x200E, 0x0007); // Disable std::this_thread::sleep_for(std::chrono::milliseconds(30)); port_->writeReg(slave_id_, 0x200D, 3); // Velocity mode std::this_thread::sleep_for(std::chrono::milliseconds(30)); port_->writeRegs(slave_id_, 0x2080, {acl_ms, acl_ms}); // Accel std::this_thread::sleep_for(std::chrono::milliseconds(30)); port_->writeRegs(slave_id_, 0x2082, {dcl_ms, dcl_ms}); // Decel std::this_thread::sleep_for(std::chrono::milliseconds(30)); port_->writeReg(slave_id_, 0x200E, 0x0008); // Enable std::this_thread::sleep_for(std::chrono::milliseconds(30)); setBrakes(true); return true; } void setBrakes(bool lock) { uint16_t val = lock ? 1 : 0; port_->writeReg(slave_id_, 0x201A, val); port_->writeReg(slave_id_, 0x201B, val); } void setRPMs(float l_rpm, float r_rpm) { int16_t l_val = static_cast(std::clamp(l_rpm, -3000.0f, 3000.0f)); int16_t r_val = static_cast(std::clamp(r_rpm, -3000.0f, 3000.0f)); port_->writeRegs( slave_id_, 0x2088, {static_cast(l_val), static_cast(r_val)}); } bool readFeedback(float &l_fb, float &r_fb, int32_t &l_tick, int32_t &r_tick, float &l_torque_a, float &r_torque_a, uint16_t &err_l, uint16_t &err_r) { std::vector regs; // 0x20A5~0x20AE 10레지스터 연속 읽기: 에러코드(0x20A5/0x20A6)부터 // 포지션 틱, RPM 피드백, 실제 토크(전류)까지 한 번의 통신으로 확보. // 무부하(공중에 뜬) 바퀴는 전류가 급격히 낮아지므로 슬립 진단에 사용, // 에러코드는 과전류/과부하 등 드라이버 알람 발생 시 원인 진단에 사용. if (port_->readRegs(slave_id_, 0x20A5, 10, regs)) { err_l = regs[0]; err_r = regs[1]; // 수정: uint32_t -> int32_t 캐스팅은 2의 보수 표현에서 안전하게 부호가 // 재해석됨. 기존의 "val - 0x100000000ULL" 방식은 uint64_t 승격 후 // int32_t로 축소 캐스팅하는 구현정의 동작(현실적으로는 대부분 동작하지만 // 명확성/이식성이 떨어짐)이라 제거함. uint32_t val_l = (static_cast(regs[2]) << 16) | regs[3]; l_tick = static_cast(val_l); uint32_t val_r = (static_cast(regs[4]) << 16) | regs[5]; r_tick = static_cast(val_r); int16_t vl = static_cast(regs[6]); int16_t vr = static_cast(regs[7]); l_fb = static_cast(vl) * 0.1f; // 0.1RPM 단위 -> RPM r_fb = static_cast(vr) * 0.1f; int16_t tl = static_cast(regs[8]); int16_t tr = static_cast(regs[9]); l_torque_a = static_cast(tl) * 0.1f; // 0.1A 단위 -> A r_torque_a = static_cast(tr) * 0.1f; return true; } return false; } // 정격전류(0x2033/0x2063)·최대전류(0x2034/0x2064) 실측 조회. 채널당 // 2레지스터씩 떨어져 있어 L/R 두 번 읽음(설정값이라 시작 시 1회만 조회). bool readCurrentLimits(uint16_t &rated_l, uint16_t &max_l, uint16_t &rated_r, uint16_t &max_r) { std::vector regs_l, regs_r; if (!port_->readRegs(slave_id_, 0x2033, 2, regs_l)) return false; if (!port_->readRegs(slave_id_, 0x2063, 2, regs_r)) return false; rated_l = regs_l[0]; max_l = regs_l[1]; rated_r = regs_r[0]; max_r = regs_r[1]; return true; } // 최대전류(0x2034/0x2064)를 좌/우 동일 값으로 설정. 단위 0.1A. void setMaxCurrent(uint16_t max_current_01a) { port_->writeReg(slave_id_, 0x2034, max_current_01a); port_->writeReg(slave_id_, 0x2064, max_current_01a); } }; // 드라이버 에러코드(0x20A5/0x20A6) 비트마스크를 사람이 읽을 수 있는 설명으로 변환 std::string decodeDriverError(uint16_t code) { if (code == 0) return ""; std::string s; auto add = [&](uint16_t bit, const char *name) { if (code & bit) { if (!s.empty()) s += "+"; s += name; } }; add(0x0001, "과전압"); add(0x0002, "저전압"); add(0x0004, "과전류"); add(0x0008, "과부하"); add(0x0010, "전류이상(예약)"); add(0x0020, "엔코더오차"); add(0x0040, "속도이상(예약)"); add(0x0080, "기준전압오류"); add(0x0100, "EEPROM오류"); add(0x0200, "홀센서오류"); add(0x0400, "모터과열"); add(0x0800, "엔코더오류"); add(0x2000, "속도설정오류"); if (s.empty()) { char buf[32]; snprintf(buf, sizeof(buf), "알수없음(0x%04X)", code); s = buf; } return s; } // ----------------------------------------------------------------------------- // Livox Mid-360S Single LiDAR Obstacle Detection & Avoidance Class // ----------------------------------------------------------------------------- struct ObstacleStatus { float min_dist_front = 999.0f; // m float min_dist_rear = 999.0f; // m float min_dist_left = 999.0f; // m float min_dist_right = 999.0f; // m bool connected = false; std::chrono::steady_clock::time_point last_update; }; class LidarObstacleDetector { private: std::string config_path_; std::atomic initialized_{false}; std::mutex status_mutex_; ObstacleStatus status_; // Filtering & ROI parameters float min_z_ = -0.40f; // m (Z axis relative to LiDAR after tilt rotation) float max_z_ = 0.80f; // m float min_r_ = 0.15f; // m (blind spot / chassis) float max_r_ = 3.50f; // m (max range) float robot_half_width_ = 0.255f; // m (robot width 0.410m / 2 + 0.05m margin = 0.255m) float pitch_deg_ = 17.0f; // deg (LiDAR downward tilt angle) float lidar_height_ = 0.460f; // m (LiDAR height above ground) float lidar_x_offset_ = 0.250f; // m (LiDAR forward offset 250mm from robot center) float cos_pitch_ = std::cos(17.0f * 3.14159265f / 180.0f); float sin_pitch_ = std::sin(17.0f * 3.14159265f / 180.0f); public: static LidarObstacleDetector *instance_; LidarObstacleDetector() { instance_ = this; } ~LidarObstacleDetector() { stop(); } void setParams(float min_z, float max_z, float robot_half_width, float pitch_deg = 17.0f, float lidar_height = 0.460f, float lidar_x_offset = 0.250f) { min_z_ = min_z; max_z_ = max_z; robot_half_width_ = robot_half_width; pitch_deg_ = pitch_deg; lidar_height_ = lidar_height; lidar_x_offset_ = lidar_x_offset; float pitch_rad = pitch_deg_ * 3.14159265358979323846f / 180.0f; cos_pitch_ = std::cos(pitch_rad); sin_pitch_ = std::sin(pitch_rad); } bool init(const std::string &config_path) { config_path_ = config_path; if (!LivoxLidarSdkInit(config_path_.c_str())) { std::cerr << "[경고] Livox Mid-360S SDK2 초기화 실패 (" << config_path_ << ")\n"; return false; } SetLivoxLidarPointCloudCallBack(PointCloudCallbackStatic, nullptr); SetLivoxLidarInfoChangeCallback(InfoChangeCallbackStatic, nullptr); initialized_ = true; std::cout << "[정보] Livox Mid-360S 라이다 수신 시작 (설정: " << config_path_ << ", 피치 기울임: " << pitch_deg_ << "도, 설치높이: " << lidar_height_ * 1000.0f << "mm, 전방 오프셋: " << lidar_x_offset_ * 1000.0f << "mm)\n"; return true; } void stop() { if (initialized_) { LivoxLidarSdkUninit(); initialized_ = false; std::cout << "[정보] Livox Mid-360S 라이다 수신기 종료.\n"; } } static void PointCloudCallbackStatic(const uint32_t handle, const uint8_t dev_type, LivoxLidarEthernetPacket *data, void *client_data) { (void)dev_type; (void)client_data; if (instance_) instance_->handlePointCloud(handle, data); } static void InfoChangeCallbackStatic(const uint32_t handle, const LivoxLidarInfo *info, void *client_data) { (void)client_data; if (!info) return; std::cout << "\n[라이다 연결 완료] Handle: " << handle << ", SN: " << info->sn << ", IP: " << info->lidar_ip << std::endl; SetLivoxLidarWorkMode(handle, kLivoxLidarNormal, nullptr, nullptr); } void handlePointCloud(uint32_t handle, LivoxLidarEthernetPacket *packet) { (void)handle; if (!packet) return; float curr_front = 999.0f; float curr_rear = 999.0f; float curr_left = 999.0f; float curr_right = 999.0f; auto parsePoint = [&](float raw_x, float raw_y, float raw_z) { // Apply 17 degree pitch rotation (LiDAR tilted downward towards ground) float x = raw_x * cos_pitch_ + raw_z * sin_pitch_; float y = raw_y; float z = -raw_x * sin_pitch_ + raw_z * cos_pitch_; if (z < min_z_ || z > max_z_) return; // Filter ground floor and ceiling float r = std::sqrt(x * x + y * y); if (r < min_r_ || r > max_r_) return; // Coordinate Frame: +X Forward, +Y Left, +Z Up // 1. Front Obstacle Zone if (x > 0.05f && std::abs(y) <= robot_half_width_) { if (x < curr_front) curr_front = x; } // 2. Rear Obstacle Zone else if (x < -0.05f && std::abs(y) <= robot_half_width_) { if (-x < curr_rear) curr_rear = -x; } // 3. Side Obstacle Zones (Front Lookahead window: X in [0.05m, 1.8m]) if (x > 0.05f && x < 1.8f) { if (y > 0.10f && y < 1.2f) { // Left Side float dist = std::sqrt(x * x + y * y); if (dist < curr_left) curr_left = dist; } else if (y < -0.10f && y > -1.2f) { // Right Side float dist = std::sqrt(x * x + y * y); if (dist < curr_right) curr_right = dist; } } }; if (packet->data_type == kLivoxLidarCartesianCoordinateHighData) { LivoxLidarCartesianHighRawPoint *pts = (LivoxLidarCartesianHighRawPoint *)packet->data; for (uint32_t i = 0; i < packet->dot_num; ++i) { parsePoint(pts[i].x / 1000.0f, pts[i].y / 1000.0f, pts[i].z / 1000.0f); } } else if (packet->data_type == kLivoxLidarCartesianCoordinateLowData) { LivoxLidarCartesianLowRawPoint *pts = (LivoxLidarCartesianLowRawPoint *)packet->data; for (uint32_t i = 0; i < packet->dot_num; ++i) { parsePoint(pts[i].x / 100.0f, pts[i].y / 100.0f, pts[i].z / 100.0f); } } std::lock_guard lock(status_mutex_); auto updateEMA = [](float &old_val, float new_val) { if (new_val < old_val) { old_val = new_val; // Fast response to approaching obstacles } else { old_val = old_val * 0.70f + new_val * 0.30f; // Smooth recovery } }; updateEMA(status_.min_dist_front, curr_front); updateEMA(status_.min_dist_rear, curr_rear); updateEMA(status_.min_dist_left, curr_left); updateEMA(status_.min_dist_right, curr_right); status_.connected = true; status_.last_update = std::chrono::steady_clock::now(); } ObstacleStatus getStatus() { std::lock_guard lock(status_mutex_); auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast( now - status_.last_update) .count() > 800) { status_.connected = false; } return status_; } }; LidarObstacleDetector *LidarObstacleDetector::instance_ = nullptr; float applyDeadzone(float val, float deadzone = 0.1f) { if (std::abs(val) < deadzone) return 0.0f; float sign = (val > 0.0f) ? 1.0f : -1.0f; float norm = (std::abs(val) - deadzone) / (1.0f - deadzone); return sign * norm; } int main(int argc, char **argv) { std::signal(SIGINT, signalHandler); std::signal(SIGTERM, signalHandler); std::string port1 = "/dev/ttyUSB0"; std::string port2 = ""; uint8_t id1 = 1; uint8_t id2 = 2; bool bcast_mode = false; float max_v = 0.40f; // m/s float max_spin_v = 0.30f; // m/s float track_width = 0.576f; // 좌우 바퀴 중심 거리 576mm float wheelbase = 0.3684f; // 전후 바퀴 중심 거리 368.4mm float wheel_radius = 0.131517f; // 사용자 실측 정밀 10.35인치 휠 반지름 float accel_rate = 120.0f; // RPM/s (최대 가속도 한계) float decel_rate = 180.0f; // RPM/s (최대 감속도 한계) float jerk_rate = 600.0f; // RPM/s^2 (직진/커브 선회 저크 한계) float spin_jerk_rate = 220.0f; // RPM/s^2 (제자리 회전 저크 한계: 고마찰 노면에서 // 정지마찰이 급격히 파괴되지 않도록 accel_rate에 // 도달하는 시간을 더 길게 늘림) float spin_arc_bias = 0.12f; // 0~1 (제자리 회전 시 섞을 최소 회전반경용 // 미세 전진 성분 비율. ICR을 로봇 중심에서 // 살짝 벗어나게 해 스크럽 마찰 저항을 줄임) float bumper_offset = 0.045f; // 바퀴 축에서 로봇 맨 앞 범퍼까지의 거리 // 바퀴별 이상(들뜸/걸림) 판정 파라미터: 속도 폐루프 특성상 "지령 대비 실제 // 속도" 하나만으로는 무부하(들뜸) 판정이 안 되므로, 동료 바퀴 대비 전류 // 편차와 함께 두 축으로 판정한다. float min_active_rpm = 3.0f; // RPM (이 미만이면 판정 보류: 정지 취급) float airborne_current_ratio = 0.35f; // 동료 바퀴 중앙값 전류 대비 이 비율 // 미만이면 무부하(들뜸)로 판정 float stall_vel_ratio = 0.5f; // 실제속도/지령속도가 이 비율 미만이면 // 속도 지연으로 판단(걸림 후보) float stall_current_a = 12.0f; // A (정격 15A 근접) 이상이면서 속도 // 지연이 동반되면 걸림/과부하로 판정 int airborne_debounce_ticks = 5; // 틱 (100Hz 기준 50ms) 연속 들뜸 판정 // 시에만 실제로 목표속도를 낮춤 std::string log_path = ""; // 비어있으면 로깅 비활성화. --log <파일경로>로 지정 // 실측 결과 정격15A/최대30A(공장 기본값) 그대로였음. 정격 위로 5A 여유는 // 남기되, 걸림/과부하 상황에서 30A까지 밀어붙이며 3초씩 버티다 과부하 // 알람이 터지는 걸 막기 위해 기본값을 20A로 낮춤. --max_current_a로 조정 가능. // 음수를 주면 미변경(공장/기존 설정 유지). float max_current_a = 20.0f; // LiDAR Parameters & Robot Physical Specs (User Specification) bool use_lidar = true; std::string lidar_config = "mid360s_config.json"; float front_stop_dist = 0.35f; // m (전방 완전 정지 거리 0.35m) float front_warn_dist = 0.50f; // m (전방 감속/경고 시작 거리 0.50m = 50cm) float side_dodge_dist = 0.50f; // m (측면 장애물 우회 감지 거리 0.50m = 50cm) float side_stop_dist = 0.20f; // m (측면 한계 접근 거리 0.20m) float max_dodge_omega = 0.35f; // rad/s (자동 회피 최대 조향 각속도) float robot_width = 0.410f; // m (로봇 가로/좌우 실측 전폭 410mm) float robot_length = 0.631f; // m (로봇 세로/전후 실측 전장 631mm) float robot_height = 0.286f; // m (로봇 전고 286mm) float lidar_height = 0.460f; // m (지면 기준 라이다 높이 460mm) float lidar_pitch = 17.0f; // deg (라이다 하향 기울임 17도) float lidar_x_offset = 0.250f; // m (로봇 중심 기준 라이다 전방 오프셋 250mm) float min_z = -lidar_height + 0.06f; // m (-0.40m: 지면 감지 방지 안전 오프셋) float max_z = 0.80f; // m (라이다 기준 상단 높이) for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--port" && i + 1 < argc) port1 = argv[++i]; else if (arg == "--port1" && i + 1 < argc) port1 = argv[++i]; else if (arg == "--port2" && i + 1 < argc) port2 = argv[++i]; else if (arg == "--id1" && i + 1 < argc) id1 = static_cast(std::stoi(argv[++i])); else if (arg == "--id2" && i + 1 < argc) id2 = static_cast(std::stoi(argv[++i])); else if (arg == "--track_width" && i + 1 < argc) track_width = std::stof(argv[++i]); else if (arg == "--wheelbase" && i + 1 < argc) wheelbase = std::stof(argv[++i]); else if (arg == "--radius" && i + 1 < argc) wheel_radius = std::stof(argv[++i]); else if (arg == "--bumper" && i + 1 < argc) bumper_offset = std::stof(argv[++i]); else if (arg == "--accel" && i + 1 < argc) accel_rate = std::stof(argv[++i]); else if (arg == "--decel" && i + 1 < argc) decel_rate = std::stof(argv[++i]); else if (arg == "--jerk" && i + 1 < argc) jerk_rate = std::stof(argv[++i]); else if (arg == "--spin_jerk" && i + 1 < argc) spin_jerk_rate = std::stof(argv[++i]); else if (arg == "--spin_arc_bias" && i + 1 < argc) spin_arc_bias = std::stof(argv[++i]); else if (arg == "--min_active_rpm" && i + 1 < argc) min_active_rpm = std::stof(argv[++i]); else if (arg == "--airborne_current_ratio" && i + 1 < argc) airborne_current_ratio = std::stof(argv[++i]); else if (arg == "--stall_vel_ratio" && i + 1 < argc) stall_vel_ratio = std::stof(argv[++i]); else if (arg == "--stall_current_a" && i + 1 < argc) stall_current_a = std::stof(argv[++i]); else if (arg == "--airborne_debounce_ticks" && i + 1 < argc) airborne_debounce_ticks = std::stoi(argv[++i]); else if (arg == "--log" && i + 1 < argc) log_path = argv[++i]; else if (arg == "--max_current_a" && i + 1 < argc) max_current_a = std::stof(argv[++i]); else if (arg == "--bcast" || arg == "--broadcast") bcast_mode = true; else if (arg == "--lidar_config" && i + 1 < argc) lidar_config = argv[++i]; else if (arg == "--front_stop" && i + 1 < argc) front_stop_dist = std::stof(argv[++i]); else if (arg == "--front_warn" && i + 1 < argc) front_warn_dist = std::stof(argv[++i]); else if (arg == "--side_dodge" && i + 1 < argc) side_dodge_dist = std::stof(argv[++i]); else if (arg == "--robot_width" && i + 1 < argc) robot_width = std::stof(argv[++i]); else if (arg == "--lidar_height" && i + 1 < argc) lidar_height = std::stof(argv[++i]); else if (arg == "--lidar_pitch" && i + 1 < argc) lidar_pitch = std::stof(argv[++i]); else if (arg == "--lidar_x_offset" && i + 1 < argc) lidar_x_offset = std::stof(argv[++i]); else if (arg == "--min_z" && i + 1 < argc) min_z = std::stof(argv[++i]); else if (arg == "--max_z" && i + 1 < argc) max_z = std::stof(argv[++i]); else if (arg == "--no_lidar") use_lidar = false; } constexpr float PI_VAL = 3.14159265358979323846f; float rpm_per_ms = 60.0f / (2.0f * PI_VAL * wheel_radius); // 4WD 유효 선회 계수 float effective_w = std::sqrt(track_width * track_width + wheelbase * wheelbase); float k_skid = (track_width + (wheelbase * wheelbase) / track_width) / 2.0f; std::cout << "\n=============================================================" "=====\n"; std::cout << " [C++17 4WD ZLAC8015D + Mid-360S 라이다 정밀 장애물 회피 시스템]\n"; std::cout << " - 로봇 스펙: 가로(전폭) " << robot_width * 1000.0f << "mm | 세로(전장) " << robot_length * 1000.0f << "mm | 높이 " << robot_height * 1000.0f << "mm\n"; std::cout << " - 라이다 설치: 지면 높이 " << lidar_height * 1000.0f << "mm | 피치 경사각 " << lidar_pitch << "도 (하향) | 전방 오프셋 " << lidar_x_offset * 1000.0f << "mm\n"; std::cout << " - 필터링 범위: 높이 Z(" << min_z << "m ~ " << max_z << "m) | 회피 정밀 전폭 " << (robot_width + 0.10f) * 1000.0f << "mm\n"; std::cout << " - 최고 속도: " << max_v << "m/s | 가속: " << accel_rate << "RPM/s | Jerk: " << jerk_rate << "RPM/s² (제자리회전: " << spin_jerk_rate << "RPM/s²) | 회전 곡선혼합: " << spin_arc_bias * 100.0f << "%\n"; std::cout << " - 휠 이상판정: 무부하(들뜸) 전류비 " << airborne_current_ratio << " | 걸림 속도비 " << stall_vel_ratio << " / 전류 " << stall_current_a << "A 이상\n"; std::cout << " - 라이다 장애물 회피 (LiDAR Dodge): " << (use_lidar ? "활성화 (Mid-360S)" : "비활성화") << "\n"; std::cout << " - 전방 정지 거리: " << front_stop_dist << "m | 전방 경고: " << front_warn_dist << "m | 측면 회피: " << side_dodge_dist << "m\n"; std::cout << "==================================================================\n"; // CSV 로깅: --log <경로> 지정 시 매 틱 지령/피드백/전류/휠상태를 파일로 기록. // 실외에서 로봇을 조종하며 화면을 동시에 읽기 어려우므로, 문제가 된 // 구간(턱 넘는 지점 등)을 나중에 잘라서 분석하기 위한 용도. std::ofstream log_stream; if (!log_path.empty()) { log_stream.open(log_path); if (log_stream.is_open()) { log_stream << "t_ms,state,ly,lx,rx," "cmd_fl,cmd_fr,cmd_rl,cmd_rr," "fb_fl,fb_fr,fb_rl,fb_rr," "amp_fl,amp_fr,amp_rl,amp_rr," "stat_fl,stat_fr,stat_rl,stat_rr," "err_fl,err_fr,err_rl,err_rr," "dist_axle,comm_ms\n"; std::cout << " - CSV 로그 기록: " << log_path << "\n"; } else { std::cerr << "[경고] 로그 파일 열기 실패: " << log_path << "\n"; } } auto log_start_time = std::chrono::steady_clock::now(); // Initialize LiDAR Detector LidarObstacleDetector lidar_detector; if (use_lidar) { // Robot half width tolerance = robot_width / 2 + 5cm margin lidar_detector.setParams(min_z, max_z, robot_width / 2.0f + 0.05f, lidar_pitch, lidar_height, lidar_x_offset); if (!lidar_detector.init(lidar_config)) { std::cout << "[경고] 라이다 연결 실패. 장애물 감지 없이 조이스틱 모드로 " "실행합니다.\n"; } } if (SDL_Init(SDL_INIT_JOYSTICK) < 0) { std::cerr << "[오류] SDL 조이스틱 초기화 실패: " << SDL_GetError() << std::endl; return 1; } if (SDL_NumJoysticks() < 1) { std::cerr << "[경고] 무선 Xbox 컨트롤러(조이스틱)가 연결되지 않았습니다.\n"; SDL_Quit(); return 1; } SDL_Joystick *joystick = SDL_JoystickOpen(0); if (!joystick) { std::cerr << "[오류] 조이스틱 열기 실패!\n"; SDL_Quit(); return 1; } std::cout << "[정보] C++ Xbox 컨트롤러 연결 성공: " << SDL_JoystickName(joystick) << std::endl; SerialPort sp1, sp2; if (!sp1.openPort(port1)) return 1; std::cout << "[정보] RS485 포트1 (" << port1 << ") 오픈 성공.\n"; SerialPort *sp2_ptr = &sp1; if (!port2.empty()) { if (sp2.openPort(port2)) { sp2_ptr = &sp2; std::cout << "[정보] RS485 포트2 (" << port2 << ") 오픈 성공.\n"; } } MotorDriver driver_front(&sp1, bcast_mode ? 0 : id1); driver_front.initDriver(150, 150); MotorDriver *driver_rear_ptr = nullptr; MotorDriver driver_rear(sp2_ptr, bcast_mode ? 0 : id2); if (!bcast_mode) { driver_rear.initDriver(150, 150); driver_rear_ptr = &driver_rear; } // 현재 드라이버에 실제로 설정된 정격/최대전류를 읽어와서 보여준다 // (문서 기본값이 아니라 실제 하드웨어에 저장된 값을 확인하기 위함). // --max_current_a가 지정되면 4바퀴 최대전류를 그 값으로 낮춘다. if (!bcast_mode) { uint16_t f_rated_l, f_max_l, f_rated_r, f_max_r; if (driver_front.readCurrentLimits(f_rated_l, f_max_l, f_rated_r, f_max_r)) { std::cout << " - 전방 드라이버 전류설정: 정격 L" << f_rated_l * 0.1f << "A/R" << f_rated_r * 0.1f << "A | 최대 L" << f_max_l * 0.1f << "A/R" << f_max_r * 0.1f << "A\n"; } else { std::cout << "[경고] 전방 드라이버 전류설정 조회 실패\n"; } if (driver_rear_ptr) { uint16_t r_rated_l, r_max_l, r_rated_r, r_max_r; if (driver_rear_ptr->readCurrentLimits(r_rated_l, r_max_l, r_rated_r, r_max_r)) { std::cout << " - 후방 드라이버 전류설정: 정격 L" << r_rated_l * 0.1f << "A/R" << r_rated_r * 0.1f << "A | 최대 L" << r_max_l * 0.1f << "A/R" << r_max_r * 0.1f << "A\n"; } else { std::cout << "[경고] 후방 드라이버 전류설정 조회 실패\n"; } } if (max_current_a >= 0.0f) { uint16_t new_max_01a = static_cast(max_current_a * 10.0f); driver_front.setMaxCurrent(new_max_01a); if (driver_rear_ptr) driver_rear_ptr->setMaxCurrent(new_max_01a); std::cout << " - 최대전류를 " << max_current_a << "A로 낮춤(4바퀴 동일)\n"; } } std::cout << "\n[정보] C++ 100Hz 초저지연 루프를 시작합니다. (종료: B 버튼 " "또는 Ctrl+C, 라이다 토글: X 버튼)\n\n"; float target_fl = 0.0f, target_fr = 0.0f; float target_rl = 0.0f, target_rr = 0.0f; float cmd_fl = 0.0f, cmd_fr = 0.0f; float cmd_rl = 0.0f, cmd_rr = 0.0f; // 각 바퀴의 현재 가속도 상태(저크 제한 프로파일용) float accel_fl = 0.0f, accel_fr = 0.0f; float accel_rl = 0.0f, accel_rr = 0.0f; const float loop_hz = 100.0f; const float dt = 1.0f / loop_hz; // 저크 제한(jerk-limited) S-curve: 가속도 자체를 매 틱 jerk_limit 만큼만 // 변화시켜 서서히 목표 가속도에 도달하게 함. 기존 구현은 목표 접근 구간만 // 부드러웠고 정지 상태에서 출발할 때는 첫 틱부터 거의 풀가속도가 걸려 // 정지마찰이 급격히 파괴되는 문제가 있었음. auto jerkLimitedStep = [dt](float current_vel, float target_vel, float ¤t_accel, float max_accel_mag, float max_decel_mag, float jerk_limit) { float diff = target_vel - current_vel; if (std::abs(diff) < 0.01f) { current_accel = 0.0f; return target_vel; } bool is_accelerating = std::abs(target_vel) > std::abs(current_vel); float accel_cap = is_accelerating ? max_accel_mag : max_decel_mag; // 이번 틱에 diff를 없애는데 필요한 가속도(부호 포함)를 물리적 한계로 클램프 float desired_accel = std::clamp(diff / dt, -accel_cap, accel_cap); float max_delta_accel = jerk_limit * dt; current_accel = std::clamp(desired_accel, current_accel - max_delta_accel, current_accel + max_delta_accel); float next_vel = current_vel + current_accel * dt; bool overshoot = (diff > 0.0f) ? (next_vel >= target_vel) : (next_vel <= target_vel); if (overshoot) { current_accel = 0.0f; return target_vel; } return next_vel; }; std::string state = "STOPPED"; bool trip_initialized = false; int32_t start_fl = 0, start_fr = 0, start_rl = 0, start_rr = 0; // 바퀴별 이상 상태(직전 틱 피드백 기반 판정, 1틱=10ms 지연으로 반영) bool airborne_fl = false, airborne_fr = false; bool airborne_rl = false, airborne_rr = false; int air_count_fl = 0, air_count_fr = 0, air_count_rl = 0, air_count_rr = 0; char wheel_status_str[5] = "OOOO"; bool fault_active = false; // 드라이버 알람(과전류/과부하 등) 발생 여부 while (g_running) { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_JOYBUTTONDOWN) { if (event.jbutton.button == 0) { // A 버튼: 트립 리셋 trip_initialized = false; } else if (event.jbutton.button == 2) { // X 버튼: 라이다 장애물 회피 토글 use_lidar = !use_lidar; std::cout << "\n[정보] 라이다 회피 기능: " << (use_lidar ? "ON" : "OFF") << std::endl; } else if (event.jbutton.button == 1 || event.jbutton.button == 6) { // B or Back g_running = 0; } } } Sint16 lx_raw = SDL_JoystickGetAxis(joystick, 0); Sint16 ly_raw = SDL_JoystickGetAxis(joystick, 1); Sint16 rx_raw = SDL_JoystickGetAxis(joystick, 3); float ly = applyDeadzone(-static_cast(ly_raw) / 32767.0f); float lx = applyDeadzone(static_cast(lx_raw) / 32767.0f); float rx = applyDeadzone(static_cast(rx_raw) / 32767.0f); if (std::abs(lx) < 0.15f) lx = 0.0f; if (std::abs(rx) < 0.15f) rx = 0.0f; float v_x = ly * max_v; // 선속도 m/s float omega = 0.0f; // 각속도 rad/s // --------------------------------------------------------------------- // LiDAR Obstacle Avoidance & Stopping Logic Integration // (요청에 따라 후진 시 측면 회피는 미적용 - 전진 시에만 좌우 회피 동작) // --------------------------------------------------------------------- std::string lidar_telemetry = "LIDAR:OFF"; if (use_lidar) { ObstacleStatus obs = lidar_detector.getStatus(); if (obs.connected) { float speed_scale = 1.0f; float avoid_omega_offset = 0.0f; // 1. Forward / Backward Automatic Stopping if (v_x > 0.01f) { if (obs.min_dist_front < front_stop_dist) { speed_scale = 0.0f; // Complete forward stop } else if (obs.min_dist_front < front_warn_dist) { speed_scale = (obs.min_dist_front - front_stop_dist) / (front_warn_dist - front_stop_dist); speed_scale = std::clamp(speed_scale, 0.0f, 1.0f); } } else if (v_x < -0.01f) { if (obs.min_dist_rear < front_stop_dist) { speed_scale = 0.0f; // Complete backward stop } } // 2. Side Dodge Steering (전진 중에만 동작) if (v_x > 0.01f && rx == 0.0f) { float right_intensity = 0.0f; float left_intensity = 0.0f; if (obs.min_dist_right < side_dodge_dist) { right_intensity = (side_dodge_dist - obs.min_dist_right) / (side_dodge_dist - side_stop_dist); right_intensity = std::clamp(right_intensity, 0.0f, 1.0f); } if (obs.min_dist_left < side_dodge_dist) { left_intensity = (side_dodge_dist - obs.min_dist_left) / (side_dodge_dist - side_stop_dist); left_intensity = std::clamp(left_intensity, 0.0f, 1.0f); } // Right obstacle -> Steer Left (+omega) // Left obstacle -> Steer Right (-omega) avoid_omega_offset = (right_intensity - left_intensity) * max_dodge_omega; } v_x *= speed_scale; char lbuf[128]; snprintf(lbuf, sizeof(lbuf), "F:%4.2fm|L:%4.2fm|R:%4.2fm %s", obs.min_dist_front > 99.0f ? 9.99f : obs.min_dist_front, obs.min_dist_left > 99.0f ? 9.99f : obs.min_dist_left, obs.min_dist_right > 99.0f ? 9.99f : obs.min_dist_right, avoid_omega_offset > 0.05f ? "[Dodge L]" : (avoid_omega_offset < -0.05f ? "[Dodge R]" : (speed_scale < 0.01f ? "[STOP!]" : "[OK]"))); lidar_telemetry = lbuf; // Add avoidance offset to user steering omega += avoid_omega_offset; } else { lidar_telemetry = "LIDAR:WAITING"; } } // 제자리 회전 여부: 왼쪽 스틱 전후(ly) 입력이 완전히 중립일 때만 진입한다. // 그렇지 않으면(주행 중 오른쪽 스틱이 드리프트 등으로 미세하게라도 감지될 // 경우) 전진 입력이 통째로 무시되고 제자리 회전 로직으로 튀어버려, 커브 // 선회 중에 갑자기 이상하게 회전하려는 문제가 생긴다. ly는 applyDeadzone을 // 거쳐 데드존 이하일 때 정확히 0.0f를 반환하므로 등호 비교가 안전하다. bool is_spin_turn = std::abs(rx) > 0.0f && ly == 0.0f; if (is_spin_turn) { // 제자리 회전 (Spin Turn) + 최소 회전반경용 미세 곡선 혼합 omega = -rx * (max_spin_v / (effective_w / 2.0f)); float v_bias = std::abs(rx) * max_spin_v * spin_arc_bias; float v_l = v_bias - omega * (effective_w / 2.0f); float v_r = v_bias + omega * (effective_w / 2.0f); target_fl = v_l * rpm_per_ms; target_fr = -v_r * rpm_per_ms; target_rl = v_l * rpm_per_ms; target_rr = -v_r * rpm_per_ms; } else { // 직진 및 차동 선회 Kinematics if (std::abs(rx) == 0.0f && std::abs(lx) == 0.0f && std::abs(omega) > 0.0f) { // Automated avoidance steering active float v_l = v_x - omega * k_skid; float v_r = v_x + omega * k_skid; target_fl = v_l * rpm_per_ms; target_fr = -v_r * rpm_per_ms; target_rl = v_l * rpm_per_ms; target_rr = -v_r * rpm_per_ms; } else { omega += -lx * (max_v / (effective_w / 2.0f)); float v_l = v_x - omega * k_skid; float v_r = v_x + omega * k_skid; target_fl = v_l * rpm_per_ms; target_fr = -v_r * rpm_per_ms; target_rl = v_l * rpm_per_ms; target_rr = -v_r * rpm_per_ms; } } // 직전 틱 피드백에서 무부하(들뜸)로 판정된 바퀴는 목표 속도를 0으로 낮춰 // 헛돌이를 억제한다. 접지력이 회복되어 전류가 정상으로 돌아오면 다음 // 판정 틱에서 자동으로 해제되어 원래 지령으로 복귀한다. if (airborne_fl) target_fl = 0.0f; if (airborne_fr) target_fr = 0.0f; if (airborne_rl) target_rl = 0.0f; if (airborne_rr) target_rr = 0.0f; if (state == "STOPPED") { if (std::abs(target_fl) > 0.1f || std::abs(target_fr) > 0.1f) { driver_front.setBrakes(false); if (driver_rear_ptr) driver_rear_ptr->setBrakes(false); std::this_thread::sleep_for(std::chrono::milliseconds(50)); state = "RUNNING"; } } else if (state == "RUNNING") { float jerk_now = is_spin_turn ? spin_jerk_rate : jerk_rate; cmd_fl = jerkLimitedStep(cmd_fl, target_fl, accel_fl, accel_rate, decel_rate, jerk_now); cmd_fr = jerkLimitedStep(cmd_fr, target_fr, accel_fr, accel_rate, decel_rate, jerk_now); cmd_rl = jerkLimitedStep(cmd_rl, target_rl, accel_rl, accel_rate, decel_rate, jerk_now); cmd_rr = jerkLimitedStep(cmd_rr, target_rr, accel_rr, accel_rate, decel_rate, jerk_now); if (std::abs(target_fl) < 0.1f && std::abs(target_fr) < 0.1f && std::abs(cmd_fl) < 0.5f && std::abs(cmd_fr) < 0.5f) { state = "STOPPING"; } } else if (state == "STOPPING") { float jerk_now = is_spin_turn ? spin_jerk_rate : jerk_rate; cmd_fl = jerkLimitedStep(cmd_fl, target_fl, accel_fl, decel_rate, decel_rate, jerk_now); cmd_fr = jerkLimitedStep(cmd_fr, target_fr, accel_fr, decel_rate, decel_rate, jerk_now); cmd_rl = jerkLimitedStep(cmd_rl, target_rl, accel_rl, decel_rate, decel_rate, jerk_now); cmd_rr = jerkLimitedStep(cmd_rr, target_rr, accel_rr, decel_rate, decel_rate, jerk_now); if (std::abs(cmd_fl) < 0.1f && std::abs(cmd_fr) < 0.1f) { cmd_fl = 0.0f; cmd_fr = 0.0f; cmd_rl = 0.0f; cmd_rr = 0.0f; accel_fl = accel_fr = accel_rl = accel_rr = 0.0f; driver_front.setRPMs(0, 0); if (driver_rear_ptr) driver_rear_ptr->setRPMs(0, 0); std::this_thread::sleep_for(std::chrono::milliseconds(50)); driver_front.setBrakes(true); if (driver_rear_ptr) driver_rear_ptr->setBrakes(true); state = "STOPPED"; } } auto comm_start = std::chrono::high_resolution_clock::now(); if (state != "STOPPED") { driver_front.setRPMs(cmd_fl, cmd_fr); if (driver_rear_ptr) driver_rear_ptr->setRPMs(cmd_rl, cmd_rr); } float fl_fb = 0, fr_fb = 0, rl_fb = 0, rr_fb = 0; float fl_amp = 0, fr_amp = 0, rl_amp = 0, rr_amp = 0; int32_t fl_tick = 0, fr_tick = 0, rl_tick = 0, rr_tick = 0; uint16_t err_f_l = 0, err_f_r = 0, err_r_l = 0, err_r_r = 0; driver_front.readFeedback(fl_fb, fr_fb, fl_tick, fr_tick, fl_amp, fr_amp, err_f_l, err_f_r); if (driver_rear_ptr) driver_rear_ptr->readFeedback(rl_fb, rr_fb, rl_tick, rr_tick, rl_amp, rr_amp, err_r_l, err_r_r); // 드라이버 알람(과전류/과부하 등) 감지: 새로 발생한 알람만 콘솔에 // 한 번 크게 출력한다 (매 틱 갱신되는 HUD 줄과 별개의 고정 줄). // 알람이 뜨면 드라이버가 명령을 무시하는 잠금 상태가 되어 소프트웨어 // 재시작만으로는 안 풀리는 경우가 많다(하드웨어 클리어/재활성화 필요). bool fault_now = err_f_l || err_f_r || err_r_l || err_r_r; if (fault_now && !fault_active) { std::cout << "\n\n[!!! 드라이버 알람 발생 !!!] " << "전방L:" << decodeDriverError(err_f_l) << " 전방R:" << decodeDriverError(err_f_r) << " 후방L:" << decodeDriverError(err_r_l) << " 후방R:" << decodeDriverError(err_r_r) << " -> 모터 정지, 전원 재시작 또는 알람클리어 필요\n\n"; } fault_active = fault_now; auto comm_end = std::chrono::high_resolution_clock::now(); float comm_ms = std::chrono::duration(comm_end - comm_start).count(); // 바퀴별 이상(들뜸/걸림) 판정: 속도 폐루프 특성상 "지령 대비 실제속도" // 만으로는 무부하(들뜸)를 구분할 수 없다 (드라이버가 부하와 무관하게 // 지령 RPM을 그대로 추종하려 하므로). 그래서 두 축으로 판정한다. // - 걸림/과부하: 실제속도가 지령을 크게 못 따라가면서 전류가 높음 (독립 판정) // - 무부하/들뜸: "같은 지령속도를 받는 짝(앞뒤 같은 쪽)" 대비 전류가 // 비정상적으로 낮음. 좌/우를 통째로 비교하면 정상적인 커브 선회에서 // 지령속도가 원래 다른 좌우 바퀴를 오탐하므로, 반드시 target_fl=target_rl, // target_fr=target_rr 로 지령이 항상 같은 같은쪽 앞뒤 쌍끼리만 비교한다. auto classifyStall = [&](float cmd_rpm, float actual_rpm, float amp) -> bool { float cmd_abs = std::abs(cmd_rpm); if (cmd_abs < min_active_rpm) return false; float vel_ratio = std::abs(actual_rpm) / cmd_abs; return vel_ratio < stall_vel_ratio && std::abs(amp) >= stall_current_a; }; bool stall_fl = classifyStall(cmd_fl, fl_fb, fl_amp); bool stall_fr = classifyStall(cmd_fr, fr_fb, fr_amp); bool stall_rl = classifyStall(cmd_rl, rl_fb, rl_amp); bool stall_rr = classifyStall(cmd_rr, rr_fb, rr_amp); bool air_now_fl = false, air_now_rl = false; bool air_now_fr = false, air_now_rr = false; auto classifyAirPair = [&](float cmd_rpm, float amp_a, float amp_b, bool &air_a, bool &air_b) { if (std::abs(cmd_rpm) < min_active_rpm) return; // 정지/저속 중이면 판정 보류 float a = std::abs(amp_a), b = std::abs(amp_b); float hi = std::max(a, b); if (hi < 0.5f) return; // 둘 다 거의 무전류(관성 주행 등)면 판정 보류 if (a < hi * airborne_current_ratio) air_a = true; if (b < hi * airborne_current_ratio) air_b = true; }; classifyAirPair(cmd_fl, fl_amp, rl_amp, air_now_fl, air_now_rl); classifyAirPair(cmd_fr, fr_amp, rr_amp, air_now_fr, air_now_rr); // 디바운스: 노이즈성 순간 전류 편차로 목표속도가 즉시 0으로 깎이지 않도록 // 연속 airborne_debounce_ticks 틱 이상 지속될 때만 실제로 개입한다. air_count_fl = air_now_fl ? air_count_fl + 1 : 0; air_count_fr = air_now_fr ? air_count_fr + 1 : 0; air_count_rl = air_now_rl ? air_count_rl + 1 : 0; air_count_rr = air_now_rr ? air_count_rr + 1 : 0; airborne_fl = air_count_fl >= airborne_debounce_ticks; airborne_fr = air_count_fr >= airborne_debounce_ticks; airborne_rl = air_count_rl >= airborne_debounce_ticks; airborne_rr = air_count_rr >= airborne_debounce_ticks; auto statusChar = [](bool air, bool stall) { return air ? 'A' : (stall ? 'S' : 'O'); }; wheel_status_str[0] = statusChar(airborne_fl, stall_fl); wheel_status_str[1] = statusChar(airborne_fr, stall_fr); wheel_status_str[2] = statusChar(airborne_rl, stall_rl); wheel_status_str[3] = statusChar(airborne_rr, stall_rr); float meters_per_tick = (2.0f * PI_VAL * wheel_radius) / 16384.0f; if (!trip_initialized && (fl_tick != 0 || fr_tick != 0)) { start_fl = fl_tick; start_fr = fr_tick; start_rl = rl_tick; start_rr = rr_tick; trip_initialized = true; } int32_t delta_fl = std::abs(fl_tick - start_fl); int32_t delta_fr = std::abs(fr_tick - start_fr); int32_t delta_rl = std::abs(rl_tick - start_rl); int32_t delta_rr = std::abs(rr_tick - start_rr); float dist_fl = delta_fl * meters_per_tick; float dist_fr = delta_fr * meters_per_tick; float dist_rl = delta_rl * meters_per_tick; float dist_rr = delta_rr * meters_per_tick; // 중앙값(4개 중 가운데 2개 평균) 기반 주행거리 산출: 웅덩이 등으로 바퀴 // 하나가 지면에서 떨어져 엔코더만 헛돌 때, 그 이상치 값이 평균(mean)을 // 오염시키지 않도록 최댓값/최솟값을 자동으로 배제함. float dist_sorted[4] = {dist_fl, dist_fr, dist_rl, dist_rr}; std::sort(dist_sorted, dist_sorted + 4); float dist_axle = (dist_sorted[1] + dist_sorted[2]) / 2.0f; float dist_bumper = dist_axle + (dist_axle > 0.001f ? bumper_offset : 0.0f); (void)dist_bumper; if (log_stream.is_open()) { float t_ms = std::chrono::duration( std::chrono::steady_clock::now() - log_start_time) .count(); log_stream << t_ms << ',' << state << ',' << ly << ',' << lx << ',' << rx << ',' << cmd_fl << ',' << cmd_fr << ',' << cmd_rl << ',' << cmd_rr << ',' << fl_fb << ',' << fr_fb << ',' << rl_fb << ',' << rr_fb << ',' << fl_amp << ',' << fr_amp << ',' << rl_amp << ',' << rr_amp << ',' << wheel_status_str[0] << ',' << wheel_status_str[1] << ',' << wheel_status_str[2] << ',' << wheel_status_str[3] << ',' << err_f_l << ',' << err_f_r << ',' << err_r_l << ',' << err_r_r << ',' << dist_axle << ',' << comm_ms << '\n'; } printf("\r\033[K[%-8s] AXLE:%5.3fm | %-28s | I(A):FL%+4.1f FR%+4.1f " "RL%+4.1f RR%+4.1f | WHL(FL/FR/RL/RR):%s%s | %4.1fms", state.c_str(), dist_axle, lidar_telemetry.c_str(), fl_amp, fr_amp, rl_amp, rr_amp, wheel_status_str, fault_active ? " [!!알람!!]" : "", comm_ms); fflush(stdout); std::this_thread::sleep_for( std::chrono::microseconds(static_cast(dt * 1000000))); } std::cout << "\n\n[정보] C++ 4WD 안전 정지 및 브레이크 잠금 처리 중...\n"; driver_front.setRPMs(0, 0); if (driver_rear_ptr) driver_rear_ptr->setRPMs(0, 0); std::this_thread::sleep_for(std::chrono::milliseconds(100)); driver_front.setBrakes(true); if (driver_rear_ptr) driver_rear_ptr->setBrakes(true); if (use_lidar) lidar_detector.stop(); if (log_stream.is_open()) { log_stream.close(); std::cout << "[정보] CSV 로그 저장 완료: " << log_path << "\n"; } if (joystick) SDL_JoystickClose(joystick); SDL_Quit(); std::cout << "[정보] C++ 4WD 제어 프로그램이 성공적으로 종료되었습니다.\n"; return 0; }