bb5a205df1
Steering angular velocity (omega) was scaled by the constant max speed (max_v) instead of the actual current forward speed (v_x). This decoupled turn tightness from throttle position: at low/medium throttle, a moderate left-stick steering input already produced an omega large enough to flip the inner wheel's commanded velocity negative, causing the robot to spin/reverse/lurch forward instead of smoothly curving. Scaling by abs(v_x) keeps turn radius proportional to speed as intended, matching prior full-throttle behavior while fixing the reversal at partial throttle. Also commits the ARM binary rebuilt on-device with this fix, per the build-on-target convention for this branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1337 lines
52 KiB
C++
1337 lines
52 KiB
C++
#include <SDL2/SDL.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <csignal>
|
|
#include <fcntl.h>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
#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<std::chrono::milliseconds>(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<uint16_t>(rx_buf[6]) |
|
|
(static_cast<uint16_t>(rx_buf[7]) << 8);
|
|
return resp_crc == recv_crc && rx_buf[0] == slave;
|
|
}
|
|
|
|
bool writeRegs(uint8_t slave, uint16_t reg,
|
|
const std::vector<uint16_t> &vals) {
|
|
size_t count = vals.size();
|
|
size_t pkt_len = 7 + count * 2 + 2;
|
|
std::vector<uint8_t> 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<uint8_t>(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<ssize_t>(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<std::chrono::milliseconds>(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<uint16_t>(rx_buf[6]) |
|
|
(static_cast<uint16_t>(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<uint16_t> &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<uint8_t> 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<std::chrono::milliseconds>(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<uint16_t>(rx_buf[expected_bytes - 2]) |
|
|
(static_cast<uint16_t>(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<uint16_t>(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<int16_t>(std::clamp(l_rpm, -3000.0f, 3000.0f));
|
|
int16_t r_val = static_cast<int16_t>(std::clamp(r_rpm, -3000.0f, 3000.0f));
|
|
port_->writeRegs(
|
|
slave_id_, 0x2088,
|
|
{static_cast<uint16_t>(l_val), static_cast<uint16_t>(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<uint16_t> 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<uint32_t>(regs[2]) << 16) | regs[3];
|
|
l_tick = static_cast<int32_t>(val_l);
|
|
|
|
uint32_t val_r = (static_cast<uint32_t>(regs[4]) << 16) | regs[5];
|
|
r_tick = static_cast<int32_t>(val_r);
|
|
|
|
int16_t vl = static_cast<int16_t>(regs[6]);
|
|
int16_t vr = static_cast<int16_t>(regs[7]);
|
|
l_fb = static_cast<float>(vl) * 0.1f; // 0.1RPM 단위 -> RPM
|
|
r_fb = static_cast<float>(vr) * 0.1f;
|
|
|
|
int16_t tl = static_cast<int16_t>(regs[8]);
|
|
int16_t tr = static_cast<int16_t>(regs[9]);
|
|
l_torque_a = static_cast<float>(tl) * 0.1f; // 0.1A 단위 -> A
|
|
r_torque_a = static_cast<float>(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<uint16_t> 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<bool> 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<std::mutex> 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<std::mutex> lock(status_mutex_);
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
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<uint8_t>(std::stoi(argv[++i]));
|
|
else if (arg == "--id2" && i + 1 < argc)
|
|
id2 = static_cast<uint8_t>(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<uint16_t>(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<float>(ly_raw) / 32767.0f);
|
|
float lx = applyDeadzone(static_cast<float>(lx_raw) / 32767.0f);
|
|
float rx = applyDeadzone(static_cast<float>(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)는 반드시 "현재 전진 속도(v_x)"에 비례해야 커브
|
|
// 반경이 스로틀과 무관하게 일정하게 유지된다. 이전 구현은 상수인
|
|
// max_v(최고속도)를 기준으로 omega를 계산해서, 저속 주행 중에는
|
|
// v_x가 작은데도 omega*k_skid 항은 고속 기준 그대로 커서 안쪽
|
|
// 바퀴(v_l 또는 v_r) 속도 부호가 뒤집혀 후진해버리는 문제가 있었다.
|
|
// (예: 저속으로 스틱을 왼쪽으로 조금만 꺾어도 로봇이 제자리 회전
|
|
// 하듯 튀거나 뒤로 갔다 앞으로 갔다 하는 증상)
|
|
omega += -lx * (std::abs(v_x) / (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<float, std::milli>(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<float, std::milli>(
|
|
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<int>(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;
|
|
} |