Skip to content

Commit

Permalink
Add support for Tabutronic Sentio e-boards
Browse files Browse the repository at this point in the history
  • Loading branch information
gkalab committed Oct 22, 2023
1 parent 1f81e5c commit 241d58e
Show file tree
Hide file tree
Showing 30 changed files with 1,857 additions and 120 deletions.
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,43 @@

Cer2nut adapts the protocol of a Certabo chess e-board to the Chessnut protocol used in the Chessnut Air and Chessnut Pro e-boards.

The adapter allows software written to support the Chessnut e-boards to be used with a Certabo e-board.
The adapter allows software written to support the Chessnut e-boards to be used with a Certabo e-board or a Tabutronic Sentio e-board.
Not all software written for the Chessnut protocol may work with this adapter and there is no guarantee that this adapter will work in the future.

Hardware requirements
---------------------
## Hardware requirements

This software is only compatible with development boards having a ESP32-S3-WROOM-1 chip and a USB OTG port on board.

Software requirements
---------------------
## Software requirements

ESP-IDF >= 5.1 https://idf.espressif.com/

Connection
----------
ESP-IDF >= 5.1.1 https://idf.espressif.com/

## Connection

**Use at Own Risk.** The Software is licensed for free under the GNU general public license version 3 and “as is” and
may not operate properly, be in final form or fully functional, and may have errors, bugs, design flaws, and other defects.
Use of the software is at your own risk, the author is not responsible for any damage to your equipment when using this software!

After flashing the firmware, connect the USB OTG port to the Certabo E-Board and the second USB port to the power supply.

![Alt text](connection_diagram.png?raw=true "Connection diagram")
![Alt text](connection_diagram.png?raw=true "Connection diagram")

## Usage

### Certabo e-boards

Place the chess pieces on the corresponding squares of the starting position. Place any additional queens on d3 and d6. On the first connection
the pieces are registered. This calibration takes a few seconds and must be repeated if there is a power interruption.

### Tabutronic Sentio e-boards

Place the chess pieces on the corresponding squares of the starting position. Do not place additional queens on the board.
Since the hardware of Sentio e-boards does not support piece recognition, there are some limitations:

* Chess games can only be started from the normal chess starting position. At the end of a game, place all the pieces back to the starting position.
* Under-promotion is not currently supported, a pawn is always promoted to a queen.

### Switching between e-boards

When switching between e-boards, always turn off the module before changing boards.
4 changes: 2 additions & 2 deletions dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies:
component_hash: null
source:
type: idf
version: 5.1.0
manifest_hash: 807dd8ead555fefc8048b460d32a2a0b531af640994a4fb2343bbea46ad81470
version: 5.1.1
manifest_hash: caed2f7c2673a29c1aa7a28abd906997aaeaf3c5fdb999993e14d95f4436397b
target: esp32s3
version: 1.0.0
2 changes: 2 additions & 0 deletions main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ idf_component_register(SRCS "vcpusb.cpp" "bleuart.cpp" "cer2nut.cpp" "cp210x_usb
"adapter/lib/CertaboLedControl.cpp"
"adapter/lib/ChessnutAdapter.cpp"
"adapter/lib/ChessnutConverter.cpp"
"adapter/lib/Sentio.cpp"
"adapter/lib/Chess0x88.cpp"
INCLUDE_DIRS ".")
target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format")
5 changes: 4 additions & 1 deletion main/adapter/lib/BoardTranslator.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <array>
#include <vector>

