thecognifly / YAMSPy

Yet Another Multiwii Serial Protocol Python Interface... for Betaflight, iNAV, etc...
https://thecognifly.github.io/
GNU General Public License v3.0
94 stars 32 forks source link

work without a loop #11

Closed johncook1212 closed 3 years ago

johncook1212 commented 3 years ago

Hello Ricardo, Hope all is good :) I've tried with gpizero and it looks nice but would like to try and use the code without keyboard. So, for that I wrote a new main function without a loop where I use YAMSPy as following:

board = MSPy(device="/dev/ttyAMA0", loglevel='WARNING', baudrate=115200) board.enter() print('after connection') CMDS = {'roll':1500,'pitch':1500,'throttle': 900,'yaw':1500,'CH5':2000,'aux2':1000} CMDS_ORDER = ['roll', 'pitch', 'throttle', 'yaw', 'CH5', 'aux2'] if board.send_RAW_RC([CMDS[ki] for ki in CMDS_ORDER]): dataHandler = board.receive_msg() board.process_recv_data(dataHandler) print('after commands')

The value of CH5 (2000) is configured to arm the drone. When I use the regular simpleUI script the drone is armed, but when I use the modified script I see in INAV configurator that CH5 indeed is changed to 2000 but the drone is not armed. The only difference is the loop. What do you think?

Beast regards and stay healthy!

John

ricardodeazambuja commented 3 years ago

I haven't tested your code because I don't have a drone with me right now, but the flight controller needs to receive a stable stream of commands (the minimum frequency depends on your configuration for the fail safe) or it will enter fail safe and that would certainly block any attempt of arming.

johncook1212 commented 3 years ago

thanks for the prompt response. I would like to emphasize that the code in the previous message is under the main function instead and replaces run_curses(keyboard_controller) I will check the configuration for fail safe via the cli but what is the minimum of commands? John

johncook1212 commented 3 years ago

I mean minimum stream of commands... :)

ricardodeazambuja commented 3 years ago

From memory I can't remember, but I would bet it needs to be more than 10Hz :) INAV configurator (Betaflight configurator) allows you to specify the delay before it goes into fail safe. You can also test it by adding a delay inside simpleUI main loop and check when it stops working (for testing whether or not the firmware has something hardcoded).

Failsafe is something very useful (it saved me from destroying things many times) and important for safety in general, so, please, don't disable it or increase the delay too much or crazy (bad) things will probably happen.

johncook1212 commented 3 years ago

I will try it and update. but in general, my intention was to write an "automatic" script that will navigate the drone in a route. for example: arm -> throttle = X -> pitch = Y -> pitch = -Y, without user interference. this is why I started with the arm method via CH% outside any loop. So the FC received a single command - to set 6 RC values. maybe it needs more?

johncook1212 commented 3 years ago

CH5 meaning set the RC values where CH5=2000

ricardodeazambuja commented 3 years ago

You need something like a loop that keeps sending the last command with a minimum frequency. However, it doesn't need to be a loop, it could be a routine based on alarm (more details about signals here).

Here is an old script where I used alarm:

#!/usr/bin/env python
"""alarmdecorator.py: A very simple way to make things a little bit less nondeterministic in Python.
Deeply inspired by https://stackoverflow.com/a/31667005
WARNING: This is NOT deterministic, real time, soft real time (??) at all!!!!
"""

import signal
from functools import wraps

class ReturnControl(Exception):
   """Raised when alarm signal is called"""
   pass

def handler(signum, frame):
    """Handler (callback) used with signal.signal"""
    # if you try any IO here (e.g. stdout), Python3 will complain
    raise ReturnControl # I'm using a custom exception trying to avoid problems

signal.signal(signal.SIGALRM, handler)

def AlarmDecorator(ts):
    """Decorator for a soft soft soft... soft real time experience in Python3.
    Parameters
    ----------
    ts : float
        Time until the function is forced to return, in seconds
    """
    signal.signal(signal.SIGALRM, handler)
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            signal.setitimer(signal.ITIMER_REAL, ts) # here is where the timer will start
            try:
                state = 0
                fn_gen = fn(*args, **kwargs)
                while state is not None:
                    state = next(fn_gen)
            except ReturnControl:
                pass
            finally:
                signal.setitimer(signal.ITIMER_REAL, 0) # always turns off the timer
                return state
            return None
        return wrapper
    return decorator

