This commit is contained in:
2026-03-06 12:29:58 +01:00
commit ab00a59f29
21 changed files with 1564 additions and 0 deletions

31
.envrc Normal file
View File

@@ -0,0 +1,31 @@
# disable nix builders because they are slow for copy operations
export NIX_BUILDERS=""
# Automatically load the Nix development environment when entering this directory
use flake
# Export ESP-IDF related environment variables
export IDF_TOOLS_PATH="$HOME/.espressif"
export CCACHE_DIR="$HOME/.ccache"
# Prevent Python bytecode generation
export PYTHONDONTWRITEBYTECODE=1
# Enable colored output for various tools
export FORCE_COLOR=1
# Set common ESP32 development variables
export ESP_TARGET=esp32
export ESP_BOARD=esp32dev
# Set locale to avoid issues
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
# Create necessary directories
mkdir -p "$IDF_TOOLS_PATH"
mkdir -p "$CCACHE_DIR"
# Show a message when the environment is loaded
echo "🚀 ESP32 Tang Server environment loaded!"
echo "ESP-IDF ready - run 'make help' for available commands"

60
.gitignore vendored Normal file
View File

@@ -0,0 +1,60 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Linker files
*.ilk
# Debugger Files
*.pdb
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# debug information files
*.dwo
# direnv
.direnv
# gitignore template for esp-idf, the official development framework for ESP32
# https://github.com/espressif/esp-idf
build/
sdkconfig
sdkconfig.old
managed_components/
.cache/
# Cryptographic files
*.jwk
*.jwe
# Other Binary files
*.pdf

17
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"configurations": [
{
"name": "NixOS",
"compileCommands": [
"${workspaceFolder}/build/compile_commands.json"
],
"includePath": [
"${workspaceFolder}/**",
"${workspaceFolder}/managed_components/**",
"${workspaceFolder}/managed_components/esp-cryptoauthlib/**"
],
"compilerPath": "/nix/store/x45d95acx6rybkb5cidrxxffval9k2pg-gcc-wrapper-14.3.0/bin/gcc"
}
],
"version": 4
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"cmake.automaticReconfigure": true,
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}

11
CMakeLists.txt Normal file
View File

@@ -0,0 +1,11 @@
# Top-level CMakeLists.txt for ESP32 Tang Server
cmake_minimum_required(VERSION 3.16)
# Set the project name
set(PROJECT_NAME "esp32-tang")
# Include ESP-IDF build system
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# Define the project
project(${PROJECT_NAME})

183
Makefile Normal file
View File

