아래는 Harmony Research Interface 기반으로 만든 두 개의 모듈을 정리한 글이다.
- (1) 10초 동안 양팔 모든 조인트 각도를 측정해서 평균 자세(Initial Posture)를 저장
- (2) 저장된 자세로 안전하게 천천히(ramp) 이동한 뒤 계속 유지(hold)
구현 영상은 YouTube에 업로드한 뒤, 아래 링크에 붙이면 된다.
✅ Demo Video: ****
목표
실험에서 자주 필요한 동작은 다음 2가지다.
- Initial posture를 “안전하게 정의”
- 10초 정도 측정해서 평균으로 잡으면 순간값보다 안정적이다.
- 정의된 initial posture로 천천히 복귀하고, 이후 계속 유지(LOCK/HOLD)
- “토크 ramp-up만”으로는 각도 자체가 천천히 움직인다는 보장이 약할 수 있으니
목표 각도를 시간에 따라 천천히 바꾸는 trajectory(보간) 방식을 사용한다.
- “토크 ramp-up만”으로는 각도 자체가 천천히 움직인다는 보장이 약할 수 있으니
파일 구성
1) capture_average_posture.cpp
- 200 Hz로 10초 측정 (초반 warmup 0.5초 버림)
- 결과를
posture_avg.csv에 저장 (한 줄)
2) move_and_hold_saved_posture.cpp
posture_avg.csv에서 목표 posture를 로드- 현재 자세 → 목표 자세로 천천히 이동(MOVE phase)
- 목표각 smoothstep 보간
- reference의 변화율을 qdotMax (rad/s)로 제한
- 초반에는 토크도 ramp_sec 동안 alpha로 점진적 증가
- 이동이 끝나면 무한 HOLD phase로 들어가 목표 자세 유지
- Ctrl+C로 종료
참고: 마지막 조인트(J7, index 6)는 torque=0 (제어하지 않음)
CSV 포맷
출력 파일: posture_avg.csv
- 한 줄 CSV
- 컬럼 순서:
left_q0,left_q1,left_q2,left_q3,left_q4,left_q5,left_q6,
right_q0,right_q1,right_q2,right_q3,right_q4,right_q5,right_q6
단위는 rad (ResearchInterface의 position_rad 그대로)
코드
(1) 10초 평균 자세 저장: capture_average_posture.cpp
/**
* @file capture_average_posture.cpp
* @brief 10초 동안 양팔 joint angle 평균 posture를 측정해서 CSV로 저장
*
* Output: posture_avg.csv
* Format (1 line):
* left_q0,...,left_q6,right_q0,...,right_q6
*/
#include "research_interface.h"
#include <array>
#include <chrono>
#include <csignal>
#include <fstream>
#include <iostream>
#include <thread>
#include <cmath>
volatile std::sig_atomic_t g_stop = 0;
void handleSigint(int) { g_stop = 1; }
constexpr int NJOINTS = harmony::armJointCount;
void readCurrentPositions(harmony::ResearchInterface& info,
std::array<double, NJOINTS>& leftPos,
std::array<double, NJOINTS>& rightPos)
{
auto leftStates = info.joints().leftArm.getOrderedStates();
auto rightStates = info.joints().rightArm.getOrderedStates();
for (int i = 0; i < NJOINTS; ++i) {
leftPos[i] = leftStates[i].position_rad;
rightPos[i] = rightStates[i].position_rad;
}
}
int main(int, const char**)
{
std::signal(SIGINT, handleSigint);
const double fs = 200.0;
const double dt = 1.0 / fs;
const int capture_sec = 10;
const double warmup_sec = 0.5;
const int total_samples = static_cast<int>(std::round(capture_sec * fs));
const int warmup_samples = static_cast<int>(std::round(warmup_sec * fs));
harmony::ResearchInterface info;
if (!info.init()) {
std::cerr << "Failed to initialize Research Interface\n";
return -1;
}
std::array<double, NJOINTS> sumL{}; sumL.fill(0.0);
std::array<double, NJOINTS> sumR{}; sumR.fill(0.0);
int used = 0;
std::cout << "[Capture] 10s average posture @ " << fs << " Hz\n";
std::cout << "Warmup discarded: " << warmup_sec << " s\n";
std::cout << "Press Ctrl+C to stop.\n";
for (int k = 0; k < total_samples && !g_stop; ++k) {
std::array<double, NJOINTS> qL{}, qR{};
readCurrentPositions(info, qL, qR);
if (k >= warmup_samples) {
for (int j = 0; j < NJOINTS; ++j) {
sumL[j] += qL[j];
sumR[j] += qR[j];
}
used++;
}
std::this_thread::sleep_for(std::chrono::milliseconds(
static_cast<int>(std::round(dt * 1000.0))
));
}
if (used <= 0) {
std::cerr << "[Capture] No valid samples collected.\n";
return -1;
}
std::array<double, NJOINTS> avgL{}, avgR{};
for (int j = 0; j < NJOINTS; ++j) {
avgL[j] = sumL[j] / static_cast<double>(used);
avgR[j] = sumR[j] / static_cast<double>(used);
}
// CSV 저장
const char* outFile = "posture_avg.csv";
std::ofstream ofs(outFile);
if (!ofs) {
std::cerr << "Failed to open output file: " << outFile << "\n";
return -1;
}
// 한 줄로 저장
for (int j = 0; j < NJOINTS; ++j) {
ofs << avgL[j] << ",";
}
for (int j = 0; j < NJOINTS; ++j) {
ofs << avgR[j];
if (j != NJOINTS - 1) ofs << ",";
}
ofs << "\n";
ofs.close();
std::cout << "[Capture] Saved: " << outFile << "\n";
std::cout << "Used samples: " << used << "\n";
return 0;
}
(2) 천천히 이동 + 계속 유지: move_and_hold_saved_posture.cpp
핵심 안전 포인트:
- 목표 각도 자체를 천천히 변화:
q_ref(t) = q_start + smoothstep(t/T) * (q_target - q_start) - reference 속도 제한:
|q_ref[k] - q_ref[k-1]| <= qdotMax * dt - 토크도 초반 ramp_sec 동안 점진적 증가:
u *= alpha
/**
* @file move_and_hold_saved_posture.cpp
* @brief posture_avg.csv 목표 posture로 "천천히" 이동 후, 계속 유지(HOLD)하는 토크 PID 제어
*
* Input: posture_avg.csv (1 line)
* left_q0..left_q6,right_q0..right_q6
*
* - MOVE phase: 현재 자세 -> 목표 자세로 천천히 이동 (보간 + 속도 제한 + 토크 ramp)
* - HOLD phase: 목표 자세를 계속 유지 (Ctrl+C까지)
* - Last joint (J7, idx 6) torque = 0
*/
#include "research_interface.h"
#include "overrides.h"
#include <array>
#include <chrono>
#include <csignal>
#include <fstream>
#include <iostream>
#include <thread>
#include <cmath>
#include <algorithm>
#include <sstream>
#include <string>
volatile std::sig_atomic_t g_stop = 0;
void handleSigint(int) { g_stop = 1; }
constexpr int NJOINTS = harmony::armJointCount;
static inline double clamp(double x, double lo, double hi) {
return std::max(lo, std::min(x, hi));
}
static inline double smoothstep(double s) {
s = clamp(s, 0.0, 1.0);
return s * s * (3.0 - 2.0 * s);
}
struct AllArmsOverrides {
harmony::ArmJointsOverride leftOverrides;
harmony::ArmJointsOverride rightOverrides;
};
AllArmsOverrides torque2override(const std::array<double, NJOINTS>& leftTau,
const std::array<double, NJOINTS>& rightTau)
{
std::array<harmony::JointOverride, NJOINTS> leftOverrides{};
std::array<harmony::JointOverride, NJOINTS> rightOverrides{};
for (int i = 0; i < NJOINTS; ++i) {
leftOverrides[i] = {0.0, 0.0, leftTau[i]};
rightOverrides[i] = {0.0, 0.0, rightTau[i]};
}
return {
harmony::ArmJointsOverride(leftOverrides),
harmony::ArmJointsOverride(rightOverrides)
};
}
void readCurrentPositions(harmony::ResearchInterface& info,
std::array<double, NJOINTS>& leftPos,
std::array<double, NJOINTS>& rightPos)
{
auto leftStates = info.joints().leftArm.getOrderedStates();
auto rightStates = info.joints().rightArm.getOrderedStates();
for (int i = 0; i < NJOINTS; ++i) {
leftPos[i] = leftStates[i].position_rad;
rightPos[i] = rightStates[i].position_rad;
}
}
bool loadPostureCSV(const std::string& path,
std::array<double, NJOINTS>& leftTarget,
std::array<double, NJOINTS>& rightTarget)
{
std::ifstream ifs(path);
if (!ifs) return false;
std::string line;
if (!std::getline(ifs, line)) return false;
std::stringstream ss(line);
std::string token;
std::array<double, 2 * NJOINTS> vals{};
int idx = 0;
while (std::getline(ss, token, ',')) {
if (idx >= 2 * NJOINTS) break;
try {
vals[idx] = std::stod(token);
} catch (...) {
return false;
}
idx++;
}
if (idx != 2 * NJOINTS) return false;
for (int j = 0; j < NJOINTS; ++j) {
leftTarget[j] = vals[j];
rightTarget[j] = vals[NJOINTS + j];
}
return true;
}
int main(int, const char**)
{
std::signal(SIGINT, handleSigint);
// ===== timing =====
const double fs = 200.0;
const double dt = 1.0 / fs;
const uint32_t T_ms = static_cast<uint32_t>(std::round(1000.0 / fs));
// ===== MOVE 설정 =====
const double move_sec = 10.0;
const double ramp_sec = 4.0;
const int total_steps = std::max(1, static_cast<int>(std::round(move_sec * fs)));
const int ramp_steps = std::max(1, static_cast<int>(std::round(ramp_sec * fs)));
// ===== 목표 posture 로드 =====
std::array<double, NJOINTS> leftTarget{}, rightTarget{};
const std::string inFile = "posture_avg.csv";
if (!loadPostureCSV(inFile, leftTarget, rightTarget)) {
std::cerr << "Failed to load posture file: " << inFile << "\n";
return -1;
}
// ===== init RI / controllers =====
harmony::ResearchInterface info;
if (!info.init()) {
std::cerr << "Failed to initialize Research Interface\n";
return -1;
}
auto left = info.makeLeftArmController();
auto right = info.makeRightArmController();
if (!left->init() || !right->init()) {
std::cerr << "Failed to initialize Arm Controllers\n";
return -1;
}
// ===== PID gains / torque limits =====
std::array<double, NJOINTS> Kp{}, Ki{}, Kd{};
std::array<double, NJOINTS> tauLimit{};
for (int i = 0; i <= 4; ++i) { // Shoulder
Kp[i] = 30.0; Ki[i] = 5.0; Kd[i] = 1.0;
tauLimit[i] = 8.0;
}
Kp[5] = 20.0; Ki[5] = 3.0; Kd[5] = 0.8; // Elbow
tauLimit[5] = 6.0;
// Last joint (J7, idx 6) off
Kp[6] = Ki[6] = Kd[6] = 0.0;
tauLimit[6] = 2.0;
// ===== reference 속도 제한 (rad/s) =====
std::array<double, NJOINTS> qdotMax{};
qdotMax.fill(0.25);
qdotMax[5] = 0.30;
qdotMax[6] = 0.0;
// ===== 시작 자세 =====
std::array<double, NJOINTS> leftStart{}, rightStart{};
readCurrentPositions(info, leftStart, rightStart);
// ===== PID state =====
std::array<double, NJOINTS> eIntL{}; eIntL.fill(0.0);
std::array<double, NJOINTS> ePrevL{}; ePrevL.fill(0.0);
std::array<double, NJOINTS> eIntR{}; eIntR.fill(0.0);
std::array<double, NJOINTS> ePrevR{}; ePrevR.fill(0.0);
// ===== ref prev (for qdot limit) =====
std::array<double, NJOINTS> leftRefPrev = leftStart;
std::array<double, NJOINTS> rightRefPrev = rightStart;
std::cout << "[Move+Hold] Loaded target posture from: " << inFile << "\n";
std::cout << "[Move] move_sec=" << move_sec << "s, ramp_sec=" << ramp_sec << "s\n";
std::cout << "[Hold] After move, keep holding until Ctrl+C.\n";
std::cout << "Last joint (J7, idx 6) torque = 0\n";
std::cout << "Press Ctrl+C to stop.\n";
// -------------------------
// MOVE phase
// -------------------------
for (int k = 0; k < total_steps && !g_stop; ++k) {
double s = (total_steps == 1) ? 1.0
: (static_cast<double>(k) / static_cast<double>(total_steps - 1));
double s_smooth = smoothstep(s);
double alpha = clamp(static_cast<double>(k) / static_cast<double>(ramp_steps), 0.0, 1.0);
std::array<double, NJOINTS> qL{}, qR{};
readCurrentPositions(info, qL, qR);
std::array<double, NJOINTS> leftRef{}, rightRef{};
for (int j = 0; j < NJOINTS; ++j) {
if (j == NJOINTS - 1) {
leftRef[j] = leftRefPrev[j];
rightRef[j] = rightRefPrev[j];
continue;
}
double qL_des = leftStart[j] + s_smooth * (leftTarget[j] - leftStart[j]);
double qR_des = rightStart[j] + s_smooth * (rightTarget[j] - rightStart[j]);
double maxStep = qdotMax[j] * dt;
leftRef[j] = clamp(qL_des, leftRefPrev[j] - maxStep, leftRefPrev[j] + maxStep);
rightRef[j] = clamp(qR_des, rightRefPrev[j] - maxStep, rightRefPrev[j] + maxStep);
}
leftRefPrev = leftRef;
rightRefPrev = rightRef;
std::array<double, NJOINTS> tauL{}; tauL.fill(0.0);
std::array<double, NJOINTS> tauR{}; tauR.fill(0.0);
for (int j = 0; j < NJOINTS; ++j) {
if (j == NJOINTS - 1) {
tauL[j] = 0.0; tauR[j] = 0.0;
eIntL[j] = ePrevL[j] = 0.0;
eIntR[j] = ePrevR[j] = 0.0;
continue;
}
// Left PID -> leftRef
const double eL = leftRef[j] - qL[j];
eIntL[j] += eL * dt;
if (Ki[j] > 1e-9) {
const double iMax = tauLimit[j] / Ki[j];
eIntL[j] = clamp(eIntL[j], -iMax, iMax);
}
const double dEL = (eL - ePrevL[j]) / dt;
double uL = (Kp[j] * eL + Ki[j] * eIntL[j] + Kd[j] * dEL);
uL *= alpha;
uL = clamp(uL, -tauLimit[j], tauLimit[j]);
tauL[j] = uL;
ePrevL[j] = eL;
// Right PID -> rightRef
const double eR = rightRef[j] - qR[j];
eIntR[j] += eR * dt;
if (Ki[j] > 1e-9) {
const double iMax = tauLimit[j] / Ki[j];
eIntR[j] = clamp(eIntR[j], -iMax, iMax);
}
const double dER = (eR - ePrevR[j]) / dt;
double uR = (Kp[j] * eR + Ki[j] * eIntR[j] + Kd[j] * dER);
uR *= alpha;
uR = clamp(uR, -tauLimit[j], tauLimit[j]);
tauR[j] = uR;
ePrevR[j] = eR;
}
auto overrides = torque2override(tauL, tauR);
left->setJointsOverride(overrides.leftOverrides);
right->setJointsOverride(overrides.rightOverrides);
std::this_thread::sleep_for(std::chrono::milliseconds(T_ms));
}
if (g_stop) {
std::cout << "\nStopped during MOVE. Removing overrides.\n";
left->removeOverride();
right->removeOverride();
return 0;
}
std::cout << "[Move] Completed. Switching to HOLD (infinite) ...\n";
// -------------------------
// HOLD phase (infinite)
// -------------------------
while (!g_stop) {
std::array<double, NJOINTS> qL{}, qR{};
readCurrentPositions(info, qL, qR);
std::array<double, NJOINTS> tauL{}; tauL.fill(0.0);
std::array<double, NJOINTS> tauR{}; tauR.fill(0.0);
for (int j = 0; j < NJOINTS; ++j) {
if (j == NJOINTS - 1) {
tauL[j] = 0.0; tauR[j] = 0.0;
eIntL[j] = ePrevL[j] = 0.0;
eIntR[j] = ePrevR[j] = 0.0;
continue;
}
const double eL = leftTarget[j] - qL[j];
eIntL[j] += eL * dt;
if (Ki[j] > 1e-9) {
const double iMax = tauLimit[j] / Ki[j];
eIntL[j] = clamp(eIntL[j], -iMax, iMax);
}
const double dEL = (eL - ePrevL[j]) / dt;
double uL = (Kp[j] * eL + Ki[j] * eIntL[j] + Kd[j] * dEL);
uL = clamp(uL, -tauLimit[j], tauLimit[j]);
tauL[j] = uL;
ePrevL[j] = eL;
const double eR = rightTarget[j] - qR[j];
eIntR[j] += eR * dt;
if (Ki[j] > 1e-9) {
const double iMax = tauLimit[j] / Ki[j];
eIntR[j] = clamp(eIntR[j], -iMax, iMax);
}
const double dER = (eR - ePrevR[j]) / dt;
double uR = (Kp[j] * eR + Ki[j] * eIntR[j] + Kd[j] * dER);
uR = clamp(uR, -tauLimit[j], tauLimit[j]);
tauR[j] = uR;
ePrevR[j] = eR;
}
auto overrides = torque2override(tauL, tauR);
left->setJointsOverride(overrides.leftOverrides);
right->setJointsOverride(overrides.rightOverrides);
std::this_thread::sleep_for(std::chrono::milliseconds(T_ms));
}
std::cout << "\nCtrl+C detected. Removing overrides and exiting.\n";
left->removeOverride();
right->removeOverride();
return 0;
}
실행 방법 (예시)
1) 평균 자세 캡처
./capture_average_posture
# => posture_avg.csv 생성
2) 천천히 이동 후 계속 유지
./move_and_hold_saved_posture
# Ctrl+C 누를 때까지 HOLD
파라미터 튜닝 포인트
capture_average_posture.cpp
capture_sec = 10warmup_sec = 0.5fs = 200
move_and_hold_saved_posture.cpp
move_sec: 돌아오는 시간(길수록 천천히 움직임)ramp_sec: 토크 ramp 시간(길수록 초반 더 부드러움)qdotMax: 가장 중요 (reference 속도 제한)Kp/Ki/Kd,tauLimit: 토크 PID 안정성 및 “락 강도” 결정
안전 메모
- 처음에는 반드시:
move_sec를 길게 (예: 12~20초)qdotMax를 낮게 (예: 0.15~0.25 rad/s)tauLimit를 낮게 시작
- 사람 착용 상태에서는 작은 변화가 크게 느껴질 수 있으니, 천천히 올리는 게 정답.