Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework generic example #149

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

Rework generic example #149

wants to merge 16 commits into from

Conversation

TanDro
Copy link
Contributor

@TanDro TanDro commented Dec 11, 2024

No description provided.

@cklb
Copy link
Contributor

cklb commented Dec 12, 2024

Thank you for reworking the example. When trying it I got a warning that a file called dependencies.cmakeis missing. Based on the old example I tried to add it and now compiling and running the simulation as well as the visualization works well.
I tried to commit the file but it is currently listed as ignored. Now I am not so sure if I am on the correct path 😅

@jhalmen
Copy link
Contributor

jhalmen commented Dec 12, 2024

@cklb you are right, they were missing, and also those files don't belong in the .gitignore file.. pushed a fix and rebased

@jhalmen
Copy link
Contributor

jhalmen commented Dec 12, 2024

also, this actually closes #148
also closes #143

otherwise it complains about missing .widgets
@cklb
Copy link
Contributor

cklb commented Dec 13, 2024

Thanks. As I failed quite spectacularly last time, would it be possible to elaborate a little more on what happens when a frame with id 25 is sent over from pywisp to the simulated rig? I mean somebody surely calls Pendulum::setInput since I can se the effect in the simulation but the whole frame handling seems to happen somewhere else now?

@jhalmen
Copy link
Contributor

jhalmen commented Dec 13, 2024

Hm I think you're confusing something.
pywisp -> rig communication are all the get*Params methods in the visu subfolder.
These can be checked quickly:

