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
10 changes: 9 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ option(ENABLE_CODE_COVERAGE "Enable code coverage (requires gcovr)" OFF)
option(ENABLE_ASAN "Build with AddressSanitizer" OFF)
option(ENABLE_UBSAN "Build with UndefinedBehaviorSanitizer" OFF)
option(BUILD_EEPROM_EDITOR "Build EEPROM editor GUI (requires imgui, glfw)" OFF)
option(BUILD_KICKUI "Build KickUI, the KickCAT GUI (requires imgui, glfw)" OFF)
option(KICKCAT_INSTALL "Install KickCAT targets, headers, CMake config and pkg-config files" ${KICKCAT_INSTALL_DEFAULT})

include(Install)
Expand Down Expand Up @@ -104,9 +105,16 @@ if (BUILD_TOOLS)
add_subdirectory(tools)
endif()

if (BUILD_EEPROM_EDITOR)
if (BUILD_EEPROM_EDITOR OR BUILD_KICKUI)
add_subdirectory(tools/common)
endif()

if (BUILD_EEPROM_EDITOR)
add_subdirectory(tools/eeprom_editor)
endif()

if (BUILD_KICKUI)
add_subdirectory(tools/kickui)
endif()

add_subdirectory(py_bindings)
4 changes: 3 additions & 1 deletion conan/conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class KickCATDev(ConanFile):
"tools": [True, False],
"master_examples": [True, False],
"eeprom_editor": [True, False],
"kickui": [True, False],
}
default_options = {
"unit_tests": False,
Expand All @@ -24,6 +25,7 @@ class KickCATDev(ConanFile):
"tools": True,
"master_examples": True,
"eeprom_editor": False,
"kickui": False,
}

generators = "CMakeDeps"
Expand All @@ -41,7 +43,7 @@ def requirements(self):
if self.options.tools or self.options.master_examples or self.options.simulation:
self.requires("argparse/3.2")

if self.options.eeprom_editor:
if self.options.eeprom_editor or self.options.kickui:
self.requires("imgui/1.92.6")
self.requires("portable-file-dialogs/0.1.0")

Expand Down
30 changes: 30 additions & 0 deletions lib/include/kickcat/CoE/OD.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef KICKCAT_COE_OD_H
#define KICKCAT_COE_OD_H

#include <array>
#include <vector>
#include <string>
#include <cstring>
Expand Down Expand Up @@ -107,6 +108,32 @@ namespace kickcat::CoE
};
char const* toString(enum DataType type);

// Wire properties of a basic CoE data type, for code that interprets raw entry
// bytes (the name is already available via toString). size 0 = string.
struct DataTypeInfo
{
DataType type;
uint16_t size; // fixed size in bytes; 0 for variable-length strings
bool is_signed;
bool is_real;
bool is_string;
};

// The fixed-width scalar + string data types, in a stable order.
constexpr std::array<DataTypeInfo, 11> BASIC_DATA_TYPES = {{
{DataType::UNSIGNED8, 1, false, false, false},
{DataType::UNSIGNED16, 2, false, false, false},
{DataType::UNSIGNED32, 4, false, false, false},
{DataType::UNSIGNED64, 8, false, false, false},
{DataType::INTEGER8, 1, true, false, false},
{DataType::INTEGER16, 2, true, false, false},
{DataType::INTEGER32, 4, true, false, false},
{DataType::INTEGER64, 8, true, false, false},
{DataType::REAL32, 4, false, true, false},
{DataType::REAL64, 8, false, true, false},
{DataType::VISIBLE_STRING, 0, false, false, true },
}};


