Skip to content

Python API

Every name PyEilik exports from import eilik, grouped by what it belongs to.

import eilik
eilik.connect(device: str | None = None) -> Robot

Opens a robot, discovering the serial port when no device is given. Use it as a context manager so the port is released even if something raises — a port left open stops the next run from connecting at all.

with eilik.connect() as robot:
robot.arm_left.to(1800)
robot.commit()
eilik.load(
source: str | Path | Sequence[bytes],
*,
fps: float = 30.0,
dance: str | None = None,
audio: bool = True,
fill: bool = False,
threshold: int = 128,
strum_arm: int = 2,
) -> Clip

Prepares a video for playing. source is a path to a video, or a sequence of ready framebuffers. dance is "simple", "guitar" or None"guitar" needs music with an audible pulse; on music without one it strums to a grid that means nothing, and "simple" will look better. Decoding needs ffmpeg; playing an already-loaded clip does not. Returns a Clip.

A connected robot. Prefer eilik.connect over constructing this directly. A Robot holds four Joint attributes — arm_left, arm_right, body, head — and a screen attribute, a Screen.

Robot.commit() -> None

Sends every staged joint in a single packet. Separate packets per joint would move the joints visibly out of step.

Robot.rest() -> None

Returns every joint to the centre.

Robot.positions() -> tuple[int, int, int, int]

Reads all four joint positions from the robot.

Robot.play(source, *, on_frame=None, **options) -> int

Plays a video on the screen, optionally dancing to its soundtrack:

robot.play("clip.mp4")
robot.play("clip.mp4", dance="simple")
robot.play("clip.mp4", dance="guitar")

source may be a path (in which case **options are forwarded to eilik.load) or an already-prepared Clip, in which case passing **options raises TypeError — they would do nothing, since conversion already happened when the clip was prepared. Blocks until the clip ends or Ctrl-C, and returns how many frames were shown. Decoding a video needs ffmpeg; a prepared clip does not. Sound comes out of this computer: the robot has a speaker, but no command exists to send audio to it.

Robot.close() -> None

Releases the serial port. Safe to call more than once.

One servo joint, reached as robot.arm_left, robot.arm_right, robot.body or robot.head. Sides are the robot’s own: arm_left is the robot’s left arm, which is the one you see on your right when facing it.

Joint.to(position: int) -> None

Stages a target. Nothing moves until Robot.commit.

Joint.position -> int

The joint’s real position, read back from the robot.

The robot’s screen, reached as robot.screen. The panel is 128×64, monochrome, one bit per pixel.

Screen.WIDTH: int # 128
Screen.HEIGHT: int # 64
Screen.BYTES: int # 1024

The size of a framebuffer: WIDTH * HEIGHT // 8.

Screen.show(source, *, threshold: int = 128, fill: bool = False) -> None

Puts something on the screen. source is either a ready 1024-byte framebuffer, sent unchanged, or anything Pillow can open — a path, a file object, or an Image — which is scaled to 128×64, reduced to one bit per pixel at threshold, and rotated to match the panel. fill crops to fill the screen instead of letterboxing. Opening an image needs Pillow (pip install 'pyeilik[image]'); the framebuffer path has no dependencies at all.

Screen.read() -> bytes

Reads back what is on the screen, as a 1024-byte framebuffer in the panel’s own order — feeding it straight back to show reproduces the picture. Use eilik.screen.unpack to get pixels the right way up for a human to look at.

Screen.clear() -> None

Blanks the screen.

Two helpers in the eilik.screen submodule, for looking at a framebuffer as pixels rather than as 1024 opaque bytes. They are not re-exported by import eilik, so reach them through the submodule:

from eilik.screen import pack, unpack
eilik.screen.unpack(framebuffer: bytes, *, rotate: bool = True) -> list[list[int]]

Turns the panel’s 1024-byte page order back into a 64 × 128 grid of 0 and 1, row by row, the way a human looks at it. The panel is mounted upside down, so by default the grid is turned back the right way up; rotate=False returns it in the panel’s own orientation.

eilik.screen.pack(rows: Sequence[Sequence[int]], *, rotate: bool = True) -> bytes

The inverse: a 64 × 128 grid of 0 and 1 becomes the 1024 bytes Screen.show sends, with the same rotate. Drawing a frame pixel by pixel and packing it needs no Pillow at all.

Frames, and optionally the motion and sound that go with them. Returned by eilik.load and accepted by Robot.play.

Clip.frames: list[bytes]

The framebuffers that make up the clip.

Clip.fps: float

The frame rate the clip plays at.

Clip.motion: list[tuple[int, int, int, int]] | None

Joint targets per motion frame, or None when the clip was loaded without dance=.

Clip.audio: Path | None

The path to the extracted soundtrack, or None when there is none to play.

Clip.style: str | None

The dance= style the clip was prepared with, or None.

Clip.duration -> float

The clip’s length in seconds: len(clip.frames) / clip.fps.

Clip.close() -> None

Deletes anything temporary this clip created, such as an extracted soundtrack.

class Status(IntEnum): ...

Mirrors eilik_status_t in include/eilik.h, in the same order. Every member is the status code eilik.errors.check maps to the matching exception below.

Success.

An invalid argument was passed to the underlying C call.

The serial port could not be opened or configured.

The serial port is already open in another process.

An input/output error occurred talking to the robot.

The robot did not acknowledge in time.

The robot sent a reply that could not be parsed.

A position fell outside the joint’s limits, in strict mode.

The servo controller has stopped running: every joint reads 0. Turn the robot off with its power switch and on again; unplugging USB is not enough, because it has an internal battery.

Every failure this library raises is one of these, and every one inherits from EilikError.

Base class for every failure this library reports.

The robot could not be reached. Named PortError rather than ConnectionError so it does not shadow the builtin of that name in callers’ code.

A subclass of PortError: another process already holds the serial port.

The robot sent something that could not be parsed.

The robot did not acknowledge in time. Shadows the builtin TimeoutError deliberately, since it means the same thing here.

The servo controller has stopped running.

A subclass of both EilikError and ValueError: a position fell outside the joint’s limits in strict mode.