@@ -0,0 +1,183 @@
# ESP32 Tang Server Makefile
# Convenient shortcuts for ESP-IDF development tasks
.DEFAULT_GOAL := help
# Configuration variables
PORT ?= /dev/ttyUSB0
BAUD ?= 115200
TARGET ?= esp32
# Help target
.PHONY: help
help:
@echo "ESP32 Tang Server Development Commands"
@echo "====================================="
@echo ""
@echo "Setup:"
@echo " setup-target Set ESP32 as build target"
@echo " menuconfig Open configuration menu"
@echo ""
@echo "Build:"
@echo " build Build the project"
@echo " clean Clean build files"
@echo " fullclean Full clean (including config)"
@echo ""
@echo "Flash & Monitor:"
@echo " flash Flash to device"
@echo " monitor Open serial monitor"
@echo " flash-monitor Flash and immediately monitor"
@echo ""
@echo "Development:"
@echo " size Show binary size analysis"
@echo " erase Erase flash completely"
@echo " bootloader Flash bootloader only"
@echo ""
@echo "Board Management:"
@echo " list-boards List connected boards"
@echo " detect-port Detect serial ports"
@echo " board-info Show board information"
@echo ""
@echo "Environment variables:"
@echo " PORT=/dev/ttyUSB0 (default serial port)"
@echo " BAUD=115200 (default baud rate)"
@echo " TARGET=esp32 (default target)"
# Setup commands
.PHONY: setup-target
setup-target:
idf.py set-target $(TARGET)
.PHONY: menuconfig
menuconfig:
idf.py menuconfig
# Build commands
.PHONY: build
build:
idf.py build
.PHONY: clean
clean:
idf.py clean
.PHONY: fullclean
fullclean:
idf.py fullclean
# Flash and monitor commands
.PHONY: flash
flash:
idf.py -p $(PORT) -b $(BAUD) flash
.PHONY: monitor
monitor:
idf.py -p $(PORT) -b $(BAUD) monitor
.PHONY: flash-monitor
flash-monitor:
idf.py -p $(PORT) -b $(BAUD) flash monitor
# Development utilities
.PHONY: size
size:
idf.py size
.PHONY: size-components
size-components:
idf.py size-components
.PHONY: size-files
size-files:
idf.py size-files
.PHONY: erase
erase:
idf.py -p $(PORT) erase-flash
.PHONY: bootloader
bootloader:
idf.py -p $(PORT) bootloader-flash
# Show partition table
.PHONY: partition-table
partition-table:
idf.py partition-table
# Generate compilation database for IDE support
.PHONY: compile-commands
compile-commands:
idf.py build --cmake-args="-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
# Development shortcuts
.PHONY: dev
dev: build flash-monitor
.PHONY: quick
quick: build flash
# Board management
.PHONY: list-boards
list-boards:
idf.py -p $(PORT) board_info || echo "Connect board to $(PORT)"
.PHONY: detect-port
detect-port:
@echo "Scanning for ESP32 devices..."
@if ls /dev/ttyUSB* >/dev/null 2>&1; then \
echo "Found USB serial devices:"; \
ls -la /dev/ttyUSB* | head -5; \
elif ls /dev/ttyACM* >/dev/null 2>&1; then \
echo "Found ACM serial devices:"; \
ls -la /dev/ttyACM* | head -5; \
else \
echo "No serial devices found. Make sure ESP32 is connected."; \
fi
.PHONY: board-info
board-info:
@echo "Board Information:"
@echo "=================="
@echo "Target: $(TARGET)"
@echo "Port: $(PORT)"
@echo "Baud: $(BAUD)"
@echo ""
@echo "ESP-IDF Version:"
@idf.py --version
@echo ""
@echo "Project status:"
@idf.py show-port-info -p $(PORT) 2>/dev/null || echo "No device connected to $(PORT)"
# Show ESP-IDF version and tools
.PHONY: version
version:
@echo "ESP-IDF Version Information:"
@idf.py --version
@echo ""
@echo "Toolchain versions:"
@xtensa-esp32-elf-gcc --version | head -1 || echo "Toolchain not found"
@python3 --version
@cmake --version | head -1
# Validate project structure
.PHONY: validate
validate:
@echo "Validating ESP-IDF project structure..."
@if [ ! -f "CMakeLists.txt" ]; then \
echo "Error: CMakeLists.txt not found"; \
exit 1; \
fi
@if [ ! -d "main" ]; then \
echo "Error: main directory not found"; \
exit 1; \
fi
@echo "Project validation passed!"
# Full development cycle
.PHONY: full-setup
full-setup: setup-target menuconfig build
@echo ""
@echo "Full setup complete! You can now:"
@echo " make flash # Flash to device"
@echo " make monitor # View serial output"
@echo " make dev # Flash and monitor"

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# KeyPi Client using SSH and ATECC608B
### Build
Set the target:
```sh
idf.py set-target
```
Use `menuconfig` to configure the KeyPi (choose `KeyPi` from the list):
```sh
idf.py menuconfig
```
To build, flash and monitor:
```sh
idf.py build flash monitor
```
### Usage
Once the ESP32 connects to the wifi: user LED will light up.
Press button once: LED starts blinking slowly -> Sends open command after 5s.
Press button twice: LED starts blinking fast -> Sends close command after 5s.
Press button again: cancels the action.

30
dependencies.lock Normal file
View File

@@ -0,0 +1,30 @@
dependencies:
esp-cryptoauthlib:
component_hash: 8d24c37df1e906f9bbee9a869ca86f263ab135179d1c2ba6062448269437b192
dependencies:
- name: idf
version: '>=4.3'
source:
git: https://github.com/espressif/esp-cryptoauthlib.git
path: .
type: git
version: d9792119ebaec0c54839e6605acd3f11dd937205
idf:
source:
type: idf
version: 5.5.2
libssh2_esp:
component_hash: 75612f8fe15b7793de2d9d2eba920e66a7aab7424963012282a419cdb86399ad
dependencies: []
source:
git: https://github.com/skuodi/libssh2_esp.git
path: .
type: git
version: 378f0bd47900bffacbf29cac328c6e9b5391c886
direct_dependencies:
- esp-cryptoauthlib
- idf
- libssh2_esp
manifest_hash: a6766e71931c845fac37dab1b735cded43d414aa83e5ce0443ba4285e1980180
target: esp32c6
version: 2.0.0

117
flake.lock generated Normal file
View File

@@ -0,0 +1,117 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1771419570,
"narHash": "sha256-bxAlQgre3pcQcaRUm/8A0v/X8d2nhfraWSFqVmMcBcU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d41bc27aaf7b6a3ba6b169db3bd5d6159cfaa47",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-esp-dev": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1767865407,
"narHash": "sha256-QWF1rZYd+HvNzLIeRS+OEBX7HF0EhWCGeLbMkgtbsIo=",
"owner": "mirrexagon",
"repo": "nixpkgs-esp-dev",
"rev": "5287d6e1ca9e15ebd5113c41b9590c468e1e001b",
"type": "github"
},
"original": {
"owner": "mirrexagon",
"repo": "nixpkgs-esp-dev",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs-esp-dev",
"nixpkgs"
],
"nixpkgs-esp-dev": "nixpkgs-esp-dev"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

189
flake.nix Normal file
View File

