Initial commit

This commit is contained in:
ZacJW 2025-07-04 09:10:36 +01:00
commit 8bfbab5a0c
12 changed files with 687 additions and 0 deletions

16
.cargo/config.toml Normal file
View file

@ -0,0 +1,16 @@
[build]
target = "xtensa-esp32s3-espidf"
[target.xtensa-esp32s3-espidf]
linker = "ldproxy"
runner = "espflash flash --monitor"
rustflags = [ "--cfg", "espidf_time64"]
[unstable]
build-std = ["std", "panic_abort"]
[env]
MCU="esp32s3"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.3.2"

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/.vscode
/.embuild
/target
/Cargo.lock

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[workspace]
resolver = "2"
members = [ "cardputer-bsc",
"test-app",
]
[profile.release]
opt-level = "s"
[profile.dev]
debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"

15
cardputer-bsc/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "cardputer-bsc"
version = "0.1.0"
edition = "2021"
[lib]
harness = false
[dependencies]
display-interface = "0.5.0"
display-interface-spi = "0.5.0"
esp-idf-hal = "0.45.2"
esp-idf-sys = "0.36.1"
mipidsi = "0.9.0"
thiserror = "2.0.11"

View file

@ -0,0 +1,85 @@
//! Create and initialize ST7789 display driver
use display_interface_spi::SPIInterface;
use esp_idf_hal::{
delay::Delay,
gpio::{AnyIOPin, Output, PinDriver},
gpio::{Gpio33, Gpio34, Gpio35, Gpio36, Gpio37},
peripheral::Peripheral,
prelude::*,
spi::{config::DriverConfig, SpiAnyPins, SpiConfig, SpiDeviceDriver, SpiDriver},
};
use mipidsi::{models::ST7789, options::Orientation, Builder, options::ColorInversion, Display};
type Drawable<'a> = Display<
SPIInterface<SpiDeviceDriver<'a, SpiDriver<'a>>, PinDriver<'a, Gpio34, Output>>,
ST7789,
PinDriver<'a, Gpio33, Output>,
>;
/// Display width
pub const DISPLAY_SIZE_WIDTH: u16 = 240;
/// Display height
pub const DISPLAY_SIZE_HEIGHT: u16 = 135;
/// Create and initialize display driver
///
/// # Examples
///
/// ```
/// use embedded_graphics::pixelcolor::Rgb565;
/// use cardputer::display;
///
/// let peripherals = Peripherals::take().unwrap();
///
/// let mut display = display::build(
/// peripherals.spi2,
/// peripherals.pins.gpio36,
/// peripherals.pins.gpio35,
/// peripherals.pins.gpio37,
/// peripherals.pins.gpio34,
/// peripherals.pins.gpio33,
/// )
/// .unwrap();
/// display.clear(Rgb565::WHITE).unwrap();
/// ```
pub fn build<'a, SPI>(
spi: impl Peripheral<P = SPI> + 'a,
sck: impl Peripheral<P = Gpio36> + 'a,
dc: impl Peripheral<P = Gpio35> + 'a,
cs: impl Peripheral<P = Gpio37> + 'a,
rs: impl Peripheral<P = Gpio34> + 'a,
rst: impl Peripheral<P = Gpio33> + 'a,
) -> Result<Drawable<'a>>
where
SPI: SpiAnyPins,
{
let spi_config = SpiConfig::new().baudrate(80.MHz().into());
let device_config = DriverConfig::new();
let spi = SpiDeviceDriver::new_single(
spi,
sck,
dc,
Option::<AnyIOPin>::None,
Some(cs),
&device_config,
&spi_config,
)?;
let rs = PinDriver::output(rs)?;
let rst = PinDriver::output(rst)?;
let mut drawable = Builder::st7789(SPIInterface::new(spi, rs))
.with_invert_colors(ColorInversion::Inverted)
.with_display_size(DISPLAY_SIZE_WIDTH, DISPLAY_SIZE_HEIGHT)
.with_window_offset_handler(|_| (40, 53))
.init(&mut Delay::new_default(), Some(rst))
.map_err(|e| anyhow!("{:?}", e))?;
drawable
.set_orientation(Orientation::Landscape(true))
.map_err(|e| anyhow!("{:?}", e))?;
drawable
.set_scroll_offset(0)
.map_err(|e| anyhow!("{:?}", e))?;
Ok(drawable)
}

View file

