Initial (and hopefully last) commit

This commit is contained in:
mia 2026-05-20 14:06:57 +02:00
commit dea8e8be68
41 changed files with 126867 additions and 0 deletions

BIN
3d/case.stl Normal file

Binary file not shown.

1
3d/readme Normal file
View file

@ -0,0 +1 @@
https://cad.onshape.com/documents/bb9db5aa964d643ccd7e46c3/w/9284d4a7438acf7663ad1fe7/e/a1c5cd401b084cdfb7fd39db?renderMode=0&uiState=6a0da37ffa326fa20df8f248

5
code/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
code/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

37
code/include/README Normal file
View file

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
code/lib/README Normal file
View file

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

16
code/platformio.ini Normal file
View file

@ -0,0 +1,16 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32-c3]
platform = espressif32
board = esp32-c3-devkitm-1
build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1
framework = arduino
lib_deps = olikraus/U8g2@^2.36.18

900
code/src/main.cpp Normal file
View file

@ -0,0 +1,900 @@
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Preferences.h>
#include <cstring>
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 9, /* data=*/ 8);
Preferences preferences;
#define BTNU 0
#define BTNR 1
#define BTND 3
#define BTNL 10
#define BUZZER 7
const uint8_t ledcChannel = 0;
const uint16_t SCREEN_W = 128;
const uint16_t SCREEN_H = 64;
const uint8_t HUD_H = 8;
const uint8_t CELL = 4;
const uint8_t GRID_W = SCREEN_W / CELL;
const uint8_t GRID_H = (SCREEN_H - HUD_H) / CELL;
const uint16_t MAX_SNAKE = GRID_W * GRID_H;
const uint16_t BASE_STEP_MS = 170;
const uint16_t MIN_STEP_MS = 85;
const uint16_t SPEEDUP_PER_FOOD_MS = 5;
const uint16_t SPRINT_STEP_MS = 50;
const uint8_t SPRINT_HOLD_FRAMES = 5;
const uint16_t MIN_TONE_FREQ = 500;
const uint16_t MAX_TONE_FREQ = 2000;
const uint16_t MENU_DEBOUNCE_MS = 150;
const uint16_t NAME_ENTRY_DWELL_MS = 350;
const uint8_t LEADERBOARD_SIZE = 5;
const uint8_t NAME_LENGTH = 8;
const uint8_t LEADERBOARD_BOARDS = 3; // Easy, Medium, Hard
// Difficulty presets and custom bounds
const uint16_t DIFF_EASY_MS = 200;
const uint16_t DIFF_MEDIUM_MS = BASE_STEP_MS;
const uint16_t DIFF_HARD_MS = 130;
const uint16_t CUSTOM_MIN_MS = 85;
const uint16_t CUSTOM_MAX_MS = 300;
// Difficulty offsets and preset speedup-per-food values
const int16_t DIFF_OFFSET_EASY = 30;
const int16_t DIFF_OFFSET_MED = 0;
const int16_t DIFF_OFFSET_HARD = -40;
const uint16_t DIFF_SPEEDUP_EASY = 3;
const uint16_t DIFF_SPEEDUP_MED = SPEEDUP_PER_FOOD_MS;
const uint16_t DIFF_SPEEDUP_HARD = 7;
struct Cell {
int8_t x;
int8_t y;
};
enum Direction : uint8_t { UP, RIGHT, DOWN, LEFT };
enum GameState : uint8_t { START, PLAYING, GAME_OVER, SETTINGS, LEADERBOARD, NAME_ENTRY };
struct LeaderboardEntry {
char name[NAME_LENGTH + 1];
uint16_t score;
};
GameState gameState = START;
// toneFreq == 0 means buzzer OFF; otherwise valid range is 500..2000 in 100 Hz steps
uint16_t toneFreq = 1200;
bool isSprinting = false;
uint8_t sprintHoldU = 0, sprintHoldR = 0, sprintHoldD = 0, sprintHoldL = 0;
// settingsMenuIndex removed; using settingsScrollOffset and dynamic list
uint32_t lastSettingsInputMs = 0;
// Settings UI state: scroll offset, total items. Selector stays in middle of visible window.
uint8_t settingsScrollOffset = 0;
const uint8_t SETTINGS_VISIBLE = 3; // visible rows
// Difficulty selection
uint8_t difficultyPreset = 1; // 0=Easy,1=Medium,2=Hard,3=Custom
uint16_t customStepMs = BASE_STEP_MS;
// Sprint enable/disable (settings)
bool sprintAllowed = true;
// Custom advanced params
uint16_t customSpeedupPerFood = SPEEDUP_PER_FOOD_MS;
uint16_t customMinStepMs = MIN_STEP_MS;
uint16_t customMaxStepMs = DIFF_MEDIUM_MS * 2;
// Calculate dynamic settings total depending on whether custom step row is present
uint8_t settingsTotal() {
// If custom difficulty selected, include extra rows: custom start, speedup, min, max
if (difficultyPreset == 3) return 10; // blank, buzzer, sprint, difficulty, customStart, customSpeedup, customMin, customMax, back, blank
return 6; // blank, buzzer, sprint, difficulty, back, blank
}
uint8_t leaderboardBoardIndexForDifficulty(uint8_t difficulty) {
return (difficulty < LEADERBOARD_BOARDS) ? difficulty : 1;
}
uint8_t activeLeaderboardBoardIndex() {
return leaderboardBoardIndexForDifficulty(difficultyPreset);
}
// NAME_ENTRY: require L+R release before next select
bool lrComboReleased = true;
LeaderboardEntry leaderboard[LEADERBOARD_BOARDS][LEADERBOARD_SIZE];
char pendingName[NAME_LENGTH + 1] = {0};
uint16_t pendingScore = 0;
uint8_t pendingLeaderboardBoard = 0;
uint8_t keyboardRow = 0;
uint8_t keyboardCol = 0;
Cell snake[MAX_SNAKE];
uint16_t snakeLen = 0;
Cell food = {0, 0};
Direction currentDir = RIGHT;
Direction nextDir = RIGHT;
uint16_t score = 0;
uint16_t highScore = 0;
uint32_t lastStepMs = 0;
uint32_t stateChangeMs = 0;
uint32_t inputIgnoreUntilMs = 0; // ignore button input until this millis() (used after name-entry OK)
void resetGame();
bool cellEquals(const Cell &first, const Cell &second) {
return first.x == second.x && first.y == second.y;
}
bool isSnakeCell(const Cell &target) {
for (uint16_t index = 0; index < snakeLen; ++index) {
if (cellEquals(snake[index], target)) return true;
}
return false;
}
void initLeaderboardDefaults() {
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
leaderboard[board][index].name[NAME_LENGTH] = '\0';
leaderboard[board][index].score = 0;
}
}
}
void loadLeaderboardBoard(uint8_t board) {
char key[16];
snprintf(key, sizeof(key), "leader%u", board);
const size_t loadedBytes = preferences.getBytes(key, leaderboard[board], sizeof(leaderboard[board]));
if (loadedBytes != sizeof(leaderboard[board])) {
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
leaderboard[board][index].name[NAME_LENGTH] = '\0';
leaderboard[board][index].score = 0;
}
}
}
void saveLeaderboardBoard(uint8_t board) {
char key[16];
snprintf(key, sizeof(key), "leader%u", board);
preferences.putBytes(key, leaderboard[board], sizeof(leaderboard[board]));
}
void loadHighScore() {
preferences.begin("snake", true);
const bool buzzerEnabledLegacy = preferences.getBool("buzzerEnabled", true);
toneFreq = preferences.getUShort("toneFreq", 1200);
difficultyPreset = preferences.getUShort("diffPreset", 1);
customStepMs = preferences.getUShort("customStepMs", BASE_STEP_MS);
customSpeedupPerFood = preferences.getUShort("customSpeedup", SPEEDUP_PER_FOOD_MS);
customMinStepMs = preferences.getUShort("customMin", MIN_STEP_MS);
customMaxStepMs = preferences.getUShort("customMax", DIFF_MEDIUM_MS * 2);
sprintAllowed = preferences.getBool("sprintAllowed", true);
initLeaderboardDefaults();
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
loadLeaderboardBoard(board);
}
if (toneFreq != 0 && (toneFreq < MIN_TONE_FREQ || toneFreq > MAX_TONE_FREQ || ((toneFreq - MIN_TONE_FREQ) % 100) != 0)) {
toneFreq = 1200;
}
if (!buzzerEnabledLegacy) {
toneFreq = 0;
}
highScore = (difficultyPreset < 3) ? leaderboard[activeLeaderboardBoardIndex()][0].score : 0;
preferences.end();
}
void saveHighScore() {
preferences.begin("snake", false);
preferences.putUShort("toneFreq", toneFreq);
preferences.putUShort("diffPreset", difficultyPreset);
preferences.putUShort("customStepMs", customStepMs);
preferences.putUShort("customSpeedup", customSpeedupPerFood);
preferences.putUShort("customMin", customMinStepMs);
preferences.putUShort("customMax", customMaxStepMs);
preferences.putBool("sprintAllowed", sprintAllowed);
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
saveLeaderboardBoard(board);
}
if (difficultyPreset < 3) {
preferences.putUShort("highScore", leaderboard[activeLeaderboardBoardIndex()][0].score);
} else {
preferences.putUShort("highScore", 0);
}
preferences.end();
}
bool qualifiesForLeaderboard(uint16_t scoreValue) {
if (difficultyPreset >= 3) return false;
const uint8_t board = activeLeaderboardBoardIndex();
return scoreValue > leaderboard[board][LEADERBOARD_SIZE - 1].score ||
leaderboard[board][LEADERBOARD_SIZE - 1].name[0] == '\0' ||
leaderboard[board][LEADERBOARD_SIZE - 1].score == 0;
}
void syncHighScoreFromLeaderboard(uint8_t board) {
highScore = leaderboard[board][0].score;
}
void insertLeaderboardEntry(uint8_t board, const char *name, uint16_t scoreValue) {
LeaderboardEntry entry = {};
strncpy(entry.name, name, NAME_LENGTH);
entry.name[NAME_LENGTH] = '\0';
entry.score = scoreValue;
uint8_t insertAt = LEADERBOARD_SIZE;
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
if (scoreValue > leaderboard[board][index].score) {
insertAt = index;
break;
}
}
if (insertAt == LEADERBOARD_SIZE) {
return;
}
for (int index = LEADERBOARD_SIZE - 1; index > static_cast<int>(insertAt); --index) {
leaderboard[board][index] = leaderboard[board][index - 1];
}
leaderboard[board][insertAt] = entry;
syncHighScoreFromLeaderboard(board);
}
void prepareNameEntry(uint16_t scoreValue) {
pendingScore = scoreValue;
memset(pendingName, 0, sizeof(pendingName));
keyboardRow = 0;
keyboardCol = 0;
pendingLeaderboardBoard = activeLeaderboardBoardIndex();
gameState = NAME_ENTRY;
}
void confirmNameEntry() {
if (pendingName[0] == '\0') {
strncpy(pendingName, "PLAYER", NAME_LENGTH);
pendingName[NAME_LENGTH] = '\0';
}
insertLeaderboardEntry(pendingLeaderboardBoard, pendingName, pendingScore);
saveHighScore();
resetGame();
gameState = LEADERBOARD;
// Prevent accidental immediate input if OK was still held: ignore inputs for 500ms
inputIgnoreUntilMs = millis() + 500;
lastSettingsInputMs = millis();
}
const char *keyboardKeyLabel(uint8_t row, uint8_t col) {
static const char *layout[4][7] = {
{"A", "B", "C", "D", "E", "F", "G"},
{"H", "I", "J", "K", "L", "M", "N"},
{"O", "P", "Q", "R", "S", "T", "U"},
{"V", "W", "X", "Y", "Z", "DEL", "OK"}
};
return layout[row][col];
}
void moveKeyboardCursor(int8_t rowDelta, int8_t colDelta) {
keyboardRow = (keyboardRow + 4 + rowDelta) % 4;
keyboardCol = (keyboardCol + 7 + colDelta) % 7;
}
void applyKeyboardSelection() {
const char *label = keyboardKeyLabel(keyboardRow, keyboardCol);
if (strcmp(label, "DEL") == 0) {
if (strlen(pendingName) > 0) {
pendingName[strlen(pendingName) - 1] = '\0';
}
} else if (strcmp(label, "OK") == 0) {
confirmNameEntry();
} else if (strlen(pendingName) < NAME_LENGTH) {
const size_t currentLength = strlen(pendingName);
pendingName[currentLength] = label[0];
pendingName[currentLength + 1] = '\0';
}
}
void toneMs(uint16_t hz, uint16_t durationMs) {
if (toneFreq == 0) return;
ledcWriteTone(ledcChannel, hz);
delay(durationMs);
ledcWriteTone(ledcChannel, 0);
}
uint16_t buzzerCycleUp(uint16_t current) {
if (current == 0) return MIN_TONE_FREQ;
if (current < MAX_TONE_FREQ) return current + 100;
return 0;
}
uint16_t buzzerCycleDown(uint16_t current) {
if (current == 0) return MAX_TONE_FREQ;
if (current > MIN_TONE_FREQ) return current - 100;
return 0;
}
void spawnFood() {
Cell candidate;
do {
candidate.x = random(0, GRID_W);
candidate.y = random(0, GRID_H);
} while (isSnakeCell(candidate));
food = candidate;
}
void resetGame() {
score = 0;
currentDir = RIGHT;
nextDir = RIGHT;
gameState = START;
stateChangeMs = millis();
snakeLen = 3;
const int8_t centerX = GRID_W / 2;
const int8_t centerY = GRID_H / 2;
snake[0] = {centerX, centerY};
snake[1] = {static_cast<int8_t>(centerX - 1), centerY};
snake[2] = {static_cast<int8_t>(centerX - 2), centerY};
spawnFood();
lastStepMs = millis();
}
bool anyButtonPressed() {
return digitalRead(BTNU) == LOW ||
digitalRead(BTNR) == LOW ||
digitalRead(BTND) == LOW ||
digitalRead(BTNL) == LOW;
}
void updateDirectionFromButtons() {
if (digitalRead(BTNU) == LOW && currentDir != DOWN) {
nextDir = UP;
sprintHoldU++;
} else {
sprintHoldU = 0;
}
if (digitalRead(BTNR) == LOW && currentDir != LEFT) {
nextDir = RIGHT;
sprintHoldR++;
} else {
sprintHoldR = 0;
}
if (digitalRead(BTND) == LOW && currentDir != UP) {
nextDir = DOWN;
sprintHoldD++;
} else {
sprintHoldD = 0;
}
if (digitalRead(BTNL) == LOW && currentDir != RIGHT) {
nextDir = LEFT;
sprintHoldL++;
} else {
sprintHoldL = 0;
}
isSprinting = sprintAllowed && ((sprintHoldU >= SPRINT_HOLD_FRAMES) ||
(sprintHoldR >= SPRINT_HOLD_FRAMES) ||
(sprintHoldD >= SPRINT_HOLD_FRAMES) ||
(sprintHoldL >= SPRINT_HOLD_FRAMES));
}
uint16_t stepIntervalMs() {
if (isSprinting) return SPRINT_STEP_MS;
int32_t baseMs = BASE_STEP_MS;
uint16_t curSpeedup = SPEEDUP_PER_FOOD_MS;
uint16_t minStep = MIN_STEP_MS;
uint16_t maxStep = BASE_STEP_MS * 2;
switch (difficultyPreset) {
case 0:
baseMs = BASE_STEP_MS + DIFF_OFFSET_EASY;
curSpeedup = DIFF_SPEEDUP_EASY;
break;
case 1:
baseMs = BASE_STEP_MS + DIFF_OFFSET_MED;
curSpeedup = DIFF_SPEEDUP_MED;
break;
case 2:
baseMs = BASE_STEP_MS + DIFF_OFFSET_HARD;
curSpeedup = DIFF_SPEEDUP_HARD;
break;
case 3:
baseMs = customStepMs;
curSpeedup = customSpeedupPerFood;
minStep = customMinStepMs;
maxStep = customMaxStepMs;
break;
}
if (baseMs < (int32_t)minStep) baseMs = minStep;
if (baseMs > (int32_t)maxStep) baseMs = maxStep;
const uint32_t speedDrop = (uint32_t)score * (uint32_t)curSpeedup;
if ((uint32_t)baseMs <= (uint32_t)minStep + speedDrop) return minStep;
int32_t result = (int32_t)baseMs - (int32_t)speedDrop;
if (result < (int32_t)minStep) return minStep;
return (uint16_t)result;
}
Cell nextHeadCell() {
Cell head = snake[0];
switch (currentDir) {
case UP: --head.y; break;
case RIGHT: ++head.x; break;
case DOWN: ++head.y; break;
case LEFT: --head.x; break;
}
return head;
}
bool outOfBounds(const Cell &cell) {
return cell.x < 0 || cell.x >= GRID_W || cell.y < 0 || cell.y >= GRID_H;
}
bool hitsSnakeBody(const Cell &cell) {
for (uint16_t index = 0; index < snakeLen; ++index) {
if (cellEquals(snake[index], cell)) return true;
}
return false;
}
void runGameStep() {
currentDir = nextDir;
const Cell newHead = nextHeadCell();
if (outOfBounds(newHead) || hitsSnakeBody(newHead)) {
stateChangeMs = millis();
if (qualifiesForLeaderboard(score)) {
prepareNameEntry(score);
} else {
gameState = GAME_OVER;
}
toneMs(330, 120);
toneMs(220, 180);
return;
}
const bool ateFood = cellEquals(newHead, food);
const uint16_t oldLen = snakeLen;
uint16_t newLen = oldLen;
if (ateFood && snakeLen < MAX_SNAKE) newLen = oldLen + 1;
for (uint16_t index = newLen - 1; index > 0; --index) {
snake[index] = snake[index - 1];
}
snake[0] = newHead;
snakeLen = newLen;
if (ateFood) {
const uint16_t pointsEarned = isSprinting ? 2 : 1;
score += pointsEarned;
toneMs(toneFreq, 35);
spawnFood();
}
}
void drawGame() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_5x8_tr);
char hudText[32];
if (isSprinting) {
snprintf(hudText, sizeof(hudText), "S:%u B:%u [SPRINT]", score, highScore);
} else {
snprintf(hudText, sizeof(hudText), "S:%u B:%u", score, highScore);
}
u8g2.drawStr(0, 7, hudText);
for (uint16_t index = 0; index < snakeLen; ++index) {
const int16_t x = snake[index].x * CELL;
const int16_t y = HUD_H + snake[index].y * CELL;
u8g2.drawBox(x, y, CELL, CELL);
}
const int16_t foodX = food.x * CELL;
const int16_t foodY = HUD_H + food.y * CELL;
u8g2.drawFrame(foodX, foodY, CELL, CELL);
u8g2.sendBuffer();
}
void drawStartScreen() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(32, 20, "SNAKE");
u8g2.setFont(u8g2_font_4x6_tr);
u8g2.drawStr(26, 40, "START: UP");
u8g2.drawStr(20, 50, "SETTINGS: RIGHT");
if (difficultyPreset == 3) {
u8g2.drawStr(4, 60, "NOT AVAILABLE IN CUSTOM");
} else {
u8g2.drawStr(14, 60, "LEADERBOARD: LEFT");
}
u8g2.sendBuffer();
}
void drawGameOverScreen() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(28, 20, "GAME OVER");
u8g2.setFont(u8g2_font_5x8_tr);
char scoreText[24];
snprintf(scoreText, sizeof(scoreText), "Score: %u", score);
u8g2.drawStr(28, 35, scoreText);
char bestText[24];
snprintf(bestText, sizeof(bestText), "Best: %u", highScore);
u8g2.drawStr(28, 48, bestText);
u8g2.drawStr(18, 62, "Press any button");
u8g2.sendBuffer();
}
void drawLeaderboardScreen() {
const uint8_t board = activeLeaderboardBoardIndex();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
// char title[28];
const char *difficultyNames[3] = {"EASY", "MED ", "HARD"};
// snprintf(title, sizeof(title), "LEADERBOARD - %s", difficultyNames[board]);
u8g2.drawStr(6, 10, "LEADERBOARD");
char ch[2] = {0, 0}; // Reusable 1-char string buffer
if (board == 1) {
ch[0] = difficultyNames[board][0]; u8g2.drawStr(93, 26, ch);
ch[0] = difficultyNames[board][1]; u8g2.drawStr(98, 36, ch);
ch[0] = difficultyNames[board][2]; u8g2.drawStr(103, 46, ch);
} else {
ch[0] = difficultyNames[board][0]; u8g2.drawStr(90, 22, ch);
ch[0] = difficultyNames[board][1]; u8g2.drawStr(95, 32, ch);
ch[0] = difficultyNames[board][2]; u8g2.drawStr(100, 42, ch);
ch[0] = difficultyNames[board][3]; u8g2.drawStr(105, 52, ch);
};
u8g2.setFont(u8g2_font_5x8_tr);
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
char line[24];
snprintf(line, sizeof(line), "%u. %-8s %3u", index + 1, leaderboard[board][index].name, leaderboard[board][index].score);
u8g2.drawStr(4, 20 + index * 8, line);
}
u8g2.setFont(u8g2_font_4x6_tr);
u8g2.drawStr(85, 64, "Back ----->");
u8g2.sendBuffer();
}
void drawNameEntryScreen() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(2, 8, "NEW RECORD - TYPE NAME");
char scoreLine[24];
snprintf(scoreLine, sizeof(scoreLine), "Score: %u", pendingScore);
u8g2.drawStr(2, 16, scoreLine);
char nameLine[24];
snprintf(nameLine, sizeof(nameLine), "Name: %-8s", pendingName);
u8g2.drawStr(2, 24, nameLine);
u8g2.setFont(u8g2_font_5x8_tr);
const uint8_t gridTop = 30;
const uint8_t cellW = 18;
const uint8_t cellH = 8;
for (uint8_t row = 0; row < 4; ++row) {
for (uint8_t col = 0; col < 7; ++col) {
const uint8_t x = col * cellW;
const uint8_t y = gridTop + row * cellH;
const char *label = keyboardKeyLabel(row, col);
const bool selected = (row == keyboardRow && col == keyboardCol);
if (selected) {
u8g2.drawBox(x + 1, y - 7, cellW - 2, cellH - 1);
u8g2.setDrawColor(0);
}
u8g2.drawStr(x + 5, y, label);
if (selected) {
u8g2.setDrawColor(1);
}
}
}
u8g2.setFont(u8g2_font_4x6_tr);
u8g2.drawStr(2, 62, "Move: arrows Select: L+R");
u8g2.sendBuffer();
}
void drawSettingsScreen() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(26, 16, "SETTINGS");
u8g2.setFont(u8g2_font_5x8_tr);
// Render a small scroll window (3 rows) with selector fixed in the middle
const uint8_t total = settingsTotal();
for (uint8_t row = 0; row < SETTINGS_VISIBLE; ++row) {
const uint8_t itemIndex = settingsScrollOffset + row;
const uint8_t y = 28 + row * 10;
bool highlight = (row == (SETTINGS_VISIBLE / 2));
if (highlight) {
u8g2.drawBox(4, y - 8, 120, 10);
u8g2.setDrawColor(0);
}
char line[40] = {0};
if (itemIndex == 0 || itemIndex == (total - 1)) {
// padding
} else {
if (total == 6) {
// 0 blank,1 buzzer,2 sprint,3 difficulty,4 back,5 blank
if (itemIndex == 1) {
if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
} else if (itemIndex == 2) {
snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
} else if (itemIndex == 3) {
const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
} else if (itemIndex == 4) {
snprintf(line, sizeof(line), "Back (save)");
}
} else {
// total == 10 => 0 blank,1 buzzer,2 sprint,3 difficulty,4 customStart,5 customSpeedup,6 customMin,7 customMax,8 back,9 blank
if (itemIndex == 1) {
if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
} else if (itemIndex == 2) {
snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
} else if (itemIndex == 3) {
const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
} else if (itemIndex == 4) {
snprintf(line, sizeof(line), " Starting speed: %ums", customStepMs);
} else if (itemIndex == 5) {
snprintf(line, sizeof(line), " Speedup: %u ms/food", customSpeedupPerFood);
} else if (itemIndex == 6) {
snprintf(line, sizeof(line), " Min: %ums", customMinStepMs);
} else if (itemIndex == 7) {
snprintf(line, sizeof(line), " Max: %ums", customMaxStepMs);
} else if (itemIndex == 8) {
snprintf(line, sizeof(line), "Back (save)");
}
}
}
u8g2.drawStr(8, y, line);
if (highlight) {
u8g2.setDrawColor(1);
}
}
// Help line (small)
u8g2.setFont(u8g2_font_4x6_tr);
u8g2.drawStr(2, 64, "UP/DN: Move L/R: Select/Edit");
u8g2.sendBuffer();
}
void setup() {
pinMode(BTNU, INPUT_PULLUP);
pinMode(BTNR, INPUT_PULLUP);
pinMode(BTND, INPUT_PULLUP);
pinMode(BTNL, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
ledcSetup(ledcChannel, 2000, 8);
ledcAttachPin(BUZZER, ledcChannel);
u8g2.begin();
randomSeed(micros());
loadHighScore();
resetGame();
}
void loop() {
updateDirectionFromButtons();
switch (gameState) {
case START: {
const uint32_t now = millis();
if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
if (difficultyPreset < 3) {
lastSettingsInputMs = now;
gameState = LEADERBOARD;
}
} else if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
lastSettingsInputMs = now;
gameState = SETTINGS;
// prepare scroll so the first selectable item (Buzzer) is under the selector
settingsScrollOffset = 0;
} else if (now >= inputIgnoreUntilMs && digitalRead(BTNU) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
// Reset the game state before starting to avoid resuming a dead snake
resetGame();
// Now enter playing state
lastSettingsInputMs = now;
delay(60);
isSprinting = false;
sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
gameState = PLAYING;
stateChangeMs = now;
lastStepMs = now;
}
drawStartScreen();
break;
}
case SETTINGS: {
const uint32_t now = millis();
// Debounce: only process input after a short delay
if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
if (digitalRead(BTNU) == LOW) {
// UP: scroll up (if possible)
if (settingsScrollOffset > 0) settingsScrollOffset--;
lastSettingsInputMs = now;
} else if (digitalRead(BTND) == LOW) {
// DOWN: scroll down (if possible)
if (settingsScrollOffset + SETTINGS_VISIBLE < settingsTotal()) settingsScrollOffset++;
lastSettingsInputMs = now;
} else if (digitalRead(BTNL) == LOW) {
// LEFT: edit focused item
const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
const uint8_t total = settingsTotal();
if (focused == 1) {
// Buzzer cycle backward: OFF <- 500 <- ... <- 2000
toneFreq = buzzerCycleDown(toneFreq);
} else if (focused == 2) {
// Sprint toggle
sprintAllowed = !sprintAllowed;
} else if (focused == 3) {
// Cycle difficulty presets backward
difficultyPreset = (difficultyPreset + 3) % 4;
if (difficultyPreset < 3) {
highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
} else {
highScore = 0;
}
} else if (focused >= 4 && focused <= 7 && total == 10) {
// Custom fields (decrease)
if (focused == 4) {
if (customStepMs > CUSTOM_MIN_MS) customStepMs -= 10;
} else if (focused == 5) {
if (customSpeedupPerFood > 1) customSpeedupPerFood -= 1;
} else if (focused == 6) {
if (customMinStepMs > CUSTOM_MIN_MS) customMinStepMs -= 5;
} else if (focused == 7) {
if (customMaxStepMs > customMinStepMs + 5) customMaxStepMs -= 5;
}
} else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
// Back
saveHighScore();
gameState = START;
resetGame();
}
lastSettingsInputMs = now;
} else if (digitalRead(BTNR) == LOW) {
// RIGHT: edit focused item
const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
const uint8_t total = settingsTotal();
if (focused == 1) {
// Buzzer cycle forward: OFF -> 500 -> ... -> 2000 -> OFF
toneFreq = buzzerCycleUp(toneFreq);
} else if (focused == 2) {
// Sprint toggle (also allow right to toggle)
sprintAllowed = !sprintAllowed;
} else if (focused == 3) {
// Cycle difficulty presets forward
difficultyPreset = (difficultyPreset + 1) % 4;
if (difficultyPreset < 3) {
highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
} else {
highScore = 0;
}
} else if (focused >= 4 && focused <= 7 && total == 10) {
// Custom fields (increase)
if (focused == 4) {
if (customStepMs < CUSTOM_MAX_MS) customStepMs += 10;
} else if (focused == 5) {
if (customSpeedupPerFood < 255) customSpeedupPerFood += 1;
} else if (focused == 6) {
if (customMinStepMs < customMaxStepMs - 5) customMinStepMs += 5;
} else if (focused == 7) {
if (customMaxStepMs < CUSTOM_MAX_MS) customMaxStepMs += 5;
}
} else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
// Back
saveHighScore();
gameState = START;
resetGame();
}
lastSettingsInputMs = now;
}
}
drawSettingsScreen();
break;
}
case LEADERBOARD: {
const uint32_t now = millis();
if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
lastSettingsInputMs = now;
gameState = START;
settingsScrollOffset = 0;
}
drawLeaderboardScreen();
break;
}
case NAME_ENTRY: {
const uint32_t now = millis();
// Require both L+R to be released before allowing another combo press
if (digitalRead(BTNL) == HIGH && digitalRead(BTNR) == HIGH) {
lrComboReleased = true;
}
// Check L+R combo first (takes priority over movement)
if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && digitalRead(BTNR) == LOW && lrComboReleased && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
// latch until both released to avoid repeats
applyKeyboardSelection();
lrComboReleased = false;
lastSettingsInputMs = now;
} else if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
// Then check individual movement buttons
if (digitalRead(BTNU) == LOW) {
moveKeyboardCursor(-1, 0);
lastSettingsInputMs = now;
} else if (digitalRead(BTND) == LOW) {
moveKeyboardCursor(1, 0);
lastSettingsInputMs = now;
} else if (digitalRead(BTNL) == LOW) {
moveKeyboardCursor(0, -1);
lastSettingsInputMs = now;
} else if (digitalRead(BTNR) == LOW) {
moveKeyboardCursor(0, 1);
lastSettingsInputMs = now;
}
}
drawNameEntryScreen();
break;
}
case PLAYING: {
const uint32_t now = millis();
if (now - lastStepMs >= stepIntervalMs()) {
lastStepMs = now;
runGameStep();
}
drawGame();
break;
}
case GAME_OVER: {
const uint32_t now = millis();
// Any button press returns to start/reset the game (debounced)
if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS) && anyButtonPressed()) {
lastSettingsInputMs = now;
delay(60);
isSprinting = false;
sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
resetGame();
}
drawGameOverScreen();
break;
}
}
delay(8);
}

11
code/test/README Normal file
View file

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

11984
pcb/espsnake.kicad_pcb Normal file

File diff suppressed because it is too large Load diff

131
pcb/espsnake.kicad_prl Normal file
View file

@ -0,0 +1,131 @@
{
"board": {
"active_layer": 2,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"drc_exclusions",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "espsnake.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

661
pcb/espsnake.kicad_pro Normal file
View file

@ -0,0 +1,661 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.0,
"height": 1.524,
"width": 3.025
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.0,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0,
0.5,
0.75,
1.25
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "espsnake.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.25,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.75,
"via_diameter": 2.3,
"via_drill": 1.0,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.1,
"diff_pair_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "smol",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"via_diameter": 1.778,
"via_drill": 1.0,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "Default",
"pattern": "Net-(J2*"
},
{
"netclass": "smol",
"pattern": "Net-(U3*"
}
]
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"38670a8d-2240-4ffc-83ec-d5b488a7d099",
"Root"
]
],
"text_variables": {}
}

8799
pcb/espsnake.kicad_sch Normal file

File diff suppressed because it is too large Load diff

104252
pcb/fp-info-cache Normal file

File diff suppressed because it is too large Load diff

7
pcb/fp-lib-table Normal file
View file

@ -0,0 +1,7 @@
(fp_lib_table
(version 7)
(lib (name "ESP32-C3_SUPERMINI_SMD")(type "KiCad")(uri "/home/mia/kicad/_librarys/ESP32-C3_SUPERMINI_SMD")(options "")(descr ""))
(lib (name "ESP32-C3_SUPERMINI_TH")(type "KiCad")(uri "/home/mia/kicad/_librarys/ESP32-C3_SUPERMINI_TH")(options "")(descr ""))
(lib (name "FS8205A")(type "KiCad")(uri "/home/mia/kicad/_librarys/FS8205A")(options "")(descr ""))
(lib (name "OLED_128X64_1.3_I2C")(type "KiCad")(uri "/home/mia/kicad/_librarys/OLED_128X64_1.3_I2C")(options "")(descr ""))
)

7
pcb/sym-lib-table Normal file
View file

@ -0,0 +1,7 @@
(sym_lib_table
(version 7)
(lib (name "ESP32-C3_SUPERMINI_SMD")(type "KiCad")(uri "/home/mia/kicad/_librarys/ESP32-C3_SUPERMINI_SMD/ESP32-C3_SUPERMINI_SMD.kicad_sym")(options "")(descr ""))
(lib (name "ESP32-C3_SUPERMINI_TH")(type "KiCad")(uri "/home/mia/kicad/_librarys/ESP32-C3_SUPERMINI_TH/ESP32-C3_SUPERMINI_TH.kicad_sym")(options "")(descr ""))
(lib (name "FS8205A")(type "KiCad")(uri "/home/mia/kicad/_librarys/FS8205A/FS8205A.kicad_sym")(options "")(descr ""))
(lib (name "OLED_128X64_1.3_I2C")(type "KiCad")(uri "/home/mia/kicad/_librarys/OLED_128X64_1.3_I2C/OLED_128X64_1.3_I2C.kicad_sym")(options "")(descr ""))
)