motor_task.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. #include <SimpleFOC.h>
  2. #include "motor_task.h"
  3. #if SENSOR_MT6701
  4. #include "mt6701_sensor.h"
  5. #elif SENSOR_TLV
  6. #include "tlv_sensor.h"
  7. #elif SENSOR_MAQ430
  8. #include "maq430_sensor.h"
  9. #endif
  10. #include "motors/motor_config.h"
  11. #include "util.h"
  12. static const float DEAD_ZONE_DETENT_PERCENT = 0.2;
  13. static const float DEAD_ZONE_RAD = 1 * _PI / 180;
  14. static const float IDLE_VELOCITY_EWMA_ALPHA = 0.001;
  15. static const float IDLE_VELOCITY_RAD_PER_SEC = 0.05;
  16. static const uint32_t IDLE_CORRECTION_DELAY_MILLIS = 500;
  17. static const float IDLE_CORRECTION_MAX_ANGLE_RAD = 5 * PI / 180;
  18. static const float IDLE_CORRECTION_RATE_ALPHA = 0.0005;
  19. MotorTask::MotorTask(const uint8_t task_core, Configuration& configuration) : Task("Motor", 4000, 1, task_core), configuration_(configuration) {
  20. queue_ = xQueueCreate(5, sizeof(Command));
  21. assert(queue_ != NULL);
  22. }
  23. MotorTask::~MotorTask() {}
  24. #if SENSOR_TLV
  25. TlvSensor encoder = TlvSensor();
  26. #elif SENSOR_MT6701
  27. MT6701Sensor encoder = MT6701Sensor();
  28. #elif SENSOR_MAQ430
  29. MagneticSensorSPI encoder = MagneticSensorSPI(MAQ430_SPI, PIN_MAQ_SS);
  30. #endif
  31. void MotorTask::run() {
  32. driver.voltage_power_supply = 5;
  33. driver.init();
  34. #if SENSOR_TLV
  35. encoder.init(&Wire, false);
  36. #elif SENSOR_MT6701
  37. encoder.init();
  38. #elif SENSOR_MAQ430
  39. SPIClass* spi = new SPIClass(HSPI);
  40. spi->begin(PIN_MAQ_SCK, PIN_MAQ_MISO, PIN_MAQ_MOSI, PIN_MAQ_SS);
  41. encoder.init(spi);
  42. #endif
  43. motor.linkDriver(&driver);
  44. motor.controller = MotionControlType::torque;
  45. motor.voltage_limit = FOC_VOLTAGE_LIMIT;
  46. motor.velocity_limit = 10000;
  47. motor.linkSensor(&encoder);
  48. // Not actually using the velocity loop built into SimpleFOC; but I'm using those PID variables
  49. // to run PID for torque (and SimpleFOC studio supports updating them easily over serial for tuning)
  50. motor.PID_velocity.P = FOC_PID_P;
  51. motor.PID_velocity.I = FOC_PID_I;
  52. motor.PID_velocity.D = FOC_PID_D;
  53. motor.PID_velocity.output_ramp = FOC_PID_OUTPUT_RAMP;
  54. motor.PID_velocity.limit = FOC_PID_LIMIT;
  55. #ifdef FOC_LPF
  56. motor.LPF_angle.Tf = FOC_LPF;
  57. #endif
  58. motor.init();
  59. encoder.update();
  60. delay(10);
  61. PB_PersistentConfiguration c = configuration_.get();
  62. motor.pole_pairs = c.motor.calibrated ? c.motor.pole_pairs : 7;
  63. motor.initFOC(c.motor.zero_electrical_offset, c.motor.direction_cw ? Direction::CW : Direction::CCW);
  64. motor.monitor_downsample = 0; // disable monitor at first - optional
  65. // disableCore0WDT();
  66. float current_detent_center = motor.shaft_angle;
  67. PB_SmartKnobConfig config = {
  68. .position = 0,
  69. .sub_position_unit = 0,
  70. .position_nonce = 0,
  71. .min_position = 0,
  72. .max_position = 1,
  73. .position_width_radians = 60 * _PI / 180,
  74. .detent_strength_unit = 0,
  75. };
  76. int32_t current_position = 0;
  77. float latest_sub_position_unit = 0;
  78. float idle_check_velocity_ewma = 0;
  79. uint32_t last_idle_start = 0;
  80. uint32_t last_publish = 0;
  81. while (1) {
  82. motor.loopFOC();
  83. // Check queue for pending requests from other tasks
  84. Command command;
  85. if (xQueueReceive(queue_, &command, 0) == pdTRUE) {
  86. switch (command.command_type) {
  87. case CommandType::CALIBRATE:
  88. calibrate();
  89. break;
  90. case CommandType::CONFIG: {
  91. // Check new config for validity
  92. PB_SmartKnobConfig& new_config = command.data.config;
  93. if (new_config.detent_strength_unit < 0) {
  94. log("Ignoring invalid config: detent_strength_unit cannot be negative");
  95. break;
  96. }
  97. if (new_config.endstop_strength_unit < 0) {
  98. log("Ignoring invalid config: endstop_strength_unit cannot be negative");
  99. break;
  100. }
  101. if (new_config.snap_point < 0.5) {
  102. log("Ignoring invalid config: snap_point must be >= 0.5 for stability");
  103. break;
  104. }
  105. if (new_config.detent_positions_count > COUNT_OF(new_config.detent_positions)) {
  106. log("Ignoring invalid config: detent_positions_count is too large");
  107. break;
  108. }
  109. if (new_config.snap_point_bias < 0) {
  110. log("Ignoring invalid config: snap_point_bias cannot be negative or there is risk of instability");
  111. break;
  112. }
  113. // Change haptic input mode
  114. bool position_updated = false;
  115. if (new_config.position != config.position
  116. || new_config.sub_position_unit != config.sub_position_unit
  117. || new_config.position_nonce != config.position_nonce) {
  118. log("applying position change");
  119. current_position = new_config.position;
  120. position_updated = true;
  121. }
  122. if (new_config.min_position <= new_config.max_position) {
  123. // Only check bounds if min/max indicate bounds are active (min >= max)
  124. if (current_position < new_config.min_position) {
  125. current_position = new_config.min_position;
  126. log("adjusting position to min");
  127. } else if (current_position > new_config.max_position) {
  128. current_position = new_config.max_position;
  129. log("adjusting position to max");
  130. }
  131. }
  132. if (position_updated || new_config.position_width_radians != config.position_width_radians) {
  133. log("adjusting detent center");
  134. float new_sub_position = position_updated ? new_config.sub_position_unit : latest_sub_position_unit;
  135. #if SK_INVERT_ROTATION
  136. float shaft_angle = -motor.shaft_angle;
  137. #else
  138. float shaft_angle = motor.shaft_angle;
  139. #endif
  140. current_detent_center = shaft_angle + new_sub_position * new_config.position_width_radians;
  141. }
  142. config = new_config;
  143. log("Got new config");
  144. // Update derivative factor of torque controller based on detent width.
  145. // If the D factor is large on coarse detents, the motor ends up making noise because the P&D factors amplify the noise from the sensor.
  146. // This is a piecewise linear function so that fine detents (small width) get a higher D factor and coarse detents get a small D factor.
  147. // Fine detents need a nonzero D factor to artificially create "clicks" each time a new value is reached (the P factor is small
  148. // for fine detents due to the smaller angular errors, and the existing P factor doesn't work well for very small angle changes (easy to
  149. // get runaway due to sensor noise & lag)).
  150. // TODO: consider eliminating this D factor entirely and just "play" a hardcoded haptic "click" (e.g. a quick burst of torque in each
  151. // direction) whenever the position changes when the detent width is too small for the P factor to work well.
  152. const float derivative_lower_strength = config.detent_strength_unit * 0.08;
  153. const float derivative_upper_strength = config.detent_strength_unit * 0.02;
  154. const float derivative_position_width_lower = radians(3);
  155. const float derivative_position_width_upper = radians(8);
  156. const float raw = derivative_lower_strength + (derivative_upper_strength - derivative_lower_strength)/(derivative_position_width_upper - derivative_position_width_lower)*(config.position_width_radians - derivative_position_width_lower);
  157. // When there are intermittent detents (set via detent_positions), disable derivative factor as this adds extra "clicks" when nearing
  158. // a detent.
  159. motor.PID_velocity.D = config.detent_positions_count > 0 ? 0 : CLAMP(
  160. raw,
  161. min(derivative_lower_strength, derivative_upper_strength),
  162. max(derivative_lower_strength, derivative_upper_strength)
  163. );
  164. break;
  165. }
  166. case CommandType::HAPTIC: {
  167. // Play a hardcoded haptic "click"
  168. float strength = command.data.haptic.press ? 5 : 1.5;
  169. motor.move(strength);
  170. for (uint8_t i = 0; i < 3; i++) {
  171. motor.loopFOC();
  172. delay(1);
  173. }
  174. motor.move(-strength);
  175. for (uint8_t i = 0; i < 3; i++) {
  176. motor.loopFOC();
  177. delay(1);
  178. }
  179. motor.move(0);
  180. motor.loopFOC();
  181. break;
  182. }
  183. }
  184. }
  185. // If we are not moving and we're close to the center (but not exactly there), slowly adjust the centerpoint to match the current position
  186. idle_check_velocity_ewma = motor.shaft_velocity * IDLE_VELOCITY_EWMA_ALPHA + idle_check_velocity_ewma * (1 - IDLE_VELOCITY_EWMA_ALPHA);
  187. if (fabsf(idle_check_velocity_ewma) > IDLE_VELOCITY_RAD_PER_SEC) {
  188. last_idle_start = 0;
  189. } else {
  190. if (last_idle_start == 0) {
  191. last_idle_start = millis();
  192. }
  193. }
  194. if (last_idle_start > 0 && millis() - last_idle_start > IDLE_CORRECTION_DELAY_MILLIS && fabsf(motor.shaft_angle - current_detent_center) < IDLE_CORRECTION_MAX_ANGLE_RAD) {
  195. current_detent_center = motor.shaft_angle * IDLE_CORRECTION_RATE_ALPHA + current_detent_center * (1 - IDLE_CORRECTION_RATE_ALPHA);
  196. }
  197. // Check where we are relative to the current nearest detent; update our position if we've moved far enough to snap to another detent
  198. float angle_to_detent_center = motor.shaft_angle - current_detent_center;
  199. #if SK_INVERT_ROTATION
  200. angle_to_detent_center = -motor.shaft_angle - current_detent_center;
  201. #endif
  202. float snap_point_radians = config.position_width_radians * config.snap_point;
  203. float bias_radians = config.position_width_radians * config.snap_point_bias;
  204. float snap_point_radians_decrease = snap_point_radians + (current_position <= 0 ? bias_radians : -bias_radians);
  205. float snap_point_radians_increase = -snap_point_radians + (current_position >= 0 ? -bias_radians : bias_radians);
  206. int32_t num_positions = config.max_position - config.min_position + 1;
  207. if (angle_to_detent_center > snap_point_radians_decrease && (num_positions <= 0 || current_position > config.min_position)) {
  208. current_detent_center += config.position_width_radians;
  209. angle_to_detent_center -= config.position_width_radians;
  210. current_position--;
  211. } else if (angle_to_detent_center < snap_point_radians_increase && (num_positions <= 0 || current_position < config.max_position)) {
  212. current_detent_center -= config.position_width_radians;
  213. angle_to_detent_center += config.position_width_radians;
  214. current_position++;
  215. }
  216. latest_sub_position_unit = -angle_to_detent_center / config.position_width_radians;
  217. float dead_zone_adjustment = CLAMP(
  218. angle_to_detent_center,
  219. fmaxf(-config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, -DEAD_ZONE_RAD),
  220. fminf(config.position_width_radians*DEAD_ZONE_DETENT_PERCENT, DEAD_ZONE_RAD));
  221. bool out_of_bounds = num_positions > 0 && ((angle_to_detent_center > 0 && current_position == config.min_position) || (angle_to_detent_center < 0 && current_position == config.max_position));
  222. motor.PID_velocity.limit = 10; //out_of_bounds ? 10 : 3;
  223. motor.PID_velocity.P = out_of_bounds ? config.endstop_strength_unit * 4 : config.detent_strength_unit * 4;
  224. // Apply motor torque based on our angle to the nearest detent (detent strength, etc is handled by the PID_velocity parameters)
  225. if (fabsf(motor.shaft_velocity) > 60) {
  226. // Don't apply torque if velocity is too high (helps avoid positive feedback loop/runaway)
  227. motor.move(0);
  228. } else {
  229. float input = -angle_to_detent_center + dead_zone_adjustment;
  230. if (!out_of_bounds && config.detent_positions_count > 0) {
  231. bool in_detent = false;
  232. for (uint8_t i = 0; i < config.detent_positions_count; i++) {
  233. if (config.detent_positions[i] == current_position) {
  234. in_detent = true;
  235. break;
  236. }
  237. }
  238. if (!in_detent) {
  239. input = 0;
  240. }
  241. }
  242. float torque = motor.PID_velocity(input);
  243. #if SK_INVERT_ROTATION
  244. torque = -torque;
  245. #endif
  246. motor.move(torque);
  247. }
  248. // Publish current status to other registered tasks periodically
  249. if (millis() - last_publish > 5) {
  250. publish({
  251. .current_position = current_position,
  252. .sub_position_unit = latest_sub_position_unit,
  253. .has_config = true,
  254. .config = config,
  255. });
  256. last_publish = millis();
  257. }
  258. delay(1);
  259. }
  260. }
  261. void MotorTask::setConfig(const PB_SmartKnobConfig& config) {
  262. Command command = {
  263. .command_type = CommandType::CONFIG,
  264. .data = {
  265. .config = config,
  266. }
  267. };
  268. xQueueSend(queue_, &command, portMAX_DELAY);
  269. }
  270. void MotorTask::playHaptic(bool press) {
  271. Command command = {
  272. .command_type = CommandType::HAPTIC,
  273. .data = {
  274. .haptic = {
  275. .press = press,
  276. },
  277. }
  278. };
  279. xQueueSend(queue_, &command, portMAX_DELAY);
  280. }
  281. void MotorTask::runCalibration() {
  282. Command command = {
  283. .command_type = CommandType::CALIBRATE,
  284. .data = {
  285. .unused = 0,
  286. }
  287. };
  288. xQueueSend(queue_, &command, portMAX_DELAY);
  289. }
  290. void MotorTask::addListener(QueueHandle_t queue) {
  291. listeners_.push_back(queue);
  292. }
  293. void MotorTask::publish(const PB_SmartKnobState& state) {
  294. for (auto listener : listeners_) {
  295. xQueueOverwrite(listener, &state);
  296. }
  297. }
  298. void MotorTask::calibrate() {
  299. // SimpleFOC is supposed to be able to determine this automatically (if you omit params to initFOC), but
  300. // it seems to have a bug (or I've misconfigured it) that gets both the offset and direction very wrong!
  301. // So this value is based on experimentation.
  302. // TODO: dig into SimpleFOC calibration and find/fix the issue
  303. log("\n\n\nStarting calibration, please DO NOT TOUCH MOTOR until complete!");
  304. delay(1000);
  305. motor.controller = MotionControlType::angle_openloop;
  306. motor.pole_pairs = 1;
  307. motor.initFOC(0, Direction::CW);
  308. float a = 0;
  309. motor.voltage_limit = FOC_VOLTAGE_LIMIT;
  310. motor.move(a);
  311. // #### Determine direction motor rotates relative to angle sensor
  312. for (uint8_t i = 0; i < 200; i++) {
  313. encoder.update();
  314. motor.move(a);
  315. delay(1);
  316. }
  317. float start_sensor = encoder.getAngle();
  318. for (; a < 3 * _2PI; a += 0.01) {
  319. encoder.update();
  320. motor.move(a);
  321. delay(1);
  322. }
  323. for (uint8_t i = 0; i < 200; i++) {
  324. encoder.update();
  325. delay(1);
  326. }
  327. float end_sensor = encoder.getAngle();
  328. motor.voltage_limit = 0;
  329. motor.move(a);
  330. log("");
  331. float movement_angle = fabsf(end_sensor - start_sensor);
  332. if (movement_angle < radians(30) || movement_angle > radians(180)) {
  333. snprintf(buf_, sizeof(buf_), "ERROR! Unexpected sensor change: start=%.2f end=%.2f", start_sensor, end_sensor);
  334. log(buf_);
  335. return;
  336. }
  337. log("Sensor measures positive for positive motor rotation:");
  338. if (end_sensor > start_sensor) {
  339. log("YES, Direction=CW");
  340. motor.initFOC(0, Direction::CW);
  341. } else {
  342. log("NO, Direction=CCW");
  343. motor.initFOC(0, Direction::CCW);
  344. }
  345. snprintf(buf_, sizeof(buf_), " (start was %.1f, end was %.1f)", start_sensor, end_sensor);
  346. log(buf_);
  347. // #### Determine pole-pairs
  348. // Rotate 20 electrical revolutions and measure mechanical angle traveled, to calculate pole-pairs
  349. uint8_t electrical_revolutions = 20;
  350. snprintf(buf_, sizeof(buf_), "Going to measure %d electrical revolutions...", electrical_revolutions);
  351. log(buf_);
  352. motor.voltage_limit = FOC_VOLTAGE_LIMIT;
  353. motor.move(a);
  354. log("Going to electrical zero...");
  355. float destination = a + _2PI;
  356. for (; a < destination; a += 0.03) {
  357. encoder.update();
  358. motor.move(a);
  359. delay(1);
  360. }
  361. log("pause..."); // Let momentum settle...
  362. for (uint16_t i = 0; i < 1000; i++) {
  363. encoder.update();
  364. delay(1);
  365. }
  366. log("Measuring...");
  367. start_sensor = motor.sensor_direction * encoder.getAngle();
  368. destination = a + electrical_revolutions * _2PI;
  369. for (; a < destination; a += 0.03) {
  370. encoder.update();
  371. motor.move(a);
  372. delay(1);
  373. }
  374. for (uint16_t i = 0; i < 1000; i++) {
  375. encoder.update();
  376. motor.move(a);
  377. delay(1);
  378. }
  379. end_sensor = motor.sensor_direction * encoder.getAngle();
  380. motor.voltage_limit = 0;
  381. motor.move(a);
  382. if (fabsf(motor.shaft_angle - motor.target) > 1 * PI / 180) {
  383. log("ERROR: motor did not reach target!");
  384. while(1) {}
  385. }
  386. float electrical_per_mechanical = electrical_revolutions * _2PI / (end_sensor - start_sensor);
  387. snprintf(buf_, sizeof(buf_), "Electrical angle / mechanical angle (i.e. pole pairs) = %.2f", electrical_per_mechanical);
  388. log(buf_);
  389. if (electrical_per_mechanical < 3 || electrical_per_mechanical > 12) {
  390. snprintf(buf_, sizeof(buf_), "ERROR! Unexpected calculated pole pairs: %.2f", electrical_per_mechanical);
  391. log(buf_);
  392. return;
  393. }
  394. int measured_pole_pairs = (int)round(electrical_per_mechanical);
  395. snprintf(buf_, sizeof(buf_), "Pole pairs set to %d", measured_pole_pairs);
  396. log(buf_);
  397. delay(1000);
  398. // #### Determine mechanical offset to electrical zero
  399. // Measure mechanical angle at every electrical zero for several revolutions
  400. motor.voltage_limit = FOC_VOLTAGE_LIMIT;
  401. motor.move(a);
  402. float offset_x = 0;
  403. float offset_y = 0;
  404. float destination1 = (floor(a / _2PI) + measured_pole_pairs / 2.) * _2PI;
  405. float destination2 = (floor(a / _2PI)) * _2PI;
  406. for (; a < destination1; a += 0.4) {
  407. motor.move(a);
  408. delay(100);
  409. for (uint8_t i = 0; i < 100; i++) {
  410. encoder.update();
  411. delay(1);
  412. }
  413. float real_electrical_angle = _normalizeAngle(a);
  414. float measured_electrical_angle = _normalizeAngle( (float)(motor.sensor_direction * measured_pole_pairs) * encoder.getMechanicalAngle() - 0);
  415. float offset_angle = measured_electrical_angle - real_electrical_angle;
  416. offset_x += cosf(offset_angle);
  417. offset_y += sinf(offset_angle);
  418. snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
  419. log(buf_);
  420. }
  421. for (; a > destination2; a -= 0.4) {
  422. motor.move(a);
  423. delay(100);
  424. for (uint8_t i = 0; i < 100; i++) {
  425. encoder.update();
  426. delay(1);
  427. }
  428. float real_electrical_angle = _normalizeAngle(a);
  429. float measured_electrical_angle = _normalizeAngle( (float)(motor.sensor_direction * measured_pole_pairs) * encoder.getMechanicalAngle() - 0);
  430. float offset_angle = measured_electrical_angle - real_electrical_angle;
  431. offset_x += cosf(offset_angle);
  432. offset_y += sinf(offset_angle);
  433. snprintf(buf_, sizeof(buf_), "%.2f, %.2f, %.2f", degrees(real_electrical_angle), degrees(measured_electrical_angle), degrees(_normalizeAngle(offset_angle)));
  434. log(buf_);
  435. }
  436. motor.voltage_limit = 0;
  437. motor.move(a);
  438. float avg_offset_angle = atan2f(offset_y, offset_x);
  439. // #### Apply settings
  440. motor.pole_pairs = measured_pole_pairs;
  441. motor.zero_electric_angle = avg_offset_angle + _3PI_2;
  442. motor.voltage_limit = FOC_VOLTAGE_LIMIT;
  443. motor.controller = MotionControlType::torque;
  444. log("");
  445. log("RESULTS:");
  446. snprintf(buf_, sizeof(buf_), " ZERO_ELECTRICAL_OFFSET: %.2f", motor.zero_electric_angle);
  447. log(buf_);
  448. if (motor.sensor_direction == Direction::CW) {
  449. log(" FOC_DIRECTION: Direction::CW");
  450. } else {
  451. log(" FOC_DIRECTION: Direction::CCW");
  452. }
  453. snprintf(buf_, sizeof(buf_), " MOTOR_POLE_PAIRS: %d", motor.pole_pairs);
  454. log(buf_);
  455. log("");
  456. log("Saving to persistent configuration...");
  457. PB_MotorCalibration calibration = {
  458. .calibrated = true,
  459. .zero_electrical_offset = motor.zero_electric_angle,
  460. .direction_cw = motor.sensor_direction == Direction::CW,
  461. .pole_pairs = motor.pole_pairs,
  462. };
  463. if (configuration_.setMotorCalibrationAndSave(calibration)) {
  464. log("Success!");
  465. }
  466. }
  467. void MotorTask::checkSensorError() {
  468. #if SENSOR_TLV
  469. if (encoder.getAndClearError()) {
  470. log("LOCKED!");
  471. }
  472. #elif SENSOR_MT6701
  473. MT6701Error error = encoder.getAndClearError();
  474. if (error.error) {
  475. snprintf(buf_, sizeof(buf_), "CRC error. Received %d; calculated %d", error.received_crc, error.calculated_crc);
  476. log(buf_);
  477. }
  478. #endif
  479. }
  480. void MotorTask::setLogger(Logger* logger) {
  481. logger_ = logger;
  482. }
  483. void MotorTask::log(const char* msg) {
  484. if (logger_ != nullptr) {
  485. logger_->log(msg);
  486. }
  487. }