$ grep "get.*Params" -A 8 visu -R 
visu/testbench.py:    def getParams(self, data):
visu/testbench.py-        payloadConfig = struct.pack('<B',
visu/testbench.py-                                    int(data[0]),
visu/testbench.py-                                    )
visu/testbench.py-
visu/testbench.py-        dataPoints = [
visu/testbench.py-            {'id': 10,
visu/testbench.py-             'msg': payloadConfig
visu/testbench.py-             },
--
visu/trajectory.py:    def getParams(self, data):
visu/trajectory.py-        payloadConfig = struct.pack('<B',
visu/trajectory.py-                               int(data[0]),
visu/trajectory.py-                               )
visu/trajectory.py-
visu/trajectory.py-        dataPoints = [
visu/trajectory.py-            {'id': 20, 'msg': payloadConfig},
visu/trajectory.py-            *packArrayToFrame(21, flatten(data[1:]), 80, 8, 4)
visu/trajectory.py-        ]

here we see the listed IDs: [10, 20, 21]

in the file inc/model.h we can also check what the rig expects to be sent/what will happen when packages are sent over:

$ grep setHandler inc/model.h
        fr.setHandler(10, *this, &Model::setModelData);
        fr.setHandler(20, *this, &Model::setTrajType);
        fr.setHandler(21, trajType, &TrajType::setData);

this shows the methods that are called on the given objects for the listed communication IDs.

rig -> pywisp communication this is always just measurements, and for now no automation ever happens as a result of sent data (there's one exception: sending a disconnect request upon a missed heartbeat, but this is hidden away and works 'automatically' if the user enables heartbeats)

on pywisp side:

$ grep handleFrame -A 4 visu -R
visu/testbench.py:    def handleFrame(self, frame):
visu/testbench.py-        dataPoints = {}
visu/testbench.py-        fid = frame.min_id
visu/testbench.py-        if fid == 15:
visu/testbench.py-            data = struct.unpack(f'<L{len(DoublePendulum.dataPoints)}d', frame.payload)
--
visu/trajectory.py:    def handleFrame(self, frame):
visu/trajectory.py-        dataPoints = {}
visu/trajectory.py-        fid = frame.min_id
visu/trajectory.py-        if fid == 25:
visu/trajectory.py-            data = struct.unpack(f'<L{len(Trajectory.dataPoints)}d', frame.payload)

this handles IDs [15, 25]

on the rig side:

$ grep "\.pack" -C 1 inc -R 
inc/model.h-        Frame f{15};
inc/model.h:        f.pack(time);
inc/model.h:        f.pack(doublePendulum.state(0));
inc/model.h:        f.pack(doublePendulum.state(2));
inc/model.h:        f.pack(doublePendulum.state(4));
inc/model.h:        f.pack(doublePendulum.in);
inc/model.h-        out.push(f);
--
inc/model.h-        Frame f{25};
inc/model.h:        f.pack(time);
inc/model.h:        f.pack(trajType.des[0]);
inc/model.h:        f.pack(trajType.des[1]);
inc/model.h-        out.push(f);

your question

what happens when a frame with id 25 is sent over from pywisp to the simulated rig?

The short answer is: nothing. such a frame would be dropped, because nobody is expecting it.
For a frame with id 25 going the other way, i.e. from the rig, to pywisp, pywisp will receive this frame, unpack the trajectory data points, and add them to the measurements for later display / export.

I mean somebody surely calls Pendulum::setInput

This is correct, but it doesn't have anything to do with a frame of id 25.
It happens in the Pendulum::tick method, which is registered to run once every millisecond, but only while the experiment is active (Experiment start / stop / heartbeat happens on frames with id 1, but this is handled internally and doesn't need to be touched by the user). This can be seen in the Model::init method:

void init() {                                                 
    // reset                                                  
    e.onEvent(e.INIT).call(*this, &Model::reset);             
                                                              
    e.during(e.RUN).every(1, doublePendulum, &Pendulum::tick);      /// <---- HERE
                                                              
    // timesteps                                              
    e.during(e.RUN).every(2, trajType, &TrajType::step);      
                                                              
    e.during(e.RUN).every(20, *this, &Model::sendModelData);  
    e.during(e.RUN).every(20, *this, &Model::sendTrajData);   
                                                              
    fr.setHandler(10, *this, &Model::setModelData);           
    fr.setHandler(20, *this, &Model::setTrajType);            
    fr.setHandler(21, trajType, &TrajType::setData);          
}                

so then every millisecond

void tick(uint32_t time, uint32_t dt) {
    this->in = input.get();            
    auto u = this->in;                 
    setInput(u);                       
                                       
    updateState(time, dt);             
};

it fetches the current input, (which was previously connected to the trajectory in the Model::setModelData which gets called on ID 10), sets is (which in the case of the sim just means storing it, in case of the actual rig means powering the motor), then 'updates the state' (so runs a step of the simulation, or in the case of the actual rig, stores measurements from all the sensors).

There are also a few things that are registered to be called only in the sim, or only on the real rig, these are then in their respective locations, to see all:

$ grep -E 'onEvent|during' . -R --exclude-dir=build --exclude-dir=.venv
./sim/sim.cpp:    e.onEvent(e.STOP).call([](uint32_t) {
./sim/sim.cpp:    e.onEvent(e.INIT).call([](uint32_t) {
./inc/model.h:        e.onEvent(e.INIT).call(*this, &Model::reset);
./inc/model.h:        e.during(e.RUN).every(1, doublePendulum, &Pendulum::tick);
./inc/model.h:        e.during(e.RUN).every(2, trajType, &TrajType::step);
./inc/model.h:        e.during(e.RUN).every(20, *this, &Model::sendModelData);
./inc/model.h:        e.during(e.RUN).every(20, *this, &Model::sendTrajData);
./stm/f4.cpp:    e.during(e.RUN).every(500, [](uint32_t, uint32_t) {
./stm/f4.cpp:    e.during(e.IDLE).every(2000, [](uint32_t, uint32_t) {
./stm/f4.cpp:    e.onEvent(e.STOP).call([](uint32_t) {

Hope this helps and isn't too much of a wall of text.

@cklb
Copy link
Contributor

cklb commented Dec 16, 2024

Thank you for the clarification. ID 20 is what I actually meant, since I already tried to pass some Trajectories down to the test rig last time and failed with the unpacking. In the end I got confused over the different actors (kernel, support, model) and how they interact with each other.

If you don't mind, I would like to transfer some of your information directly into the examples documentation. Although some of it may be better rest (or already present) in the tool-lib docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants