hardware/board_scanner.h¶
Hall effect sensor board scanning via multiplexed GPIO.
This module reads 96 A3144 hall effect sensors through 6x 16:1 multiplexers to detect piece presence on the physical chess board and in the two capture/storage zones flanking it.
Hardware Layout: ─────────────── 6x CD74HC4067 (or equivalent) 16:1 digital mux:
Board sensors (64 total, one per square): MUX 1: files a-b → 16 sensors MUX 2: files c-d → 16 sensors MUX 3: files e-f → 16 sensors MUX 4: files g-h → 16 sensors
Storage zone sensors (32 total, 16 per side): MUX 0: black storage (assumed left side) → 16 sensors MUX 5: white storage (right side of board) → 16 sensors
Each storage mux has 16 channels mapped to slots: Channels 0-7: left column, top to bottom Channels 8-15: right column, top to bottom
The two sides use different left/right labels: MUX 0 (black storage): ch0-7 = P1-P8, ch8-15 = R1,R2,B1,B2,N1,N2,Q,K MUX 5 (white storage): ch0-7 = R1,R2,B1,B2,N1,N2,Q,K, ch8-15 = P1-P8
Wiring: ─────── 4 shared select lines (S0-S3) → Pi GPIO, drive all 6 muxes 6 mux output lines (SIG) → Pi GPIO, one per mux
Sensor mapping within each board mux (channel → square): For mux 1 (files a-b): Channel 0 = a8, 1 = a7, …, 7 = a1 Channel 8 = b8, 9 = b7, …, 15 = b1 The same pattern repeats for muxes 2-4 covering c-d, e-f, g-h.
A3144 sensor output: LOW = magnet detected (piece present) HIGH = no magnet (square empty) (active-low with 10K pull-up resistor per sensor)
Variables
-
constexpr int DEFAULT_PIN_S0 = 17¶
-
constexpr int DEFAULT_PIN_S1 = 27¶
-
constexpr int DEFAULT_PIN_S2 = 22¶
-
constexpr int DEFAULT_PIN_S3 = 23¶
-
constexpr int DEFAULT_PIN_MUX0 = 4¶
-
constexpr int DEFAULT_PIN_MUX1 = 7¶
-
constexpr int DEFAULT_PIN_MUX2 = 8¶
-
constexpr int DEFAULT_PIN_MUX3 = 9¶
-
constexpr int DEFAULT_PIN_MUX4 = 10¶
-
constexpr int DEFAULT_PIN_MUX5 = 11¶
-
constexpr int BOARD_MUX_COUNT = 4¶
-
constexpr int STORAGE_MUX_COUNT = 2¶
-
constexpr int TOTAL_MUX_COUNT = 6¶
-
constexpr int CHANNELS_PER_MUX = 16¶
-
constexpr int TOTAL_SQUARES = 64¶
-
constexpr int STORAGE_SLOTS_PER_SIDE = 16¶
-
constexpr int MUX_SETTLE_US = 1000¶
-
constexpr int DEBOUNCE_SCANS = 3¶
-
constexpr int DEBOUNCE_DELAY_MS = 50¶
-
constexpr int DIAGNOSTIC_CHANNEL_HOLD_MS = 1000¶
-
constexpr int POLL_INTERVAL_MS = 20¶
-
constexpr int ILLEGAL_STATE_ALERT_MS = 10000¶
-
struct ScannerObserver¶
- #include <board_scanner.h>
-
class BoardScanner¶
- #include <board_scanner.h>
Public Functions
-
BoardScanner()¶
Construct with default GPIO pin assignments
-
BoardScanner(const int sPins[4], const int mPins[TOTAL_MUX_COUNT])¶
Construct with custom GPIO pin assignments
- Parameters:
sPins – Array of 4 select-line GPIO pins (BCM)
mPins – Array of 6 mux-output GPIO pins (BCM)
-
~BoardScanner()¶
-
bool init()¶
Initialize GPIO and configure pins. Must be called before scanning.
- Returns:
true if GPIO setup succeeded
-
void enableSimulation()¶
Enable simulation mode: no GPIO access, scanBoard() returns the value set by setSimulatedOccupancy(). Use this for testing on machines without GPIO or without sensor hardware attached.
-
inline bool isSimulation() const¶
-
inline void setSimulatedOccupancy(Bitboard occ)¶
Set the simulated occupancy (only used in simulation mode)
-
inline void setSimulatedStorage(uint16_t blackStorage, uint16_t whiteStorage)¶
Set simulated storage zone occupancy (only in simulation mode) Each uint16_t is a bitmask: bit 0-7 = piece slots, 8-15 = pawn slots
-
Bitboard scanBoard()¶
Perform a single raw scan of all 64 board squares. Cycles through all 16 mux channels, reading 4 board mux outputs per channel.
- Returns:
64-bit occupancy bitboard (bit 0 = a8, bit 63 = h1)
-
Bitboard scanBoardDebounced()¶
Perform a debounced board scan: repeats scanBoard() until the result is stable for DEBOUNCE_SCANS consecutive reads.
- Returns:
Stable 64-bit occupancy bitboard
-
uint16_t scanStorage(bool isWhiteSide)¶
Scan one side’s storage zone (16 slots).
- Parameters:
isWhiteSide – true for white storage (MUX 5), false for black (MUX 0)
- Returns:
16-bit bitmask of occupied slots
-
uint16_t scanStorageDebounced(bool isWhiteSide)¶
Scan storage with debouncing.
-
ScannerMoveResult waitForLegalMove(Bitboard currentOccupancy, uint16_t currentBlackStorage, uint16_t currentWhiteStorage, const PieceTracker &tracker, int timeoutMs = 0, void (*illegalStateCallback)() = nullptr, void (*wrongSlotCallback)() = nullptr, const std::atomic<bool> *cancelFlag = nullptr, const ScannerObserver *observer = nullptr)¶
Wait for the human player to make a legal move on the physical board.
This is the primary interface for human move input. It works by:
Polling until the board OR storage state changes
Waiting for both board and storage to stabilize (debounce)
Diffing the new state against the baselines
Checking if the diff matches exactly one legal engine move, using the specific storage slot to disambiguate captures:
If a storage slot gained a piece → identify which piece was captured via PieceTracker, match the capture
If storage is unchanged → this is a non-capture move
If the wrong slot was used → reject and alert the player
If no legal move matches, going back to step 1 (the move isn’t finished yet — the player is still mid-move)
If the board sits in an unrecognized state for too long, calling illegalStateCallback so the caller can alert the player
This approach cleanly handles:
Long pauses (piece held in the air — board unstable or unchanged, storage unchanged → keep waiting)
Captures (source square clears on board, storage slot gains a piece → unambiguous via slot identity)
Take-backs (piece lifted and put back → board returns to original, storage unchanged → keep waiting)
Castling (intermediate state doesn’t match; final state with both king and rook in new positions does, storage unchanged)
En passant (board shows 2 cleared + 1 set, storage gains a piece)
- Parameters:
currentOccupancy – The board occupancy before the expected move
currentBlackStorage – Black storage zone baseline
currentWhiteStorage – White storage zone baseline
tracker – PieceTracker for mapping storage slots to pieces
timeoutMs – Maximum time to wait (0 = wait forever)
illegalStateCallback – Optional callback invoked when the board has been in an unrecognized state for ILLEGAL_STATE_ALERT_MS. Called once per unrecognized-state episode. Can be nullptr.
wrongSlotCallback – Optional callback invoked when a captured piece is placed in the wrong storage slot. Can be nullptr.
- Returns:
ScannerMoveResult with the matched engine move, or failure info
-
inline void setLastScan(Bitboard occ)¶
Update the last known stable state (call after engine processes a move)
-
inline uint16_t getLastBlackStorage() const¶
Get/set storage zone baselines. These track what the storage zones looked like before the human’s turn so we can detect when a new piece arrives (capture).
-
inline uint16_t getLastWhiteStorage() const¶
-
inline void setLastStorage(uint16_t blackSt, uint16_t whiteSt)¶
-
inline bool isInitialized() const¶
Check if the scanner has been initialized
-
void printStorageScan(uint16_t storage, bool isWhiteSide)¶
Print a visual representation of a storage zone scan
-
void diagnosticScan(int channelHoldMs = DIAGNOSTIC_CHANNEL_HOLD_MS)¶
Scan and print each sensor individually (for hardware debugging). Slowly cycles through every channel and prints the raw reading.
Public Static Functions
-
static int detectLiftedSquare(Bitboard before, Bitboard after)¶
Pure lift-detection helper (unit-testable without the poll loop): if
afterdiffers frombeforeby exactly one occupied→empty square and nothing was newly occupied, returns that square (a piece in the air); otherwise -1. A completed move (one clear + one set) is deliberately not a lift. This is the LED subsystem’s onPieceLifted trigger (PLAN §6).
-
static bool inferMove(Bitboard before, Bitboard after, int &from, int &to)¶
Determine the source and destination squares from an occupancy change. This is a raw diff-based inference — it does NOT validate against legal moves. Prefer waitForLegalMove() for game play.
For a normal move: one bit clears (source), one bit sets (destination) For a capture: one bit clears (source), destination stays set For castling: two bits clear, two bits set (king + rook) For en passant: two bits clear, one bit sets
- Parameters:
before – Occupancy before the move
after – Occupancy after the move
from – [out] Source square index (0-63)
to – [out] Destination square index (0-63)
- Returns:
true if a valid move pattern was detected
-
static void squaresToMoveString(int from, int to, char *moveStr, char promotion = '\0')¶
Build a UCI move string (e.g. “e2e4”) from source and destination squares.
- Parameters:
from – Source square index (0-63, engine encoding)
to – Destination square index (0-63, engine encoding)
moveStr – [out] Buffer for the move string (at least 6 chars)
promotion – Promotion character (‘q’,’r’,’b’,’n’) or ‘\0’ for none
Private Functions
-
void setMuxChannel(int channel)¶
Set the mux address (0-15) on the shared select lines
-
bool readMuxOutput(int muxIndex)¶
Read a single mux output pin
- Returns:
true if piece is present (sensor active-low, so LOW = present)
-
int readMuxRawLevel(int muxIndex)¶
Read a single mux output pin as a raw digital level.
- Returns:
0 for LOW, 1 for HIGH, -1 if unavailable/error.
-
int readSelectRawLevel(int selectIndex)¶
Read back one select line as a raw digital level.
- Returns:
0 for LOW, 1 for HIGH, -1 if unavailable/error.
-
int toSquareIndex(int muxIndex, int channel)¶
Map a physical board mux/channel pair to a bitboard square index.
Mux 1, channel 0 → square 0 (a8) Mux 1, channel 15 → square 57 (b1) Mux 4, channel 15 → square 63 (h1)
-
int toStorageSlot(bool isWhiteSide, int channel)¶
Map a storage mux channel to the normalized StartingSlot index used by PieceTracker and move matching.
-
bool matchLegalMove(Bitboard before, Bitboard after, int captureSlot, bool capturedIsWhite, const PieceTracker &tracker, int &move)¶
Try to match an observed board + storage state against the engine’s legal moves.
Capture disambiguation uses storage slot identity:
When a storage slot gains a piece, the slot tells us which specific piece was captured (by its starting position).
The PieceTracker tells us where that piece currently sits.
We only match captures whose target is that square.
For non-capture moves, no storage slot should have changed.
- Parameters:
before – Board occupancy before the move
after – Board occupancy after the move
captureSlot – The specific storage slot that gained a piece, or SLOT_NONE if no storage change detected
capturedIsWhite – True if the captured piece is white (i.e., it appeared in white’s storage zone)
tracker – PieceTracker for mapping slot → current square
move – [out] The matched engine move (if exactly one matches)
- Returns:
true if exactly one legal move matches the observed change
Private Members
-
int selectPins[4]¶
-
int muxPins[TOTAL_MUX_COUNT]¶
-
bool initialized¶
-
bool simulationMode¶
-
uint16_t simulatedBlackStorage¶
-
uint16_t simulatedWhiteStorage¶
-
uint16_t lastBlackStorage¶
-
uint16_t lastWhiteStorage¶
Friends
- friend class TestableScanner
-
BoardScanner()¶