Features/digital safe firmware (#2)

* Added digital safe firmware

* Adjusted build pipelines

---------

Co-authored-by: Andriy Malyshenko <andriy@malyshenko.com>
This commit is contained in:
Andriy Malyshenko
2023-03-01 20:51:13 +01:00
committed by GitHub
parent 706d6f5419
commit 4fafe270f9
12 changed files with 773 additions and 3 deletions

41
.github/workflows/build-safe.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Build t1616-password-entry
on:
push:
paths:
- ./firmware/t1616-password-entry
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v2
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Set up Python
uses: actions/setup-python@v2
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install --upgrade platformio
- name: Run PlatformIO
working-directory: ./firmware/t1616-password-entry
run: pio run

View File

@@ -1,6 +1,10 @@
name: PlatformIO CI
name: Build t1616-starter
on: [ push, pull_request, workflow_dispatch ]
on:
push:
paths:
- ./firmware/t1616-starter
pull_request:
jobs:
build:

View File

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

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"
]
}

View File

@@ -0,0 +1,29 @@
[env]
platform = atmelmegaavr
framework = arduino
monitor_speed = 115200
monitor_port = /dev/ttyUSB1
upload_port = /dev/ttyUSB0
board_build.f_cpu = 16000000ul
; Serial pins are used on Keypad
; build_flags =
; -D DEBUG
lib_deps =
SPI
Wire
LiquidCrystal
Keypad
[env:tiny1616-serialupdi]
board = ATtiny1616
upload_speed = 230400
upload_flags =
--tool
uart
--device
attiny1616
--uart
$UPLOAD_PORT
--clk
$UPLOAD_SPEED
upload_command = pymcuprog write --erase $UPLOAD_FLAGS --filename $SOURCE

View File

@@ -0,0 +1,51 @@
/**
Arduino Electronic Safe
Copyright (C) 2020, Uri Shaked.
Released under the MIT License.
*/
#pragma once
#include <LiquidCrystal_I2C.h>
// Our custom icon numbers
#define ICON_LOCKED_CHAR (byte)0
#define ICON_UNLOCKED_CHAR (byte)1
// This is a standard icon on the LCD1602 character set
#define ICON_RIGHT_ARROW (byte)126
const byte iconLocked[8] PROGMEM = {
0b01110,
0b10001,
0b10001,
0b11111,
0b11011,
0b11011,
0b11111,
};
const byte iconUnlocked[8] PROGMEM = {
0b01110,
0b10000,
0b10000,
0b11111,
0b11011,
0b11011,
0b11111,
};
class Icons
{
public:
Icons(LiquidCrystal_I2C &lcd)
{
byte icon[8];
memcpy_P(icon, iconLocked, sizeof(icon));
lcd.createChar(ICON_LOCKED_CHAR, icon);
memcpy_P(icon, iconUnlocked, sizeof(icon));
lcd.createChar(ICON_UNLOCKED_CHAR, icon);
}
};

View File

@@ -0,0 +1,91 @@
#pragma once
#include <tinyNeoPixel.h>
#include "Tone.h"
#define PIN_RGB PIN_PA7
#define LED_COUNT 1
class Light
{
private:
tinyNeoPixel *pixels = new tinyNeoPixel(LED_COUNT, PIN_RGB, NEO_GRB + NEO_KHZ800);
Tone *tone;
uint32_t seq_starts = 0;
uint32_t seq_ends = 0;
uint8_t seq_max = 0;
uint8_t seq = 0;
uint32_t seq_color = 0;
void setColor(uint32_t color);
void flash(uint32_t color, uint8_t count, uint32_t delay_ms);
public:
Light(Tone *tone) : tone(tone)
{
pixels->begin();
};
void lock();
void denied();
void unlock();
void loop();
};
void Light::setColor(uint32_t color)
{
pixels->clear();
if (color != 0)
for (uint8_t i = 0; i < LED_COUNT; i++)
pixels->setPixelColor(i, color);
pixels->show();
}
void Light::flash(uint32_t color, uint8_t count, uint32_t delay_ms)
{
seq = 0;
seq_max = count * 2 - 1;
seq_starts = millis();
seq_ends = seq_starts + delay_ms;
seq_color = color;
pinMode(PIN_RGB, OUTPUT);
setColor(color);
}
void Light::lock()
{
auto blue = pixels->Color(0, 0, 0xff);
flash(blue, 1, 500);
}
void Light::denied()
{
tone->beep(500, 500);
auto red = pixels->Color(0xff, 0, 0);
flash(red, 5, 200);
}
void Light::unlock()
{
tone->beep(500, 150, true);
auto green = pixels->Color(0, 0xff, 0);
flash(green, 3, 250);
}
void Light::loop()
{
if (seq <= seq_max)
{
if (millis() > seq_ends)
{
uint32_t color = (seq % 2 == 0) ? seq_color : pixels->Color(0, 0, 0);
setColor(color);
seq++;
uint32_t delay_ms = seq_ends - seq_starts;
seq_starts = seq_ends;
seq_ends += delay_ms;
}
}
}

View File

@@ -0,0 +1,82 @@
/**
Arduino Electronic Safe
Copyright (C) 2020, Uri Shaked.
Released under the MIT License.
*/
#pragma once
#include <EEPROM.h>
/* Safe state */
#define EEPROM_ADDR_LOCKED 0
#define EEPROM_ADDR_CODE_LEN 1
#define EEPROM_ADDR_CODE 2
#define EEPROM_EMPTY 0xff
#define SAFE_STATE_OPEN (char)0
#define SAFE_STATE_LOCKED (char)1
class SafeState {
public:
SafeState();
void lock();
bool unlock(String code);
bool locked();
bool hasCode();
void setCode(String newCode);
private:
void setLock(bool locked);
bool _locked;
};
SafeState::SafeState() {
this->_locked = EEPROM.read(EEPROM_ADDR_LOCKED) == SAFE_STATE_LOCKED;
}
void SafeState::lock() {
this->setLock(true);
}
bool SafeState::locked() {
return this->_locked;
}
bool SafeState::hasCode() {
auto codeLength = EEPROM.read(EEPROM_ADDR_CODE_LEN);
return codeLength != EEPROM_EMPTY;
}
void SafeState::setCode(String newCode) {
EEPROM.write(EEPROM_ADDR_CODE_LEN, newCode.length());
for (byte i = 0; i < newCode.length(); i++) {
EEPROM.write(EEPROM_ADDR_CODE + i, newCode[i]);
}
}
bool SafeState::unlock(String code) {
auto codeLength = EEPROM.read(EEPROM_ADDR_CODE_LEN);
if (codeLength == EEPROM_EMPTY) {
// There was no code, so unlock always succeeds
this->setLock(false);
return true;
}
if (code.length() != codeLength) {
return false;
}
for (byte i = 0; i < code.length(); i++) {
auto digit = EEPROM.read(EEPROM_ADDR_CODE + i);
if (digit != code[i]) {
return false;
}
}
this->setLock(false);
return true;
}
void SafeState::setLock(bool locked) {
this->_locked = locked;
EEPROM.write(EEPROM_ADDR_LOCKED, locked ? SAFE_STATE_LOCKED : SAFE_STATE_OPEN);
}

View File

@@ -0,0 +1,55 @@
#pragma once
#include <Arduino.h>
#define PIN_BUZZER PIN_PC2
#define NO_TONE \
noTone(PIN_BUZZER); \
digitalWrite(PIN_BUZZER, HIGH);
class Tone
{
private:
uint32_t beep_start = 0;
uint32_t beep_end = 0;
uint32_t beep_freq = 0;
bool beeping = false;
bool isHappy = false;
public:
Tone()
{
digitalWrite(PIN_BUZZER, HIGH);
};
void beep(uint32_t how_long, uint32_t freq, bool happy = false);
void happyBeep(uint32_t how_long, uint32_t freq);
void loop();
};
void Tone::beep(uint32_t how_long, uint32_t freq, bool happy)
{
beep_start = millis();
beep_end = beep_start + how_long;
beep_freq = freq;
tone(PIN_BUZZER, beep_freq);
isHappy = happy;
beeping = true;
}
void Tone::loop()
{
if (beeping)
{
if (isHappy)
{
tone(PIN_BUZZER, beep_freq + (millis() - beep_start));
}
if (millis() > beep_end)
{
NO_TONE;
beeping = false;
}
}
}