@@ -0,0 +1,189 @@
{
description = "ESP32 Tang Server Development Environment";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs-esp-dev = {
url = "github:mirrexagon/nixpkgs-esp-dev";
};
nixpkgs.follows = "nixpkgs-esp-dev/nixpkgs";
};
outputs =
{
self,
nixpkgs,
flake-utils,
nixpkgs-esp-dev,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
config = {
permittedInsecurePackages = [
"python3.13-ecdsa-0.19.1"
];
};
overlays = [ nixpkgs-esp-dev.overlays.default ];
};
in
{
devShells.default = pkgs.mkShell {
name = "esp32-tang-dev";
buildInputs =
with pkgs;
[
python3Packages.pandas
# ESP-IDF with full toolchain
esp-idf-full
jose
clevis
# Development tools
gnumake
cmake
ninja
ccache
# Serial communication tools
picocom
screen
minicom
# Development utilities
curl
wget
unzip
file
which
jq
# Text processing tools
gawk
gnused
gnugrep
# Optional development tools
clang-tools
bear
]
++ lib.optionals stdenv.isLinux [
# Linux-specific packages for USB device access
udev
libusb1
];
shellHook = ''
echo "🚀 ESP32 Tang Server Development Environment"
echo "============================================="
echo
echo "ESP-IDF: $(idf.py --version 2>/dev/null || echo 'Ready')"
echo "Python: $(python3 --version)"
echo "CMake: $(cmake --version | head -1)"
echo
echo "Available commands:"
echo " 📦 idf.py build - Build the project"
echo " 📡 idf.py flash - Flash to device"
echo " 💻 idf.py monitor - Serial monitor"
echo " 🔧 idf.py flash monitor - Flash and monitor"
echo " idf.py menuconfig - Configuration menu"
echo " 🎯 idf.py set-target - Set target (esp32)"
echo
echo "Or use the Makefile shortcuts:"
echo " make setup-target - Set ESP32 target"
echo " make build - Build project"
echo " make flash-monitor - Flash and monitor"
echo " make menuconfig - Configuration"
echo
echo "Quick start:"
echo " 1. Set target: make setup-target"
echo " 2. Configure: make menuconfig"
echo " 3. Build: make build"
echo " 4. Flash & Monitor: make flash-monitor PORT=/dev/ttyUSB0"
echo
# Set up development environment
export IDF_TOOLS_PATH="$HOME/.espressif"
export CCACHE_DIR="$HOME/.ccache"
# Create necessary directories
mkdir -p "$IDF_TOOLS_PATH"
mkdir -p "$CCACHE_DIR"
# Check for serial devices
if ls /dev/ttyUSB* >/dev/null 2>&1; then
echo "📡 Found USB serial devices:"
ls -la /dev/ttyUSB* 2>/dev/null
elif ls /dev/ttyACM* >/dev/null 2>&1; then
echo "📡 Found ACM serial devices:"
ls -la /dev/ttyACM* 2>/dev/null
else
echo "📡 No serial devices found. Connect your ESP32 board."
fi
# Check serial permissions
if ! groups | grep -q dialout 2>/dev/null && ! groups | grep -q uucp 2>/dev/null; then
echo
echo " Note: You may need to add your user to the 'dialout' group"
echo " to access serial devices:"
echo " sudo usermod -a -G dialout $USER"
echo " Then log out and log back in."
fi
echo
echo "Ready for ESP32 development with Arduino support! 🎯"
echo
'';
# allow /dev/ access
extraDevPaths = [
"/dev/ttyUSB*"
"/dev/ttyACM*"
];
# Environment variables
IDF_TOOLS_PATH = "$HOME/.espressif";
CCACHE_DIR = "$HOME/.ccache";
# Prevent Python from creating __pycache__ directories
PYTHONDONTWRITEBYTECODE = "1";
# Enable colored output
FORCE_COLOR = "1";
# Set locale to avoid issues
LANG = "C.UTF-8";
LC_ALL = "C.UTF-8";
};
devShells.minimal = pkgs.mkShell {
name = "esp32-tang-minimal";
buildInputs = with pkgs; [
esp-idf-full
picocom
];
shellHook = ''
echo " ESP32 Tang Server (Minimal Environment)"
echo "========================================"
echo "Ready for ESP32 development!"
echo
export IDF_TOOLS_PATH="$HOME/.espressif"
export CCACHE_DIR="$HOME/.ccache"
mkdir -p "$CCACHE_DIR"
'';
extraDevPaths = [
"/dev/ttyUSB*"
"/dev/ttyACM*"
];
};
}
);
}

6
main/CMakeLists.txt Normal file
View File

@@ -0,0 +1,6 @@
idf_component_register(SRCS "main.c"
"wifi.c"
"atecc608a.c"
"ssh_client.c"
INCLUDE_DIRS "."
REQUIRES mbedtls esp-cryptoauthlib esp_wifi nvs_flash driver)

45
main/Kconfig.projbuild Normal file
View File