if __name__ == "__main__":
    # Usage example... and testing ;)
    import time
    import os

    ts = 1/1000
    @AlarmDecorator(ts) # remember, the argument passed is in seconds
    def countdown(state=0):
        """The functions using the AlarmDecorator must be a generator and return their current state.
        Ideally, it should receive as an argument the initial state, so it can continue from there.
        If a state takes longer than the value passed to the decorator (0.1s in this example), it will
        never finish executing it and it will get stuck there FOREVER :)
        """
        try:
            while state is not None: # according to https://www.python.org/dev/peps/pep-0008/#programming-recommendations
                if state==0:
                    state += 1
                elif state==1:
                    state += 1
                elif state==2:
                    state += 1
                elif state==3:
                    state += 1
                elif state==10:
                    state = None # yielding a None value will stop it
                                 # and cancel the timer.
        finally:
            yield state # finally will guarantee it will always return the current state

    # After calling the line below, it should go through State 0, 1, 2 and 3 until the alarm goes off.
    # At that point, it will show what was the state when the alarm went off.
    # This example is not taking into account the time spent by the print statements (as well as time.time(), format, subtractions, divisions...)

    print("Alarm set for {:e} seconds ({:.2f}Hz)!".format(ts,1/ts))

    start_time = time.time()
    curr_state = countdown(state=2)
    curr_time = time.time()
    print("This is the returned state value from the first call (time {0:e} - {1:.2f}Hz): {2}".format(curr_time-start_time, 
                                                                                                    1/(curr_time-start_time), curr_state))

    curr_state = countdown(state=curr_state)
    final_time = time.time()
    print("And this is the returned state value from the second call (time {0:e} - {1:.2f}Hz): {2}".format(final_time-curr_time, 
                                                                                                        1/(curr_time-start_time), curr_state))

    print("Alarm set for {:e} seconds ({:.2f}Hz)!".format(ts,1/ts))
    start_time = time.time()
    curr_state = countdown(state=2)
    curr_time = time.time()
    print("This is the returned state value from the first call (time {0:e} - {1:.2f}Hz): {2}".format(curr_time-start_time, 
                                                                                                    1/(curr_time-start_time), curr_state))

    curr_state = countdown(state=curr_state)
    final_time = time.time()
    print("And this is the returned state value from the second call (time {0:e} - {1:.2f}Hz): {2}".format(final_time-curr_time, 
                                                                                                        1/(curr_time-start_time), curr_state))
johncook1212 commented 3 years ago

Thanks, I will read that and make the test on the loop as you said and update :) John

johncook1212 commented 3 years ago

Hi, so I did some debug today and here are the findings:

  1. At the INAV configurator FAILSAFE is activated after 500[ms] where no signal/command is sent to the drone. I've added sleep() at the main loop and it stopped working with "time.sleep(0.171)". at time.sleep(0.17) it worked.
  2. Trying to understand this number, I've stopped at the data object inside send_RAW_RC. The message length is 18 bytes, with baudrate=115200, the FC can handle ~ 800msgs if I am not wrong. So, I didn't understand why sleep for 170[ms] worked and little delta destroyed it. what do you think?
  3. Regarding signals - did you mean to define a handler for signal.alarm(predetermined cycle) etc? If yes, I think I should read more about interrupts and multithreadingin python. Appreciate if you have a good reference :)

Thanks John

ricardodeazambuja commented 3 years ago

1/0.17 is around 6Hz, but the loop inside simpleUI also has its own delays, so the total period will be probably bigger than 0.17s and the frequency smaller than 6Hz. As I told you, I was expecting the minimum frequency to be around 10Hz. I suggested the signal.alarm because that way you would always send a command to the FC without the need of a fast loop. However, remember that you need something that avoids reaching the end of the script, or the interpreter will exit.

johncook1212 commented 3 years ago

The interesting thing is that the average GUI cycle showed in the terminal ~ 9.81[Hz] If you want I cant upload the script here or specify where Input the sleep. John

ricardodeazambuja commented 3 years ago

The simpleUI loop actually runs at different speeds for different things. The GUI is the slowest, but the part sending / receiving commands from the FC runs faster.

johncook1212 commented 3 years ago

ok, thanks Ricardo!

ricardodeazambuja commented 3 years ago

@johncook1212 I just found about rich and I thought you would be interested in trying to build a better user interface using it (instead of curses): https://rich.readthedocs.io/en/latest/layout.html