View File

@@ -0,0 +1,326 @@
#pragma once
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include "SafeState.h"
#include "Icons.h"
#include "Light.h"
#include "Tone.h"
#define PIN_LENGTH 4
#define DELAY_LOCK 800
#define DELAY_UNLOCK 800
#define DELAY_MISMATCH 800
#define DELAY_DENIED 1600
#define DELAY_WELCOME 500
#define DELAY_UNLOCKED 1000
#define DELAY_STEPS 8
#define KEY_BEEP_MS 50
#define IDLE_TIMEOUT 8000
enum State
{
WELCOME_1,
WELCOME_2,
CHECK_LOCKED,
SAFE_LOCKED,
SAFE_LOCKED_INPUTKEY,
SAFE_LOCKED_VALIDATE,
SAFE_LOCKED_DOUNLOCK,
SAFE_LOCKED_DENIED,
SAFE_UNLOCKED,
SAFE_UNLOCKED_ASKKEY,
SAFE_UNLOCKED_INPUTKEY,
SAFE_UNLOCKED_REPEATKEY,
SAFE_UNLOCKED_MATCH,
SAFE_UNLOCKED_MISMATCH,
SAFE_UNLOCKED_DOLOCK,
WAIT_SCREEN_START,
WAIT_SCREEN_PROGRESS,
WAIT_SCREEN_NONBLOCKING,
INPUT_PIN,
};
class UI
{
private:
LiquidCrystal_I2C *lcd;
Keypad *keypad;
SafeState *safeState;
Light *light;
Tone *tone;
State state = WELCOME_1;
void setState(State _state)
{
// Serial.printf("State) { %d -> %d\n", state, _state);
state = _state;
};
uint32_t idle_since = 0;
// Nonblocking wait
uint32_t wait_from = 0;
uint32_t wait_till = 0;
uint8_t wait_step = 0;
State state_after_break;
void wait(uint32_t how_long, State state_after, bool showProgress = true);
// Nonblocking pin reading
char key[PIN_LENGTH + 1] = {0};
char pin[PIN_LENGTH + 1] = {0};
uint8_t pin_keys;
State state_after_pin;
void askpin(State state_after);
protected:
void showWaitScreen(int delayMillis);
public:
UI(LiquidCrystal_I2C *lcd, Keypad *keypad, SafeState *safeState, Light *light, Tone *tone)
: lcd(lcd), keypad(keypad), safeState(safeState), light(light), tone(tone)
{
idle_since = millis();
};
void loop();
};
void UI::loop()
{
static bool newCodeNeeded;
if (state == WELCOME_1)
{
lcd->setCursor(4, 0);
lcd->print("Welcome!");
wait(DELAY_WELCOME, WELCOME_2, false);
}
else if (state == WELCOME_2)
{
lcd->setCursor(0, 2);
String message = "Tiny1616 Safe";
for (byte i = 0; i < message.length(); i++)
{
lcd->print(message[i]);
delay(50);
}
wait(DELAY_WELCOME, CHECK_LOCKED, false);
}
else if (state == CHECK_LOCKED)
{
setState(safeState->locked() ? SAFE_LOCKED : SAFE_UNLOCKED);
}
else if (state == SAFE_LOCKED)
{
lcd->clear();
lcd->setCursor(0, 0);
lcd->write(ICON_LOCKED_CHAR);
lcd->print(" Safe Locked! ");
lcd->write(ICON_LOCKED_CHAR);
setState(SAFE_LOCKED_INPUTKEY);
}
else if (state == SAFE_UNLOCKED)
{
lcd->clear();
lcd->setCursor(0, 0);
lcd->write(ICON_UNLOCKED_CHAR);
lcd->setCursor(2, 0);
lcd->print(" # to lock");
lcd->setCursor(15, 0);
lcd->write(ICON_UNLOCKED_CHAR);
newCodeNeeded = true;
if (safeState->hasCode())
{
lcd->setCursor(0, 1);
lcd->print(" A = new code");
newCodeNeeded = false;
}
setState(SAFE_UNLOCKED_ASKKEY);
}
else if (state == SAFE_LOCKED_INPUTKEY)
{
askpin(SAFE_LOCKED_VALIDATE);
}
else if (state == SAFE_LOCKED_VALIDATE)
{
bool unlockedSuccessfully = safeState->unlock(String(pin));
wait(DELAY_UNLOCK, unlockedSuccessfully ? SAFE_LOCKED_DOUNLOCK : SAFE_LOCKED_DENIED);
}
else if (state == SAFE_LOCKED_DOUNLOCK)
{
light->unlock();
lcd->clear();
lcd->setCursor(0, 0);
lcd->write(ICON_UNLOCKED_CHAR);
lcd->setCursor(4, 0);
lcd->print("Unlocked!");
lcd->setCursor(15, 0);
lcd->write(ICON_UNLOCKED_CHAR);
wait(DELAY_UNLOCKED, SAFE_UNLOCKED, false);
}
else if (state == SAFE_LOCKED_DENIED)
{
lcd->clear();
lcd->setCursor(0, 0);
lcd->print("Access Denied!");
light->denied();
wait(DELAY_DENIED, SAFE_LOCKED);
}
else if (state == SAFE_UNLOCKED_INPUTKEY)
{
lcd->clear();
lcd->setCursor(0, 0);
lcd->print("Enter new code) {");
askpin(SAFE_UNLOCKED_REPEATKEY);
}
else if (state == SAFE_UNLOCKED_REPEATKEY)
{
strcpy(key, pin);
lcd->clear();
lcd->setCursor(0, 0);
lcd->print("Confirm new code");
askpin(SAFE_UNLOCKED_MATCH);
}
else if (state == SAFE_UNLOCKED_MATCH)
{
if (strcmp(key, pin) == 0)
{
safeState->setCode(String(pin));
setState(SAFE_UNLOCKED_DOLOCK);
}
else
{
lcd->clear();
lcd->setCursor(1, 0);
lcd->print("Code mismatch");
lcd->setCursor(0, 1);
lcd->print("Safe not locked!");
wait(DELAY_MISMATCH, SAFE_UNLOCKED);
}
}
else if (state == SAFE_UNLOCKED_DOLOCK)
{
lcd->clear();
lcd->setCursor(5, 0);
lcd->write(ICON_UNLOCKED_CHAR);
lcd->print(" ");
lcd->write(ICON_RIGHT_ARROW);
lcd->print(" ");
lcd->write(ICON_LOCKED_CHAR);
light->lock();
safeState->lock();
wait(DELAY_LOCK, CHECK_LOCKED);
}
else if (state == WAIT_SCREEN_PROGRESS)
{
uint32_t total = wait_till - wait_from;
uint32_t passed = millis() - wait_from;
uint32_t wait_step_ms = total / DELAY_STEPS;
if (passed > wait_step * wait_step_ms)
{
wait_step++;
lcd->print("#");
}
if (millis() > wait_till)
setState(state_after_break);
}
else if (state == WAIT_SCREEN_NONBLOCKING)
{
if (millis() > wait_till)
setState(state_after_break);
}
else if (state == INPUT_PIN)
{
{
char key = keypad->getKey();
if (key != NO_KEY)
idle_since = millis();
if (key >= '0' && key <= '9')
{
tone->beep(KEY_BEEP_MS, 2000);
lcd->print('*');
pin[pin_keys] = key;
pin_keys++;
}
if (pin_keys >= PIN_LENGTH)
setState(state_after_pin);
}
}
else if (state == SAFE_UNLOCKED_ASKKEY)
{
auto key = keypad->getKey();
if (key != NO_KEY)
idle_since = millis();
if (key == 'A' || key == '#')
{
tone->beep(KEY_BEEP_MS, 2000);
if (key == 'A' || newCodeNeeded)
setState(SAFE_UNLOCKED_INPUTKEY);
else
setState(SAFE_UNLOCKED_DOLOCK);
}
}
lcd->setBacklight(idle_since + IDLE_TIMEOUT > millis());
}
void UI::wait(uint32_t how_long, State state_after, bool showProgress)
{
wait_step = 0;
wait_from = millis();
wait_till = wait_from + how_long;
state_after_break = state_after;
if (showProgress)
{
lcd->setCursor(0, 1);
lcd->print(" [ ] ");
lcd->setCursor(4, 1);
setState(WAIT_SCREEN_PROGRESS);
}
else
{
setState(WAIT_SCREEN_NONBLOCKING);
}
};
void UI::askpin(State state_after)
{
// todo: 4 symbols only?
lcd->setCursor(5, 1);
lcd->print("[____]");
lcd->setCursor(6, 1);
pin_keys = 0;
state_after_pin = state_after;
setState(INPUT_PIN);
}

View File

@@ -0,0 +1,76 @@
#include <Wire.h>
/* Display */
#include <LiquidCrystal_I2C.h>
#define PIN_LCD_EN 2
#define PIN_LCD_RW 1
#define PIN_LCD_RES 0
#define PIN_LCD_LED 3
#define PIN_LCD_D4 4
#define PIN_LCD_D5 5
#define PIN_LCD_D6 6
#define PIN_LCD_D7 7
#define LCD_I2C_ADDR 0x3f
LiquidCrystal_I2C _lcd(
LCD_I2C_ADDR, PIN_LCD_EN, PIN_LCD_RW, PIN_LCD_RES,
PIN_LCD_D4, PIN_LCD_D5, PIN_LCD_D6, PIN_LCD_D7,
PIN_LCD_LED, POSITIVE);
/* Keypad setup */
#include <Keypad.h>
const byte KEYPAD_ROWS = 4;
const byte KEYPAD_COLS = 4;
byte rowPins[KEYPAD_ROWS] = {PIN_PC1, PIN_PC0, PIN_PB0, PIN_PB1};
byte colPins[KEYPAD_COLS] = {PIN_PB2, PIN_PB3, PIN_PB4, PIN_PB5};
char keys[KEYPAD_ROWS][KEYPAD_COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}};
Keypad _keypad = Keypad(makeKeymap(keys), rowPins, colPins, KEYPAD_ROWS, KEYPAD_COLS);
/* SafeState stores the secret code in EEPROM */
#include "SafeState.h"
SafeState _safeState;
#include "Tone.h"
Tone _tone;
#include "Light.h"
Light _light(&_tone);
#include "Icons.h"
Icons _icons(&_lcd);
#include "UI.h"
UI _ui(&_lcd, &_keypad, &_safeState, &_light, &_tone);
void setup()
{
#ifdef DEBUG
Serial.begin(115200);
Serial.println("Started");
#endif
Wire.swap();
_lcd.begin(16, 2);
/* Make sure the physical lock is sync with the EEPROM state */
if (_safeState.locked())
_light.lock();
else
_light.unlock();
}
void loop()
{
_ui.loop();
_tone.loop();
_light.loop();
delay(50);
}

View File

@@ -18,7 +18,7 @@ upload_protocol = jtag2updi
[env:tiny1616-serialupdi]
board = ATtiny1616
upload_speed = 57600
upload_speed = 230400
upload_flags =
--tool
uart