tino / pyFirmata

Python interface for the Firmata (http://firmata.org/) protocol. It is compliant with Firmata 2.1. Any help with updating to 2.2 is welcome. The Capability Query is implemented, but the Pin State Query feature not yet.
MIT License
576 stars 193 forks source link

Missing example of handlers - migrating to 2.2? #12

Closed cefn closed 11 years ago

cefn commented 11 years ago

I think I understand the way that handlers are bound to Firmata events (via add_cmd_handler) but I'm flummoxed how to tell what kind of structure my handler function should have so that introspection serves out arguments correctly for a new command binding (beyond the analog and digital bindings you've already provided).

Are there examples or documentation I've missed, for example, detailing how to add a handler for an arbitrary string command, as sent from the Firmata function "sendString" ? If not, would it be possible to create an example or a note about how add_cmd_handler works/might work for the range of possible transmissions listed at... http://www.firmata.org/wiki/V2.2ProtocolDetails ...or even... http://www.firmata.org/wiki/V2.3ProtocolDetails

Is it simply about defining a function with a single named/unnamed byte argument per byte expected beyond the command byte, then unmarshalling them in ways which reflect the protocol?

I'm guessing that moving towards 2.2/2.3 is mainly about this kind of binding, but I don't know if there's a structural problem with the current code passing through the newer messages since 2.1. This might be another area which I can get to in coming months.

tino commented 11 years ago

There are actually two types of handlers, which is what causes the confusion I think. There are handlers for 'normal' commands, like "report analog pin", "report digital port" and "report version". And there are also handlers for sysex-commands, like "report firmware". The difference is that the former take a specified number of arguments (usually two, a least and most significant byte, to be able to define numbers higher than 256) and the latter take any number of arguments, as everything is read until the END_SYSEX bytes comes along. See the iterate method for how this works.

So if you want to define a non-sysex handler, use the args you need. If you define a handler for a sysex command (which you probably want to do for custom commands), you can do the same if the number of arguments is fixed, or use *data as in the _handle_report_firmware handler.

Looking at it now, I might split the handler in two types later (sysex/non-sysex) as that looks a little clearer.

About moving towards 2.2/2.3, I think the only thing really needed is implementing the remaining sysex commands for capabilities querying and the like. But it might include some refactoring to make this easier. I will probably look at this next week.

cefn commented 11 years ago

Thanks for taking the time for such a detailed reply. This information should be more than enough for me to put together my code examples.

In particular I'm aiming to write logic on both sides of the connection which permits the marshalling and unmarshalling of canonical Arduino data types (based on 8-bit), into a 7-bit transport, as per the code below in Arduino, and the equivalent code in python below that. Please note these code blocks have not been tested, but in some ways code is its own best description.

/** Symmetric with decodefrom7 - encodes bytes to 7-bit values, with an overflow byte 
* to store extra bits. Returns the number of total bytes written to the destination byte array*/
int encodeTo7(byte* srcBytes, byte* dstBytes, int srcCount){
  byte overflowByte = 0;
  byte overflowPos = 0;
  int dstPos = 0; //current writing position in dst array
  for(int srcPos = 0; srcPos < srcCount; srcPos++){

    //store msb in overflowByte, filling bits from left, shifting one extra to avoid msb
    overflowByte |= (srcBytes[srcPos] & 0x80) >> (overflowPos + 1); //store msb in overflow byte using next free position
    overflowPos++; //update overflow write position

    //passthrough the 7-bit truncated byte filling destination array from left
    dstBytes[dstPos] = srcBytes[srcPos] & ~0x80; //write the 7 allowed bits (zeroing the msb)
    dstPos++;//update destination write position

    //write overflow byte to stream when it's full or stream is ending
    if(overflowPos == 7 || srcPos == srcCount-1){ //byte full or stream ended
      dstBytes[dstPos] = overflowByte; //write the 7 allowed bits
      dstPos++; //update destination write position
      overflowByte = 0; //reset overflow byte
      overflowPos = 0; //start writing from beginning again
    }
  }
  return dstPos;
}

/** Should be symmetric with encodeTo7(). It populates the specified number of output bytes by reading
* 7 bits from each source byte and retrieving the extra bit from a 7-bit overflow byte which is written after 
* after each frame of 7 bytes (with the final frame potentially being less than 7 bytes). */
void decodeFrom7(byte* srcBytes, byte* dstBytes, int dstCount){
  int srcPos = 0;
  int partialFrameLength = dstCount % 7; //find out number of bytes stored in last frame (remainder from frames of 7)
  //int srcCount = ((dstCount / 7) * 8) + (partialFrameLength == 0 ? 0 : partialFrameLength + 1); //count of source bytes needed
  int overflowPos = 0; //keeps track of the next overflow bit to read from the overflow byte 
  for(int dstPos = 0; dstPos < dstCount; dstPos++){

    //get the 7 bits from the current byte, and reconstruct the final bit from the overflowbyte
    int frameLength = dstPos + partialFrameLength < dstCount ? 7 : partialFrameLength; //frame length (not including overflow byte)
    dstBytes[dstPos] = srcBytes[srcPos] | ((srcBytes[srcPos - overflowPos + frameLength] << (1 + overflowPos)) | 0x80); //read extra overflow bit from predictably offset overflow byte 
                                                                                                      //(srcPos - overflowPos is the start of the frame)
    overflowPos++; //update overflow read position
    srcPos++; //update src read position
    if(overflowPos == 7){
      overflowPos = 0;
      srcPos++; //update read position to skip the overflow byte
    }
  }
}
# returns the bytes encoded see canonical Arduino code for comments - this is a python port
def encode7(srcbytes):

    dstbytes = array()
    overflowbyte = 0
    overflowpos = 0
    dstpos = 0
    for(srcpos, srcbyte in enumerate(srcbytes):
        overflowbyte |= (srcvalues[srcpos] & 0x80) >> (overflowpos + 1)
        overflowpos += 1
    dstbytes.push(srcbytes[srcpos] & ~0x80)
    dstpos += 1

    if(overflowPos == 7 || srcPos == srcCount-1):
        dstbytes.push(overflowbyte);
        dstpos += 1
        overflowbyte = 0
        overflowpos = 0

    return dstbytes

# returns the bytes decoded, see canonical Arduino code for comments - this is a port
def decode7(srcbytes, dstcount):
    dstbytes = array()
    srcpos = 0
    partialframelength = srcbytes.length mod 8
    if(partialframelength != 0):
        partialframelength -= 1

    overflowpos = 0

    for(dstpos in range(dstcount)):
        framelength = 7 if (dstpos + partialframelength < dstcount) else partialframelength 
        dstbytes.push(srcbytes[srcpos] | ((srcbytes[srcpos - overflowpos + framelength] << (1 + overflowpos)) | 0x80))
        overflowpos += 1
        srcpos += 1
        if(overflowpos == 7):
            overflowpos = 0
            srcpos += 1

    return dstbytes