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

Complete the "binary" documentation such that anybody can write a "wrapper" #106

Open
DesiOtaku opened this issue May 10, 2024 · 2 comments

Comments

@DesiOtaku
Copy link

Right now, inputmodule-rs is a python + rust project. Great for command line, not so great for developers who wish not to touch either language.

Good news is that there is some binary level documentation available here: https://github.com/FrameworkComputer/inputmodule-rs/blob/main/commands.md

However, it is currently incomplete. Things like how the bytes are oriented for images (left to right then down, or up to down then left to right?), how to make a custom animation, etc. This would be useful for developers who wish to make their own "wrapper" (like a C/C++ wrapper, or somebody wants to write everything in Java) and would not need an external dependency.

@mathsaey
Copy link

For anybody who stumbles on this issue and is also wondering how the DrawBW call works.

As @DesiOtaku pointed out, the byte orientation is not immediately obvious; if you write a loop which sets every bit one by one, the activated pixel will first move right to left, after which it suddenly jumps to the next line. The location where this occurs is not the same on each line either, which makes it tricky to figure out what is going on.

There are two things which cause confusion here:

  1. The LED Matrix is a 34x9 grid, while the DrawBW call accepts 39 bytes (i.e. 39x8 bits) of data. Thus, the dimensions of both "grids" don't match. The real grid has a width of 9 while the binary grid has a width of 8.
  • If you do the loop experiment I mentioned above, you will notice the "jump" to a new line always occurs on a multiple of 8.
  1. The bits in each byte are read right-to-left instead left-to-right. Thus, setting the first bit will activate the 8th pixel on line 0 of the LED matrix.

Once you know these two things, you can figure out how to map a {x, y} coordinate on the LED matrix to a {x, y} coordinate that identifies which bit you need to set:

  1. "Flatten" the coordinate in the LED Matrix 34x9 grid to an offset in an array:
offset = y * 9 + x
  1. We can now do the inverse and turn this offset into an {x, y} coordinate where y represents the byte to modify and x represents the bit in the byte to modify.
y = offset % 8
x = 7 - (offset % 8)

@mb720
Copy link

mb720 commented Oct 19, 2024

For drawing black and white, I think it's helpful to imagine the LED matrix as a list of 306 pixels that are turned on or off with a list of bits. After turning the bits into bytes, we can send those bytes to the LED module:

def black_white_draw_pixels(dev):
    """ Use a list of 306 bits to draw a pattern of black and white pixels on the LED module."""

    # Each bit represents the state of an LED (1 is on, 0 is off)
    bits = [
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 1, 0, 1, 0, 0, 0,
            0, 0, 1, 0, 0, 0, 1, 0, 0,
            0, 1, 0, 0, 0, 0, 0, 1, 0,
            1, 0, 0, 0, 0, 0, 0, 0, 1,
            0, 1, 0, 0, 0, 0, 0, 1, 0,
            0, 0, 1, 0, 0, 0, 1, 0, 0,
            0, 0, 0, 1, 0, 1, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0,

            0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0,

            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            1, 1, 1, 1, 1, 1, 1, 1, 1,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,

            0, 0, 0, 0, 0, 0, 0, 0, 0,
            1, 0, 0, 0, 0, 0, 0, 0, 1,
            0, 1, 0, 0, 0, 0, 0, 1, 0,
            0, 0, 1, 0, 0, 0, 1, 0, 0,
            0, 0, 0, 1, 0, 1, 0, 0, 0,
            0, 0, 0, 0, 1, 0, 0, 0, 0,
            0, 0, 0, 1, 0, 1, 0, 0, 0,
            0, 0, 1, 0, 0, 0, 1, 0, 0,
            0, 1, 0, 0, 0, 0, 0, 1, 0,
            1, 0, 0, 0, 0, 0, 0, 0, 1,

            0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0
          ]

    bytes = bits_to_bytes(bits)
    send_draw_command(dev, bytes)

If you define the bits like this, you can actually see the patterns in the code that will end up on the LED matrix.

Here's the rest of the code:

def main():
    print("Start of program")
    black_white_draw_pixels("/dev/ttyACM0")
    print("End of program")


def bits_to_bytes(bits):
    """Convert a list of bits to a list of bytes."""
    import math
    nr_of_bytes = math.ceil(len(bits) / 8)
    bytes = [0x00 for _ in range(nr_of_bytes)]

    for bit_index, bit in enumerate(bits):
        byte_index = int(bit_index / 8)
        bit_index_within_byte = bit_index % 8
        if bit:
            # Set the byte's bit to one at bit_index_within_byte
            bytes[byte_index] |= 1 << bit_index_within_byte

    return bytes


def send_draw_command(dev, bytes):
    """Send a draw command to the LED module that turns LEDs on or off.

    The draw command (0x06) requires 39 bytes to be sent to the module. With a 34x9 matrix, we need 34*9 = 306 bits to represent the individual LED's state (on or off). 306 bits fit into 39 bytes: 306/8 = 38.25.

    The bits in those bytes represent the state of the individual LEDs on the module. The first bit represents the top left LED, the second bit the LED to the right of it, and so forth. The 10th bit represents the LED in the second row, first column, and so forth.
    """
    draw_cmd_byte = 0x06
    send_command(dev, draw_cmd_byte, bytes)


def send_command(dev, command, parameters=[], with_response=False):
    """Send a command to the device. Opens a new serial connection every time."""
    fwk_magic = [0x32, 0xAC]
    return send_command_raw(dev, fwk_magic + [command] + parameters, with_response)


def send_command_raw(dev, command, with_response=False):
    import serial

    """Send a command to the device. Opens a new serial connection every time."""
    try:
        with serial.Serial(dev, 115200) as s:
            s.write(command)

            # Response size from here:
            # https://github.com/FrameworkComputer/inputmodule-rs/blob/main/commands.md

            response_size = 32
            if with_response:
                res = s.read(response_size)
                print(f"Received: {res}")
                return res
    except (IOError, OSError) as _ex:
        disconnect_dev(dev.device)
        print("Error: ", ex)

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

No branches or pull requests

3 participants