@@ -0,0 +1,45 @@
menu "KeyPi"
config WIFI_SSID
string "Wi-Fi SSID"
default ""
help
SSID of the Wi-Fi network to connect to.
config WIFI_PASSWORD
string "Wi-Fi Password"
default ""
help
Password for the Wi-Fi network.
config SSH_HOSTNAME
string "SSH Server Hostname or IP"
default "192.168.1.1"
help
Hostname or IP address of the SSH server (door controller).
config SSH_PORT
int "SSH Server Port"
default 22
help
TCP port of the SSH server.
config SSH_USERNAME
string "SSH Username"
default "pi"
help
Username to authenticate with on the SSH server.
config SSH_OPEN_COMMAND
string "SSH Open Command"
default "door open"
help
Shell command to execute on the SSH server to open the door.
config SSH_LOCK_COMMAND
string "SSH Lock Command"
default "door lock"
help
Shell command to execute on the SSH server to lock the door.
endmenu

103
main/atecc608a.c Normal file
View File

@@ -0,0 +1,103 @@
#include "atecc608a.h"
#include "sdkconfig.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
static const char *TAG = "atecc608b";
/* -------------------------------------------------------------------------
* Module-private state
* ---------------------------------------------------------------------- */
static ATCAIfaceCfg s_atecc_cfg;
static uint8_t s_config_data[128];
static bool s_config_valid = false;
/* -------------------------------------------------------------------------
* Internal helpers
* ---------------------------------------------------------------------- */
static bool read_config_zone(void)
{
ATCA_STATUS status = atcab_read_config_zone(s_config_data);
if (status != ATCA_SUCCESS) {
ESP_LOGI(TAG, "Failed to read config zone (0x%02X) check wiring", status);
return false;
}
s_config_valid = true;
return true;
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
bool atecc608B_init(void)
{
ESP_LOGI(TAG, "Initialising ATECC608B...");
s_atecc_cfg.iface_type = ATCA_I2C_IFACE;
s_atecc_cfg.devtype = ATECC608B;
s_atecc_cfg.atcai2c.address = CONFIG_ATCA_I2C_ADDRESS;
s_atecc_cfg.atcai2c.bus = 0;
s_atecc_cfg.atcai2c.baud = CONFIG_ATCA_I2C_BAUD_RATE;
s_atecc_cfg.wake_delay = 1500;
s_atecc_cfg.rx_retries = 20;
ESP_LOGI(TAG, "SDA=GPIO%d SCL=GPIO%d addr=0x%02X (7-bit 0x%02X)",
CONFIG_ATCA_I2C_SDA_PIN, CONFIG_ATCA_I2C_SCL_PIN,
CONFIG_ATCA_I2C_ADDRESS, CONFIG_ATCA_I2C_ADDRESS >> 1);
ATCA_STATUS status = atcab_init(&s_atecc_cfg);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_init failed: 0x%02X", status);
return false;
}
vTaskDelay(pdMS_TO_TICKS(100));
status = atcab_wakeup();
if (status != ATCA_SUCCESS) {
ESP_LOGW(TAG, "Wake returned 0x%02X (may be normal)", status);
}
atcab_idle();
vTaskDelay(pdMS_TO_TICKS(50));
ESP_LOGI(TAG, "ATECC608B initialised");
return true;
}
void atecc608B_print_config(void)
{
ESP_LOGI(TAG, "=== ATECC608B Configuration Zone ===");
uint8_t serial[9];
ATCA_STATUS status = atcab_read_serial_number(serial);
if (status != ATCA_SUCCESS) {
ESP_LOGE(TAG, "Failed to read serial number (0x%02X) check wiring", status);
return;
}
if (!read_config_zone()) {
return;
}
uint8_t *c = s_config_data;
for (int i = 0; i < 128; i++) {
if (i % 16 == 0) printf("\n0x%02X: ", i);
printf("%02X ", c[i]);
}
printf("\n\n");
ESP_LOGI(TAG, "=== End of ATECC608B Configuration ===");
}
void atecc608B_release(void)
{
atcab_release();
}

24
main/atecc608a.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef ATECC608B_H
#define ATECC608B_H
#include "cryptoauthlib.h"
#include <stdbool.h>
/**
* Initialise the ATECC608B over I2C.
* Returns true on success.
*/
bool atecc608B_init(void);
/**
* Read the full configuration zone and print it to the console.
* Useful for first-time setup diagnostics.
*/
void atecc608B_print_config(void);
/**
* Release cryptoauthlib resources.
*/
void atecc608B_release(void);
#endif /* ATECC608B_H */

8
main/idf_component.yml Normal file
View File

@@ -0,0 +1,8 @@
---
dependencies:
idf:
version: ">=4.1.0"
esp-cryptoauthlib:
git: https://github.com/espressif/esp-cryptoauthlib.git
libssh2_esp:
git: https://github.com/skuodi/libssh2_esp.git

281
main/main.c Normal file
View File

@@ -0,0 +1,281 @@
/*
* keypitecc door controller
*
* Hardware:
* GPIO 9 Boot button (active-low, press = GND)
* GPIO 15 User LED (active-high, 1 = on)
*
* Button state machine (5-second window):
* IDLE
* └─ press ──► PENDING_OPEN (slow blink, will run SSH open command)
* └─ press ──► PENDING_LOCK (fast blink, will run SSH lock command)
* └─ press ──► IDLE (cancelled)
* Any pending state expires after 5 s → command is executed → IDLE
*
* LED idle behaviour:
* WiFi connected → LED ON
* WiFi disconnected → LED OFF
*/
#include "atecc608a.h"
#include "ssh_client.h"
#include "wifi.h"
#include "sdkconfig.h"
#include <driver/gpio.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/timers.h>
#include <nvs_flash.h>
#include <stdbool.h>
#include <stdint.h>
static const char *TAG = "main";
/* -------------------------------------------------------------------------
* GPIO defines
* ---------------------------------------------------------------------- */
#define GPIO_BTN 9 /* Boot button, active-low */
#define GPIO_LED 15 /* User LED, active-high */
/* -------------------------------------------------------------------------
* Button / LED types
* ---------------------------------------------------------------------- */
typedef enum {
BTN_STATE_IDLE,
BTN_STATE_PENDING_OPEN,
BTN_STATE_PENDING_LOCK,
} btn_state_t;
typedef enum {
LED_MODE_WIFI_STATUS, /* ON = connected, OFF = disconnected */
LED_MODE_SLOW_BLINK, /* 500 ms on / 500 ms off (open pending) */
LED_MODE_FAST_BLINK, /* 100 ms on / 100 ms off (lock pending) */
} led_mode_t;
typedef enum {
EVT_BUTTON_PRESS,
EVT_TIMER_EXPIRED,
} event_type_t;
/* -------------------------------------------------------------------------
* Shared state
* ---------------------------------------------------------------------- */
static volatile led_mode_t s_led_mode = LED_MODE_WIFI_STATUS;
static btn_state_t s_btn_state = BTN_STATE_IDLE;
static QueueHandle_t s_event_queue;
static TimerHandle_t s_action_timer;
/* Debounce: ignore button edges within 200 ms of the previous one */
static volatile TickType_t s_last_btn_tick = 0;
/* -------------------------------------------------------------------------
* SSH task spawned on demand so the button task stays responsive
* ---------------------------------------------------------------------- */
static void ssh_task(void *arg)
{
const char *cmd = (const char *)arg;
ESP_LOGI(TAG, "Executing SSH command: %s", cmd);
bool ok = ssh_execute_command(cmd);
ESP_LOGI(TAG, "SSH command %s: %s", cmd, ok ? "succeeded" : "FAILED");
vTaskDelete(NULL);
}
static void trigger_ssh(const char *cmd)
{
/* Stack size 16 kB SSH needs heap + TLS, keep priority low */
if (xTaskCreate(ssh_task, "ssh", 16384, (void *)cmd, 3, NULL) != pdPASS) {
ESP_LOGE(TAG, "Failed to create SSH task");
}
}
/* -------------------------------------------------------------------------
* FreeRTOS software timer callback runs in timer daemon task context
* ---------------------------------------------------------------------- */
static void action_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
event_type_t evt = EVT_TIMER_EXPIRED;
xQueueSend(s_event_queue, &evt, 0);
}
/* -------------------------------------------------------------------------
* GPIO ISR
* ---------------------------------------------------------------------- */
static void IRAM_ATTR button_isr(void *arg)
{
(void)arg;
TickType_t now = xTaskGetTickCountFromISR();
if ((now - s_last_btn_tick) < pdMS_TO_TICKS(200)) {
return; /* debounce */
}
s_last_btn_tick = now;
event_type_t evt = EVT_BUTTON_PRESS;
xQueueSendFromISR(s_event_queue, &evt, NULL);
}
/* -------------------------------------------------------------------------
* Button task owns the state machine
* ---------------------------------------------------------------------- */
static void button_task(void *arg)
{
(void)arg;
event_type_t evt;
for (;;) {
if (xQueueReceive(s_event_queue, &evt, portMAX_DELAY) != pdTRUE) {
continue;
}
switch (s_btn_state) {
case BTN_STATE_IDLE:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: PENDING_OPEN in 5s (press again for LOCK, third press cancels)");
s_btn_state = BTN_STATE_PENDING_OPEN;
s_led_mode = LED_MODE_SLOW_BLINK;
xTimerStart(s_action_timer, 0);
}
break;
case BTN_STATE_PENDING_OPEN:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: switching to PENDING_LOCK");
s_btn_state = BTN_STATE_PENDING_LOCK;
s_led_mode = LED_MODE_FAST_BLINK;
xTimerReset(s_action_timer, 0); /* restart 5s from now */
} else if (evt == EVT_TIMER_EXPIRED) {
ESP_LOGI(TAG, "Timer expired: executing OPEN command");
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
trigger_ssh(CONFIG_SSH_OPEN_COMMAND);
}
break;
case BTN_STATE_PENDING_LOCK:
if (evt == EVT_BUTTON_PRESS) {
ESP_LOGI(TAG, "Button: action CANCELLED");
xTimerStop(s_action_timer, 0);
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
} else if (evt == EVT_TIMER_EXPIRED) {
ESP_LOGI(TAG, "Timer expired: executing LOCK command");
s_btn_state = BTN_STATE_IDLE;
s_led_mode = LED_MODE_WIFI_STATUS;
trigger_ssh(CONFIG_SSH_LOCK_COMMAND);
}
break;
}
}
}
/* -------------------------------------------------------------------------
* LED task drives GPIO 15 according to the current led_mode
* ---------------------------------------------------------------------- */
static void led_task(void *arg)
{
(void)arg;
bool led_state = false;
for (;;) {
led_mode_t mode = s_led_mode; /* atomic read (single-byte enum) */
switch (mode) {
case LED_MODE_WIFI_STATUS:
led_state = wifi_is_connected();
gpio_set_level(GPIO_LED, led_state ? 0 : 1);
vTaskDelay(pdMS_TO_TICKS(250));
break;
case LED_MODE_SLOW_BLINK:
gpio_set_level(GPIO_LED, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(GPIO_LED, 0);
vTaskDelay(pdMS_TO_TICKS(500));
break;
case LED_MODE_FAST_BLINK:
gpio_set_level(GPIO_LED, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_LED, 0);
vTaskDelay(pdMS_TO_TICKS(100));
break;
}
}
}
/* -------------------------------------------------------------------------
* GPIO initialisation
* ---------------------------------------------------------------------- */
static void gpio_init(void)
{
/* LED output */
gpio_config_t led_cfg = {
.pin_bit_mask = (1ULL << GPIO_LED),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&led_cfg);
gpio_set_level(GPIO_LED, 1); /* off (active-high) */
/* Button input with interrupt on falling edge (active-low) */
gpio_config_t btn_cfg = {
.pin_bit_mask = (1ULL << GPIO_BTN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&btn_cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_BTN, button_isr, NULL);
}
/* -------------------------------------------------------------------------
* app_main
* ---------------------------------------------------------------------- */
void app_main(void)
{
/* NVS (required by WiFi) */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* ATECC608B */
if (!atecc608B_init()) {
ESP_LOGW(TAG, "ATECC608B init failed SSH authentication will not work");
} else {
atecc608B_print_config();
ssh_print_public_key(); /* print key for authorized_keys setup */
}
/* WiFi */
wifi_init();
/* GPIO and event infrastructure */
gpio_init();
s_event_queue = xQueueCreate(4, sizeof(event_type_t));
configASSERT(s_event_queue);
/* 5-second one-shot timer (auto-reload disabled) */
s_action_timer = xTimerCreate("action", pdMS_TO_TICKS(5000),
pdFALSE, NULL, action_timer_cb);
configASSERT(s_action_timer);
/* Tasks */
xTaskCreate(button_task, "button", 4096, NULL, 5, NULL);
xTaskCreate(led_task, "led", 2048, NULL, 4, NULL);
ESP_LOGI(TAG, "keypitecc ready press the boot button to operate the door");
}

285
main/ssh_client.c Normal file
View File

@@ -0,0 +1,285 @@
#include "ssh_client.h"
#include "cryptoauthlib.h"
#include "sdkconfig.h"
#include <arpa/inet.h>
#include <esp_log.h>
#include <libssh2.h>
#include <mbedtls/base64.h>
#include <mbedtls/sha256.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
static const char *TAG = "ssh_client";
/* -------------------------------------------------------------------------
* SSH blob / key helpers
* ---------------------------------------------------------------------- */
/* Write a big-endian uint32 into buf. */
static void write_be32(uint8_t *buf, uint32_t val)
{
buf[0] = (val >> 24) & 0xFF;
buf[1] = (val >> 16) & 0xFF;
buf[2] = (val >> 8) & 0xFF;
buf[3] = val & 0xFF;
}
/**
* Format one ECDSA integer as an SSH mpint:
* 4-byte big-endian length | optional 0x00 pad | value (leading zeros stripped)
*
* Returns number of bytes written to buf.
*/
static uint32_t write_mpint(uint8_t *buf, const uint8_t *val, uint32_t size)
{
uint32_t start = 0;
while (start < (size - 1) && val[start] == 0x00) {
start++;
}
bool pad = (val[start] & 0x80) != 0;
uint32_t len = (size - start) + (pad ? 1 : 0);
uint32_t off = 0;
buf[off++] = (len >> 24) & 0xFF;
buf[off++] = (len >> 16) & 0xFF;
buf[off++] = (len >> 8) & 0xFF;
buf[off++] = len & 0xFF;
if (pad) {
buf[off++] = 0x00;
}
memcpy(&buf[off], &val[start], size - start);
return off + (size - start);
}
/**
* Build a 104-byte SSH public key blob for ecdsa-sha2-nistp256:
* [uint32 19] "ecdsa-sha2-nistp256"
* [uint32 8] "nistp256"
* [uint32 65] 0x04 || X(32) || Y(32)
*
* @param atecc_pubkey 64-byte raw public key (X||Y) from ATECC608B.
* @param out_blob Must point to a buffer of at least 104 bytes.
*/
static void build_pubkey_blob(const uint8_t *atecc_pubkey, uint8_t *out_blob)
{
uint32_t off = 0;
/* Key type */
const char *key_type = "ecdsa-sha2-nistp256";
write_be32(&out_blob[off], 19); off += 4;
memcpy(&out_blob[off], key_type, 19); off += 19;
/* Curve name */
const char *curve = "nistp256";
write_be32(&out_blob[off], 8); off += 4;
memcpy(&out_blob[off], curve, 8); off += 8;
/* Uncompressed EC point: 0x04 || X || Y */
write_be32(&out_blob[off], 65); off += 4;
out_blob[off++] = 0x04;
memcpy(&out_blob[off], atecc_pubkey, 64);
/* off += 64 — total = 104 */
}
/* -------------------------------------------------------------------------
* libssh2 signing callback (ATECC608B signs the hash)
* ---------------------------------------------------------------------- */
static int sign_callback(LIBSSH2_SESSION *session,
unsigned char **sig, size_t *sig_len,
const unsigned char *data, size_t data_len,
void **abstract)
{
(void)session;
(void)abstract;
uint8_t digest[32];
uint8_t raw_sig[64];
/* Hash the challenge data */
mbedtls_sha256(data, data_len, digest, 0);
/* Sign with the ATECC608B hardware key in slot 0 */
if (atcab_sign(0, digest, raw_sig) != ATCA_SUCCESS) {
ESP_LOGE(TAG, "ATECC608B signing failed!");
return -1;
}
/* Encode as [mpint R][mpint S] (no outer string wrapper) */
unsigned char *buf = (unsigned char *)malloc(80);
if (!buf) return -1;
uint32_t off = 0;
off += write_mpint(&buf[off], &raw_sig[0], 32); /* R */
off += write_mpint(&buf[off], &raw_sig[32], 32); /* S */
*sig = buf;
*sig_len = off;
return 0;
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
void ssh_print_public_key(void)
{
uint8_t raw_key[64];
ATCA_STATUS st = atcab_get_pubkey(0, raw_key);
if (st != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st);
return;
}
uint8_t blob[104];
build_pubkey_blob(raw_key, blob);
size_t b64_len = 0;
mbedtls_base64_encode(NULL, 0, &b64_len, blob, sizeof(blob));
unsigned char *b64 = (unsigned char *)malloc(b64_len + 1);
if (!b64) {
ESP_LOGE(TAG, "malloc failed for base64 buffer");
return;
}
mbedtls_base64_encode(b64, b64_len, &b64_len, blob, sizeof(blob));
b64[b64_len] = '\0';
printf("\n=== SSH Public Key (add to authorized_keys) ===\n");
printf("ecdsa-sha2-nistp256 %s keypitecc\n", b64);
printf("================================================\n\n");
free(b64);
}
bool ssh_execute_command(const char *cmd)
{
if (!cmd) return false;
/* --- Read public key blob ------------------------------------------ */
uint8_t raw_key[64];
ATCA_STATUS st = atcab_get_pubkey(0, raw_key);
if (st != ATCA_SUCCESS) {
ESP_LOGE(TAG, "atcab_get_pubkey failed: 0x%02X", st);
return false;
}
uint8_t pubkey_blob[104];
build_pubkey_blob(raw_key, pubkey_blob);
/* --- TCP connect ------------------------------------------------------- */
int rc;
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
ESP_LOGE(TAG, "socket() failed");
return false;
}
struct sockaddr_in sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(CONFIG_SSH_PORT);
sin.sin_addr.s_addr = inet_addr(CONFIG_SSH_HOSTNAME);
if (sin.sin_addr.s_addr == 0xFFFFFFFF) {
struct hostent *hp = gethostbyname(CONFIG_SSH_HOSTNAME);
if (!hp) {
ESP_LOGE(TAG, "gethostbyname(%s) failed", CONFIG_SSH_HOSTNAME);
close(sock);
return false;
}
sin.sin_addr.s_addr = ((struct ip4_addr *)hp->h_addr)->addr;
}
if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) != 0) {
ESP_LOGE(TAG, "connect to %s:%d failed", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT);
close(sock);
return false;
}
ESP_LOGI(TAG, "TCP connected to %s:%d", CONFIG_SSH_HOSTNAME, CONFIG_SSH_PORT);
/* --- libssh2 session -------------------------------------------------- */
rc = libssh2_init(0);
if (rc) {
ESP_LOGE(TAG, "libssh2_init failed: %d", rc);
close(sock);
return false;
}
LIBSSH2_SESSION *session = libssh2_session_init();
if (!session) {
ESP_LOGE(TAG, "libssh2_session_init failed");
libssh2_exit();
close(sock);
return false;
}
rc = libssh2_session_handshake(session, sock);
if (rc) {
ESP_LOGE(TAG, "SSH handshake failed: %d", rc);
goto cleanup_session;
}
ESP_LOGI(TAG, "SSH handshake OK");
/* --- Authenticate with ATECC608B hardware key ------------------------- */
void *abstract = NULL;
rc = libssh2_userauth_publickey(session, CONFIG_SSH_USERNAME,
pubkey_blob, sizeof(pubkey_blob),
sign_callback, &abstract);
if (rc != 0) {
ESP_LOGE(TAG, "SSH public-key authentication failed: %d", rc);
goto cleanup_session;
}
ESP_LOGI(TAG, "SSH authenticated as '%s'", CONFIG_SSH_USERNAME);
/* --- Open channel and execute command --------------------------------- */
LIBSSH2_CHANNEL *channel = libssh2_channel_open_session(session);
if (!channel) {
ESP_LOGE(TAG, "Failed to open SSH channel");
goto cleanup_session;
}
ESP_LOGI(TAG, "Executing: %s", cmd);
rc = libssh2_channel_exec(channel, cmd);
if (rc != 0) {
ESP_LOGE(TAG, "channel_exec failed: %d", rc);
libssh2_channel_free(channel);
goto cleanup_session;
}
/* --- Read output ------------------------------------------------------- */
char buf[256];
int bytes;
ESP_LOGI(TAG, "--- command output ---");
while ((bytes = libssh2_channel_read(channel, buf, sizeof(buf) - 1)) > 0) {
buf[bytes] = '\0';
printf("%s", buf);
}
ESP_LOGI(TAG, "--- end output ---");
/* --- Shutdown --------------------------------------------------------- */
libssh2_channel_close(channel);
int exit_status = libssh2_channel_get_exit_status(channel);
ESP_LOGI(TAG, "Command exited with status: %d", exit_status);
libssh2_channel_free(channel);
libssh2_session_disconnect(session, "Bye");
libssh2_session_free(session);
libssh2_exit();
close(sock);
return (exit_status == 0);
cleanup_session:
libssh2_session_disconnect(session, "Error");
libssh2_session_free(session);
libssh2_exit();
close(sock);
return false;
}

25
main/ssh_client.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef SSH_CLIENT_H
#define SSH_CLIENT_H
#include <stdbool.h>
/**
* Read the ATECC608B public key, format it as an SSH authorized_keys entry,
* and print it to the console. Call this once at boot so the key can be
* added to the server's authorized_keys file.
*
* Output format:
* ecdsa-sha2-nistp256 <base64-blob> keypitecc
*/
void ssh_print_public_key(void);
/**
* Open a TCP connection to the configured SSH server, authenticate using the
* ATECC608B hardware key, execute @p cmd, log the output, and disconnect.
*
* @param cmd Shell command string to run on the remote host.
* @return true on success, false on any error.
*/
bool ssh_execute_command(const char *cmd);
#endif /* SSH_CLIENT_H */

81
main/wifi.c Normal file
View File

@@ -0,0 +1,81 @@
#include "wifi.h"
#include "sdkconfig.h"
#include <esp_event.h>
#include <esp_log.h>
#include <esp_netif.h>
#include <esp_wifi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <string.h>
static const char *TAG = "wifi";
static EventGroupHandle_t s_wifi_event_group;
static const int WIFI_CONNECTED_BIT = BIT0;
static volatile bool s_connected = false;
/* -------------------------------------------------------------------------
* Event handler
* ---------------------------------------------------------------------- */
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi connecting...");
} else if (event_base == WIFI_EVENT &&
event_id == WIFI_EVENT_STA_DISCONNECTED) {
s_connected = false;
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
esp_wifi_connect();
ESP_LOGI(TAG, "WiFi disconnected, reconnecting...");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "WiFi connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
s_connected = true;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
/* -------------------------------------------------------------------------
* Public API
* ---------------------------------------------------------------------- */
void wifi_init(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
configASSERT(sta_netif);
ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, "keypitecc"));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL));
wifi_config_t wifi_config = {};
strncpy((char *)wifi_config.sta.ssid, CONFIG_WIFI_SSID,
sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, CONFIG_WIFI_PASSWORD,
sizeof(wifi_config.sta.password) - 1);
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Connecting to SSID: %s", CONFIG_WIFI_SSID);
}
bool wifi_is_connected(void)
{
return s_connected;
}

19
main/wifi.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef WIFI_H
#define WIFI_H
#include <stdbool.h>
/**
* Initialise WiFi in STA mode, connect to the SSID defined in Kconfig,
* and register event handlers. Blocks until a first connection attempt
* is made, but does NOT block forever reconnection is handled in the
* background.
*/
void wifi_init(void);
/**
* Returns true when the station has a valid IP address (i.e. WiFi is up).
*/
bool wifi_is_connected(void);
#endif /* WIFI_H */

15
sdkconfig.defaults Normal file
View File

@@ -0,0 +1,15 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.2 Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c6"
CONFIG_WIFI_SSID="DoNotSetTheRealValueHere"
CONFIG_WIFI_PASSWORD="PutTheRealPassInTheSdkconfigFile"
CONFIG_SSH_HOSTNAME="192.168.178.1"
CONFIG_SSH_USERNAME="user"
CONFIG_SSH_OPEN_COMMAND="open"
CONFIG_SSH_LOCK_COMMAND="lock"
CONFIG_ATECC608A_TCUSTOM=y
CONFIG_ATCA_I2C_SDA_PIN=22
CONFIG_ATCA_I2C_SCL_PIN=23
CONFIG_ATCA_I2C_BAUD_RATE=1000000
CONFIG_LIBSSH2_DEBUG_ENABLE=n