surriworks/engineering
Project · robotics

Chessbot

A real wooden-piece chessboard that knows what's on it. Magnetic sensors under the squares feed a Raspberry Pi, which works out each move and checks it against the rules of chess. It talks to an Arduino that drives a two-motor parallel arm with an electromagnet to move pieces back.

Pythonpython-chessRaspberry PigpiozeroArduino / C++AccelStepperKinematicsTkinter
64reed switches, 16 GPIO pins
0.103°per motor step (200 × 17.52 gearing)
4.5 cmsquare size
8buttons: 2 move-confirm per side, 4 menu

The problem

A sensor under each square can only tell you whether that square is occupied. It can't tell a knight from a queen, or which side a piece belongs to. A capture leaves the occupancy grid looking almost the same as before, and castling changes four squares at once.

So the board has to reason. It starts from a known legal position, watches the order in which squares empty and fill, and asks the chess engine which legal move would produce exactly the board it now sees.

System

Reed-switch lattice8 rows × 8 cols Buttonsconfirm · promotion GPIO scan Raspberry Pi · main.py ChessGameSimulatorHOLM / HOTM · detect_move python-chesslegal-move validation Gameplay · RobotArmHandlerturns · promotion · arm planning Tkinter debugger USB serial Arduino 2 × stepper (5-bar) servo lift electromagnet limit-switch homing 16×2 I²C LCD
The Pi owns the game logic and the Arduino owns real-time motion. They talk over a plain-text serial protocol.

Sensing 64 squares with 16 pins

Each square has a reed switch that closes when a magnet in the base of a piece sits on it. The switches are wired as a matrix: the Pi drives one row pin high at a time and reads all eight column pins. A full scan takes eight row strobes, and the whole board costs only 16 GPIO pins.

def sense(self):
    for i, row in enumerate(self.rows):
        row.on()
        for j, column in enumerate(self.columns):
            self.matrix[i][j] = int(column.is_pressed)
        row.off()
    return self.matrix

From occupancy to moves

The game keeps two histories:

When a player presses their confirm button, detect_move compares the last legal matrix with the latest frame. It uses the frames in between to settle the ambiguous cases:

Quiet move

One square empties and one fills. That gives the source and destination directly.

Capture

Only the source empties, because the destination was occupied before and still is. The detector finds the square that went empty before the moving piece was lifted, which is where the captured piece was removed.

Castling

Two squares empty and two fill. If one pair is the king moving two files along a rank, that's the move, and python-chess moves the rook too.

En passant

Two squares empty and one fills. The square lifted later is the moving pawn and the earlier one is the captured pawn.

The candidate is then checked against board.legal_moves. If it isn't legal, the LCD shows INVALID MOVE / ADJUST BOARD and nothing is committed. A pawn reaching the last rank pauses the game until the player picks Q, R, B or N with the four menu buttons.

Example: the capture exd5 after 1.e4 d5, as the sensors see it.

Green squares are occupied. The lifted square in each frame is outlined. Frame 1 shows d5 emptying before e4 does, which is how the detector knows d5 was the capture square rather than an empty one.

The arm: a 5-bar parallel linkage

Instead of a gantry, pieces are moved by a planar five-bar linkage. Two stepper motors sit 26 cm apart, each driving a 22 cm link, and two 27 cm links meet at the end effector. Both motors stay on the base, so the moving parts are light. The trade-off is that the inverse kinematics is a pair of triangles rather than a straight line.

def calc_angles(x, y):
    beta1 = atan2(y, l0 + x)
    beta2 = atan2(y, l0 - x)
    alpha1 = acos((l1**2 + (l0+x)**2 + y**2 - l2**2) / (2*l1*hypot(l0+x, y)))
    alpha2 = acos((l1**2 + (l0-x)**2 + y**2 - l2**2) / (2*l1*hypot(l0-x, y)))
    return beta1 + alpha1, pi - beta2 - alpha2   # left, right shoulder

Tap or drag over the board below. The arm is solved live with the same equations and link lengths as the code. The step counts use the real 200-step motor and 17.52:1 gearing.

e4target square
–x, y (cm)
–shoulder angles
–motor steps from 90°

To pick up a piece, the Arduino lowers a servo-mounted electromagnet to a height set by the piece type (pawns, minor pieces and king/queen each have their own), switches it on and lifts. Squares close to the motors fall into "under the line" zones that need a different approach path, and the arm handler classifies them from a lookup grid.

Pi ↔ Arduino protocol

Commands are newline-terminated text at 9600 baud, and each command gets an explicit acknowledgement. That makes it easy to drive the hardware by hand from the debugger's serial console.

CommandReplyEffect
PINGPONGHeartbeat, polled every 2 s
LCD a, bLCD SUCCESSWrite two lines to the 16×2 display
MOVE s1 s2STEPPERS MOVEDRelative move of both steppers, run together
HOME–Slow approach to each limit switch, back off, zero
EM_D n / EM_R nEM_dropped / EM_roseEase the magnet servo to angle n
EM_ON / EM_OFFEM_on / EM_offSwitch the electromagnet

Until the Pi connects, the LCD cycles through status lines every five seconds, such as Consulting / Stockfish, Hating / London and Forking / knights.

Tooling

Debugger app

A Tkinter window that mirrors the board's live occupancy grid and button states. It also has a raw serial console, a trajectory canvas for sending the arm to a clicked square, and a per-task timing view of each pass through the main loop (lattice, buttons, chessboard, gameplay, arm, serial, GUI).

Self-updating boot

On power-up, a script runs diagnostics and makes sure SSH is enabled. It then pulls the latest code, installs dependencies, and compiles and flashes the Arduino sketch with arduino-cli before launching the game. Each stage reports its log to a remote endpoint, so the board can be debugged without a screen.

Status: board sensing, move detection, rules enforcement, promotion and the serial/LCD layer all work. Kinematics, homing and electromagnet control work on their own, and connecting the arm to the game loop so it plays moves automatically is still in progress.

Source on GitHub →