constexpr bool isBasic(DataType type)
{
Expand Down Expand Up @@ -213,6 +240,9 @@ namespace kickcat::CoE
std::string description;

void* data{nullptr};
// Internal ownership flag: true once PDO mapping redirects `data` into the
// process image (dtor must not free it). Set only on the data object a PDO
// maps; NOT a per-field "ready" API and never set on a 0x16xx/0x1Axx mapping object.
bool is_mapped{false};

/// Called before access
Expand Down
26 changes: 26 additions & 0 deletions lib/include/kickcat/CoE/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ namespace kickcat::CoE
constexpr uint16_t SM_COM_TYPE = 0x1C00; // each sub-entry described SM[x] com type (mailbox in/out, PDO in/out, not used)
constexpr uint16_t SM_CHANNEL = 0x1C10; // each entry is associated with the mapped PDOs (if in used)

// One entry of a PDO mapping object (0x1600.../0x1A00...): the 32-bit subindex
// value packs the mapped object's index, subindex and bit length (ETG.1000.6 /
// CiA-301). A padding gap has index 0.
struct PdoMappingEntry
{
uint16_t index;
uint8_t subindex;
uint8_t bitlen;
};

constexpr uint32_t toMappingWord(PdoMappingEntry entry)
{
return (static_cast<uint32_t>(entry.index) << 16)
| (static_cast<uint32_t>(entry.subindex) << 8)
| static_cast<uint32_t>(entry.bitlen);
}

constexpr PdoMappingEntry fromMappingWord(uint32_t word)
{
return PdoMappingEntry{
static_cast<uint16_t>(word >> 16),
static_cast<uint8_t>(word >> 8),
static_cast<uint8_t>(word),
};
}

struct Header
{
uint16_t number : 9,
Expand Down
15 changes: 15 additions & 0 deletions lib/include/kickcat/OS/Mutex.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ namespace kickcat
private:
Mutex& mutex_; ///< Reference to the mutex managed by the guard
};

/// RAII manager for a non-blocking lock attempt. owns() reports whether the
/// lock was taken; the mutex is released on destruction only if owned.
class TryLockGuard
{
public:
TryLockGuard(Mutex& mutex);
~TryLockGuard();

bool owns() const { return owned_; }

private:
Mutex& mutex_;
bool owned_;
};
}

#endif
1 change: 1 addition & 0 deletions lib/include/kickcat/Ring.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define KICKCAT_RING_H

#include <cstddef>
#include <cstdint>

namespace kickcat
{
Expand Down
79 changes: 55 additions & 24 deletions lib/include/kickcat/SimulatorControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,49 @@ namespace kickcat::sim
ControlPayload payload;
};

struct ControlResponse
// Acknowledgement of a command: the echoed argument plus the outcome.
struct SetLinkAck
{
ControlCommand::Type type;
uint8_t ok; // 0: command rejected (bad arguments / unknown type)
ControlPayload payload; // result, valid only when ok == 1
SetLink link;
uint8_t ok; // 0: command rejected (bad arguments / unknown type)
};

// Frame-timing window the simulator emits unsolicited (one per N frames).
// Times are nanoseconds over the last `window` frames.
struct SimStats
{
uint64_t window;
uint64_t min_ns;
uint64_t max_ns;
uint64_t avg_ns;
};

union EventPayload
{
SetLinkAck set_link_ack;
SimStats stats;
};

// Everything the simulator sends back to the host travels on one return stream:
// command acks AND unsolicited events (frame stats today; more later). The host
// drains the stream and dispatches on `type`. Adding an event kind means a new
// Type tag and an EventPayload member -- no new channel.
struct ControlEvent
{
enum class Type : uint16_t
{
SetLinkAck,
FrameStats,
};

Type type;
EventPayload payload;
};

// The messages are byte-copied through a shared-memory ring.
static_assert(std::is_trivially_copyable_v<ControlCommand>);
static_assert(std::is_trivially_copyable_v<ControlResponse>);
static_assert(std::is_trivially_copyable_v<ControlEvent>);
static_assert(std::is_trivially_copyable_v<SimStats>);

// Shared-memory transport: the segment holds a small header plus the POD ring
// Contexts; each side wraps them with its own LockedRing whose pointers are
Expand All @@ -62,8 +95,8 @@ namespace kickcat::sim
{
public:
static constexpr uint32_t RING_SIZE = 64; // power of two
using CommandRing = LockedRing<ControlCommand, RING_SIZE>;
using ResponseRing = LockedRing<ControlResponse, RING_SIZE>;
using CommandRing = LockedRing<ControlCommand, RING_SIZE>;
using EventRing = LockedRing<ControlEvent, RING_SIZE>;

ControlChannel() = default;
ControlChannel(ControlChannel const&) = delete;
Expand All @@ -76,22 +109,20 @@ namespace kickcat::sim

bool sendCommand(ControlCommand const& cmd) { return commands_->push(cmd); }
bool nextCommand(ControlCommand& out) { return commands_->tryPop(out); }
bool sendResponse(ControlResponse const& r) { return responses_->push(r); }
bool nextResponse(ControlResponse& out) { return responses_->tryPop(out); }
bool sendEvent(ControlEvent const& e) { return events_->push(e); }
bool nextEvent(ControlEvent& out) { return events_->tryPop(out); }

// Command and response rings are the same size and used 1:1, so room for a
// response guarantees the matching command's ack will fit.
bool responseSpaceAvailable() { return responses_->size() < RING_SIZE; }
bool eventSpaceAvailable() { return events_->size() < RING_SIZE; }

private:
static constexpr uint32_t MAGIC = 0x53494d43; // 'SIMC'

struct Storage
{
uint32_t magic;
uint32_t layout_size;
CommandRing::Context commands;
ResponseRing::Context responses;
uint32_t magic;
uint32_t layout_size;
CommandRing::Context commands;
EventRing::Context events;
};

void open(std::string const& name, bool init)
Expand All @@ -106,12 +137,12 @@ namespace kickcat::sim
{
std::memset(storage, 0, sizeof(Storage));
}
commands_ = std::make_unique<CommandRing>(storage->commands);
responses_ = std::make_unique<ResponseRing>(storage->responses);
commands_ = std::make_unique<CommandRing>(storage->commands);
events_ = std::make_unique<EventRing>(storage->events);
if (init)
{
commands_->init();
responses_->init();
events_->init();
storage->layout_size = sizeof(Storage);
storage->magic = MAGIC; // stamp last: a peer only sees it once ready
}
Expand All @@ -121,9 +152,9 @@ namespace kickcat::sim
}
}

