|
|
|
@@ -1,12 +1,14 @@
|
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <csignal>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <map>
|
|
|
|
|
#include <mutex>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
@@ -601,34 +603,330 @@ public:
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
// RadioMaster Pocket + XR1 V1.0 RC 수신기 (USB-to-TTL 시리얼) 입력 모듈
|
|
|
|
|
// 실기 캡처로 확인한 실제 프로토콜: CRSF(Crossfire), 420000bps, 8N1.
|
|
|
|
|
// 프레임: [0]=Sync(0xC8) [1]=Length [2]=FrameType [3..]=Payload [-1]=CRC8
|
|
|
|
|
// FrameType 0x16(RC_CHANNELS_PACKED)의 22바이트 payload에 16채널이 11비트씩
|
|
|
|
|
// 리틀엔디안으로 패킹되어 있다 (raw 172~1811 -> 1000~2000us로 선형 변환).
|
|
|
|
|
// 참고: mac/mac_code (맥에서 정상 동작 확인된 레퍼런스 파이썬 스크립트)와
|
|
|
|
|
// 동일한 파싱 방식을 따른다 — 실측 결과 CRC8이 표준 DVB-S2(poly 0xD5) 값과
|
|
|
|
|
// 맞지 않아(송신기별 구현 차이로 추정) 레퍼런스와 동일하게 CRC는 검증하지
|
|
|
|
|
// 않고 Sync+Length 기반 프레이밍만 신뢰한다.
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
#ifndef BOTHER
|
|
|
|
|
#define BOTHER 0010000 // 커널 termbits.h: CBAUDEX와 동일값, 임의 보드레이트 지정용
|
|
|
|
|
#endif
|
|
|
|
|
struct termios2 { // <asm/termbits.h>의 커널 ABI와 동일 레이아웃 (glibc의
|
|
|
|
|
// struct termios와 이름 충돌을 피하기 위해 직접 선언).
|
|
|
|
|
// 420000bps처럼 표준 B-상수가 없는 보드레이트를
|
|
|
|
|
// TCSETS2/BOTHER ioctl로 설정하기 위해 필요하다.
|
|
|
|
|
tcflag_t c_iflag, c_oflag, c_cflag, c_lflag;
|
|
|
|
|
cc_t c_line;
|
|
|
|
|
cc_t c_cc[19];
|
|
|
|
|
speed_t c_ispeed, c_ospeed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct RcConfig {
|
|
|
|
|
std::string port = "/dev/ttyUSB0"; // 현재 고정 배선: RC 수신기(XR1) = ttyUSB0
|
|
|
|
|
int baud = 420000; // CRSF 표준 보드레이트
|
|
|
|
|
|
|
|
|
|
int ch_steer = 1; // CH1: 좌우 조향
|
|
|
|
|
int ch_throttle = 2; // CH2: 전후진
|
|
|
|
|
int ch_brake = 5; // CH5: 비상 브레이크
|
|
|
|
|
int ch_speed = 6; // CH6: 속도 모드
|
|
|
|
|
|
|
|
|
|
// 실기 캡처로 확인한 실측값 (좌 1005 ~ 중 1481~1503 ~ 우 2000)
|
|
|
|
|
int steer_min = 1005, steer_center_lo = 1481, steer_center_hi = 1503,
|
|
|
|
|
steer_max = 2000;
|
|
|
|
|
// 실측값: 후진끝 1005 ~ 중 1500 ~ 전진끝 2000 (CH2는 값이 높을수록 전진 —
|
|
|
|
|
// CH1과 반대 방향이라 아래 3.3 매핑 로직도 그에 맞춰 부호가 반대다)
|
|
|
|
|
int throttle_min = 1005, throttle_center_lo = 1480, throttle_center_hi = 1520,
|
|
|
|
|
throttle_max = 2000;
|
|
|
|
|
|
|
|
|
|
int brake_threshold = 1500; // 이상이면 브레이크 ON
|
|
|
|
|
int speed_threshold = 1500; // 이상이면 저속 모드
|
|
|
|
|
|
|
|
|
|
// CH1/CH2(연속 아날로그 스틱) 슬루레이트 한계: 메인 루프 한 틱(~10ms)
|
|
|
|
|
// 사이에 raw 값이 이 이상 튀면 사람이 스틱으로 낼 수 없는 변화로 보고
|
|
|
|
|
// 손상된 프레임으로 간주해 버린다. CH5/CH6(스위치)는 원래 한 번에 크게
|
|
|
|
|
// 뛰는 게 정상이라 이 제한을 적용하지 않는다.
|
|
|
|
|
int max_slew_per_tick = 200;
|
|
|
|
|
|
|
|
|
|
float v_max_low = 0.3f; // m/s (CH6 저속 모드)
|
|
|
|
|
float v_max_high = 1.5f; // m/s (CH6 고속 모드)
|
|
|
|
|
|
|
|
|
|
int failsafe_timeout_ms = 200; // 이 시간 이상 신호 미수신 시 비상정지
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class RcReceiver {
|
|
|
|
|
public:
|
|
|
|
|
explicit RcReceiver(const RcConfig &cfg) : cfg_(cfg) {}
|
|
|
|
|
~RcReceiver() { stop(); }
|
|
|
|
|
|
|
|
|
|
void start() {
|
|
|
|
|
running_ = true;
|
|
|
|
|
thread_ = std::thread(&RcReceiver::run, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void stop() {
|
|
|
|
|
running_ = false;
|
|
|
|
|
if (thread_.joinable())
|
|
|
|
|
thread_.join();
|
|
|
|
|
closePort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 채널 raw 값 조회. 아직 한 번도 수신하지 못했으면 false.
|
|
|
|
|
bool getChannel(int ch, int &out_val) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
auto it = channels_.find(ch);
|
|
|
|
|
if (it == channels_.end())
|
|
|
|
|
return false;
|
|
|
|
|
out_val = it->second;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 포트가 열려 있고, failsafe_timeout_ms 이내에 유효한 패킷을 수신했는가.
|
|
|
|
|
bool isConnected() {
|
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
if (!port_open_ || channels_.empty())
|
|
|
|
|
return false;
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
now - last_rx_time_)
|
|
|
|
|
.count() <= cfg_.failsafe_timeout_ms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
RcConfig cfg_;
|
|
|
|
|
std::atomic<bool> running_{false};
|
|
|
|
|
std::thread thread_;
|
|
|
|
|
int fd_ = -1;
|
|
|
|
|
|
|
|
|
|
std::mutex mutex_;
|
|
|
|
|
std::map<int, int> channels_;
|
|
|
|
|
std::chrono::steady_clock::time_point last_rx_time_{};
|
|
|
|
|
bool port_open_ = false;
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> frame_buf_;
|
|
|
|
|
|
|
|
|
|
// 채널별 미디언(median-of-3) 필터 히스토리. 이 송신기는 CRC/체크섬이
|
|
|
|
|
// 표준 방식으로 검증되지 않아(전수조사로 확인) 단발성 손상 프레임이
|
|
|
|
|
// 섞여 들어올 수 있는데, 최근 3개 raw 값 중 가운데 값을 채택하면 3개
|
|
|
|
|
// 중 하나가 손상돼도 자동으로 걸러진다. 스위치 채널도 전환 시 최대
|
|
|
|
|
// 1프레임(~4ms)만 지연될 뿐이라 안전하게 전 채널에 적용 가능하다.
|
|
|
|
|
std::map<int, std::array<int, 2>> raw_history_;
|
|
|
|
|
std::map<int, int> history_count_;
|
|
|
|
|
|
|
|
|
|
static int median3(int a, int b, int c) {
|
|
|
|
|
if ((a <= b && b <= c) || (c <= b && b <= a))
|
|
|
|
|
return b;
|
|
|
|
|
if ((b <= a && a <= c) || (c <= a && a <= b))
|
|
|
|
|
return a;
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static constexpr uint8_t CRSF_SYNC = 0xC8;
|
|
|
|
|
static constexpr uint8_t CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16;
|
|
|
|
|
static constexpr size_t CRSF_MAX_FRAME_LEN = 64; // 스펙상 프레임은 훨씬 작음
|
|
|
|
|
|
|
|
|
|
bool openPort() {
|
|
|
|
|
fd_ = open(cfg_.port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY);
|
|
|
|
|
if (fd_ < 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
fcntl(fd_, F_SETFL, O_NONBLOCK);
|
|
|
|
|
|
|
|
|
|
// CRSF 표준 420000bps는 termios의 표준 B-상수 목록에 없어 TCSETS2 +
|
|
|
|
|
// BOTHER + c_ispeed/c_ospeed(임의 정수 보드레이트) ioctl로 설정한다.
|
|
|
|
|
struct termios2 tio;
|
|
|
|
|
if (ioctl(fd_, TCGETS2, &tio) < 0) {
|
|
|
|
|
close(fd_);
|
|
|
|
|
fd_ = -1;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tio.c_cflag &= ~CBAUD;
|
|
|
|
|
tio.c_cflag |= BOTHER;
|
|
|
|
|
tio.c_ispeed = static_cast<speed_t>(cfg_.baud);
|
|
|
|
|
tio.c_ospeed = static_cast<speed_t>(cfg_.baud);
|
|
|
|
|
|
|
|
|
|
tio.c_cflag &= ~PARENB;
|
|
|
|
|
tio.c_cflag &= ~CSTOPB;
|
|
|
|
|
tio.c_cflag &= ~CSIZE;
|
|
|
|
|
tio.c_cflag |= CS8;
|
|
|
|
|
tio.c_cflag &= ~CRTSCTS;
|
|
|
|
|
tio.c_cflag |= CREAD | CLOCAL;
|
|
|
|
|
|
|
|
|
|
// 바이너리 프로토콜이므로 raw 모드(비정규/논캐노니컬)로 읽는다.
|
|
|
|
|
tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | IEXTEN);
|
|
|
|
|
tio.c_iflag &= ~(IXON | IXOFF | IXANY | IGNBRK | BRKINT | PARMRK |
|
|
|
|
|
ISTRIP | INLCR | IGNCR | ICRNL);
|
|
|
|
|
tio.c_oflag &= ~OPOST;
|
|
|
|
|
tio.c_cc[VMIN] = 0;
|
|
|
|
|
tio.c_cc[VTIME] = 0;
|
|
|
|
|
|
|
|
|
|
if (ioctl(fd_, TCSETS2, &tio) < 0) {
|
|
|
|
|
close(fd_);
|
|
|
|
|
fd_ = -1;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
tcflush(fd_, TCIOFLUSH);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void closePort() {
|
|
|
|
|
if (fd_ >= 0) {
|
|
|
|
|
close(fd_);
|
|
|
|
|
fd_ = -1;
|
|
|
|
|
}
|
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
port_open_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 축적된 바이트 버퍼에서 CRSF 프레임을 최대한 뽑아낸다. Sync 바이트를
|
|
|
|
|
// 찾고, 선언된 Length만큼 버퍼에 데이터가 쌓일 때까지 기다린 뒤
|
|
|
|
|
// RC_CHANNELS_PACKED(0x16) 프레임이면 16채널(11비트씩)을 언패킹한다.
|
|
|
|
|
// (CRC8은 이 송신기에서 표준 DVB-S2 값과 맞지 않아 검증하지 않음 — 실기
|
|
|
|
|
// 캡처로 확인된 mac/mac_code 레퍼런스와 동일한 방식.)
|
|
|
|
|
void parseCrsfBuffer() {
|
|
|
|
|
std::map<int, int> parsed;
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
while (i < frame_buf_.size()) {
|
|
|
|
|
if (frame_buf_[i] != CRSF_SYNC) {
|
|
|
|
|
++i;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (i + 1 >= frame_buf_.size())
|
|
|
|
|
break; // length 바이트 대기
|
|
|
|
|
|
|
|
|
|
uint8_t length = frame_buf_[i + 1];
|
|
|
|
|
size_t total_len = static_cast<size_t>(length) + 2;
|
|
|
|
|
if (length < 2 || total_len > CRSF_MAX_FRAME_LEN) {
|
|
|
|
|
// Sync처럼 보이는 노이즈 바이트: 1바이트만 버리고 재동기화
|
|
|
|
|
++i;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (i + total_len > frame_buf_.size())
|
|
|
|
|
break; // 프레임 전체 수신 대기
|
|
|
|
|
|
|
|
|
|
uint8_t frame_type = frame_buf_[i + 2];
|
|
|
|
|
if (frame_type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED && length >= 23) {
|
|
|
|
|
// payload = frame_buf_[i+3 .. i+total_len-2] (CRC 바이트 제외 22바이트
|
|
|
|
|
// = 176비트 = 16채널×11비트). uint64_t 하나에 통째로 담으면 64비트를
|
|
|
|
|
// 넘는 시프트가 발생해 정의되지 않은 동작(UB)이 되므로, 32비트 롤링
|
|
|
|
|
// 비트 누산기로 바이트를 하나씩 채워가며 11비트씩 뽑아낸다.
|
|
|
|
|
uint32_t bit_accum = 0;
|
|
|
|
|
int bit_count = 0;
|
|
|
|
|
int byte_idx = 0;
|
|
|
|
|
for (int ch = 0; ch < 16; ++ch) {
|
|
|
|
|
while (bit_count < 11) {
|
|
|
|
|
bit_accum |= static_cast<uint32_t>(frame_buf_[i + 3 + byte_idx])
|
|
|
|
|
<< bit_count;
|
|
|
|
|
bit_count += 8;
|
|
|
|
|
++byte_idx;
|
|
|
|
|
}
|
|
|
|
|
int raw_val = static_cast<int>(bit_accum & 0x7FFU);
|
|
|
|
|
bit_accum >>= 11;
|
|
|
|
|
bit_count -= 11;
|
|
|
|
|
int us_val = static_cast<int>(
|
|
|
|
|
std::lround((raw_val - 172) * 1000.0 / 1639.0 + 1000.0));
|
|
|
|
|
|
|
|
|
|
// 미디언-3 필터: 최근 2개 raw 값 + 이번 값 중 가운데 값을 채택.
|
|
|
|
|
// 히스토리는 항상 raw 값으로 갱신한다(필터링된 결과가 아니라).
|
|
|
|
|
int ch_num = ch + 1;
|
|
|
|
|
std::array<int, 2> &hist = raw_history_[ch_num];
|
|
|
|
|
int &count = history_count_[ch_num];
|
|
|
|
|
int filtered = us_val;
|
|
|
|
|
if (count >= 2) {
|
|
|
|
|
filtered = median3(hist[0], hist[1], us_val);
|
|
|
|
|
} else {
|
|
|
|
|
++count; // 워밍업 구간(처음 2개)은 그대로 통과
|
|
|
|
|
}
|
|
|
|
|
hist[0] = hist[1];
|
|
|
|
|
hist[1] = us_val;
|
|
|
|
|
parsed[ch_num] = filtered; // CH1..CH16 (1-based)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
i += total_len;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 처리하지 못한 잔여 바이트(다음 프레임 일부)만 남기고 버퍼 정리
|
|
|
|
|
frame_buf_.erase(frame_buf_.begin(), frame_buf_.begin() + i);
|
|
|
|
|
if (frame_buf_.size() > CRSF_MAX_FRAME_LEN * 4) // 비정상 누적 방지
|
|
|
|
|
frame_buf_.clear();
|
|
|
|
|
|
|
|
|
|
if (!parsed.empty()) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
for (auto &kv : parsed)
|
|
|
|
|
channels_[kv.first] = kv.second;
|
|
|
|
|
last_rx_time_ = std::chrono::steady_clock::now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void run() {
|
|
|
|
|
// now() - 1001ms로 초기화: time_point::min()을 쓰면 첫 비교에서
|
|
|
|
|
// "now - sentinel"이 부호있는 정수 오버플로우(UB)를 일으켜 -O3에서
|
|
|
|
|
// 재연결 시도 자체가 영구히 스킵될 수 있다.
|
|
|
|
|
auto last_open_attempt =
|
|
|
|
|
std::chrono::steady_clock::now() - std::chrono::milliseconds(1001);
|
|
|
|
|
char buf[256];
|
|
|
|
|
|
|
|
|
|
while (running_) {
|
|
|
|
|
if (fd_ < 0) {
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
now - last_open_attempt)
|
|
|
|
|
.count() > 1000) {
|
|
|
|
|
last_open_attempt = now;
|
|
|
|
|
if (openPort()) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
port_open_ = true;
|
|
|
|
|
std::cout << "\n[정보] RC 수신기 포트(" << cfg_.port
|
|
|
|
|
<< ") 연결 성공.\n";
|
|
|
|
|
} else {
|
|
|
|
|
std::cerr << "\n[경고] RC 수신기 포트(" << cfg_.port
|
|
|
|
|
<< ") 열기 실패. 1초 후 재시도.\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ssize_t n = read(fd_, buf, sizeof(buf));
|
|
|
|
|
if (n > 0) {
|
|
|
|
|
frame_buf_.insert(frame_buf_.end(), buf, buf + n);
|
|
|
|
|
parseCrsfBuffer();
|
|
|
|
|
} else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
|
|
|
|
std::cerr << "\n[경고] RC 수신기 포트 통신 오류. 재연결 시도...\n";
|
|
|
|
|
closePort();
|
|
|
|
|
} else {
|
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
|
|
|
std::signal(SIGINT, signalHandler);
|
|
|
|
|
std::signal(SIGTERM, signalHandler);
|
|
|
|
|
|
|
|
|
|
std::string port1 = "/dev/ttyUSB0";
|
|
|
|
|
std::string port1 = "/dev/ttyUSB1"; // 현재 고정 배선: 모터 드라이버 = ttyUSB1
|
|
|
|
|
std::string port2 = "";
|
|
|
|
|
uint8_t id1 = 1;
|
|
|
|
|
uint8_t id2 = 2;
|
|
|
|
|
bool bcast_mode = false;
|
|
|
|
|
float max_v = 0.40f; // m/s
|
|
|
|
|
RcConfig rc_cfg;
|
|
|
|
|
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 (최대 가속도 한계)
|
|
|
|
|
// 가속은 역기전력 보호회로가 하드웨어로 구성되어 목표값에 1:1로 즉시
|
|
|
|
|
// 추종하므로 별도의 가속 한계값이 없다. decel_rate/jerk_rate(및
|
|
|
|
|
// spin_jerk_rate)는 감속에만 적용된다.
|
|
|
|
|
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 jerk_rate = 600.0f; // RPM/s^2 (직진/커브 선회 감속 저크 한계)
|
|
|
|
|
float spin_jerk_rate = 220.0f; // RPM/s^2 (제자리 회전 감속 저크 한계)
|
|
|
|
|
float spin_arc_bias = 0.12f; // 0~1 (제자리 회전 시 섞을 최소 회전반경용
|
|
|
|
|
// 미세 전진 성분 비율. ICR을 로봇 중심에서
|
|
|
|
|
// 살짝 벗어나게 해 스크럽 마찰 저항을 줄임)
|
|
|
|
@@ -690,8 +988,6 @@ int main(int argc, char **argv) {
|
|
|
|
|
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)
|
|
|
|
@@ -738,6 +1034,48 @@ int main(int argc, char **argv) {
|
|
|
|
|
max_z = std::stof(argv[++i]);
|
|
|
|
|
else if (arg == "--no_lidar")
|
|
|
|
|
use_lidar = false;
|
|
|
|
|
else if (arg == "--rc_port" && i + 1 < argc)
|
|
|
|
|
rc_cfg.port = argv[++i];
|
|
|
|
|
else if (arg == "--rc_baud" && i + 1 < argc)
|
|
|
|
|
rc_cfg.baud = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_ch_steer" && i + 1 < argc)
|
|
|
|
|
rc_cfg.ch_steer = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_ch_throttle" && i + 1 < argc)
|
|
|
|
|
rc_cfg.ch_throttle = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_ch_brake" && i + 1 < argc)
|
|
|
|
|
rc_cfg.ch_brake = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_ch_speed" && i + 1 < argc)
|
|
|
|
|
rc_cfg.ch_speed = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_steer_min" && i + 1 < argc)
|
|
|
|
|
rc_cfg.steer_min = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_steer_center_lo" && i + 1 < argc)
|
|
|
|
|
rc_cfg.steer_center_lo = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_steer_center_hi" && i + 1 < argc)
|
|
|
|
|
rc_cfg.steer_center_hi = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_steer_max" && i + 1 < argc)
|
|
|
|
|
rc_cfg.steer_max = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_throttle_min" && i + 1 < argc)
|
|
|
|
|
rc_cfg.throttle_min = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_throttle_center_lo" && i + 1 < argc)
|
|
|
|
|
rc_cfg.throttle_center_lo = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_throttle_center_hi" && i + 1 < argc)
|
|
|
|
|
rc_cfg.throttle_center_hi = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_throttle_max" && i + 1 < argc)
|
|
|
|
|
rc_cfg.throttle_max = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_brake_threshold" && i + 1 < argc)
|
|
|
|
|
rc_cfg.brake_threshold = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_speed_threshold" && i + 1 < argc)
|
|
|
|
|
rc_cfg.speed_threshold = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_max_slew" && i + 1 < argc)
|
|
|
|
|
rc_cfg.max_slew_per_tick = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_vmax_low" && i + 1 < argc)
|
|
|
|
|
rc_cfg.v_max_low = std::stof(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_vmax_high" && i + 1 < argc)
|
|
|
|
|
rc_cfg.v_max_high = std::stof(argv[++i]);
|
|
|
|
|
else if (arg == "--rc_failsafe_ms" && i + 1 < argc)
|
|
|
|
|
rc_cfg.failsafe_timeout_ms = std::stoi(argv[++i]);
|
|
|
|
|
else if (arg == "--max_spin_v" && i + 1 < argc)
|
|
|
|
|
max_spin_v = std::stof(argv[++i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr float PI_VAL = 3.14159265358979323846f;
|
|
|
|
@@ -761,10 +1099,16 @@ int main(int argc, char **argv) {
|
|
|
|
|
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 << " - 최고 속도: 저속 " << rc_cfg.v_max_low << "m/s / 고속 "
|
|
|
|
|
<< rc_cfg.v_max_high << "m/s (CH" << rc_cfg.ch_speed
|
|
|
|
|
<< " 전환) | 가속: 즉시 추종(1:1) | 감속 Jerk: " << jerk_rate
|
|
|
|
|
<< "RPM/s² (제자리회전: " << spin_jerk_rate
|
|
|
|
|
<< "RPM/s²) | 회전 곡선혼합: " << spin_arc_bias * 100.0f << "%\n";
|
|
|
|
|
std::cout << " - RC 수신기: " << rc_cfg.port << " @" << rc_cfg.baud
|
|
|
|
|
<< "bps | 조향 CH" << rc_cfg.ch_steer << " | 스로틀 CH"
|
|
|
|
|
<< rc_cfg.ch_throttle << " | 브레이크 CH" << rc_cfg.ch_brake
|
|
|
|
|
<< " | 속도모드 CH" << rc_cfg.ch_speed
|
|
|
|
|
<< " | Fail-safe: " << rc_cfg.failsafe_timeout_ms << "ms\n";
|
|
|
|
|
std::cout << " - 휠 이상판정: 무부하(들뜸) 전류비 " << airborne_current_ratio
|
|
|
|
|
<< " | 걸림 속도비 " << stall_vel_ratio << " / 전류 "
|
|
|
|
|
<< stall_current_a << "A 이상\n";
|
|
|
|
@@ -783,7 +1127,8 @@ int main(int argc, char **argv) {
|
|
|
|
|
if (!log_path.empty()) {
|
|
|
|
|
log_stream.open(log_path);
|
|
|
|
|
if (log_stream.is_open()) {
|
|
|
|
|
log_stream << "t_ms,state,ly,lx,rx,"
|
|
|
|
|
log_stream << "t_ms,state,rc_steer,rc_throttle,rc_ok,brake,vmax,vx,omega,"
|
|
|
|
|
"ch1,ch2,ch3,ch4,ch5,ch6,ch7,ch8,"
|
|
|
|
|
"cmd_fl,cmd_fr,cmd_rl,cmd_rr,"
|
|
|
|
|
"fb_fl,fb_fr,fb_rl,fb_rr,"
|
|
|
|
|
"amp_fl,amp_fr,amp_rl,amp_rr,"
|
|
|
|
@@ -809,26 +1154,23 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (SDL_Init(SDL_INIT_JOYSTICK) < 0) {
|
|
|
|
|
std::cerr << "[오류] SDL 조이스틱 초기화 실패: " << SDL_GetError()
|
|
|
|
|
<< std::endl;
|
|
|
|
|
return 1;
|
|
|
|
|
// 모터(--port)와 RC 수신기(--rc_port)가 같은 물리 포트를 가리키면 두
|
|
|
|
|
// 스레드가 같은 장치의 read()를 두고 경쟁해 RC 라인 파싱이 깨진다.
|
|
|
|
|
// 흔한 실수(예: 모터 미연결 상태에서 RC 수신기가 ttyUSB0에 꽂혀있는데
|
|
|
|
|
// --rc_port를 기본값 ttyUSB1로 둔 채 실행)를 바로 알아차리도록 경고한다.
|
|
|
|
|
if (port1 == rc_cfg.port) {
|
|
|
|
|
std::cout << "\n[경고] --port(모터, " << port1 << ")와 --rc_port(RC 수신기, "
|
|
|
|
|
<< rc_cfg.port
|
|
|
|
|
<< ")가 동일한 포트로 설정되어 있습니다. 두 기능이 같은 장치를 "
|
|
|
|
|
"동시에 읽으면 RC 데이터 파싱이 깨질 수 있으니 서로 다른 "
|
|
|
|
|
"포트로 지정하세요.\n\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
RcReceiver rc(rc_cfg);
|
|
|
|
|
rc.start();
|
|
|
|
|
std::cout << "[정보] RadioMaster Pocket / XR1 V1.0 RC 수신기 리더 시작 ("
|
|
|
|
|
<< rc_cfg.port << "). 신호 수신 전까지는 안전을 위해 브레이크가 "
|
|
|
|
|
"걸린 상태로 대기합니다.\n";
|
|
|
|
|
|
|
|
|
|
SerialPort sp1, sp2;
|
|
|
|
|
if (!sp1.openPort(port1))
|
|
|
|
@@ -887,8 +1229,9 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "\n[정보] C++ 100Hz 초저지연 루프를 시작합니다. (종료: B 버튼 "
|
|
|
|
|
"또는 Ctrl+C, 라이다 토글: X 버튼)\n\n";
|
|
|
|
|
std::cout << "\n[정보] C++ 100Hz 초저지연 루프를 시작합니다. (종료: Ctrl+C, "
|
|
|
|
|
"라이다 회피 " << (use_lidar ? "활성화" : "비활성화")
|
|
|
|
|
<< " 상태로 시작 - 변경하려면 --no_lidar 옵션을 사용하세요)\n\n";
|
|
|
|
|
|
|
|
|
|
float target_fl = 0.0f, target_fr = 0.0f;
|
|
|
|
|
float target_rl = 0.0f, target_rr = 0.0f;
|
|
|
|
@@ -902,13 +1245,14 @@ int main(int argc, char **argv) {
|
|
|
|
|
const float loop_hz = 100.0f;
|
|
|
|
|
const float dt = 1.0f / loop_hz;
|
|
|
|
|
|
|
|
|
|
// 저크 제한(jerk-limited) S-curve: 가속도 자체를 매 틱 jerk_limit 만큼만
|
|
|
|
|
// 변화시켜 서서히 목표 가속도에 도달하게 함. 기존 구현은 목표 접근 구간만
|
|
|
|
|
// 부드러웠고 정지 상태에서 출발할 때는 첫 틱부터 거의 풀가속도가 걸려
|
|
|
|
|
// 정지마찰이 급격히 파괴되는 문제가 있었음.
|
|
|
|
|
// 가속: 역기전력 보호회로가 하드웨어로 구성되어 더 이상 소프트웨어로
|
|
|
|
|
// 완만하게 램프업할 필요가 없어져, 조이스틱 목표값에 1:1로 즉시 추종한다.
|
|
|
|
|
// 감속: 기존과 동일하게 저크 제한(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 ¤t_accel, float max_decel_mag,
|
|
|
|
|
float jerk_limit) {
|
|
|
|
|
float diff = target_vel - current_vel;
|
|
|
|
|
if (std::abs(diff) < 0.01f) {
|
|
|
|
|
current_accel = 0.0f;
|
|
|
|
@@ -916,9 +1260,13 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool is_accelerating = std::abs(target_vel) > std::abs(current_vel);
|
|
|
|
|
float accel_cap = is_accelerating ? max_accel_mag : max_decel_mag;
|
|
|
|
|
if (is_accelerating) {
|
|
|
|
|
current_accel = 0.0f;
|
|
|
|
|
return target_vel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 이번 틱에 diff를 없애는데 필요한 가속도(부호 포함)를 물리적 한계로 클램프
|
|
|
|
|
float desired_accel = std::clamp(diff / dt, -accel_cap, accel_cap);
|
|
|
|
|
float desired_accel = std::clamp(diff / dt, -max_decel_mag, max_decel_mag);
|
|
|
|
|
|
|
|
|
|
float max_delta_accel = jerk_limit * dt;
|
|
|
|
|
current_accel = std::clamp(desired_accel, current_accel - max_delta_accel,
|
|
|
|
@@ -943,47 +1291,121 @@ int main(int argc, char **argv) {
|
|
|
|
|
bool airborne_rl = false, airborne_rr = false;
|
|
|
|
|
int air_count_fl = 0, air_count_fr = 0, air_count_rl = 0, air_count_rr = 0;
|
|
|
|
|
// 착지(airborne 해제) 직후 한 틱 동안: 차체는 이미 그 속도로 움직이고
|
|
|
|
|
// 있는데 cmd는 정지출발용 accel_rate로 천천히 재가속되면, 그 사이 바퀴가
|
|
|
|
|
// 지면 이동속도를 못 따라가 끌리며 긁히는 문제가 있어 즉시 target으로
|
|
|
|
|
// 스냅시켜 지연을 없앤다.
|
|
|
|
|
// 있는데 cmd가 뒤늦게 target을 쫓아가면 그 사이 바퀴가 지면 이동속도를
|
|
|
|
|
// 못 따라가 끌리며 긁히는 문제가 있어 즉시 target으로 스냅시켜 지연을
|
|
|
|
|
// 없앤다.
|
|
|
|
|
bool just_landed_fl = false, just_landed_fr = false;
|
|
|
|
|
bool just_landed_rl = false, just_landed_rr = false;
|
|
|
|
|
char wheel_status_str[5] = "OOOO";
|
|
|
|
|
bool fault_active = false; // 드라이버 알람(과전류/과부하 등) 발생 여부
|
|
|
|
|
bool prev_brake_on = false; // 브레이크(CH5/Fail-safe) 직전 틱 상태
|
|
|
|
|
bool prev_rc_ok = false; // RC 연결 직전 틱 상태 (Fail-safe 로그용)
|
|
|
|
|
float omega_max = (effective_w > 0.0f) ? (max_spin_v / (effective_w / 2.0f))
|
|
|
|
|
: 0.0f; // CH1 조향 최대 각속도
|
|
|
|
|
// CRC 미검증으로 인해 손상된 CRSF 프레임이 드물게 통과할 수 있다.
|
|
|
|
|
// (실측 결과 이 송신기는 표준 CRC8 변형 어떤 것과도 안 맞아 — poly
|
|
|
|
|
// 전수조사+여러 바이트 범위로 확인해도 일치율이 최대 33% 수준이라
|
|
|
|
|
// CRC 자체를 신뢰할 수 없다고 판단, 검증 시도를 포기함.) 대신 두 가지
|
|
|
|
|
// 물리적 타당성 검사로 걸러낸다:
|
|
|
|
|
// 1) 캘리브레이션 범위(steer_min~max, throttle_min~max) 밖의 값 거부
|
|
|
|
|
// 2) 슬루레이트: 한 틱(~10ms) 사이에 사람이 스틱으로 낼 수 없을 만큼
|
|
|
|
|
// 크게 튀는 값 거부 (예: 1227 -> 1800 같은 순간 점프)
|
|
|
|
|
// 두 경우 모두 그 틱은 버리고 마지막으로 유효했던 값을 그대로 사용한다.
|
|
|
|
|
int last_raw_steer = rc_cfg.steer_center_lo;
|
|
|
|
|
int last_raw_throttle = 1500;
|
|
|
|
|
bool steer_initialized = false, throttle_initialized = 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// RC 수신기 채널 읽기 (doc/joystick.md §2, §3 기능 명세서 기준)
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
int raw_steer = rc_cfg.steer_center_lo, raw_throttle = 1500;
|
|
|
|
|
int raw_brake = 1011, raw_speed_mode = 1011;
|
|
|
|
|
bool rc_ok = rc.isConnected();
|
|
|
|
|
rc.getChannel(rc_cfg.ch_steer, raw_steer);
|
|
|
|
|
rc.getChannel(rc_cfg.ch_throttle, raw_throttle);
|
|
|
|
|
rc.getChannel(rc_cfg.ch_brake, raw_brake);
|
|
|
|
|
rc.getChannel(rc_cfg.ch_speed, raw_speed_mode);
|
|
|
|
|
|
|
|
|
|
bool steer_range_ok =
|
|
|
|
|
raw_steer >= rc_cfg.steer_min && raw_steer <= rc_cfg.steer_max;
|
|
|
|
|
bool steer_slew_ok = !steer_initialized ||
|
|
|
|
|
std::abs(raw_steer - last_raw_steer) <=
|
|
|
|
|
rc_cfg.max_slew_per_tick;
|
|
|
|
|
if (steer_range_ok && steer_slew_ok) {
|
|
|
|
|
last_raw_steer = raw_steer;
|
|
|
|
|
steer_initialized = true;
|
|
|
|
|
} else {
|
|
|
|
|
raw_steer = last_raw_steer; // 범위 밖 또는 비정상 점프 -> 손상된 프레임으로 간주
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Sint16 lx_raw = SDL_JoystickGetAxis(joystick, 0);
|
|
|
|
|
Sint16 ly_raw = SDL_JoystickGetAxis(joystick, 1);
|
|
|
|
|
Sint16 rx_raw = SDL_JoystickGetAxis(joystick, 3);
|
|
|
|
|
bool throttle_range_ok = raw_throttle >= rc_cfg.throttle_min &&
|
|
|
|
|
raw_throttle <= rc_cfg.throttle_max;
|
|
|
|
|
bool throttle_slew_ok = !throttle_initialized ||
|
|
|
|
|
std::abs(raw_throttle - last_raw_throttle) <=
|
|
|
|
|
rc_cfg.max_slew_per_tick;
|
|
|
|
|
if (throttle_range_ok && throttle_slew_ok) {
|
|
|
|
|
last_raw_throttle = raw_throttle;
|
|
|
|
|
throttle_initialized = true;
|
|
|
|
|
} else {
|
|
|
|
|
raw_throttle = last_raw_throttle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
// CH1~CH8 raw 값 전체를 확인용으로 읽는다 (CH5=브레이크 값을 실시간으로
|
|
|
|
|
// 직접 눈으로 검증할 수 있도록 HUD/CSV에 그대로 노출).
|
|
|
|
|
int raw_ch[9]; // 인덱스 1..8 사용
|
|
|
|
|
for (int ch = 1; ch <= 8; ++ch) {
|
|
|
|
|
if (!rc.getChannel(ch, raw_ch[ch]))
|
|
|
|
|
raw_ch[ch] = 0; // 아직 수신 못한 채널은 0으로 표시
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::abs(lx) < 0.15f)
|
|
|
|
|
lx = 0.0f;
|
|
|
|
|
if (std::abs(rx) < 0.15f)
|
|
|
|
|
rx = 0.0f;
|
|
|
|
|
if (!rc_ok && prev_rc_ok) {
|
|
|
|
|
std::cout << "\n[경고] RC 신호 Fail-safe 발동 (" << rc_cfg.failsafe_timeout_ms
|
|
|
|
|
<< "ms 이상 미수신) -> 비상 정지.\n";
|
|
|
|
|
}
|
|
|
|
|
prev_rc_ok = rc_ok;
|
|
|
|
|
|
|
|
|
|
float v_x = ly * max_v; // 선속도 m/s
|
|
|
|
|
float omega = 0.0f; // 각속도 rad/s
|
|
|
|
|
// 3.1 브레이크 최우선 로직 (CH5) - 미수신(Fail-safe) 상태도 동일하게 처리
|
|
|
|
|
bool brake_on = !rc_ok || (raw_brake >= rc_cfg.brake_threshold);
|
|
|
|
|
|
|
|
|
|
// 3.2 속도 제한 설정 (CH6)
|
|
|
|
|
float v_max_now = (raw_speed_mode >= rc_cfg.speed_threshold)
|
|
|
|
|
? rc_cfg.v_max_low
|
|
|
|
|
: rc_cfg.v_max_high;
|
|
|
|
|
|
|
|
|
|
float v_x = 0.0f; // 선속도 m/s
|
|
|
|
|
float omega = 0.0f; // 각속도 rad/s
|
|
|
|
|
|
|
|
|
|
if (!brake_on) {
|
|
|
|
|
// 3.3 전후진 속도 계산 (CH2) — 실측 결과 raw 값이 높을수록 전진(+),
|
|
|
|
|
// 낮을수록 후진(-)이라 CH1(조향)과는 반대 부호 방향이다.
|
|
|
|
|
int t = std::clamp(raw_throttle, rc_cfg.throttle_min, rc_cfg.throttle_max);
|
|
|
|
|
if (t >= rc_cfg.throttle_center_lo && t <= rc_cfg.throttle_center_hi) {
|
|
|
|
|
v_x = 0.0f;
|
|
|
|
|
} else if (t < rc_cfg.throttle_center_lo) {
|
|
|
|
|
v_x = -(static_cast<float>(rc_cfg.throttle_center_lo - t) /
|
|
|
|
|
static_cast<float>(rc_cfg.throttle_center_lo - rc_cfg.throttle_min)) *
|
|
|
|
|
v_max_now;
|
|
|
|
|
} else {
|
|
|
|
|
v_x = (static_cast<float>(t - rc_cfg.throttle_center_hi) /
|
|
|
|
|
static_cast<float>(rc_cfg.throttle_max - rc_cfg.throttle_center_hi)) *
|
|
|
|
|
v_max_now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3.4 좌우 조향 각속도 계산 (CH1)
|
|
|
|
|
int s = std::clamp(raw_steer, rc_cfg.steer_min, rc_cfg.steer_max);
|
|
|
|
|
if (s >= rc_cfg.steer_center_lo && s <= rc_cfg.steer_center_hi) {
|
|
|
|
|
omega = 0.0f;
|
|
|
|
|
} else if (s < rc_cfg.steer_center_lo) {
|
|
|
|
|
omega = -(static_cast<float>(rc_cfg.steer_center_lo - s) /
|
|
|
|
|
static_cast<float>(rc_cfg.steer_center_lo - rc_cfg.steer_min)) *
|
|
|
|
|
omega_max;
|
|
|
|
|
} else {
|
|
|
|
|
omega = (static_cast<float>(s - rc_cfg.steer_center_hi) /
|
|
|
|
|
static_cast<float>(rc_cfg.steer_max - rc_cfg.steer_center_hi)) *
|
|
|
|
|
omega_max;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// LiDAR Obstacle Avoidance & Stopping Logic Integration
|
|
|
|
@@ -1011,8 +1433,8 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Side Dodge Steering (전진 중에만 동작)
|
|
|
|
|
if (v_x > 0.01f && rx == 0.0f) {
|
|
|
|
|
// 2. Side Dodge Steering (전진 중, 사용자 조향 입력이 없을 때만 동작)
|
|
|
|
|
if (v_x > 0.01f && omega == 0.0f) {
|
|
|
|
|
float right_intensity = 0.0f;
|
|
|
|
|
float left_intensity = 0.0f;
|
|
|
|
|
|
|
|
|
@@ -1054,17 +1476,18 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 제자리 회전 여부: 왼쪽 스틱 전후(ly) 입력이 완전히 중립일 때만 진입한다.
|
|
|
|
|
// 그렇지 않으면(주행 중 오른쪽 스틱이 드리프트 등으로 미세하게라도 감지될
|
|
|
|
|
// 경우) 전진 입력이 통째로 무시되고 제자리 회전 로직으로 튀어버려, 커브
|
|
|
|
|
// 선회 중에 갑자기 이상하게 회전하려는 문제가 생긴다. ly는 applyDeadzone을
|
|
|
|
|
// 거쳐 데드존 이하일 때 정확히 0.0f를 반환하므로 등호 비교가 안전하다.
|
|
|
|
|
bool is_spin_turn = std::abs(rx) > 0.0f && ly == 0.0f;
|
|
|
|
|
// 제자리 회전 여부: CH2(전후진) 입력이 완전히 중립(v_x == 0.0f)이면서
|
|
|
|
|
// CH1(조향) 입력이 있을 때만 진입한다. v_x는 데드존 처리 시 정확히
|
|
|
|
|
// 0.0f로 설정되므로 등호 비교가 안전하다.
|
|
|
|
|
bool is_spin_turn = (v_x == 0.0f) && (std::abs(omega) > 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;
|
|
|
|
|
// 제자리 회전 (Spin Turn) + 최소 회전반경용 미세 곡선 혼합 (스크럽
|
|
|
|
|
// 마찰 저항 감소를 위해 ICR을 로봇 중심에서 살짝 벗어나게 함)
|
|
|
|
|
float norm = (omega_max > 0.0f)
|
|
|
|
|
? std::clamp(std::abs(omega) / omega_max, 0.0f, 1.0f)
|
|
|
|
|
: 0.0f;
|
|
|
|
|
float v_bias = norm * 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);
|
|
|
|
|
|
|
|
|
@@ -1073,33 +1496,13 @@ int main(int argc, char **argv) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
// 직진 및 차동 선회 Kinematics (CH1 조향 + 라이다 회피 오프셋 포함)
|
|
|
|
|
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으로 낮춰
|
|
|
|
@@ -1114,12 +1517,33 @@ int main(int argc, char **argv) {
|
|
|
|
|
if (airborne_rr)
|
|
|
|
|
target_rr = 0.0f;
|
|
|
|
|
|
|
|
|
|
if (state == "STOPPED") {
|
|
|
|
|
// 3.1 브레이크 최우선 로직 (CH5/Fail-safe): 저크 램프를 건너뛰고 즉시
|
|
|
|
|
// 0으로 스냅 후 브레이크를 잠근다. 해제되면 STOPPED 상태에서 아래
|
|
|
|
|
// 상태머신이 정상적으로 재가속을 시작한다.
|
|
|
|
|
// 전자식 브레이크(솔레노이드)는 오직 CH5로만 제어한다 — 스로틀을 중립으로
|
|
|
|
|
// 되돌려 자연 정지했을 때는 잠그지 않고, CH5를 눌렀을 때만 잠근다.
|
|
|
|
|
if (brake_on && !prev_brake_on) {
|
|
|
|
|
cmd_fl = cmd_fr = cmd_rl = 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";
|
|
|
|
|
} else if (!brake_on && prev_brake_on) {
|
|
|
|
|
driver_front.setBrakes(false);
|
|
|
|
|
if (driver_rear_ptr)
|
|
|
|
|
driver_rear_ptr->setBrakes(false);
|
|
|
|
|
}
|
|
|
|
|
prev_brake_on = brake_on;
|
|
|
|
|
|
|
|
|
|
if (brake_on) {
|
|
|
|
|
// target_fl..rr은 이미 0(v_x=omega=0)이며 STOPPED 상태를 유지한다.
|
|
|
|
|
} else 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") {
|
|
|
|
@@ -1147,14 +1571,10 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
cmd_fl = jerkLimitedStep(cmd_fl, target_fl, accel_fl, decel_rate, jerk_now);
|
|
|
|
|
cmd_fr = jerkLimitedStep(cmd_fr, target_fr, accel_fr, decel_rate, jerk_now);
|
|
|
|
|
cmd_rl = jerkLimitedStep(cmd_rl, target_rl, accel_rl, decel_rate, jerk_now);
|
|
|
|
|
cmd_rr = jerkLimitedStep(cmd_rr, target_rr, accel_rr, 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) {
|
|
|
|
@@ -1162,14 +1582,10 @@ int main(int argc, char **argv) {
|
|
|
|
|
}
|
|
|
|
|
} 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);
|
|
|
|
|
cmd_fl = jerkLimitedStep(cmd_fl, target_fl, accel_fl, decel_rate, jerk_now);
|
|
|
|
|
cmd_fr = jerkLimitedStep(cmd_fr, target_fr, accel_fr, decel_rate, jerk_now);
|
|
|
|
|
cmd_rl = jerkLimitedStep(cmd_rl, target_rl, accel_rl, decel_rate, jerk_now);
|
|
|
|
|
cmd_rr = jerkLimitedStep(cmd_rr, target_rr, accel_rr, decel_rate, jerk_now);
|
|
|
|
|
|
|
|
|
|
if (std::abs(cmd_fl) < 0.1f && std::abs(cmd_fr) < 0.1f) {
|
|
|
|
|
cmd_fl = 0.0f;
|
|
|
|
@@ -1180,10 +1596,8 @@ int main(int argc, char **argv) {
|
|
|
|
|
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);
|
|
|
|
|
// 스로틀 중립 복귀로 인한 자연 정지: 전자식 브레이크는 잠그지 않는다
|
|
|
|
|
// (CH5를 눌렀을 때만 잠금 — 위 브레이크 최우선 로직 참고).
|
|
|
|
|
state = "STOPPED";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1333,8 +1747,13 @@ int main(int argc, char **argv) {
|
|
|
|
|
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
|
|
|
|
|
log_stream << t_ms << ',' << state << ',' << raw_steer << ','
|
|
|
|
|
<< raw_throttle << ',' << (rc_ok ? 1 : 0) << ','
|
|
|
|
|
<< (brake_on ? 1 : 0) << ',' << v_max_now << ',' << v_x << ','
|
|
|
|
|
<< omega << ',' << raw_ch[1] << ',' << raw_ch[2] << ','
|
|
|
|
|
<< raw_ch[3] << ',' << raw_ch[4] << ',' << raw_ch[5] << ','
|
|
|
|
|
<< raw_ch[6] << ',' << raw_ch[7] << ',' << raw_ch[8] << ','
|
|
|
|
|
<< 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]
|
|
|
|
@@ -1344,9 +1763,20 @@ int main(int argc, char **argv) {
|
|
|
|
|
<< 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,
|
|
|
|
|
// RC 조종기 입력값 피드백: 실기 조작 중 CH1~CH8 raw 값(CH5=브레이크 값
|
|
|
|
|
// 직접 확인용 포함)과 그로부터 계산된 선속도/각속도 지령을 실시간으로
|
|
|
|
|
// 확인할 수 있도록 HUD에 표시한다.
|
|
|
|
|
const char *speed_mode_str =
|
|
|
|
|
(v_max_now <= rc_cfg.v_max_low + 0.001f) ? "LOW " : "HIGH";
|
|
|
|
|
printf("\r\033[K[%-8s] RC:%s%s "
|
|
|
|
|
"C1:%4d C2:%4d C3:%4d C4:%4d C5:%4d C6:%4d C7:%4d C8:%4d "
|
|
|
|
|
"SPD:%s VX:%+4.2f OMG:%+4.2f | 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(), rc_ok ? "OK" : "LOST", brake_on ? "[BRK]" : " ",
|
|
|
|
|
raw_ch[1], raw_ch[2], raw_ch[3], raw_ch[4], raw_ch[5], raw_ch[6],
|
|
|
|
|
raw_ch[7], raw_ch[8], speed_mode_str, v_x, omega,
|
|
|
|
|
dist_axle, lidar_telemetry.c_str(), fl_amp, fr_amp,
|
|
|
|
|
rl_amp, rr_amp, wheel_status_str,
|
|
|
|
|
fault_active ? " [!!알람!!]" : "", comm_ms);
|
|
|
|
|
fflush(stdout);
|
|
|
|
@@ -1373,9 +1803,7 @@ int main(int argc, char **argv) {
|
|
|
|
|
std::cout << "[정보] CSV 로그 저장 완료: " << log_path << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (joystick)
|
|
|
|
|
SDL_JoystickClose(joystick);
|
|
|
|
|
SDL_Quit();
|
|
|
|
|
rc.stop();
|
|
|
|
|
|
|
|
|
|
std::cout << "[정보] C++ 4WD 제어 프로그램이 성공적으로 종료되었습니다.\n";
|
|
|
|
|
return 0;
|
|
|
|
|