Skip to content

Commit 1efb5c7

Browse files
Demo V0.1 Fixed Structure
1 parent a3f2a1b commit 1efb5c7

File tree

33 files changed

+2306
-0
lines changed

33 files changed

+2306
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ venv/
6060
*.vscode_debug_path/
6161
lib/*
6262

63+
# Headers staged by sdk/tools/sync_headers.py (duplicates of src/)
64+
sdk/src/bsk_sdk/include/Basilisk/architecture/_GeneralModuleFiles/
65+
sdk/src/bsk_sdk/include/Basilisk/architecture/messaging/
66+
sdk/src/bsk_sdk/include/Basilisk/architecture/utilities/
67+
6368
# Python packaging
6469
*.egg-info
6570
build/

docs/source/Support/Developer.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The following support files help with writing Basilisk modules.
1616
Developer/createHtmlDocumentation
1717
Developer/bskModuleCheckoutList
1818
Developer/UnderstandingBasilisk
19+
Developer/bskSdkV1
1920
Developer/migratingBskModuleToBsk2
2021
Developer/MigratingToPython3
2122
Developer/addSupportData
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
Basilisk SDK Version 1
2+
======================
3+
4+
.. contents:: Outline
5+
:local:
6+
7+
Purpose
8+
-------
9+
10+
The Basilisk SDK (``bsk-sdk``) defines the public surface that external plugin
11+
authors can rely on when integrating new simulation capabilities with the
12+
core runtime. Version 1 focuses on establishing a stable contract for Python
13+
and C++ plugin authors and capturing the minimal tooling that ships inside the
14+
Basilisk source tree.
15+
16+
Scope and Deliverables
17+
----------------------
18+
19+
Version 1 guarantees the following artifacts:
20+
21+
- ``bsk_core.plugins``: the runtime registry responsible for discovering
22+
entry-point advertised plugins and exposing them under ``Basilisk.modules``.
23+
- ``bsk-sdk``: a small Python package that publishes the SDK headers, declares a
24+
dependency on the ``pybind11`` headers required by the helper macros, and
25+
provides :func:`bsk_sdk.include_dir` / :func:`bsk_sdk.include_dirs` helpers for
26+
build scripts.
27+
- A companion ``sync_headers.py`` utility (``sdk/tools``) keeps the vendored
28+
Basilisk ``architecture`` headers in sync with the main source tree.
29+
- ``sdk/include/bsk/sdk.hpp``: a single header that wraps the pybind11
30+
boilerplate required for C++ factories and enforces the default constructible
31+
+ ``Reset``/``UpdateState`` interface contract. The same header is shipped by
32+
:mod:`bsk-sdk`.
33+
- A consolidated ``plugins`` example package containing both Python and C++
34+
implementations that demonstrate the expected packaging and registration
35+
patterns.
36+
37+
Any other files in the repository are explicitly *not* part of the SDK
38+
agreement for this release.
39+
40+
Plugin Registry API
41+
-------------------
42+
43+
The ``bsk_core.plugins.PluginRegistry`` class is the primary integration
44+
point for third-party plugins. The registry is responsible for staging plugin
45+
definitions until the runtime exports them under ``Basilisk.modules``.
46+
47+
The public methods guaranteed in v1 are:
48+
49+
.. code-block:: python
50+
51+
class PluginRegistry:
52+
def register_python_module(self, name: str, cls: type[sysModel.SysModel]) -> None: ...
53+
def register_factory(self, name: str, factory: Any) -> None: ...
54+
55+
``register_python_module`` accepts any subclass of
56+
``Basilisk.architecture.sysModel.SysModel`` and exposes it as a class on
57+
``Basilisk.modules`` using the provided name. ``register_factory`` stores an
58+
opaque object under the supplied name. Factories are expected to be callables
59+
returning Basilisk-compatible module instances, but v1 defers any runtime shape
60+
validation to keep the surface area small.
61+
62+
Plugins must advertise a ``register(registry)`` callable through the
63+
``basilisk.plugins`` entry-point group. During startup Basilisk resolves the
64+
entry-point, imports the containing module, and invokes the callable with the
65+
shared registry instance.
66+
67+
Python Plugin Pattern
68+
---------------------
69+
70+
Pure-Python plugins should follow the pattern demonstrated in
71+
``plugins/src/python/Basilisk/ExternalModules/customPythonModule.py``:
72+
73+
.. code-block:: python
74+
75+
from Basilisk.architecture import sysModel
76+
77+
class ExamplePluginModule(sysModel.SysModel):
78+
def Reset(self, current_sim_nanos):
79+
...
80+
81+
def UpdateState(self, current_sim_nanos, call_time):
82+
...
83+
84+
def register(registry):
85+
registry.register_python_module("ExamplePluginModule", ExamplePluginModule)
86+
87+
The distribution's ``pyproject.toml`` must expose the ``register`` function via
88+
89+
.. code-block:: toml
90+
91+
[project.entry-points."basilisk.plugins"]
92+
example = "bsk_example_plugin.simple:register"
93+
94+
At runtime users import the module from ``Basilisk.modules``:
95+
96+
.. code-block:: python
97+
98+
from Basilisk import modules
99+
100+
plugin_cls = modules.ExamplePluginModule
101+
instance = plugin_cls()
102+
instance.Reset(0)
103+
instance.UpdateState(0, 0)
104+
105+
C++ Plugin Pattern
106+
------------------
107+
108+
Native extensions should include ``sdk/include/bsk/sdk.hpp`` to inherit
109+
the pybind11 binding helpers. When building outside the Basilisk source tree
110+
the :mod:`bsk-sdk` package exposes the headers via
111+
``import bsk_sdk; bsk_sdk.include_dir()`` (or ``include_dirs()`` to also capture
112+
the ``Basilisk`` subdirectory and ``pybind11`` include path). Version 1
113+
guarantees the availability of:
114+
115+
- ``bsk::plugin::register_basic_plugin``
116+
- ``BSK_PLUGIN_PYBIND_MODULE``
117+
118+
The ``BSK_PLUGIN_PYBIND_MODULE`` macro defines both the pybind11 module and the
119+
``create_factory`` callable consumed by the Basilisk runtime. The expected class
120+
contract mirrors the Python case: default constructible with ``Reset`` and
121+
``UpdateState`` methods.
122+
123+
.. code-block:: cpp
124+
125+
#include <bsk/sdk.hpp>
126+
127+
class ExampleCppModule {
128+
public:
129+
void Reset(double current_sim_nanos);
130+
void UpdateState(double current_sim_nanos, double call_time);
131+
};
132+
133+
BSK_PLUGIN_PYBIND_MODULE(_example_cpp, ExampleCppModule, "ExampleCppModule");
134+
135+
The companion Python package should lazily import the extension, extract the
136+
factory, and register it:
137+
138+
.. code-block:: python
139+
140+
from importlib import import_module
141+
142+
def register(registry):
143+
ext = import_module("bsk_example_plugin_cpp._example_cpp")
144+
factory = ext.create_factory()
145+
registry.register_factory("ExampleCppFactory", factory)
146+
147+
Limitations and Future Work
148+
---------------------------
149+
150+
Version 1 intentionally leaves several items out of scope so they can be
151+
designed with real-world feedback:
152+
153+
- The SDK header is distributed from the Basilisk source tree and is not
154+
published as a standalone artifact.
155+
- Factories registered via ``register_factory`` are treated as opaque callables;
156+
Basilisk does not verify their type or interface beyond name collisions.
157+
- The helper header requires C++17 and a compatible pybind11 toolchain.
158+
- Plugin lifecycle hooks beyond ``Reset``/``UpdateState`` will be designed as
159+
future Basilisk modules adopt richer interfaces.
160+
161+
Feedback on these gaps is welcome and will inform the roadmap for subsequent
162+
SDK revisions.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
cmake_minimum_required(VERSION 3.18)
2+
project(bsk_plugin_example LANGUAGES CXX)
3+
4+
set(CMAKE_CXX_STANDARD 17)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
7+
find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)
8+
9+
execute_process(
10+
COMMAND "${Python3_EXECUTABLE}" -c "import bsk_sdk; print('\\n'.join(bsk_sdk.include_dirs()), end='')"
11+
OUTPUT_VARIABLE BSK_SDK_INCLUDE_OUTPUT
12+
RESULT_VARIABLE BSK_SDK_RESULT
13+
)
14+
if(NOT BSK_SDK_RESULT EQUAL 0)
15+
message(FATAL_ERROR "Failed to locate bsk-sdk include directories. Is the package installed?")
16+
endif()
17+
string(REPLACE "\r" "" BSK_SDK_INCLUDE_OUTPUT "${BSK_SDK_INCLUDE_OUTPUT}")
18+
string(STRIP "${BSK_SDK_INCLUDE_OUTPUT}" BSK_SDK_INCLUDE_OUTPUT)
19+
string(REPLACE "\n" ";" BSK_SDK_INCLUDE_DIRS "${BSK_SDK_INCLUDE_OUTPUT}")
20+
list(GET BSK_SDK_INCLUDE_DIRS 1 BSK_SDK_BASILISK_INCLUDE)
21+
22+
Python3_add_library(_custom_cpp MODULE ExternalModules/CustomCppModule/custom_cpp_module.cpp WITH_SOABI)
23+
target_compile_features(_custom_cpp PRIVATE cxx_std_17)
24+
target_include_directories(
25+
_custom_cpp
26+
PRIVATE
27+
${BSK_SDK_INCLUDE_DIRS}
28+
)
29+
target_link_libraries(_custom_cpp PRIVATE Python3::Module)
30+
31+
set(BSK_ARCHITECTURE_SOURCES
32+
"${BSK_SDK_BASILISK_INCLUDE}/architecture/_GeneralModuleFiles/sys_model.cpp"
33+
"${BSK_SDK_BASILISK_INCLUDE}/architecture/utilities/bskLogging.cpp"
34+
"${BSK_SDK_BASILISK_INCLUDE}/architecture/utilities/moduleIdGenerator/moduleIdGenerator.cpp"
35+
)
36+
37+
target_sources(_custom_cpp PRIVATE ${BSK_ARCHITECTURE_SOURCES})
38+
39+
install(
40+
TARGETS _custom_cpp
41+
DESTINATION Basilisk/ExternalModules
42+
COMPONENT extensions
43+
)
44+
45+
install(
46+
DIRECTORY src/python/
47+
DESTINATION .
48+
COMPONENT python
49+
)
50+
51+
install(
52+
DIRECTORY msgPayloadDefC msgPayloadDefCpp
53+
DESTINATION Basilisk
54+
COMPONENT python
55+
)
56+
57+
if(APPLE)
58+
# 1) Don't hide symbols for this target (avoid losing PyInit export)
59+
set_target_properties(_custom_cpp PROPERTIES
60+
C_VISIBILITY_PRESET default
61+
CXX_VISIBILITY_PRESET default
62+
VISIBILITY_INLINES_HIDDEN OFF
63+
)
64+
65+
endif()
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#include <algorithm>
2+
#include <array>
3+
#include <cstdint>
4+
#include <vector>
5+
6+
#include <Basilisk/architecture/_GeneralModuleFiles/sys_model.h>
7+
#include <Basilisk/architecture/messaging/messaging.h>
8+
9+
#include <bsk/sdk.hpp>
10+
#include <pybind11/pybind11.h>
11+
#include <pybind11/stl.h>
12+
13+
namespace {
14+
15+
struct CustomPluginMsgPayload
16+
{
17+
std::array<double, 3> dataVector{};
18+
};
19+
20+
class CustomCppModule : public SysModel
21+
{
22+
public:
23+
CustomCppModule()
24+
: output_writer_(dataOutMsg.addAuthor())
25+
, input_reader_(input_channel_.addSubscriber())
26+
, input_writer_(input_channel_.addAuthor())
27+
{
28+
this->ModelTag = "CustomCppModule";
29+
}
30+
31+
void Reset(uint64_t /*current_sim_nanos*/) override
32+
{
33+
reset_called_ = true;
34+
update_called_ = false;
35+
steps_ = 0;
36+
last_input_ = {};
37+
last_output_ = {};
38+
last_update_nanos_ = 0;
39+
}
40+
41+
void UpdateState(uint64_t current_sim_nanos) override
42+
{
43+
update_called_ = true;
44+
++steps_;
45+
46+
if (input_reader_.isLinked() && input_reader_.isWritten()) {
47+
last_input_ = input_reader_();
48+
}
49+
50+
last_output_ = last_input_;
51+
last_output_.dataVector[0] += static_cast<double>(steps_);
52+
last_output_.dataVector[2] = static_cast<double>(current_sim_nanos) * 1e-9;
53+
54+
output_writer_(&last_output_, this->moduleID, current_sim_nanos);
55+
last_update_nanos_ = current_sim_nanos;
56+
}
57+
58+
void set_input_payload(CustomPluginMsgPayload payload)
59+
{
60+
// WriteFunctor wants a non-const pointer; payload is local, so OK.
61+
input_writer_(&payload, this->moduleID, last_update_nanos_);
62+
}
63+
64+
CustomPluginMsgPayload last_input() const { return last_input_; }
65+
CustomPluginMsgPayload last_output() const { return last_output_; }
66+
uint64_t last_update_nanos() const { return last_update_nanos_; }
67+
bool reset_called() const { return reset_called_; }
68+
bool update_called() const { return update_called_; }
69+
70+
private:
71+
Message<CustomPluginMsgPayload> dataOutMsg;
72+
Message<CustomPluginMsgPayload> input_channel_;
73+
74+
WriteFunctor<CustomPluginMsgPayload> output_writer_;
75+
ReadFunctor<CustomPluginMsgPayload> input_reader_;
76+
WriteFunctor<CustomPluginMsgPayload> input_writer_;
77+
78+
CustomPluginMsgPayload last_input_{};
79+
CustomPluginMsgPayload last_output_{};
80+
81+
uint64_t last_update_nanos_ = 0;
82+
bool reset_called_ = false;
83+
bool update_called_ = false;
84+
int steps_ = 0;
85+
};
86+
87+
} // namespace
88+
89+
PYBIND11_MODULE(_custom_cpp, m)
90+
{
91+
namespace py = pybind11;
92+
93+
py::class_<CustomPluginMsgPayload>(m, "CustomPluginMsgPayload")
94+
.def(py::init<>())
95+
.def(py::init([](const std::vector<double>& values) {
96+
CustomPluginMsgPayload payload;
97+
for (std::size_t i = 0; i < std::min(values.size(), payload.dataVector.size()); ++i) {
98+
payload.dataVector[i] = values[i];
99+
}
100+
return payload;
101+
}))
102+
.def_readwrite("dataVector", &CustomPluginMsgPayload::dataVector);
103+
104+
py::class_<CustomCppModule>(m, "CustomCppModule")
105+
.def(py::init<>())
106+
.def("Reset", &CustomCppModule::Reset, py::arg("current_sim_nanos"))
107+
.def("UpdateState", &CustomCppModule::UpdateState, py::arg("current_sim_nanos"))
108+
.def("set_input_payload", &CustomCppModule::set_input_payload, py::arg("payload"))
109+
.def_property_readonly("last_input", &CustomCppModule::last_input)
110+
.def_property_readonly("last_output", &CustomCppModule::last_output)
111+
.def_property_readonly("last_update_nanos", &CustomCppModule::last_update_nanos)
112+
.def_property_readonly("reset_called", &CustomCppModule::reset_called)
113+
.def_property_readonly("update_called", &CustomCppModule::update_called);
114+
115+
m.def("create_factory", []() { return bsk::plugin::make_factory<CustomCppModule>(); });
116+
}

0 commit comments

Comments
 (0)