SharedMemory shm_{};
std::unique_ptr<CommandRing> commands_;
std::unique_ptr<ResponseRing> responses_;
SharedMemory shm_{};
std::unique_ptr<CommandRing> commands_;
std::unique_ptr<EventRing> events_;
};

// Master-side producer: creates and owns the channel.
Expand All @@ -135,8 +166,8 @@ namespace kickcat::sim
bool breakLink(uint16_t a, uint16_t b) { return setLink(a, b, 0); }
bool healLink(uint16_t a, uint16_t b) { return setLink(a, b, 1); }

bool send(ControlCommand const& cmd) { return channel_.sendCommand(cmd); }
bool nextResponse(ControlResponse& out) { return channel_.nextResponse(out); }
bool send(ControlCommand const& cmd) { return channel_.sendCommand(cmd); }
bool nextEvent(ControlEvent& out) { return channel_.nextEvent(out); }

private:
bool setLink(uint16_t a, uint16_t b, uint8_t up)
Expand Down
1 change: 1 addition & 0 deletions lib/include/kickcat/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ namespace kickcat
ERROR_ACK = 0x10 // Acknowledge flag request - check AL_STATUS
};
char const* toString(State state);
char const* toShortString(State state);

char const* ALStatus_to_string(int32_t code);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ namespace kickcat::sim
void attach(std::string const& name);
void drain(); // no-op until attached

// Publish the latest frame-timing window for the host to display.
// No-op until attached.
void publishStats(SimStats const& s);

private:
ControlResponse apply(ControlCommand const& cmd);
ControlEvent apply(ControlCommand const& cmd);

ControlChannel channel_;
EmulatedNetwork& network_;
Expand Down
34 changes: 24 additions & 10 deletions lib/simulation/src/SimulatorControlServer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,34 @@ namespace kickcat::sim
{
return;
}
// Stop if the response ring can't take an ack, so a command is never
// applied without being acknowledged (host fell behind draining responses).
// Stop if the event ring can't take an ack, so a command is never applied
// without being acknowledged (host fell behind draining the return stream).
ControlCommand cmd{};
while (channel_.responseSpaceAvailable() and channel_.nextCommand(cmd))
while (channel_.eventSpaceAvailable() and channel_.nextCommand(cmd))
{
channel_.sendResponse(apply(cmd));
channel_.sendEvent(apply(cmd));
}
}

ControlResponse SimulatorControlServer::apply(ControlCommand const& cmd)
void SimulatorControlServer::publishStats(SimStats const& s)
{
ControlResponse response{};
response.type = cmd.type;
response.ok = 0;
if (not attached_)
{
return;
}
// Best-effort event: stats are lossy by nature, so a full ring just drops
// this window. Acks keep their guaranteed slot via drain()'s space check.
ControlEvent event{};
event.type = ControlEvent::Type::FrameStats;
event.payload.stats = s;
channel_.sendEvent(event);
}

ControlEvent SimulatorControlServer::apply(ControlCommand const& cmd)
{
ControlEvent response{};
response.type = ControlEvent::Type::SetLinkAck;
response.payload.set_link_ack.ok = 0;

if (cmd.type == ControlCommand::Type::SetLink)
{
Expand All @@ -45,8 +59,8 @@ namespace kickcat::sim
{
bool const up = (link.up != 0);
network_.setLinkState(link.node_a, link.node_b, up);
response.ok = 1;
response.payload.set_link = link;
response.payload.set_link_ack.ok = 1;
response.payload.set_link_ack.link = link;
}
}

Expand Down
Loading
Loading