Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/enforce-bump-flag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Enforce Version Bump Flag

on:
pull_request:
types: [opened, edited, synchronize]
branches:
- main
- master

jobs:
check-pr-description:
name: Check for bump flag
runs-on: ubuntu-latest
steps:
- name: Verify exactly one bump level in description
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
MAJOR=0
MINOR=0
PATCH=0

# Check for Major variants
if echo "$PR_BODY" | grep -iqE '(bump version major|bump major version|#major)'; then
MAJOR=1
fi

# Check for Minor variants
if echo "$PR_BODY" | grep -iqE '(bump version minor|bump minor version|#minor)'; then
MINOR=1
fi

# Check for Patch variants
if echo "$PR_BODY" | grep -iqE '(bump version patch|bump patch version|#patch)'; then
PATCH=1
fi

# Calculate total unique levels found
TOTAL=$((MAJOR + MINOR + PATCH))

if [ "$TOTAL" -eq 1 ]; then
echo "✅ Exactly one version bump level found."
exit 0
elif [ "$TOTAL" -eq 0 ]; then
echo "❌ Error: PR description must contain a version bump flag."
echo "Expected one of: #major, bump version minor, bump patch version, etc."
exit 1
else
echo "❌ Error: Conflicting version bump levels found. Please specify ONLY major, minor, OR patch."
exit 1
fi
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ FetchContent_MakeAvailable(jsd)

####### Build #######
add_subdirectory(src)
add_subdirectory(utils)


####### Test Suite #######
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ To learn more about fastcat, checkout the following documents:

- [Fastcat Primer](doc/fastcat_primer.md)
- [Complete list of Fastcat Device Configuration YAML Parameters](doc/fastcat_device_config_parameters.md)
- [Command-line Utilities](doc/utilities.md) — `jsd_slaveinfo`, `elmo_vel_profile`, `elmo_pos_profile`
- 2021 Aeroconf paper submission `Fastcat: An Open-Source Library for Composable
EtherCAT Control Systems`
- README for build details
Expand Down Expand Up @@ -57,6 +58,39 @@ $ cmake ..
$ make
```

### Network Port Access for EtherCAT

The shipped utilities (`jsd_slaveinfo`, `elmo_vel_profile`, `elmo_pos_profile`) and any modules built using fastcat open raw EtherCAT sockets and need `CAP_NET_ADMIN` + `CAP_NET_RAW` permissions.
This can be achieved either by running the binary as the root user, or by using the `sudo setcap` command to permit non-root users to access the raw socket.
For example, you can permit a non-root user to run the `elmo_vel_profile` binary with the command:

```bash
$ sudo setcap cap_net_admin,cap_net_raw=eip /absolute/path/to/build/bin/elmo_vel_profile
```

**The raw socketr permissions are cleared on every relink**, so you must re-run `sudo setcap` after each rebuild.

#### Auto-setcap during build

For dev machines, build with `-DAUTO_SETCAP=ON` to automatically run `sudo setcap` on `elmo_vel_profile` and `elmo_pos_profile` after each link:

```bash
$ cmake -S . -B build -DAUTO_SETCAP=ON
$ cmake --build build -j
```

The option defaults to **OFF** so fastcat builds cleanly on machines without sudoers configured (CI, headless servers, etc.). `jsd_slaveinfo` is built by the JSD dependency and is not covered by this flag — set its capabilities manually.

#### Password-less sudo for `setcap`

`AUTO_SETCAP=ON` only works without prompting if `sudo setcap` is whitelisted in sudoers. Run `sudo visudo` and add a single line (replace `<your_username>` with your account name):

```
<your_username> ALL=(ALL) NOPASSWD: /usr/sbin/setcap
```

This narrowly grants password-less access to `setcap` only — `sudo` for any other command still prompts as usual.

### Tests

The following commands will execute the unit tests:
Expand Down Expand Up @@ -174,4 +208,4 @@ Violations of these rules will be considered errors and should be patched immedi

## License

fastcat is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for more details.
fastcat is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for more details.
208 changes: 208 additions & 0 deletions doc/utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# fastcat Utilities

fastcat ships three command-line utilities. After a top-level build, all of
them land in `build/bin/`:

| Binary | Purpose |
|---|---|
| `jsd_slaveinfo` | Enumerate EtherCAT slaves on a given interface |
| `elmo_vel_profile` | Run a trapezoidal velocity profile on a single actuator |
| `elmo_pos_profile` | Run a trapezoidal position profile on a single actuator |

`jsd_slaveinfo` is built as part of fastcat's [JSD](https://github.com/nasa-jpl/jsd)
dependency. The two `elmo_*_profile` binaries are native fastcat utilities
under `utils/`.

## Source layout

| Path | Contents |
|---|---|
| `utils/profile_utils.{h,cpp}` | Logic shared by both profile tools: argument parsing, profile duration math, Elmo state-machine helpers, and the reset / control-loop / graceful-halt sequences |
| `utils/elmo_vel_profile.cpp` | `main()` for the velocity tool — CSV schema and the `ACTUATOR_PROF_VEL_CMD` it issues |
| `utils/elmo_pos_profile.cpp` | `main()` for the position tool — CSV schema and the `ACTUATOR_PROF_POS_CMD` it issues |
| `utils/test_profile_utils.cc` | GTest unit tests for the hardware-independent parts of `profile_utils` |

The unit tests are registered with CTest alongside the main test suite and need
no hardware:

```
ctest -R test_profile_utils --output-on-failure
```

## Capabilities

All three utilities open raw EtherCAT sockets, which requires `CAP_NET_ADMIN`
and `CAP_NET_RAW`. Grant the capabilities directly to the binary instead of
running under `sudo`:

```
sudo setcap cap_net_admin,cap_net_raw=eip build/bin/<binary>
```

`setcap` requires the absolute path. **Capabilities are cleared on every
rebuild**, so re-run `setcap` after each `cmake --build`.

### Auto-setcap during build (optional)

For dev machines with password-less sudo configured, build with
`-DAUTO_SETCAP=ON` to automatically run `setcap` on the profile binaries
after each link:

```
cmake -S . -B build -DAUTO_SETCAP=ON
cmake --build build -j
```

The option defaults to **OFF** so that fastcat builds cleanly on machines
without sudoers configured (CI, headless servers, etc.).

To enable password-less `setcap`, run `sudo visudo` and add:

```
<your_username> ALL=(ALL) NOPASSWD: /usr/sbin/setcap
```

Note: `AUTO_SETCAP` only applies to `elmo_vel_profile` and `elmo_pos_profile`.
`jsd_slaveinfo` is built by the JSD dependency and is not covered by this
option — set its capabilities manually after each rebuild.

---

## `jsd_slaveinfo`

Enumerates EtherCAT slaves on the given network interface and prints a
summary of each one (vendor ID, product code, configured PDOs, etc.). Useful
for confirming the bus is wired correctly and that every expected drive shows
up before launching a fastcat-based application.

### Usage

```
./build/bin/jsd_slaveinfo <ifname>
```

Example:

```
./build/bin/jsd_slaveinfo eth_ecat
```

For more detailed documentation see the JSD project's own
[README](https://github.com/nasa-jpl/jsd).

---

## `elmo_vel_profile`

Runs a trapezoidal velocity profile on one actuator described by a fastcat
YAML config. The profile accelerates from rest to a cruise speed, holds for a
specified duration, then decelerates back to zero. Telemetry is streamed to a
timestamped CSV file in the current working directory.

### Usage

```
./build/bin/elmo_vel_profile <config> <actuator_name> <accel> <cruise_speed> <cruise_duration>
```

| Argument | Description |
|---|---|
| `config` | Path to fastcat YAML config (relative to CWD) |
| `actuator_name` | Must match a `name:` field of a device in the YAML |
| `accel` | Trapezoid acceleration / deceleration (rad/s²) |
| `cruise_speed` | Cruise velocity (rad/s) |
| `cruise_duration` | Hold time at cruise speed (s) |

### Example

```
cd build/bin
./elmo_vel_profile ../../example_configs/single_elmo.yaml gold_act_1 2.0 5.0 3.0
```

### Telemetry output

Written to `<YYYYMMDD>_<HHMMSS>_vel_prof_telem.csv` in CWD with one row per
control tick:

| Column | Units | Description |
|---|---|---|
| `unix_time_s` | seconds since 1970-01-01 UTC | Wall-clock timestamp for correlating with other telemetry sources |
| `relative_time_s` | seconds | Time since the start of the control loop |
| `cmd_velocity_rad_s` | rad/s | Velocity commanded by the trapezoid generator |
| `actual_velocity_rad_s` | rad/s | Velocity reported by the drive (heavily quantized at low encoder resolution) |
| `actual_current_A` | A | Motor current reported by the drive |

---

## `elmo_pos_profile`

Runs a trapezoidal position profile on one actuator. The trapezoid shape is
chosen automatically based on the requested distance: triangular if the move
is short enough that the drive can't reach `max_velocity` before having to
decelerate, otherwise trapezoidal with a cruise phase at `max_velocity`.
Negative `relative_position` values move in the negative direction.

### Usage

```
./build/bin/elmo_pos_profile <config> <actuator_name> <accel> <max_velocity> <relative_position>
```

| Argument | Description |
|---|---|
| `config` | Path to fastcat YAML config (relative to CWD) |
| `actuator_name` | Must match a `name:` field of a device in the YAML |
| `accel` | Acceleration / deceleration (rad/s²) |
| `max_velocity` | Cruise velocity cap (rad/s) |
| `relative_position` | Position change relative to the current position (rad); may be negative |

### Example

```
cd build/bin
./elmo_pos_profile ../../example_configs/single_elmo.yaml gold_act_1 2.0 5.0 3.0
./elmo_pos_profile ../../example_configs/single_elmo.yaml gold_act_1 2.0 5.0 -3.0
```

### Telemetry output

Written to `<YYYYMMDD>_<HHMMSS>_pos_prof_telem.csv` in CWD with one row per
control tick:

| Column | Units | Description |
|---|---|---|
| `unix_time_s` | seconds since 1970-01-01 UTC | Wall-clock timestamp |
| `relative_time_s` | seconds | Time since the start of the control loop |
| `cmd_position_rad` | rad | Position commanded by the trapezoid generator |
| `actual_position_rad` | rad | Position reported by the drive |
| `actual_velocity_rad_s` | rad/s | Velocity reported by the drive (noisy at low resolution) |
| `actual_current_A` | A | Motor current reported by the drive |

---

## Loop rate guidance

Both profile utilities take their loop rate from the YAML's
`target_loop_rate_hz`. **64 Hz is the recommended minimum.** Each binary
prints a warning at startup if the configured rate is below 64 Hz, but does
not refuse to run.

The reason is encoder quantization noise on the velocity feedback signal.
For a typical low-resolution encoder (42 counts per revolution), the
single-tick velocity resolution is roughly `2π × loop_rate / counts_per_rev`:

| Loop rate | Velocity resolution per encoder count |
|---|---|
| 32 Hz | ~4.8 rad/s |
| 64 Hz | ~2.4 rad/s |
| 128 Hz | ~1.2 rad/s |

At 32 Hz on a 42 cpr encoder, the quantization noise on `actual_velocity` is
large enough to trip the Elmo drive's internal speed-tracking limit during a
position profile (CSP mode), even when the *actual* mechanical velocity is
well within bounds. 64 Hz is sufficient headroom for the Elmo's default
tracking thresholds; higher rates further suppress quantization noise.

This applies equally to higher-resolution encoders — the noise is just less
visible because each count corresponds to a smaller velocity step.
47 changes: 47 additions & 0 deletions utils/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
message("Building fastcat profile utilities")

include_directories(
${CMAKE_BINARY_DIR}/include
${YAMLCPP_INCLUDE_DIR}
)

add_library(profile_utils STATIC profile_utils.cpp)
target_include_directories(profile_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(profile_utils fastcat ${YAMLCPP_LIBRARY} pthread rt)

add_executable(elmo_vel_profile elmo_vel_profile.cpp)
target_link_libraries(elmo_vel_profile profile_utils)

add_executable(elmo_pos_profile elmo_pos_profile.cpp)
target_link_libraries(elmo_pos_profile profile_utils)

option(AUTO_SETCAP "Run sudo setcap on profile utilities post-build" OFF)
if(AUTO_SETCAP)
foreach(tgt elmo_vel_profile elmo_pos_profile)
add_custom_command(TARGET ${tgt} POST_BUILD
COMMAND sudo setcap cap_net_admin,cap_net_raw=eip $<TARGET_FILE:${tgt}>
COMMENT "Setting capabilities on ${tgt}")
endforeach()
endif()

####### Unit tests #######
# The profile utilities are unit tested independently of the top-level test
# suite so that everything for these tools lives under utils/.
if(BUILD_TESTS AND CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
find_package(GTest)
find_package(Threads REQUIRED)
if(GTEST_FOUND)
# utils/ is added before the root enable_testing(), so opt in here
enable_testing()
add_executable(test_profile_utils test_profile_utils.cc)
target_link_libraries(test_profile_utils PRIVATE
${GTEST_BOTH_LIBRARIES}
Threads::Threads
profile_utils
)
target_include_directories(test_profile_utils PUBLIC ${GTEST_INCLUDE_DIRS})
add_test(NAME test_profile_utils COMMAND test_profile_utils)
else()
message(WARNING "GTest not found, profile utility tests will not be built")
endif()
endif()
Loading
Loading