dgibson / python-smadata2

Python code for communicating with SMA photovoltaic inverters within built in Bluetooth
GNU General Public License v2.0
23 stars 12 forks source link

waiter() function #3

Open qurm opened 5 years ago

qurm commented 5 years ago

Hi,

I came across this project while looking for a fully Python-based SMA inverter tool, that would be easier to maintain & modify than the various C language sbfspot projects. It looks like a well structured code base - thanks for your efforts!

I have spent some hours understanding and commenting, but there's one area that puzzles me still. Can you explain the decorator function below, and how it works with the various tx & rx functions & the @waiter decorator? What is the purpose of creating the various _waitcond attributes and the waitvar ?

def waiter(fn):
    """ Adds wait conditions to itself, used with connection.wait() to wait for packets
    :param fn: like rx_raw, rx_outer, rx_ppp from the connection class
    :return: waitfn()
    """
    def waitfn(self, *args):
        fn(self, *args)
        if hasattr(self, '__waitcond_' + fn.__name__):
            wc = getattr(self, '__waitcond_' + fn.__name__)
            if wc is None:
                self.waitvar = args
            else:
                self.waitvar = wc(*args)
    return waitfn

Thanks, Andy

dgibson commented 5 years ago

On Sun, Feb 17, 2019 at 06:44:54AM +0000, qurm wrote:

Hi,

I came across this project while looking for a fully Python-based SMA inverter tool, that would be easier to maintain & modify than the various C language sbfspot projects. It looks like a well structured code base - thanks for your efforts!

I have spent some hours understanding and commenting, but there's one area that puzzles me still. Can you explain the function below, and how it works with the various tx & rx functions & the @waiter decorator?

Heh, it's a while since I wrote that.. had to figure out what I was doing again.

So the waiter stuff is either a neat trick or a gross hack - take yor pick - to deal with the fact that messages back from the inverter as basically asynchronous, but we want to present a synchronous interface to the user - call a function, something happens, you get a response as the return from that function.

In particular it's a way of doing that without heavyweight infrastructure like having a receiver thread, or having to write nearly all of the Rx path as a hard to decipher state machine.

If you look at the rx*() functions you'll see that they take some data from the inverter, decipher one protocol layer (roughly speaking) then pass that up to another rx*() function to decipher the next layer. At the end we reach rx_6560() which does.. basically nothing.

The trouble is that while the Rx functions know how to decipher packets from the inverter, they don't actually know what kind of information we're expecting from the inverter at the moment, so they don't know what to do with the data.

If you look at the "operation" functions - the ones that do some specific task and return some information obtained from the inverter, you'll see they generally have the form: ... tx_something() wait_something() ...

The tx*() function sends some request to the inverter, then we wait for a response. The wait*() functions are wrappers around wait(), which is the magic bit.

wait() takes parameters saying what type of packet we're looking for at what protocol layer. It pokes those into some special variables then just calls rx() until another special variable is set.

The trick is that the Rx functions have been decorated with @waiter, which augments the bare Rx function with code to check if the special wait variables are set, and if so check the results of the Rx to see if it's something we're currently waiting for, and if so put it somewhere that wait will be able to find it.

The waiter() function implements that decorator - it's a function that takes a function (the bare rx method) and returns a function (the rx method augmented with the logic to check what we're waiting for).

This could all be done by open-coding the checks for what we're waiting for in each of the relevant Rx functions, but there are quite a lot of Rx layers and using the decorator avoids that boilerplate at each level.

-- David Gibson | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you. NOT the other | way around! http://www.ozlabs.org/~dgibson

qurm commented 5 years ago

Thanks, and that all makes sense! I could not really identify the pattern, though could see it was a like a stack.

There is a line in waitfn(), where you save the args, self.waitvar = args. Would that not get overwritten if there were async packets being received out of order?

Anyway, it seems to work, so I'll leave it alone!!

BTW I did see a different response to a "HELLO" to the one in your code, and was easily fixed, but I guess this may be dependent on the specific inverter model & firmware. I have a SMA 5000TL, about 5 years old.

I am also getting Error responses from the inverter when using the sma2-explore tool and trying various send2 commands from your protocol.txt file. Possibly another small difference, but I should be able to work it out.

Thanks for the response, Andy

dgibson commented 5 years ago

On Thu, Feb 21, 2019 at 12:09:23PM +0000, qurm wrote:

Thanks, and that all makes sense! I could not really identify the pattern, though could see it was a like a stack.

There is a line in waitfn(), where you save the args, self.waitvar = args. Would that not get overwritten if there were async packets being received out of order?

Theoretically, yes. The reason this works is that while the protocol is asynchronous at the lower levels, it's effectively synchronous at the higher levels. So when we request a wait, we don't expect to get a packet matching the criteria we've set other than the one that's in response to the command we've set.

Like I said, this is kind of a hack - it's not an approach I'd recommend as a model for a protocol stack in most cases. In this case it seemed to work out as a good balance between handling asynchrony to the extent we need to, without having to structure the entire tool around asynchronous events.

Anyway, it seems to work, so I'll leave it alone!!

BTW I did see a different response to a "HELLO" to the one in your code, and was easily fixed, but I guess this may be dependent on the specific inverter model & firmware. I have a SMA 5000TL, about 5 years old.

Yeah, it can definitely vary depending on the inverter model. It also depends on the configuration - in particular there's a "net id" which some people have at least partly worked out, but I don't have any implementation of.

I believe mine is also an SB 5000TL (well, two of them).

I am also getting Error responses from the inverter when using the sma2-explore tool and trying various send2 commands from your protocol.txt file. Possibly another small difference, but I should be able to work it out.

Yours could well be configured for a different "net id" mode; I believe that changes a bunch of things about the protocol.

-- David Gibson | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you. NOT the other | way around! http://www.ozlabs.org/~dgibson