#include "CertaboPiece.h"
Expand All @@ -14,7 +15,9 @@ class BoardTranslator {
public:
virtual void hasPieceRecognition(bool canRecognize) = 0;

virtual void translate(std::vector<CertaboPiece> board) = 0;
virtual void translate(std::vector<CertaboPiece> const& board) = 0;

virtual void translateOccupiedSquares(std::array<bool, 64> const& occupied) = 0;
};

} // namespace eboard
1 change: 0 additions & 1 deletion main/adapter/lib/CalibrationSquare.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ void eboard::CalibrationSquare::calibratePiece(std::vector<std::vector<CertaboPi
piece = CertaboPiece(pc.getId());
if (pc.getId() != PieceId{}) {
completeForSquareFunction(toSquare(square));
//printf("calibration complete for square %d\n", toSquare(square));
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions main/adapter/lib/CapturePiece.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <cstdint>

namespace eboard {

class CapturePiece {

public:
CapturePiece(uint8_t piece, uint8_t fromSquare, uint8_t toSquare)
: piece(piece), fromSquare(fromSquare), toSquare(toSquare){};

uint8_t getPiece() {
return piece;
}
uint8_t getFromSquare() {
return fromSquare;
}
uint8_t getToSquare() {
return toSquare;
}

private:
uint8_t piece;
uint8_t fromSquare;
uint8_t toSquare;
};

} // namespace eboard
27 changes: 21 additions & 6 deletions main/adapter/lib/CertaboBoardMessageParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@

#include "CertaboBoardMessageParser.h"
#include "ChessData.h"
#include "Sentio.h"

using eboard::CertaboBoardMessageParser;
using eboard::Sentio;

CertaboBoardMessageParser::CertaboBoardMessageParser(Stones stones, CallbackFunction callbackFunction)
: parser(CertaboParser(*this)), stones(std::move(stones)), callback(std::move(callbackFunction)) {}
CertaboBoardMessageParser::CertaboBoardMessageParser(CallbackFunction callbackFunction,
PieceRecognitionCallbackFunction pieceRecognitionCallbackFunction)
: parser(CertaboParser(*this)), callback(std::move(callbackFunction)),
sentio(Sentio([this](std::array<StoneId, 64> board) {
callback(board);
})),
pieceRecognitionCallback(std::move(pieceRecognitionCallbackFunction)) {}

void CertaboBoardMessageParser::parse(uint8_t* msg, size_t data_len) {
parser.parse(msg, data_len);
}

void eboard::CertaboBoardMessageParser::hasPieceRecognition(bool canRecognize) {
pieceRecognition = canRecognize;
pieceRecognitionCallback(canRecognize);
}

void eboard::CertaboBoardMessageParser::translate(std::vector<CertaboPiece> board) {
void eboard::CertaboBoardMessageParser::translate(std::vector<CertaboPiece> const& board) {
std::array<StoneId, 64> newBoard = {};
int i = 0;
for (auto& piece : board) {
Expand All @@ -32,7 +39,11 @@ void eboard::CertaboBoardMessageParser::translate(std::vector<CertaboPiece> boar
averageLastBoards(newBoard);
}

void CertaboBoardMessageParser::averageLastBoards(std::array<StoneId, 64> newBoard) {
void eboard::CertaboBoardMessageParser::translateOccupiedSquares(std::array<bool, 64> const& occupied) {
sentio.occupiedSquares(occupied);
}

void CertaboBoardMessageParser::averageLastBoards(std::array<StoneId, 64> const &newBoard) {
while (boardHistory.size() > 2) {
boardHistory.pop_front();
}
Expand All @@ -47,8 +58,8 @@ void CertaboBoardMessageParser::averageLastBoards(std::array<StoneId, 64> newBoa
}
callback(avg_board);
}

using MapEntry = std::pair<uint8_t, std::size_t>;

int CertaboBoardMessageParser::mostFrequent(std::vector<StoneId>& entries) {
std::unordered_map<StoneId, std::size_t> freqMap;
std::for_each(entries.begin(), entries.end(), [&](StoneId& elem) {
Expand All @@ -65,3 +76,7 @@ int CertaboBoardMessageParser::toSquare(int index) {
int col = index % 8;
return row * 8 + col;
}

void eboard::CertaboBoardMessageParser::updateStones(eboard::Stones const& newStones) {
stones = newStones;
}
16 changes: 11 additions & 5 deletions main/adapter/lib/CertaboBoardMessageParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,33 @@
#include "BoardTranslator.h"
#include "CertaboCalibrator.h"
#include "CertaboParser.h"
#include "Sentio.h"

namespace eboard {

using CallbackFunction = std::function<void(std::array<StoneId, 64>)>;
using CallbackFunction = std::function<void(std::array<StoneId, 64> const&)>;
using PieceRecognitionCallbackFunction = std::function<void(bool hasPieceRecognition)>;

/**
* CertaboBoardMessageParser translates raw board data to the internal board representation.
* It uses the stone IDs from a calibrator to map the raw IDs to stones.
*/
class CertaboBoardMessageParser : public BoardTranslator {
public:
CertaboBoardMessageParser(Stones, CallbackFunction callbackFunction);
CertaboBoardMessageParser(CallbackFunction callbackFunction,
PieceRecognitionCallbackFunction pieceRecognitionCallbackFunction);
~CertaboBoardMessageParser() override = default;

public:
void hasPieceRecognition(bool canRecognize) override;
void translate(std::vector<CertaboPiece> board) override;
void translate(std::vector<CertaboPiece> const& board) override;
void translateOccupiedSquares(std::array<bool, 64> const& occupied) override;

void parse(uint8_t* msg, size_t data_len);
void updateStones(Stones const& newStones);

private:
void averageLastBoards(std::array<StoneId, 64> newBoard);
void averageLastBoards(std::array<StoneId, 64> const &newBoard);
/**
* Returns the element with the highest frequency in a vector.
* implementation from https://devptr.com/find-most-frequent-element-in-vector-in-c/
Expand All @@ -39,10 +44,11 @@ class CertaboBoardMessageParser : public BoardTranslator {
static int mostFrequent(std::vector<StoneId>& entries);
static int toSquare(int index);

bool pieceRecognition = false;
CertaboParser parser;
Stones stones;
CallbackFunction callback;
Sentio sentio;
PieceRecognitionCallbackFunction pieceRecognitionCallback;
std::list<std::array<StoneId, 64>> boardHistory;
};

Expand Down
2 changes: 1 addition & 1 deletion main/adapter/lib/CertaboCalibrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void CertaboCalibrator::hasPieceRecognition(bool) {
// ignore
}

void CertaboCalibrator::translate(std::vector<CertaboPiece> board) {
void CertaboCalibrator::translate(std::vector<CertaboPiece> const& board) {
receivedBoards.push_back(board);
if (receivedBoards.size() >= 7 && !calibrationComplete) {
if (checkPieces()) {
Expand Down
8 changes: 6 additions & 2 deletions main/adapter/lib/CertaboCalibrator.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ using Stones = std::map<CertaboPiece, StoneId>;
/**
* Function to be called when calibration is complete.
*/
using CalibrationCompleteFunction = std::function<void(Stones)>;
using CalibrationCompleteFunction = std::function<void(Stones&)>;

/**
* CertaboCalibrator calibrates the board.
Expand All @@ -41,7 +41,11 @@ class CertaboCalibrator : public BoardTranslator {
* Function called by the used parser to translate the raw piece information.
* @param board parsed board with raw piece information
*/
void translate(std::vector<CertaboPiece> board) override;
void translate(std::vector<CertaboPiece> const& board) override;

void translateOccupiedSquares(std::array<bool, 64> const& board) override{
// ignore
};

/**
* Calibrate raw board data.
Expand Down
8 changes: 6 additions & 2 deletions main/adapter/lib/CertaboLedControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using eboard::CertaboLedControl;

std::vector<uint8_t> CertaboLedControl::LEDS_OFF{0, 0, 0, 0, 0, 0, 0, 0};
std::vector<uint8_t> const CertaboLedControl::LEDS_OFF{0, 0, 0, 0, 0, 0, 0, 0};

CertaboLedControl::CertaboLedControl(ToUsbFunction toUsb) : toUsb(std::move(toUsb)), keepRunning(true) {
processCommands();
Expand All @@ -20,6 +20,10 @@ void CertaboLedControl::ledCommand(std::vector<uint8_t> const& command) {
pendingCommands.push_back(command);
}

void CertaboLedControl::setProcessingTime(int processingTimeMillis) {
processingTimeMs = processingTimeMillis;
}

void CertaboLedControl::processCommands() {
processingThread = std::thread([this]() {
while (keepRunning) {
Expand All @@ -34,7 +38,7 @@ void CertaboLedControl::processCommands() {
uint64_t currentTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
if ((lastCommandTime + 600) <= currentTime && !pendingCommands.empty()) {
if ((lastCommandTime + processingTimeMs) <= currentTime && !pendingCommands.empty()) {
auto cmd = pendingCommands.front();
pendingCommands.pop_front();
if (cmd != lastCommand) {
Expand Down
5 changes: 4 additions & 1 deletion main/adapter/lib/CertaboLedControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ class CertaboLedControl {

void ledCommand(std::vector<uint8_t> const& command);

void setProcessingTime(int processingTimeMillis);

private:
static std::vector<uint8_t> LEDS_OFF;
static std::vector<uint8_t> const LEDS_OFF;
void processCommands();

ToUsbFunction toUsb;
std::list<std::vector<uint8_t>> pendingCommands;
int processingTimeMs = 600;
uint64_t lastCommandTime = 0;
std::vector<uint8_t> lastCommand;
std::atomic_bool keepRunning;
Expand Down
43 changes: 36 additions & 7 deletions main/adapter/lib/CertaboParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ void CertaboParser::parse(uint8_t* msg, size_t data_len) {
if (input_str.find("\r\n") == std::string::npos) {
buffer.insert(buffer.end(), data.begin(), data.end());
} else {
std::vector<std::string> split_data = strsplit<std::string>(input_str, ":");
std::vector<std::string> split_data = strSplit<std::string>(input_str, ":");
int index = 0;
for (auto& part : split_data) {
if (!part.empty() && !parsePart(part) && index == (split_data.size() - 1)) {
Expand All @@ -39,26 +39,25 @@ bool CertaboParser::parsePart(std::string& part) {
return (x == '\r' || x == '\n' || x == 'L');
}),
part.end());
std::vector<std::string> split_input = strsplit<std::string>(part, " ");
std::vector<std::string> split_input = strSplit<std::string>(part, " ");
if (!pieceRecognition && split_input.size() > 8) {
pieceRecognition = true;
translator.hasPieceRecognition(true);
}
if (pieceRecognition) {
return parseWithPieceInfo(split_input);
} else {
// TODO Sentio eboard
return parseWithoutPieceInfo(split_input);
}
return false;
}

bool CertaboParser::parseWithPieceInfo(std::vector<std::string> split_input) {
if (split_input.size() >= 320) {
bool CertaboParser::parseWithPieceInfo(std::vector<std::string>& splitInput) {
if (splitInput.size() >= 320) {
std::vector<CertaboPiece> board;
for (int square = 0; square < 64; square++) {
std::array<uint8_t, 5> piece_id{0, 0, 0, 0, 0};
for (int i = 0; i < 5; i++) {
std::basic_string<char>& str = split_input.at(square * 5 + i);
std::basic_string<char>& str = splitInput.at(square * 5 + i);
errno = 0;
char* p_end{};
const char* p = str.c_str();
Expand All @@ -80,3 +79,33 @@ bool CertaboParser::parseWithPieceInfo(std::vector<std::string> split_input) {
return false;
}
}

bool CertaboParser::parseWithoutPieceInfo(std::vector<std::string>& splitInput) {
if (splitInput.size() >= 8) {
std::array<bool, 64> board{};
for (int i = 0, row = 0; row < 8; row++) {
std::basic_string<char>& str = splitInput.at(row);
errno = 0;
char* p_end{};
const char* p = str.c_str();
const long value = std::strtol(p, &p_end, 10);
if ((p && *p_end != 0) || p == p_end || errno == ERANGE) {
buffer.clear();
return false;
} else {
uint8_t b = value; // std::stoi(str); -- no error checking with stoi
for (int col = 7; col > -1; col--) {
if ((b & (1 << col)) != 0) {
board[i] = true; // occupied
}
i++;
}
}
}
translator.translateOccupiedSquares(board);
buffer.clear();
return true;
} else {
return false;
}
}
Loading

0 comments on commit 241d58e

Please sign in to comment.