@ -0,0 +1,500 @@
//! Keyboard scanner that converts the 74HC138 decoding results
//! into the Vector of pressed key codes.
//!
//! ### 4x14 keymap
//!
//! ```text
//! A2 A2 A0 | Y0 | Y1 | Y2 | Y3 | Y4 | Y5 | Y6
//! H - - | L | L | L | L | L | L | L
//! L - - | L | L | L | L | L | L | L
//! ----------+--------+-------+-------+-------+-------+-------+-------
//! - H H | ` 1 2 3 4 5 6 7 8 9 0 - = DEL
//! - H L | TAB q w e r t y u i o p [ ] \
//! - L H | FN SHT a s d f g h j k l ; ' ENT
//! - L L | CTL OPT ALT z x c v b n m , . / SPC
//! ```
// Adapted from https://github.com/syurazo/cardputer which is distributed under the following license
// MIT License
//
// Copyright (c) 2024 syurazo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use esp_idf_hal::{
gpio::{Gpio11, Gpio13, Gpio15, Gpio3, Gpio4, Gpio5, Gpio6, Gpio7, Gpio8, Gpio9},
gpio::{Input, Level, Output, PinDriver},
peripheral::Peripheral,
};
use esp_idf_sys::EspError;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KeyImprint {
Backquote,
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Zero,
Minus,
Equal,
Backspace,
Tab,
Q,
W,
E,
R,
T,
Y,
U,
I,
O,
P,
OpenSquareBracket,
CloseSquareBracket,
Backslash,
LeftFn,
LeftShift,
A,
S,
D,
F,
G,
H,
J,
K,
L,
SemiColon,
Quote,
Enter,
LeftCtrl,
LeftOpt,
LeftAlt,
Z,
X,
C,
V,
B,
N,
M,
Comma,
Period,
Slash,
Space,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Modified {
Graph(char),
Escape,
Enter,
Space,
Tab,
LeftCursor,
DownCursor,
UpCursor,
RightCursor,
Backspace,
Delete,
}
macro_rules! graph {
($x:expr) => {
Modified::Graph($x)
};
}
#[derive(Debug, PartialEq, Clone, Copy)]
/// Conversion rule
pub struct ConversionRule(KeyImprint, Modified, Modified);
impl ConversionRule {
/// Convert according to the state of Fn and Shift key
pub fn modified(&self, is_fn_pressed: bool, is_shift_pressed: bool) -> Modified {
match (self.0, is_fn_pressed, is_shift_pressed) {
(KeyImprint::SemiColon, true, _) => Modified::UpCursor,
(KeyImprint::Period, true, _) => Modified::DownCursor,
(KeyImprint::Slash, true, _) => Modified::RightCursor,
(KeyImprint::Comma, true, _) => Modified::LeftCursor,
(KeyImprint::Backquote, true, _) => Modified::Escape,
(KeyImprint::Backspace, true, _) => Modified::Delete,
(_, _, true) => self.2,
(_, _, _) => self.1,
}
}
/// Returns the imprint of the key assigned to the rule
pub fn imprint(&self) -> KeyImprint {
self.0
}
}
#[derive(Debug, Copy, Clone)]
/// Define the type of key as modifier key and normal key
pub enum KeyType {
Modifier(KeyImprint),
Normal(ConversionRule),
}
impl KeyType {
pub fn imprint(&self) -> KeyImprint {
match self {
KeyType::Modifier(x) => *x,
KeyType::Normal(x) => x.imprint(),
}
}
}
macro_rules! normal {
($x:expr,$y:expr,$z:expr) => {
KeyType::Normal(ConversionRule($x, $y, $z))
};
}
const COLUMN_MAP: [[usize; 7]; 2] = [[1, 3, 5, 7, 9, 11, 13], [0, 2, 4, 6, 8, 10, 12]];
const KEY_MAP: [[KeyType; 14]; 4] = [
[
KeyType::Modifier(KeyImprint::LeftCtrl),
KeyType::Modifier(KeyImprint::LeftOpt),
KeyType::Modifier(KeyImprint::LeftAlt),
normal!(KeyImprint::Z, graph!('z'), graph!('Z')),
normal!(KeyImprint::X, graph!('x'), graph!('X')),
normal!(KeyImprint::C, graph!('c'), graph!('C')),
normal!(KeyImprint::V, graph!('v'), graph!('V')),
normal!(KeyImprint::B, graph!('b'), graph!('B')),
normal!(KeyImprint::N, graph!('n'), graph!('N')),
normal!(KeyImprint::M, graph!('m'), graph!('M')),
normal!(KeyImprint::Comma, graph!(','), graph!('<')),
normal!(KeyImprint::Period, graph!('.'), graph!('>')),
normal!(KeyImprint::Slash, graph!('/'), graph!('?')),
normal!(KeyImprint::Space, Modified::Space, Modified::Space),
],
[
KeyType::Modifier(KeyImprint::LeftFn),
KeyType::Modifier(KeyImprint::LeftShift),
normal!(KeyImprint::A, graph!('a'), graph!('A')),
normal!(KeyImprint::S, graph!('s'), graph!('S')),
normal!(KeyImprint::D, graph!('d'), graph!('D')),
normal!(KeyImprint::F, graph!('f'), graph!('F')),
normal!(KeyImprint::G, graph!('g'), graph!('G')),
normal!(KeyImprint::H, graph!('h'), graph!('H')),
normal!(KeyImprint::J, graph!('j'), graph!('J')),
normal!(KeyImprint::K, graph!('k'), graph!('K')),
normal!(KeyImprint::L, graph!('l'), graph!('L')),
normal!(KeyImprint::SemiColon, graph!(';'), graph!(':')),
normal!(KeyImprint::Quote, graph!('\''), graph!('"')),
normal!(KeyImprint::Enter, Modified::Enter, Modified::Enter),
],
[
normal!(KeyImprint::Tab, Modified::Tab, Modified::Tab),
normal!(KeyImprint::Q, graph!('q'), graph!('Q')),
normal!(KeyImprint::W, graph!('w'), graph!('W')),
normal!(KeyImprint::E, graph!('e'), graph!('E')),
normal!(KeyImprint::R, graph!('r'), graph!('R')),
normal!(KeyImprint::T, graph!('t'), graph!('T')),
normal!(KeyImprint::Y, graph!('y'), graph!('Y')),
normal!(KeyImprint::U, graph!('u'), graph!('U')),
normal!(KeyImprint::I, graph!('i'), graph!('I')),
normal!(KeyImprint::O, graph!('o'), graph!('O')),
normal!(KeyImprint::P, graph!('p'), graph!('P')),
normal!(KeyImprint::OpenSquareBracket, graph!('['), graph!('{')),
normal!(KeyImprint::CloseSquareBracket, graph!(']'), graph!('}')),
normal!(KeyImprint::Backslash, graph!('\\'), graph!('|')),
],
[
normal!(KeyImprint::Backquote, graph!('`'), graph!('~')),
normal!(KeyImprint::One, graph!('1'), graph!('!')),
normal!(KeyImprint::Two, graph!('2'), graph!('@')),
normal!(KeyImprint::Three, graph!('3'), graph!('#')),
normal!(KeyImprint::Four, graph!('4'), graph!('$')),
normal!(KeyImprint::Five, graph!('5'), graph!('%')),
normal!(KeyImprint::Six, graph!('6'), graph!('^')),
normal!(KeyImprint::Seven, graph!('7'), graph!('&')),
normal!(KeyImprint::Eight, graph!('8'), graph!('*')),
normal!(KeyImprint::Nine, graph!('9'), graph!('(')),
normal!(KeyImprint::Zero, graph!('0'), graph!(')')),
normal!(KeyImprint::Minus, graph!('-'), graph!('_')),
normal!(KeyImprint::Equal, graph!('='), graph!('+')),
normal!(
KeyImprint::Backspace,
Modified::Backspace,
Modified::Backspace
),
],
];
macro_rules! pin_level {
($x:expr) => {
match $x {
0 => Level::Low,
_ => Level::High,
}
};
}
/// Keyboard scanner trait
pub trait KeyboardScanner {
type Error: std::error::Error;
/// Scan the keyboard and return the Vector of KeyType.
fn scan_pressed_keytypes(&mut self) -> Result<Vec<KeyType>, Self::Error>;
}
/// Keyboard scanner for Cardputer
///
/// # Examples
///
/// ```
/// use cardputer::keyboard::{Keyboard, KeyImprint};
///
/// let peripherals = Peripherals::take().unwrap();
///
/// let mut keyboard = Keyboard::new(
/// peripherals.pins.gpio8,
/// peripherals.pins.gpio9,
/// peripherals.pins.gpio11,
/// peripherals.pins.gpio13,
/// peripherals.pins.gpio15,
/// peripherals.pins.gpio3,
/// peripherals.pins.gpio4,
/// peripherals.pins.gpio5,
/// peripherals.pins.gpio6,
/// peripherals.pins.gpio7,
/// )
/// .unwrap();
/// let keys: Vec<KeyImprint> = keyboard.scan_pressed_keys().unwrap();
/// ```
pub struct Keyboard<'a> {
addr0: PinDriver<'a, Gpio8, Output>,
addr1: PinDriver<'a, Gpio9, Output>,
addr2: PinDriver<'a, Gpio11, Output>,
y0: PinDriver<'a, Gpio13, Input>,
y1: PinDriver<'a, Gpio15, Input>,
y2: PinDriver<'a, Gpio3, Input>,
y3: PinDriver<'a, Gpio4, Input>,
y4: PinDriver<'a, Gpio5, Input>,
y5: PinDriver<'a, Gpio6, Input>,
y6: PinDriver<'a, Gpio7, Input>,
}
#[derive(Debug, Error)]
pub enum KeyboardError {
#[error("Failed to setup pin driver: {0}")]
PinDriverError(EspError),
#[error("Failed to set pin level: {0}")]
SetLevelError(EspError),
}
impl<'a> Keyboard<'a> {
/// Create new scanner.
#[allow(clippy::too_many_arguments)]
pub fn new(
a0: impl Peripheral<P = Gpio8> + 'a,
a1: impl Peripheral<P = Gpio9> + 'a,
a2: impl Peripheral<P = Gpio11> + 'a,
y0: impl Peripheral<P = Gpio13> + 'a,
y1: impl Peripheral<P = Gpio15> + 'a,
y2: impl Peripheral<P = Gpio3> + 'a,
y3: impl Peripheral<P = Gpio4> + 'a,
y4: impl Peripheral<P = Gpio5> + 'a,
y5: impl Peripheral<P = Gpio6> + 'a,
y6: impl Peripheral<P = Gpio7> + 'a,
) -> Result<Self, KeyboardError> {
Ok(Self {
addr0: PinDriver::output(a0).map_err(KeyboardError::PinDriverError)?,
addr1: PinDriver::output(a1).map_err(KeyboardError::PinDriverError)?,
addr2: PinDriver::output(a2).map_err(KeyboardError::PinDriverError)?,
y0: PinDriver::input(y0).map_err(KeyboardError::PinDriverError)?,
y1: PinDriver::input(y1).map_err(KeyboardError::PinDriverError)?,
y2: PinDriver::input(y2).map_err(KeyboardError::PinDriverError)?,
y3: PinDriver::input(y3).map_err(KeyboardError::PinDriverError)?,
y4: PinDriver::input(y4).map_err(KeyboardError::PinDriverError)?,
y5: PinDriver::input(y5).map_err(KeyboardError::PinDriverError)?,
y6: PinDriver::input(y6).map_err(KeyboardError::PinDriverError)?,
})
}
/// Scan the keyboard and return the Vector of KeyImprint.
pub fn scan_pressed_keys(&mut self) -> Result<Vec<KeyImprint>, KeyboardError> {
let keys = self
.scan_pressed_keytypes()?
.iter()
.map(|x| x.imprint())
.collect();
Ok(keys)
}
}
impl KeyboardScanner for Keyboard<'_> {
type Error = KeyboardError;
fn scan_pressed_keytypes(&mut self) -> Result<Vec<KeyType>, Self::Error> {
let mut keys: Vec<KeyType> = vec![];
for i in 0..8 {
self.addr0.set_level(pin_level!(i & 0b00000001)).map_err(KeyboardError::SetLevelError)?;
self.addr1.set_level(pin_level!(i & 0b00000010)).map_err(KeyboardError::SetLevelError)?;
self.addr2.set_level(pin_level!(i & 0b00000100)).map_err(KeyboardError::SetLevelError)?;
let inputs: [Level; 7] = [
self.y0.get_level(),
self.y1.get_level(),
self.y2.get_level(),
self.y3.get_level(),
self.y4.get_level(),
self.y5.get_level(),
self.y6.get_level(),
];
for (j, decoded) in inputs.iter().enumerate() {
if *decoded == Level::High {
continue;
}
let (col, row) = if i < 4 {
(COLUMN_MAP[0][j], i)
} else {
(COLUMN_MAP[1][j], i - 4)
};
keys.push(KEY_MAP[row][col]);
}
}
Ok(keys)
}
}
/// Structure that scans the keyboard and keeps track of state changes
///
/// # Examples
///
/// ```
/// use cardputer::keyboard::{Keyboard, KeyboardState};
///
/// let peripherals = Peripherals::take().unwrap();
///
/// let mut keyboard = Keyboard::new(
/// peripherals.pins.gpio8,
/// peripherals.pins.gpio9,
/// peripherals.pins.gpio11,
/// peripherals.pins.gpio13,
/// peripherals.pins.gpio15,
/// peripherals.pins.gpio3,
/// peripherals.pins.gpio4,
/// peripherals.pins.gpio5,
/// peripherals.pins.gpio6,
/// peripherals.pins.gpio7,
/// )
/// .unwrap();
///
/// let mut keyboard_state = KeyboardState::default();
/// keyboard_state.update(&mut keyboard).unwrap();
/// log::info!("{:?}", keyboard_state.pressed_keys());
/// log::info!("{:?}", keyboard_state.released_keys());
/// ```
#[derive(Debug, Default)]
pub struct KeyboardState {
is_fn_pressed: bool,
is_ctrl_pressed: bool,
is_shift_pressed: bool,
is_alt_pressed: bool,
hold_keys: Vec<ConversionRule>,
pressed_keys: Vec<ConversionRule>,
released_keys: Vec<ConversionRule>,
}
impl KeyboardState {
/// Get the latest key state and update the Pressed/Released state
pub fn update(&mut self, keyboard: &mut impl KeyboardScanner<Error = KeyboardError>) -> Result<(), KeyboardError> {
let mut new_hold_keys: Vec<ConversionRule> = Vec::new();
self.pressed_keys.clear();
self.released_keys.clear();
self.is_fn_pressed = false;
self.is_ctrl_pressed = false;
self.is_shift_pressed = false;
self.is_alt_pressed = false;
for pressed in keyboard.scan_pressed_keytypes()?.iter() {
match pressed {
KeyType::Modifier(KeyImprint::LeftFn) => self.is_fn_pressed = true,
KeyType::Modifier(KeyImprint::LeftCtrl) => self.is_ctrl_pressed = true,
KeyType::Modifier(KeyImprint::LeftShift) => self.is_shift_pressed = true,
KeyType::Modifier(KeyImprint::LeftAlt) => self.is_alt_pressed = true,
KeyType::Normal(h) => {
new_hold_keys.push(*h);
if !self.hold_keys.contains(&h) {
self.pressed_keys.push(*h);
}
}
_ => {}
}
}
for key in self.hold_keys.iter() {
if !new_hold_keys.contains(key) {
self.released_keys.push(*key);
}
}
self.hold_keys = new_hold_keys;
Ok(())
}
pub fn pressed_keys(&self) -> Vec<Modified> {
self.pressed_keys
.iter()
.map(|x| x.modified(self.is_fn_pressed, self.is_shift_pressed))
.collect()
}
pub fn released_keys(&self) -> Vec<Modified> {
self.released_keys
.iter()
.map(|x| x.modified(self.is_fn_pressed, self.is_shift_pressed))
.collect()
}
pub fn hold_keys(&self) -> Vec<Modified> {
self.hold_keys
.iter()
.map(|x| x.modified(self.is_fn_pressed, self.is_shift_pressed))
.collect()
}
pub fn is_fn_pressed(&self) -> bool {
self.is_fn_pressed
}
pub fn is_ctrl_pressed(&self) -> bool {
self.is_ctrl_pressed
}
pub fn is_shift_pressed(&self) -> bool {
self.is_shift_pressed
}
pub fn is_alt_pressed(&self) -> bool {
self.is_alt_pressed
}
}

5
cardputer-bsc/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod keyboard;
pub mod display;
#[cfg(test)]
fn main() {}

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "esp"

23
test-app/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "test-app"
version = "0.1.0"
authors = ["ZacJW <zac@zacjw.com>"]
edition = "2021"
resolver = "2"
rust-version = "1.77"
[[bin]]
name = "test-app"
harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors
[features]
default = []
experimental = ["esp-idf-svc/experimental"]
[dependencies]
log = "0.4"
esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync"] }
[build-dependencies]
embuild = "0.33"

3
test-app/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
embuild::espidf::sysenv::output();
}

View file

@ -0,0 +1,10 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granularity for thread sleeps (10 ms by default).
#CONFIG_FREERTOS_HZ=1000
# Workaround for https://github.com/espressif/esp-idf/issues/7631
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n

10
test-app/src/main.rs Normal file
View file

@ -0,0 +1,10 @@
fn main() {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
log::info!("Hello, world!");
}