Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e22d4fb9e2
|
||
|
|
7240773581
|
||
|
|
9b604aab4e
|
||
|
|
8a2fe2a18b
|
||
|
|
6839a46358
|
||
|
|
4e2992f244
|
||
|
|
09d2c5760a
|
||
|
|
55e921712f
|
||
|
|
ad3eb59d58
|
||
|
|
202b9e49f7
|
||
|
|
17a228faa7
|
||
|
|
2e28f71d48
|
||
|
|
d0d82ee8fb
|
||
|
|
ef7866a29f
|
||
|
|
ed53611e0f
|
||
|
|
4587e1d1ab
|
||
|
|
2b92a6832d
|
||
|
|
d7cd0e9cfa
|
||
|
|
6db58e14a7
|
||
|
|
1a2cc21fbb
|
||
|
|
bdb344752f
|
||
|
|
12f9d2ec2c
|
||
|
|
7299521ab1
|
||
|
|
8df7e693bf
|
||
|
|
0e1e40254b
|
||
|
|
daf220600e
|
||
|
|
465a17789d
|
@@ -3,3 +3,4 @@ cmake-build-debug
|
||||
build/
|
||||
.idea/
|
||||
sdkconfig.old
|
||||
.~lock*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Hello World Example
|
||||
# Breadflow
|
||||
|
||||
Starts a FreeRTOS task to print "Hello World"
|
||||
is a toaster oven mod with a thermistor, SSR and ESP32.
|
||||
|
||||
See the README.md file in the upper level 'examples' directory for more information about examples.
|
||||
It can bake bread or reflow PCBs.
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,56 @@
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
|
||||
fn main() {
|
||||
for entry in fs::read_dir(".").unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if !path.display().to_string().ends_with(".ico") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = path.file_stem().unwrap().to_str().unwrap();
|
||||
|
||||
Command::new("/bin/convert").arg(&path).arg("./tmp.xbm").output().unwrap();
|
||||
|
||||
let content = fs::read_to_string("./tmp.xbm").unwrap();
|
||||
|
||||
let a = content.find('{').unwrap() + 1;
|
||||
let b = content.find('}').unwrap();
|
||||
|
||||
let bytes = &content[a..b].trim();
|
||||
|
||||
let bytes = bytes.split(",")
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| u8::from_str_radix(&s[2..], 16).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(bytes.len() == 8);
|
||||
|
||||
let mut columns : [u8; 5] = [0,0,0,0,0];
|
||||
|
||||
for (n, r) in bytes.iter().enumerate() {
|
||||
for i in 0..5 {
|
||||
if 0 != *r & (1<<i) {
|
||||
columns[i] |= 1 << n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testchar(c : char) -> bool {
|
||||
c.is_alphanumeric() || c=='_'
|
||||
}
|
||||
|
||||
let mut name = name.replace(|c:char|!testchar(c), "_");
|
||||
if !name.starts_with(testchar) {
|
||||
name = format!("_{}", name);
|
||||
}
|
||||
|
||||
println!("const uint8_t {}[5] = {{ {} }};", name, columns.iter().map(|c| format!("{:#04x}", !c)).collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
|
||||
let _ = fs::remove_file("./tmp.xbm");
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,124 @@
|
||||
#include "bitmaps.h"
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
/* 21x22: no_flame */
|
||||
// ········██····█······
|
||||
// ·········██···██·····
|
||||
// ··········██·········
|
||||
// ··········███········
|
||||
// ·····█····███···█····
|
||||
// ···██····████··███···
|
||||
// ··██···█████··███····
|
||||
// ··█···█████··███·····
|
||||
// ·██··█████··███······
|
||||
// ·█··█████··███·······
|
||||
// ·█··████··███··█·····
|
||||
// ····███··███··███····
|
||||
// ·····█··███···███··█·
|
||||
// ·······███··█████··██
|
||||
// ·█····███··███████··█
|
||||
// █····███···███████···
|
||||
// ██··███··█··██████···
|
||||
// █··███··██···█████···
|
||||
// ··███··███···█████···
|
||||
// ·███··██·····█·███···
|
||||
// ███··█·········██····
|
||||
// ·█··███·······██·····
|
||||
static const uint8_t G_NO_FLAME_BYTES[] = { 0x00, 0x00, 0xc0, 0x60, 0x20, 0x10, 0x80, 0xc0, 0xc1, 0xe3, 0xfe, 0x7c, 0x38, 0x80, 0xc3, 0xe2, 0x70, 0x20, 0x00, 0x00, 0x00, 0x80, 0x47, 0x01, 0x00, 0x0e, 0x9f, 0xcf, 0xe7, 0x73, 0x39, 0x1c, 0xce, 0xe7, 0xe3, 0xf9, 0xfc, 0xf8, 0xc0, 0x00, 0x30, 0x60, 0x13, 0x39, 0x1c, 0x0e, 0x27, 0x33, 0x29, 0x0c, 0x06, 0x07, 0x00, 0x00, 0x01, 0x0f, 0x27, 0x3f, 0x1f, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
/* 21x22: flame */
|
||||
// ········██····█······
|
||||
// ·········██···██·····
|
||||
// ··········██·········
|
||||
// ··········███········
|
||||
// ·····█····███········
|
||||
// ···██····█████···█···
|
||||
// ··██···███████··█····
|
||||
// ··█···███████··██····
|
||||
// ·██··████████·██·····
|
||||
// ·█··████████··██·····
|
||||
// ·█··████████·███·····
|
||||
// ····████████·████····
|
||||
// ·····████·███·███··█·
|
||||
// ······███·███████··██
|
||||
// ·█·····██··███████··█
|
||||
// █·······██·███████···
|
||||
// ██·····███··██████···
|
||||
// █████·████···█████···
|
||||
// ·█████████···█████···
|
||||
// ··██████·····█·███···
|
||||
// ···███·········██····
|
||||
// ····███·······██·····
|
||||
static const uint8_t G_FLAME_BYTES[] = { 0x00, 0x00, 0xc0, 0x60, 0x20, 0x10, 0x80, 0xc0, 0xc1, 0xe3, 0xfe, 0xfc, 0xf8, 0x60, 0x03, 0x82, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x80, 0x47, 0x01, 0x00, 0x0e, 0x1f, 0x3f, 0x7f, 0xff, 0x8f, 0x3f, 0xff, 0xf1, 0xec, 0xff, 0xff, 0xf8, 0xc0, 0x00, 0x30, 0x60, 0x03, 0x07, 0x0e, 0x1e, 0x3e, 0x3c, 0x2e, 0x0f, 0x07, 0x07, 0x00, 0x00, 0x01, 0x0f, 0x27, 0x3f, 0x1f, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
/* 84x48: boot_logo */
|
||||
// ····················································································
|
||||
// ·····································································████████·······
|
||||
// ························································█████████████····█···██·····
|
||||
// ···················█·······························█████····█·███·····███······█····
|
||||
// ······█·█····································██████···██·█████··███████·········█···
|
||||
// ·······█······························███████····██·█████·█·······███··········█····
|
||||
// ······█·█······················███████·····███·███████··██···················██·····
|
||||
// ···························██████·██·███·███·███████████·████·············███·······
|
||||
// ······················████████████·██··███··█████████·█████·············██·██·······
|
||||
// ··················█████·██·████···█·████···███████·███·············█████···██·······
|
||||
// ·············███████████·█████████████···██·······█············█████··█·█·█·██······
|
||||
// ············██████████·██████████··███████·················█████··█·██·█···███······
|
||||
// ·········██████··█··████·██·███···████·█···············████···█··███··█······██·····
|
||||
// ······███·████████████████████··██················█████··█···█·█·█···········█·█····
|
||||
// ···███████····················██··············████·█····█·····█················█····
|
||||
// ····██··························█·············█···█····██······················█····
|
||||
// ··██·····························█·······█·█·█···█····██·······················█····
|
||||
// ·█······························█·······█·█·█···█·█····························█····
|
||||
// ·█····························██·······█·█·█·····█····················█········█····
|
||||
// ··██······················████·█·█·█··█·······························█······█··█···
|
||||
// ····████···················█··█·█·█··█································█······█··█···
|
||||
// ·······█···················██·█·██··························█·········█······█··█···
|
||||
// ·······█····················█·██······█·····················█··········█·····█··█···
|
||||
// ·······█····················█·█········█··········█··········█·········█·····███····
|
||||
// ········█···················█··········█···········█·········█·····██████████·······
|
||||
// ········█····················█·········█············█·········█·███·················
|
||||
// ········█····················█··········█···········█·······████····················
|
||||
// ········█·····················█·········█············█·█████························
|
||||
// ········█······················█·········█·········████·····························
|
||||
// ········█·······················█········█····█████···██················█·█·········
|
||||
// ·········█······················█·········█·██···························█··········
|
||||
// ·········█······················█·······████····························█·█·········
|
||||
// ·········█······················█····███·····················█······················
|
||||
// ·········█·······················████·······················███·····················
|
||||
// ·········█·····················███···························█······················
|
||||
// ·········█········██████████████····················································
|
||||
// ··········█·██████·····························································█····
|
||||
// ···········█····························································█······█····
|
||||
// ··························································█············█·······█····
|
||||
// ··████·································█··················█······██····█··█····█····
|
||||
// ··█···█···██████···█████████····█·····███████···███████···█·····█··█····█·██····█···
|
||||
// ··█···█··██·····█·█·█··········█·█······█····█·█·█·······█·····█····█···█··█····█···
|
||||
// ··████····█·····██··█··········█·█······█····█···██████··█·····█····█···█··█····█···
|
||||
// ··█···█···██████··███████·····█···█·····█···█··███·······█·····█···█·····█·██···█···
|
||||
// ··█····█··██········█········███████····█··█·····█·······█······█·█······█·█·█·█····
|
||||
// ··█···█···█·██······█·····██··█····█··█████······█········█████··█········█···█·····
|
||||
// ██████····█···███····█████···█·····██············█··································
|
||||
// ··········█·····█···································································
|
||||
static const uint8_t G_BOOT_LOGO_BYTES[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x20, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0xc0, 0xc0, 0x40, 0xc0, 0xc0, 0x40, 0xc0, 0xa0, 0xa0, 0x20, 0xa0, 0xa0, 0xe0, 0x60, 0xd0, 0x90, 0xd0, 0xd0, 0xf0, 0xf0, 0xc8, 0xe8, 0xe8, 0xb8, 0xb8, 0x64, 0xd4, 0xb4, 0x94, 0x9c, 0x14, 0x0c, 0x0c, 0x1c, 0x14, 0x34, 0x34, 0x34, 0x12, 0x1a, 0x0a, 0x0a, 0x06, 0x82, 0x82, 0x82, 0x44, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xc0, 0xc0, 0x60, 0x60, 0x60, 0x50, 0x30, 0x30, 0x38, 0x3c, 0x3c, 0x2c, 0x2c, 0x3c, 0x2e, 0x2e, 0x3e, 0x3e, 0x37, 0x3d, 0x2b, 0x3f, 0x3d, 0x2f, 0x3f, 0x3f, 0x5f, 0x4d, 0xad, 0x25, 0x16, 0x1d, 0x1f, 0x1e, 0x0a, 0x1b, 0x09, 0x0d, 0x04, 0x02, 0x03, 0x03, 0xc3, 0x43, 0x43, 0x43, 0xa5, 0x63, 0x23, 0x22, 0x21, 0x91, 0xd1, 0x31, 0x11, 0x08, 0x08, 0x28, 0x58, 0x2c, 0x04, 0x34, 0x1c, 0x16, 0x0a, 0x0a, 0x16, 0x0a, 0x05, 0x01, 0x04, 0x0b, 0x0f, 0x3c, 0x10, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x09, 0x09, 0x10, 0x10, 0x10, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x38, 0xe8, 0x08, 0xf4, 0x4c, 0x32, 0x29, 0x10, 0x08, 0x00, 0x10, 0x48, 0x84, 0x02, 0x05, 0x02, 0x05, 0x02, 0x01, 0x00, 0x00, 0x02, 0x05, 0x82, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x60, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x80, 0x87, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x08, 0x10, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8c, 0xb0, 0xc0, 0x80, 0x40, 0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x11, 0x16, 0x18, 0x30, 0x28, 0x08, 0x08, 0x08, 0x08, 0x04, 0x05, 0x06, 0x04, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0xa1, 0x41, 0xa1, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x0f, 0x10, 0x20, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0c, 0x05, 0x06, 0x02, 0x02, 0x02, 0x01, 0x01, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x07, 0x02, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x7f, 0x44, 0x44, 0x44, 0x2b, 0x10, 0x00, 0x02, 0xff, 0x19, 0x29, 0x29, 0x49, 0x49, 0xc6, 0x04, 0x0a, 0x09, 0x3f, 0x49, 0x49, 0x49, 0x49, 0x41, 0x21, 0x21, 0x00, 0x50, 0x38, 0x16, 0x11, 0x16, 0x18, 0x70, 0x40, 0x00, 0x21, 0x21, 0x3f, 0x21, 0x21, 0x11, 0x09, 0x06, 0x00, 0x0a, 0x09, 0x7f, 0x05, 0x05, 0x05, 0x05, 0x05, 0x00, 0x00, 0x1e, 0x21, 0x20, 0x20, 0x20, 0x20, 0x0e, 0x11, 0x20, 0x10, 0x09, 0x06, 0x00, 0x00, 0x00, 0x07, 0x18, 0x21, 0x1f, 0x08, 0x10, 0x20, 0x10, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
static const struct BitmapImage bitmaps[] = {
|
||||
{ .name="no_flame", .width=21, .height=22, .bytes=G_NO_FLAME_BYTES },
|
||||
{ .name="flame", .width=21, .height=22, .bytes=G_FLAME_BYTES },
|
||||
{ .name="boot_logo", .width=84, .height=48, .bytes=G_BOOT_LOGO_BYTES },
|
||||
{}
|
||||
};
|
||||
|
||||
const struct BitmapImage *Bitmap_Get(const char *name) {
|
||||
const struct BitmapImage *ptr = &bitmaps[0];
|
||||
while (ptr->name) {
|
||||
if (0 == strcmp(ptr->name, name)) {
|
||||
return ptr;
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,172 @@
|
||||
use std::fs::{self,OpenOptions};
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
let mut c_constants = Vec::new();
|
||||
let mut c_table = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(".").unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if !path.display().to_string().ends_with(".ico") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = path.file_stem().unwrap().to_str().unwrap();
|
||||
|
||||
Command::new("/bin/convert").arg(&path).arg("./tmp.xbm").output().unwrap();
|
||||
|
||||
let content = fs::read_to_string("./tmp.xbm").unwrap();
|
||||
|
||||
println!("\n------------------------------------------------------------------------\n");
|
||||
|
||||
println!("{}", content);
|
||||
|
||||
let mut lines = content.lines();
|
||||
let l1 = lines.nth(0).unwrap();
|
||||
let l2 = lines.nth(0).unwrap();
|
||||
|
||||
let w : usize = l1[l1.find("width").unwrap()+6..].parse().unwrap();
|
||||
let h : usize = l2[l2.find("height").unwrap()+7..].parse().unwrap();
|
||||
|
||||
let a = content.find('{').unwrap() + 1;
|
||||
let b = content.find('}').unwrap();
|
||||
let bytes = &content[a..b].trim();
|
||||
|
||||
let bytes = bytes.split(",")
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| ! u8::from_str_radix(&s[2..], 16).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
print!("---SOURCE--\n");
|
||||
for y in 0..h {
|
||||
for x in 0..(w-1)/8+1 {
|
||||
for xx in 0..8 {
|
||||
if (bytes[y*(((w-1)/8+1)) + x] & (1u8 << xx)) != 0 {
|
||||
print!("█");
|
||||
} else {
|
||||
print!("·");
|
||||
}
|
||||
}
|
||||
}
|
||||
print!("\n");
|
||||
}
|
||||
println!("---\n");
|
||||
|
||||
println!("name {},\nw {} h {},\nbytes {}", name, w, h, bytes.iter().map(|c| format!("{:#04x}", c)).collect::<Vec<_>>().join(", "));
|
||||
|
||||
let bit_xy = |x : usize, y : usize| {
|
||||
let xx = x / 8;
|
||||
let nthbit = x % 8;
|
||||
let n = y*((w-1)/8+1) + xx;
|
||||
if n >= bytes.len() {
|
||||
0u8
|
||||
} else {
|
||||
let r = ((bytes[n] & (1 << nthbit)) >> nthbit) as u8;
|
||||
r
|
||||
}
|
||||
};
|
||||
|
||||
let result_cols = w;
|
||||
let result_rows = ((h-1)/8)+1;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for y in 0..result_rows {
|
||||
for x in 0..result_cols {
|
||||
let mut buf = 0u8;
|
||||
for yy in 0..8 {
|
||||
buf |= bit_xy(x, y * 8 + yy) << yy;
|
||||
}
|
||||
result.push(buf);
|
||||
}
|
||||
}
|
||||
|
||||
//println!("final bytes {}", result.iter().map(|c| format!("{:#04x}", c)).collect::<Vec<_>>().join(", "));
|
||||
|
||||
let mut render = String::new();
|
||||
print!("---\n");
|
||||
for y in 0..result_rows {
|
||||
for yy in 0..8 {
|
||||
if y*8+yy >= h {
|
||||
break;
|
||||
}
|
||||
render.push_str("// ");
|
||||
for x in 0..result_cols {
|
||||
if (result[y*result_cols + x] & (1u8 << yy)) != 0 {
|
||||
print!("█");
|
||||
render.push_str("█");
|
||||
} else {
|
||||
print!("·");
|
||||
render.push_str("·");
|
||||
}
|
||||
}
|
||||
if yy == 7 {
|
||||
print!("/ end of byte row");
|
||||
}
|
||||
render.push_str("\n");
|
||||
print!("\n");
|
||||
}
|
||||
}
|
||||
println!("---\n");
|
||||
|
||||
fn testchar(c : char) -> bool {
|
||||
c.is_alphanumeric() || c=='_'
|
||||
}
|
||||
|
||||
let mut name = name.replace(|c:char|!testchar(c), "_");
|
||||
if !name.starts_with(testchar) {
|
||||
name = format!("_{}", name);
|
||||
}
|
||||
|
||||
c_constants.push(format!("\n/* {}x{}: {} */\n{}static const uint8_t G_{}_BYTES[] = {{ {} }};",
|
||||
w,h,
|
||||
name,
|
||||
render,
|
||||
name.to_uppercase(),
|
||||
result.iter().map(|c| format!("{:#04x}", c)).collect::<Vec<_>>().join(", ")
|
||||
));
|
||||
|
||||
c_table.push(format!("{{ .name=\"{}\", .width={}, .height={}, .bytes=G_{}_BYTES }}",
|
||||
name,
|
||||
w, h,
|
||||
name.to_uppercase()
|
||||
));
|
||||
}
|
||||
|
||||
let c_source = format!(r##"#include "bitmaps.h"
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
{}
|
||||
|
||||
static const struct BitmapImage bitmaps[] = {{
|
||||
{},
|
||||
{{}}
|
||||
}};
|
||||
|
||||
const struct BitmapImage *Bitmap_Get(const char *name) {{
|
||||
const struct BitmapImage *ptr = &bitmaps[0];
|
||||
while (ptr->name) {{
|
||||
if (0 == strcmp(ptr->name, name)) {{
|
||||
return ptr;
|
||||
}}
|
||||
ptr++;
|
||||
}}
|
||||
return NULL;
|
||||
}}
|
||||
"##,
|
||||
c_constants.join("\n"),
|
||||
c_table.join(",\n ")
|
||||
);
|
||||
|
||||
let mut f = OpenOptions::new().write(true).create(true).truncate(true).open("./bitmaps.c").unwrap();
|
||||
f.write(c_source.as_bytes()).unwrap();
|
||||
|
||||
let _ = fs::remove_file("./tmp.xbm");
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,8 @@
|
||||
set(COMPONENT_ADD_INCLUDEDIRS include)
|
||||
|
||||
set(COMPONENT_SRCDIRS
|
||||
"src")
|
||||
|
||||
#set(COMPONENT_REQUIRES)
|
||||
|
||||
register_component()
|
||||
@@ -0,0 +1,2 @@
|
||||
General purpose, mostly platofrm-idependent utilities
|
||||
that may be used by other components.
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
COMPONENT_SRCDIRS := src
|
||||
COMPONENT_ADD_INCLUDEDIRS := include
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
||||
*/
|
||||
|
||||
#ifndef BASE16_H_
|
||||
#define BASE16_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* Calculate length of base16-encoded data
|
||||
* @param raw_len Raw data length
|
||||
* @return Encoded string length (excluding NUL)
|
||||
*/
|
||||
static inline size_t base16_encoded_len(size_t raw_len) {
|
||||
return (2 * raw_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum length of base16-decoded string
|
||||
* @param encoded Encoded string
|
||||
* @return Maximum length of raw data
|
||||
*/
|
||||
static inline size_t base16_decoded_max_len(const char *encoded) {
|
||||
return ((strlen(encoded) + 1) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base16-encode data
|
||||
*
|
||||
* The buffer must be the correct length for the encoded string. Use
|
||||
* something like
|
||||
*
|
||||
* char buf[ base16_encoded_len ( len ) + 1 ];
|
||||
*
|
||||
* (the +1 is for the terminating NUL) to provide a buffer of the
|
||||
* correct size.
|
||||
*
|
||||
* @param raw Raw data
|
||||
* @param len Length of raw data
|
||||
* @param encoded Buffer for encoded string
|
||||
*/
|
||||
extern void base16_encode(uint8_t *raw, size_t len, char *encoded);
|
||||
|
||||
/**
|
||||
* Base16-decode data
|
||||
*
|
||||
* The buffer must be large enough to contain the decoded data. Use
|
||||
* something like
|
||||
*
|
||||
* char buf[ base16_decoded_max_len ( encoded ) ];
|
||||
*
|
||||
* to provide a buffer of the correct size.
|
||||
* @param encoded Encoded string
|
||||
* @param raw Raw data
|
||||
* @return Length of raw data, or negative error
|
||||
*/
|
||||
extern int base16_decode(const char *encoded, uint8_t *raw);
|
||||
|
||||
#endif /* BASE16_H_ */
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2019/09/13.
|
||||
*/
|
||||
|
||||
#ifndef CSPEMU_DATETIME_H
|
||||
#define CSPEMU_DATETIME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum weekday {
|
||||
MONDAY = 1,
|
||||
TUESDAY,
|
||||
WEDNESDAY,
|
||||
THURSDAY,
|
||||
FRIDAY,
|
||||
SATURDAY,
|
||||
SUNDAY
|
||||
};
|
||||
_Static_assert(MONDAY==1, "enum weekday numbering Mon");
|
||||
_Static_assert(SUNDAY==7, "enum weekday numbering Sun");
|
||||
|
||||
enum month {
|
||||
JANUARY = 1,
|
||||
FEBRUARY,
|
||||
MARCH,
|
||||
APRIL,
|
||||
MAY,
|
||||
JUNE,
|
||||
JULY,
|
||||
AUGUST,
|
||||
SEPTEMBER,
|
||||
OCTOBER,
|
||||
NOVEMBER,
|
||||
DECEMBER
|
||||
};
|
||||
_Static_assert(JANUARY==1, "enum month numbering Jan");
|
||||
_Static_assert(DECEMBER==12, "enum month numbering Dec");
|
||||
|
||||
/** Abbreviated weekday names */
|
||||
extern const char *DT_WKDAY_NAMES[];
|
||||
/** Full-length weekday names */
|
||||
extern const char *DT_WKDAY_NAMES_FULL[];
|
||||
/** Abbreviated month names */
|
||||
extern const char *DT_MONTH_NAMES[];
|
||||
/** Full-length month names */
|
||||
extern const char *DT_MONTH_NAMES_FULL[];
|
||||
|
||||
typedef struct datetime {
|
||||
uint16_t year;
|
||||
enum month month;
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint8_t min;
|
||||
uint8_t sec;
|
||||
enum weekday wkday; // 1=monday
|
||||
} datetime_t;
|
||||
|
||||
// Templates for printf
|
||||
#define DT_FORMAT_DATE "%d/%d/%d"
|
||||
#define DT_SUBS_DATE(dt) (dt).year, (dt).month, (dt).day
|
||||
|
||||
#define DT_FORMAT_TIME "%d:%02d:%02d"
|
||||
#define DT_SUBS_TIME(dt) (dt).hour, (dt).min, (dt).sec
|
||||
|
||||
#define DT_FORMAT_DATE_WK DT_FORMAT_WK " " DT_FORMAT_DATE
|
||||
#define DT_SUBS_DATE_WK(dt) DT_SUBS_WK(dt), DT_SUBS_DATE(dt)
|
||||
|
||||
#define DT_FORMAT_WK "%s"
|
||||
#define DT_SUBS_WK(dt) DT_WKDAY_NAMES[(dt).wkday]
|
||||
|
||||
#define DT_FORMAT_DATE_TIME DT_FORMAT_DATE " " DT_FORMAT_TIME
|
||||
#define DT_SUBS_DATE_TIME(dt) DT_SUBS_DATE(dt), DT_SUBS_TIME(dt)
|
||||
|
||||
#define DT_FORMAT DT_FORMAT_DATE_WK " " DT_FORMAT_TIME
|
||||
#define DT_SUBS(dt) DT_SUBS_DATE_WK(dt), DT_SUBS_TIME(dt)
|
||||
|
||||
// base century for two-digit year conversions
|
||||
#define DT_CENTURY 2000
|
||||
// start year for weekday computation
|
||||
#define DT_START_YEAR 2019
|
||||
// January 1st weekday of DT_START_YEAR
|
||||
#define DT_START_WKDAY TUESDAY
|
||||
// max date supported by 2-digit year RTC counters (it can't check Y%400==0 with only two digits)
|
||||
#define DT_END_YEAR 2399
|
||||
|
||||
typedef union __attribute__((packed)) {
|
||||
struct __attribute__((packed)) {
|
||||
uint8_t ones : 4;
|
||||
uint8_t tens : 4;
|
||||
};
|
||||
uint8_t byte;
|
||||
} bcd_t;
|
||||
_Static_assert(sizeof(bcd_t) == 1, "Bad bcd_t len");
|
||||
|
||||
/** Check if a year is leap */
|
||||
static inline bool is_leap_year(int year)
|
||||
{
|
||||
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a datetime could be valid (ignores leap years)
|
||||
*
|
||||
* @param[in] dt
|
||||
* @return basic validations passed
|
||||
*/
|
||||
bool datetime_is_valid(const datetime_t *dt);
|
||||
|
||||
/**
|
||||
* Set weekday based on a date in a given datetime
|
||||
*
|
||||
* @param[in,out] dt
|
||||
* @return success
|
||||
*/
|
||||
bool datetime_set_weekday(datetime_t *dt);
|
||||
|
||||
/**
|
||||
* Get weekday for given a date
|
||||
*
|
||||
* @param year - year number
|
||||
* @param month - 1-based month number
|
||||
* @param day - 1-based day number
|
||||
* @return weekday
|
||||
*/
|
||||
enum weekday date_weekday(uint16_t year, enum month month, uint8_t day);
|
||||
|
||||
|
||||
#endif //CSPEMU_DATETIME_H
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief A simple way of dumping memory to a hex output
|
||||
*
|
||||
* \addtogroup Hexdump
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define HEX_DUMP_LINE_BUFF_SIZ 16
|
||||
|
||||
extern void hex_dump(FILE * fp,void *src, int len);
|
||||
extern void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len);
|
||||
/**
|
||||
* }@
|
||||
*/
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* General purpose, platform agnostic, reusable utils
|
||||
*/
|
||||
|
||||
#ifndef COMMON_UTILS_UTILS_H
|
||||
#define COMMON_UTILS_UTILS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "base16.h"
|
||||
#include "datetime.h"
|
||||
#include "hexdump.h"
|
||||
|
||||
/** Convert a value to BCD struct */
|
||||
static inline bcd_t num2bcd(uint8_t value)
|
||||
{
|
||||
return (bcd_t) {.ones=value % 10, .tens=value / 10};
|
||||
}
|
||||
|
||||
/** Convert unpacked BCD to value */
|
||||
static inline uint8_t bcd2num(uint8_t tens, uint8_t ones)
|
||||
{
|
||||
return tens * 10 + ones;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append to a buffer.
|
||||
*
|
||||
* In case the buffer capacity is reached, it is reverted to the previous length (by replacing the NUL byte)
|
||||
* and NULL is returned.
|
||||
*
|
||||
* @param buf - buffer position to append at; if NULL is given, the function immediately returns NULL.
|
||||
* @param appended - string to append
|
||||
* @param pcap - pointer to a capacity variable
|
||||
* @return the new end of the string (null byte); use as 'buf' for following appends
|
||||
*/
|
||||
char *append(char *buf, const char *appended, size_t *pcap);
|
||||
|
||||
/**
|
||||
* Append, re-allocating as needed.
|
||||
*
|
||||
* @param head - pointer to heap buffer head, may be updated on realloc.
|
||||
* @param size - string size pointer, may be updated on realloc.
|
||||
* @param appended - string to append
|
||||
* @return false on alloc error
|
||||
*/
|
||||
bool append_realloc(char **head, size_t *cap, const char *appended);
|
||||
|
||||
/**
|
||||
* Test if a file descriptor is valid (e.g. when cleaning up after a failed select)
|
||||
*
|
||||
* @param fd - file descriptor number
|
||||
* @return is valid
|
||||
*/
|
||||
bool fd_is_valid(int fd);
|
||||
|
||||
/**
|
||||
* parse user-provided string as boolean
|
||||
*
|
||||
* @param str
|
||||
* @return is true
|
||||
*/
|
||||
bool parse_boolean_arg(const char *str);
|
||||
|
||||
/**
|
||||
* complementary function to parse_boolean_arg() that matches strings
|
||||
* meaning 'false'. Can be used together with the positive version
|
||||
* in case there can be other values as well.
|
||||
*
|
||||
* @param str
|
||||
* @return is false
|
||||
*/
|
||||
bool parse_boolean_arg_false(const char *str);
|
||||
|
||||
/** Check equality of two strings; returns bool */
|
||||
#define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0)
|
||||
|
||||
/** Check prefix equality of two strings; returns bool */
|
||||
#define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0)
|
||||
|
||||
/** Check if a string starts with a substring; returns bool */
|
||||
#define strstarts(a, b) strneq((a), (b), (int)strlen((b)))
|
||||
|
||||
#ifndef MIN
|
||||
/** Get min of two numbers */
|
||||
#define MIN(a, b) ((a) > (b) ? (b) : (a))
|
||||
#endif
|
||||
|
||||
#ifndef MAX
|
||||
/** Get max of two values */
|
||||
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#ifndef STR
|
||||
#define STR_HELPER(x) #x
|
||||
/** Stringify a token */
|
||||
#define STR(x) STR_HELPER(x)
|
||||
#endif
|
||||
|
||||
#endif //COMMON_UTILS_UTILS_H
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG = "base16";
|
||||
|
||||
void base16_encode(uint8_t *raw, size_t len, char *encoded) {
|
||||
uint8_t *raw_bytes = raw;
|
||||
char *encoded_bytes = encoded;
|
||||
size_t remaining = len;
|
||||
|
||||
for (; remaining--; encoded_bytes += 2)
|
||||
snprintf(encoded_bytes, 3, "%02X", *(raw_bytes++));
|
||||
|
||||
}
|
||||
|
||||
int base16_decode(const char *encoded, uint8_t *raw) {
|
||||
const char *encoded_bytes = encoded;
|
||||
uint8_t *raw_bytes = raw;
|
||||
char buf[3];
|
||||
char *endp;
|
||||
size_t len;
|
||||
|
||||
while (encoded_bytes[0]) {
|
||||
if (!encoded_bytes[1]) {
|
||||
ESP_LOGE(TAG, "Base16-encoded string \"%s\" has invalid length\n",
|
||||
encoded);
|
||||
return -22;
|
||||
}
|
||||
memcpy(buf, encoded_bytes, 2);
|
||||
buf[2] = '\0';
|
||||
*(raw_bytes++) = strtoul(buf, &endp, 16);
|
||||
if (*endp != '\0') {
|
||||
ESP_LOGE(TAG,"Base16-encoded string \"%s\" has invalid byte \"%s\"\n",
|
||||
encoded, buf);
|
||||
return -22;
|
||||
}
|
||||
encoded_bytes += 2;
|
||||
}
|
||||
len = (raw_bytes - raw);
|
||||
return (len);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "common_utils/utils.h"
|
||||
|
||||
char *append(char *buf, const char *appended, size_t *pcap)
|
||||
{
|
||||
char c;
|
||||
char *buf0 = buf;
|
||||
size_t cap = *pcap;
|
||||
|
||||
if (buf0 == NULL) return NULL;
|
||||
if (appended == NULL || appended[0] == 0) return buf0;
|
||||
|
||||
if (*pcap < strlen(appended)+1) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while (cap > 1 && 0 != (c = *appended++)) {
|
||||
*buf++ = c;
|
||||
cap--;
|
||||
}
|
||||
assert(cap > 0);
|
||||
|
||||
*pcap = cap;
|
||||
*buf = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool append_realloc(char **head, size_t *cap, const char *appended) {
|
||||
if (!head) return NULL;
|
||||
if (!*head) return NULL;
|
||||
if (!cap) return NULL;
|
||||
if (!appended) return NULL;
|
||||
|
||||
size_t cursize = strlen(*head);
|
||||
size_t needed = strlen(appended) + 1;
|
||||
size_t remains = *cap - cursize;
|
||||
|
||||
if (remains < needed) {
|
||||
size_t need_extra = needed - remains;
|
||||
size_t newsize = *cap + need_extra;
|
||||
char *new = realloc(*head, newsize);
|
||||
if (!new) return false;
|
||||
*head = new;
|
||||
*cap = newsize;
|
||||
}
|
||||
|
||||
strcpy(*head + cursize, appended);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fd_is_valid(int fd)
|
||||
{
|
||||
return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
|
||||
}
|
||||
|
||||
bool parse_boolean_arg(const char *str)
|
||||
{
|
||||
if (0 == strcasecmp(str, "on")) return true;
|
||||
if (0 == strcmp(str, "1")) return true;
|
||||
if (0 == strcasecmp(str, "yes")) return true;
|
||||
if (0 == strcasecmp(str, "enable")) return true;
|
||||
if (0 == strcasecmp(str, "en")) return true;
|
||||
if (0 == strcasecmp(str, "y")) return true;
|
||||
if (0 == strcasecmp(str, "a")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parse_boolean_arg_false(const char *str)
|
||||
{
|
||||
if (0 == strcasecmp(str, "off")) return true;
|
||||
if (0 == strcmp(str, "0")) return true;
|
||||
if (0 == strcasecmp(str, "no")) return true;
|
||||
if (0 == strcasecmp(str, "disable")) return true;
|
||||
if (0 == strcasecmp(str, "dis")) return true;
|
||||
if (0 == strcasecmp(str, "n")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "common_utils/datetime.h"
|
||||
|
||||
const char *DT_WKDAY_NAMES[] = {
|
||||
[MONDAY] = "Mon",
|
||||
[TUESDAY] = "Tue",
|
||||
[WEDNESDAY] = "Wed",
|
||||
[THURSDAY] = "Thu",
|
||||
[FRIDAY] = "Fri",
|
||||
[SATURDAY] = "Sat",
|
||||
[SUNDAY] = "Sun"
|
||||
};
|
||||
|
||||
const char *DT_WKDAY_NAMES_FULL[] = {
|
||||
[MONDAY] = "Monday",
|
||||
[TUESDAY] = "Tuesday",
|
||||
[WEDNESDAY] = "Wednesday",
|
||||
[THURSDAY] = "Thursday",
|
||||
[FRIDAY] = "Friday",
|
||||
[SATURDAY] = "Saturday",
|
||||
[SUNDAY] = "Sunday"
|
||||
};
|
||||
|
||||
const char *DT_MONTH_NAMES[] = {
|
||||
[JANUARY] = "Jan",
|
||||
[FEBRUARY] = "Feb",
|
||||
[MARCH] = "Mar",
|
||||
[APRIL] = "Apr",
|
||||
[MAY] = "May",
|
||||
[JUNE] = "Jun",
|
||||
[JULY] = "Jul",
|
||||
[AUGUST] = "Aug",
|
||||
[SEPTEMBER] = "Sep",
|
||||
[OCTOBER] = "Oct",
|
||||
[NOVEMBER] = "Nov",
|
||||
[DECEMBER] = "Dec"
|
||||
};
|
||||
|
||||
const char *DT_MONTH_NAMES_FULL[] = {
|
||||
[JANUARY] = "January",
|
||||
[FEBRUARY] = "February",
|
||||
[MARCH] = "March",
|
||||
[APRIL] = "April",
|
||||
[MAY] = "May",
|
||||
[JUNE] = "June",
|
||||
[JULY] = "July",
|
||||
[AUGUST] = "August",
|
||||
[SEPTEMBER] = "September",
|
||||
[OCTOBER] = "October",
|
||||
[NOVEMBER] = "November",
|
||||
[DECEMBER] = "December"
|
||||
};
|
||||
|
||||
static const uint16_t MONTH_LENGTHS[] = { /* 1-based, normal year */
|
||||
[JANUARY]=31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||
};
|
||||
_Static_assert(sizeof(MONTH_LENGTHS) / sizeof(uint16_t) == 13, "Months array length");
|
||||
|
||||
static const uint16_t MONTH_YEARDAYS[] = { /* 1-based */
|
||||
[JANUARY]=0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 // // days until 1st of month
|
||||
};
|
||||
_Static_assert(sizeof(MONTH_YEARDAYS) / sizeof(uint16_t) == 13, "Months array length");
|
||||
|
||||
_Static_assert(MONDAY < SUNDAY, "Weekday ordering");
|
||||
|
||||
bool datetime_is_valid(const datetime_t *dt)
|
||||
{
|
||||
if (dt == NULL) return false;
|
||||
|
||||
// check month first to avoid out-of-bounds read from the MONTH_LENGTHS table
|
||||
if (!(dt->month >= JANUARY && dt->month <= DECEMBER)) return false;
|
||||
|
||||
int monthlen = MONTH_LENGTHS[dt->month];
|
||||
if (dt->month == FEBRUARY && is_leap_year(dt->year)) {
|
||||
monthlen = 29;
|
||||
}
|
||||
|
||||
return dt->sec < 60 &&
|
||||
dt->min < 60 &&
|
||||
dt->hour < 24 &&
|
||||
dt->wkday >= MONDAY &&
|
||||
dt->wkday <= SUNDAY &&
|
||||
dt->year >= DT_START_YEAR &&
|
||||
dt->year <= DT_END_YEAR &&
|
||||
dt->day >= 1 &&
|
||||
dt->day <= monthlen;
|
||||
}
|
||||
|
||||
bool datetime_set_weekday(datetime_t *dt)
|
||||
{
|
||||
dt->wkday = MONDAY; // prevent the validator func erroring out on invalid weekday
|
||||
if (!datetime_is_valid(dt)) return false;
|
||||
dt->wkday = date_weekday(dt->year, dt->month, dt->day);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum weekday date_weekday(uint16_t year, enum month month, uint8_t day)
|
||||
{
|
||||
uint16_t days = (DT_START_WKDAY - MONDAY) + (year - DT_START_YEAR) * 365 + MONTH_YEARDAYS[month] + (day - 1);
|
||||
|
||||
for (uint16_t i = DT_START_YEAR; i <= year; i++) {
|
||||
if (is_leap_year(i) && (i < year || month > FEBRUARY)) days++;
|
||||
}
|
||||
|
||||
return MONDAY + days % 7;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* util.c
|
||||
*
|
||||
* Created on: Aug 12, 2009
|
||||
* Author: johan
|
||||
*/
|
||||
|
||||
// adapted from libgomspace
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "common_utils/hexdump.h"
|
||||
|
||||
|
||||
//! Dump memory to debugging output
|
||||
/**
|
||||
* Dumps a chunk of memory to the screen
|
||||
*/
|
||||
void hex_dump(FILE * fp, void *src, int len) {
|
||||
int i, j=0, k;
|
||||
char text[17];
|
||||
|
||||
text[16] = '\0';
|
||||
//printf("Hex dump:\r\n");
|
||||
fprintf(fp, "%p : ", src);
|
||||
for(i=0; i<len; i++) {
|
||||
j++;
|
||||
fprintf(fp, "%02X ", ((volatile unsigned char *)src)[i]);
|
||||
if(j == 8)
|
||||
fputc(' ', fp);
|
||||
if(j == 16) {
|
||||
j = 0;
|
||||
memcpy(text, &((char *)src)[i-15], 16);
|
||||
for(k=0; k<16; k++) {
|
||||
if((text[k] < 32) || (text[k] > 126)) {
|
||||
text[k] = '.';
|
||||
}
|
||||
}
|
||||
fprintf(fp, " |%s|\n\r", text);
|
||||
if(i<len-1) {
|
||||
fprintf(fp, "%p : ", src+i+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i % 16)
|
||||
fprintf(fp, "\r\n");
|
||||
}
|
||||
|
||||
void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len)
|
||||
{
|
||||
unsigned i;
|
||||
|
||||
fprintf(fp, "%0*x", addr_size, pos);
|
||||
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ; i++)
|
||||
{
|
||||
if (!(i % 8))
|
||||
fputc(' ', fp);
|
||||
if (i < len)
|
||||
fprintf(fp, " %02x", (unsigned char)line[i]);
|
||||
else
|
||||
fputs(" ", fp);
|
||||
}
|
||||
fputs(" |", fp);
|
||||
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ && i < len; i++)
|
||||
{
|
||||
if (line[i] >= 32 && line[i] <= 126)
|
||||
fprintf(fp, "%c", (unsigned char)line[i]);
|
||||
else
|
||||
fputc('.', fp);
|
||||
}
|
||||
fputs("|\r\n", fp);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
set(COMPONENT_ADD_INCLUDEDIRS
|
||||
"include")
|
||||
|
||||
set(COMPONENT_SRCDIRS
|
||||
"src")
|
||||
|
||||
set(COMPONENT_REQUIRES tcpip_adapter esp_http_server httpd_utils common_utils)
|
||||
|
||||
register_component()
|
||||
@@ -0,0 +1,2 @@
|
||||
File and template serving support for the http_server bundled with ESP-IDF.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
COMPONENT_SRCDIRS := src
|
||||
COMPONENT_ADD_INCLUDEDIRS := include
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Created on 2018/10/17 by Ondrej Hruska <ondra@ondrovo.com>
|
||||
//
|
||||
|
||||
#ifndef FBNODE_EMBEDDED_FILES_H
|
||||
#define FBNODE_EMBEDDED_FILES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <esp_err.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
struct embedded_file_info {
|
||||
const uint8_t * start;
|
||||
const uint8_t * end;
|
||||
const char * name;
|
||||
const char * mime;
|
||||
};
|
||||
|
||||
enum file_access_level {
|
||||
/** Public = file accessed by a wildcard route */
|
||||
FILE_ACCESS_PUBLIC = 0,
|
||||
/** Protected = file included in a template or explicitly specified in a route */
|
||||
FILE_ACCESS_PROTECTED = 1,
|
||||
/** Files protected against read-out */
|
||||
FILE_ACCESS_PRIVATE = 2,
|
||||
};
|
||||
|
||||
extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[];
|
||||
extern const size_t EMBEDDED_FILE_LOOKUP_LEN;
|
||||
|
||||
/**
|
||||
* Find an embedded file by its name.
|
||||
*
|
||||
* This function is weak. It crawls the EMBEDDED_FILE_LOOKUP table and checks for exact match, also
|
||||
* testing with www_get_static_file_access_check if the access is allowed.
|
||||
*
|
||||
* @param name - file name
|
||||
* @param access - access level (public - wildcard fallthrough, protected - files for the server, loaded explicitly)
|
||||
* @param[out] file - the file struct is stored here if found, unchanged if not found.
|
||||
* @return status code
|
||||
*/
|
||||
esp_err_t www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file);
|
||||
|
||||
/**
|
||||
* Check file access permission (if using the default www_get_static_file implementation).
|
||||
*
|
||||
* This function is weak. The default implementation returns always true.
|
||||
*/
|
||||
bool www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access);
|
||||
|
||||
#endif //FBNODE_EMBEDDED_FILES_H
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// This module implements token substitution in files served by the server.
|
||||
//
|
||||
// Tokens are in the form {token}, or {escape:token}, where escape can be:
|
||||
// - h ... html escape (plain text in a html file, attribute value)
|
||||
// - j ... js escape (for use in JS strings)
|
||||
//
|
||||
// When no escape is specified, the token substitution is written verbatim into the response.
|
||||
//
|
||||
// var foo = "{j:foo}";
|
||||
// <input value="{h:old_value}">
|
||||
// {generated-html-goes-here}
|
||||
//
|
||||
// Token can be made optional by adding '?' at the end (this can't be used for includes).
|
||||
// Such token then simply becomes empty string when not substituted, as opposed to being included in the page verbatim.
|
||||
//
|
||||
// <input value="{h:old_value?}">
|
||||
//
|
||||
// token names can contain alnum, dash, period and underscore, and are case sensitive.
|
||||
//
|
||||
//
|
||||
// It is further possible to include a static file with optional key-value replacements. These serve as defaults.
|
||||
//
|
||||
// {@_subfile.html}
|
||||
// {@_subfile.html|key=value lalala}
|
||||
// {@_subfile.html|key=value lalala|other=value}
|
||||
//
|
||||
// File inclusion can be nested, and the files can use replacement tokens as specified by the include statement
|
||||
//
|
||||
// Created on 2019/01/24 by Ondrej Hruska <ondra@ondrovo.com>
|
||||
//
|
||||
|
||||
#ifndef FBNODE_TOKEN_SUBS_H
|
||||
#define FBNODE_TOKEN_SUBS_H
|
||||
|
||||
#include "embedded_files.h"
|
||||
#include <rom/queue.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_server.h>
|
||||
|
||||
/** Max length of a token buffer (must suffice for all included filenames) */
|
||||
#define MAX_TOKEN_LEN 32
|
||||
|
||||
/** Max length of a key-value substitution when using tpl_kv_replacer;
|
||||
* This is also used internally for in-line replacements in file imports. */
|
||||
#define TPL_KV_KEY_LEN 24
|
||||
/** Max length of a substituion in tpl_kv_replacer */
|
||||
#define TPL_KV_SUBST_LEN 64
|
||||
|
||||
/**
|
||||
* Escape type - argument for httpd_resp_send_chunk_escaped()
|
||||
*/
|
||||
typedef enum {
|
||||
TPL_ESCAPE_NONE = 0,
|
||||
TPL_ESCAPE_HTML,
|
||||
TPL_ESCAPE_JS,
|
||||
} tpl_escape_t;
|
||||
|
||||
enum {
|
||||
HTOPT_NONE = 0,
|
||||
HTOPT_NO_HEADERS = 1 << 0,
|
||||
HTOPT_NO_CLOSE = 1 << 1,
|
||||
HTOPT_INCLUDE = HTOPT_NO_HEADERS|HTOPT_NO_CLOSE,
|
||||
};
|
||||
|
||||
/**
|
||||
* Send string using a given escaping scheme
|
||||
*
|
||||
* @param r
|
||||
* @param buf - buf to send
|
||||
* @param len - buf len, or HTTPD_RESP_USE_STRLEN
|
||||
* @param escape - escaping type
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape);
|
||||
|
||||
/**
|
||||
* Template substitution callback. Data shall be sent using httpd_resp_send_chunk_escaped().
|
||||
*
|
||||
* @param[in,out] context - user-defined page state data
|
||||
* @param[in] token - replacement token
|
||||
* @return ESP_OK if the token was substituted, ESP_ERR_NOT_FOUND if it is unknown, other errors on e.g. send failure
|
||||
*/
|
||||
typedef esp_err_t (*template_subst_t)(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape);
|
||||
|
||||
/**
|
||||
* Send a template file as a response. The content type from the file struct will be used.
|
||||
*
|
||||
* Use HTOPT_INCLUDE when used to embed a file inside a template.
|
||||
*
|
||||
* @param r - request
|
||||
* @param file_index - file index in EMBEDDED_FILE_LOOKUP
|
||||
* @param replacer - substitution callback, can be NULL if only includes are to be processed
|
||||
* @param context - arbitrary context, will be passed to the replacer function; can be NULL
|
||||
* @param opts - flag options (HTOPT_*)
|
||||
*/
|
||||
esp_err_t httpd_send_template_file(httpd_req_t *r, int file_index, template_subst_t replacer, void *context, uint32_t opts);
|
||||
|
||||
/**
|
||||
* Same as httpd_send_template_file, but using an `embedded_file_info` struct.
|
||||
*/
|
||||
esp_err_t httpd_send_template_file_struct(httpd_req_t *r, const struct embedded_file_info *file, template_subst_t replacer, void *context, uint32_t opts);
|
||||
|
||||
/**
|
||||
* Process and send a string template.
|
||||
* The content-type header should be set beforehand, if different from the default (text/html).
|
||||
*
|
||||
* Use HTOPT_INCLUDE when used to embed a file inside a template.
|
||||
*
|
||||
* @param r - request
|
||||
* @param template - template string (does not have to be terminated by a null byte)
|
||||
* @param template_len - length of the template string; -1 to use strlen()
|
||||
* @param replacer - substitution callback, can be NULL if only includes are to be processed
|
||||
* @param context - arbitrary context, will be passed to the replacer function; can be NULL
|
||||
* @param opts - flag options (HTOPT_*)
|
||||
*/
|
||||
esp_err_t httpd_send_template(httpd_req_t *r, const char *template, ssize_t template_len, template_subst_t replacer, void *context, uint32_t opts);
|
||||
|
||||
/**
|
||||
* Send a static file. This can be used to just send a file, or to embed a static template as a token substitution.
|
||||
*
|
||||
* Use HTOPT_INCLUDE when used to embed a file inside a template.
|
||||
*
|
||||
* Note: use httpd_resp_send_chunk_escaped() or httpd_resp_send_chunk() to send a plain string.
|
||||
*
|
||||
* @param r - request
|
||||
* @param file_index - file index in EMBEDDED_FILE_LOOKUP
|
||||
* @param escape - escape option
|
||||
* @param opts - flag options (HTOPT_*)
|
||||
* @return
|
||||
*/
|
||||
esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts);
|
||||
|
||||
/**
|
||||
* Same as httpd_send_template_file, but using an `embedded_file_info` struct.
|
||||
*/
|
||||
esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts);
|
||||
|
||||
struct tpl_kv_entry {
|
||||
char key[TPL_KV_KEY_LEN]; // copied here
|
||||
char subst[TPL_KV_SUBST_LEN]; // copied here
|
||||
char *subst_heap;
|
||||
SLIST_ENTRY(tpl_kv_entry) link;
|
||||
};
|
||||
|
||||
SLIST_HEAD(tpl_kv_list, tpl_kv_entry);
|
||||
|
||||
/**
|
||||
* key-value replacer that works with a dynamically allocated SLIST.
|
||||
*
|
||||
* @param r - request
|
||||
* @param context - context - must be a pointer to `struct tpl_kv_list`
|
||||
* @param token - token to replace
|
||||
* @param escape - escape option
|
||||
* @return OK/not found/other
|
||||
*/
|
||||
esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape);
|
||||
|
||||
/**
|
||||
* Add a pair into the substitutions list
|
||||
*
|
||||
* @param head - list head
|
||||
* @param key - key, copied
|
||||
* @param subst - value, copied
|
||||
* @return success (fails if malloc failed)
|
||||
*/
|
||||
esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst);
|
||||
|
||||
/**
|
||||
* Add a heap-allocated string to the replacer.
|
||||
*
|
||||
* @param head - list head
|
||||
* @param key - key, copied
|
||||
* @param subst - value, copied
|
||||
* @return success (fails if malloc failed)
|
||||
*/
|
||||
esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst);
|
||||
|
||||
/**
|
||||
* Convenience function that converts an IP address to string and adds it as a substitution
|
||||
*
|
||||
* @param head - list head
|
||||
* @param key - key, copied
|
||||
* @param ip4h - host order ipv4 address
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h);
|
||||
|
||||
/** add int as a substitution; key is copied */
|
||||
esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num);
|
||||
|
||||
/** add long as a substitution; key is copied */
|
||||
esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num);
|
||||
|
||||
/** add printf-formatted value; key is copied */
|
||||
esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...)
|
||||
__attribute__((format(printf,3,4)));
|
||||
|
||||
/**
|
||||
* Init a substitutions list (on the stack)
|
||||
*
|
||||
* @return the list
|
||||
*/
|
||||
static inline struct tpl_kv_list tpl_kv_init(void)
|
||||
{
|
||||
return (struct tpl_kv_list) {.slh_first = NULL};
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the list (head is left alone because it was allocated on the stack)
|
||||
* @param head
|
||||
*/
|
||||
void tpl_kv_free(struct tpl_kv_list *head);
|
||||
|
||||
/**
|
||||
* Send the map as an ASCII table separated by Record Separator (30) and Unit Separator (31).
|
||||
* Content type is set to application/octet-stream.
|
||||
*
|
||||
* key 31 value 30
|
||||
* key 31 value 30
|
||||
* key 31 value
|
||||
*
|
||||
* @param req
|
||||
*/
|
||||
esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head);
|
||||
|
||||
#endif //FBNODE_TOKEN_SUBS_H
|
||||
@@ -0,0 +1,29 @@
|
||||
Place the `rebuild_file_tables.php` script in a `files` subfolder of the main component.
|
||||
It requires PHP 7 to run.
|
||||
|
||||
This is what the setup should look like
|
||||
|
||||
```
|
||||
main/files/embed/index.html
|
||||
main/files/rebuild_file_tables.php
|
||||
main/CMakeLists.txt
|
||||
main/main.c
|
||||
```
|
||||
|
||||
Add this to your CMakeLists.txt before `register_component`:
|
||||
|
||||
```
|
||||
#begin staticfiles
|
||||
#end staticfiles
|
||||
```
|
||||
|
||||
The script will update CMakeLists.txt and generate `files_enum.c` and `files_enum.h` when run.
|
||||
|
||||
```
|
||||
main/files/files_enum.h
|
||||
main/files/files_enum.c
|
||||
```
|
||||
|
||||
Ensure `files/files_enum.c` is included in the build.
|
||||
|
||||
`www_get_static_file()` is implemented as weak to let you provide custom access authentication logic.
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
|
||||
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
|
||||
|
||||
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
|
||||
|
||||
|
||||
// List all files
|
||||
$files = scandir(__DIR__.'/embed');
|
||||
|
||||
$files = array_filter(array_map(function ($f) {
|
||||
if (!is_file(__DIR__.'/embed/'.$f)) return null;
|
||||
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh$/', $f)) return null;
|
||||
|
||||
echo "Found: $f\n";
|
||||
return $f;
|
||||
}, $files));
|
||||
|
||||
sort($files);
|
||||
|
||||
$formatted = array_filter(array_map(function ($f) {
|
||||
return "\"files/embed/$f\"";
|
||||
}, $files));
|
||||
|
||||
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
|
||||
|
||||
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
|
||||
"#begin staticfiles\n".
|
||||
"set(COMPONENT_EMBED_FILES\n ".
|
||||
implode("\n ", $formatted) . ")\n".
|
||||
"#end staticfiles",
|
||||
$cmake);
|
||||
|
||||
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
|
||||
|
||||
|
||||
// Generate a list of files
|
||||
|
||||
$num = 0;
|
||||
$enum_keys = array_map(function ($f) use(&$num) {
|
||||
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
|
||||
return 'FILE_'. $a.' = '.($num++);
|
||||
}, $files);
|
||||
|
||||
$keylist = implode(",\n ", $enum_keys);
|
||||
|
||||
$struct_array = [];
|
||||
|
||||
$externs = array_map(function ($f) use (&$struct_array) {
|
||||
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
|
||||
|
||||
$start = '_binary_'. strtolower($a).'_start';
|
||||
$end = '_binary_'. strtolower($a).'_end';
|
||||
|
||||
static $mimes = array(
|
||||
'txt' => 'text/plain',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'php' => 'text/html',
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'xml' => 'application/xml',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'flv' => 'video/x-flv',
|
||||
|
||||
'pem' => 'application/x-pem-file',
|
||||
|
||||
// images
|
||||
'png' => 'image/png',
|
||||
'jpe' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'bmp' => 'image/bmp',
|
||||
'ico' => 'image/vnd.microsoft.icon',
|
||||
'tiff' => 'image/tiff',
|
||||
'tif' => 'image/tiff',
|
||||
'svg' => 'image/svg+xml',
|
||||
'svgz' => 'image/svg+xml',
|
||||
|
||||
// archives
|
||||
'zip' => 'application/zip',
|
||||
'rar' => 'application/x-rar-compressed',
|
||||
'exe' => 'application/x-msdownload',
|
||||
'msi' => 'application/x-msdownload',
|
||||
'cab' => 'application/vnd.ms-cab-compressed',
|
||||
|
||||
// audio/video
|
||||
'mp3' => 'audio/mpeg',
|
||||
'qt' => 'video/quicktime',
|
||||
'mov' => 'video/quicktime',
|
||||
|
||||
// adobe
|
||||
'pdf' => 'application/pdf',
|
||||
'psd' => 'image/vnd.adobe.photoshop',
|
||||
'ai' => 'application/postscript',
|
||||
'eps' => 'application/postscript',
|
||||
'ps' => 'application/postscript',
|
||||
|
||||
// ms office
|
||||
'doc' => 'application/msword',
|
||||
'rtf' => 'application/rtf',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
|
||||
// open office
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
);
|
||||
|
||||
$parts = explode('.', $f);
|
||||
$suffix = end($parts);
|
||||
$mime = $mimes[$suffix] ?? 'application/octet-stream';
|
||||
|
||||
$len = filesize('embed/'.$f);
|
||||
|
||||
$struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},";
|
||||
|
||||
return
|
||||
'extern const uint8_t '.$start.'[];'."\n".
|
||||
'extern const uint8_t '.$end.'[];';
|
||||
}, $files);
|
||||
|
||||
$externlist = implode("\n", $externs);
|
||||
$structlist = implode("\n ", $struct_array);
|
||||
|
||||
|
||||
file_put_contents('files_enum.h', <<<FILE
|
||||
// Do not change, auto-generated by gen_staticfiles.php
|
||||
|
||||
#ifndef _EMBEDDED_FILES_ENUM_H
|
||||
#define _EMBEDDED_FILES_ENUM_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum embedded_file_id {
|
||||
$keylist,
|
||||
FILE_MAX
|
||||
};
|
||||
|
||||
struct embedded_file_info {
|
||||
const uint8_t * start;
|
||||
const uint8_t * end;
|
||||
const char * name;
|
||||
const char * mime;
|
||||
};
|
||||
|
||||
$externlist
|
||||
|
||||
extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[];
|
||||
|
||||
#endif // _EMBEDDED_FILES_ENUM_H
|
||||
|
||||
FILE
|
||||
);
|
||||
|
||||
file_put_contents("files_enum.c", <<<FILE
|
||||
// Do not change, auto-generated by gen_staticfiles.php
|
||||
#include <stdint.h>
|
||||
#include "files_enum.h"
|
||||
|
||||
const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
|
||||
$structlist
|
||||
};
|
||||
|
||||
FILE
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
#include <esp_err.h>
|
||||
#include "fileserver/embedded_files.h"
|
||||
#include "string.h"
|
||||
|
||||
esp_err_t __attribute__((weak))
|
||||
www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file)
|
||||
{
|
||||
// simple search by name
|
||||
for(int i = 0; i < EMBEDDED_FILE_LOOKUP_LEN; i++) {
|
||||
if (0 == strcmp(EMBEDDED_FILE_LOOKUP[i].name, name)) {
|
||||
*file = &EMBEDDED_FILE_LOOKUP[i];
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
bool __attribute__((weak))
|
||||
www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <rom/queue.h>
|
||||
#include <lwip/ip4_addr.h>
|
||||
#include <sys/param.h>
|
||||
#include <common_utils/utils.h>
|
||||
#include <fileserver/token_subs.h>
|
||||
|
||||
#include "fileserver/embedded_files.h"
|
||||
#include "fileserver/token_subs.h"
|
||||
|
||||
#define ESP_TRY(x) \
|
||||
do { \
|
||||
esp_err_t try_er = (x); \
|
||||
if (try_er != ESP_OK) return try_er; \
|
||||
} while(0)
|
||||
|
||||
static const char* TAG = "token_subs";
|
||||
|
||||
// TODO implement buffering to avoid sending many tiny chunks when escaping
|
||||
|
||||
/* encode for HTML. returns 0 or 1 - 1 = success */
|
||||
static esp_err_t send_html_chunk(httpd_req_t *r, const char *data, ssize_t len)
|
||||
{
|
||||
assert(r);
|
||||
assert(data);
|
||||
|
||||
int start = 0, end = 0;
|
||||
char c;
|
||||
if (len < 0) len = (int) strlen(data);
|
||||
if (len==0) return ESP_OK;
|
||||
|
||||
for (end = 0; end < len; end++) {
|
||||
c = data[end];
|
||||
if (c == 0) {
|
||||
// we found EOS
|
||||
break; // not return - the last chunk is printed after the loop
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'' || c == '<' || c == '>') {
|
||||
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, """, 5));
|
||||
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "'", 5));
|
||||
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "<", 4));
|
||||
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, ">", 4));
|
||||
}
|
||||
|
||||
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* encode for JS. returns 0 or 1 - 1 = success */
|
||||
static esp_err_t send_js_chunk(httpd_req_t *r, const char *data, ssize_t len)
|
||||
{
|
||||
assert(r);
|
||||
assert(data);
|
||||
|
||||
int start = 0, end = 0;
|
||||
char c;
|
||||
if (len < 0) len = (int) strlen(data);
|
||||
if (len==0) return ESP_OK;
|
||||
|
||||
for (end = 0; end < len; end++) {
|
||||
c = data[end];
|
||||
if (c == 0) {
|
||||
// we found EOS
|
||||
break; // not return - the last chunk is printed after the loop
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\\' || c == '/' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') {
|
||||
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "\\\"", 2));
|
||||
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "\\'", 2));
|
||||
else if (c == '\\') ESP_TRY(httpd_resp_send_chunk(r, "\\\\", 2));
|
||||
else if (c == '/') ESP_TRY(httpd_resp_send_chunk(r, "\\/", 2));
|
||||
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "\\u003C", 6));
|
||||
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "\\u003E", 6));
|
||||
else if (c == '\n') ESP_TRY(httpd_resp_send_chunk(r, "\\n", 2));
|
||||
else if (c == '\r') ESP_TRY(httpd_resp_send_chunk(r, "\\r", 2));
|
||||
}
|
||||
|
||||
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape)
|
||||
{
|
||||
switch (escape) {
|
||||
default: // this enum should be exhaustive, but in case something went wrong, just print it verbatim
|
||||
|
||||
case TPL_ESCAPE_NONE:
|
||||
return httpd_resp_send_chunk(r, buf, len);
|
||||
|
||||
case TPL_ESCAPE_HTML:
|
||||
return send_html_chunk(r, buf, len);
|
||||
|
||||
case TPL_ESCAPE_JS:
|
||||
return send_js_chunk(r, buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts)
|
||||
{
|
||||
assert(file_index < EMBEDDED_FILE_LOOKUP_LEN);
|
||||
const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index];
|
||||
|
||||
return httpd_send_static_file_struct(r, file, escape, opts);
|
||||
}
|
||||
|
||||
esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts)
|
||||
{
|
||||
if (0 == (opts & HTOPT_NO_HEADERS)) {
|
||||
ESP_TRY(httpd_resp_set_type(r, file->mime));
|
||||
ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "max-age=86400, public, must-revalidate"));
|
||||
}
|
||||
|
||||
ESP_TRY(httpd_resp_send_chunk_escaped(r, (const char *) file->start, (size_t)(file->end - file->start), escape));
|
||||
|
||||
if (0 == (opts & HTOPT_NO_CLOSE)) {
|
||||
ESP_TRY(httpd_resp_send_chunk(r, NULL, 0));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t httpd_send_template_file(httpd_req_t *r,
|
||||
int file_index,
|
||||
template_subst_t replacer,
|
||||
void *context,
|
||||
uint32_t opts)
|
||||
{
|
||||
assert(file_index < EMBEDDED_FILE_LOOKUP_LEN);
|
||||
const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index];
|
||||
return httpd_send_template_file_struct(r,file,replacer,context,opts);
|
||||
}
|
||||
|
||||
esp_err_t httpd_send_template_file_struct(httpd_req_t *r,
|
||||
const struct embedded_file_info *file,
|
||||
template_subst_t replacer,
|
||||
void *context,
|
||||
uint32_t opts)
|
||||
{
|
||||
if (0 == (opts & HTOPT_NO_HEADERS)) {
|
||||
ESP_TRY(httpd_resp_set_type(r, file->mime));
|
||||
ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "no-cache, no-store, must-revalidate"));
|
||||
}
|
||||
|
||||
return httpd_send_template(r, (const char *) file->start, (size_t)(file->end - file->start), replacer, context, opts);
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape)
|
||||
{
|
||||
assert(context);
|
||||
assert(token);
|
||||
|
||||
struct tpl_kv_entry *entry;
|
||||
struct tpl_kv_list *head = context;
|
||||
SLIST_FOREACH(entry, head, link) {
|
||||
if (0==strcmp(entry->key, token)) {
|
||||
if (entry->subst_heap) {
|
||||
if (entry->subst_heap[0]) {
|
||||
return httpd_resp_send_chunk_escaped(r, entry->subst_heap, -1, escape);
|
||||
}
|
||||
} else {
|
||||
if (entry->subst[0]) {
|
||||
return httpd_resp_send_chunk_escaped(r, entry->subst, -1, escape);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
struct stacked_replacer_context {
|
||||
template_subst_t replacer0;
|
||||
void *context0;
|
||||
template_subst_t replacer1;
|
||||
void *context1;
|
||||
};
|
||||
|
||||
esp_err_t stacked_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape)
|
||||
{
|
||||
assert(context);
|
||||
assert(token);
|
||||
|
||||
struct stacked_replacer_context *combo = context;
|
||||
|
||||
if (ESP_OK == combo->replacer0(r, combo->context0, token, escape)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (ESP_OK == combo->replacer1(r, combo->context1, token, escape)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t httpd_send_template(httpd_req_t *r,
|
||||
const char *template, ssize_t template_len,
|
||||
template_subst_t replacer,
|
||||
void *context,
|
||||
uint32_t opts)
|
||||
{
|
||||
if (template_len < 0) template_len = strlen(template);
|
||||
|
||||
// replacer and context may be NULL
|
||||
assert(template);
|
||||
assert(r);
|
||||
|
||||
// data end
|
||||
const char * const end = template + template_len;
|
||||
|
||||
// start of to-be-processed data
|
||||
const char * pos = template;
|
||||
|
||||
// start position for finding opening braces, updated after a failed match to avoid infinite loop on the same bad token
|
||||
const char * searchpos = pos;
|
||||
|
||||
// tokens must be copied to a buffer to allow adding the terminating null byte
|
||||
char token_buf[MAX_TOKEN_LEN];
|
||||
|
||||
while (pos < end) {
|
||||
const char * openbr = strchr(searchpos, '{');
|
||||
if (openbr == NULL) {
|
||||
// no more templates
|
||||
ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos)));
|
||||
break;
|
||||
}
|
||||
|
||||
// this brace could start a valid template. check if it seems valid...
|
||||
const char * closebr = strchr(openbr, '}');
|
||||
if (closebr == NULL) {
|
||||
// there are no further closing braces, so it can't be a template
|
||||
|
||||
// we also know there can't be any more substitutions, because they would lack a closing } too
|
||||
ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos)));
|
||||
break;
|
||||
}
|
||||
|
||||
// see if the braces content looks like a token
|
||||
const char *t = openbr + 1;
|
||||
bool token_valid = true;
|
||||
struct tpl_kv_list substitutions_head = tpl_kv_init();
|
||||
struct tpl_kv_entry *new_subst_pair = NULL;
|
||||
|
||||
// a token can be either a name for replacement by the replacer func, or an include with static kv replacements
|
||||
bool is_include = false;
|
||||
bool token_is_optional = false;
|
||||
const char *token_end = NULL; // points one char after the end of the token
|
||||
|
||||
// parsing the token
|
||||
{
|
||||
if (*t == '@') {
|
||||
ESP_LOGD(TAG, "Parsing an Include token");
|
||||
is_include = true;
|
||||
t++;
|
||||
}
|
||||
|
||||
enum {
|
||||
P_NAME, P_KEY, P_VALUE
|
||||
} state = P_NAME;
|
||||
|
||||
const char *kv_start = NULL;
|
||||
while (t != closebr || state == P_VALUE) {
|
||||
char c = *t;
|
||||
|
||||
if (state == P_NAME) {
|
||||
if (!((c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '.' || c == '_' || c == '-' || c == ':')) {
|
||||
|
||||
if (!is_include && c == '?') {
|
||||
token_end = t;
|
||||
token_is_optional = true;
|
||||
} else {
|
||||
if (is_include && c == '|') {
|
||||
token_end = t;
|
||||
state = P_KEY;
|
||||
kv_start = t + 1;
|
||||
// pipe separates the include's filename and literal substitutions
|
||||
// we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
|
||||
}
|
||||
else {
|
||||
token_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (state == P_KEY) {
|
||||
if (!((c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '.' || c == '_' || c == '-')) {
|
||||
if (c == '=') {
|
||||
new_subst_pair = calloc(1, sizeof(struct tpl_kv_entry));
|
||||
const size_t klen = MIN(TPL_KV_KEY_LEN, t - kv_start);
|
||||
strncpy(new_subst_pair->key, kv_start, klen);
|
||||
new_subst_pair->key[klen] = 0;
|
||||
|
||||
kv_start = t + 1;
|
||||
|
||||
state = P_VALUE;
|
||||
// pipe separates the include's filename and literal substitutions
|
||||
// we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (state == P_VALUE) {
|
||||
if (c == '|' || c == '}') {
|
||||
const size_t vlen = MIN(TPL_KV_SUBST_LEN, t - kv_start);
|
||||
strncpy(new_subst_pair->subst, kv_start, vlen);
|
||||
new_subst_pair->subst[vlen] = 0;
|
||||
|
||||
// attach the kv pair to the list
|
||||
SLIST_INSERT_HEAD(&substitutions_head, new_subst_pair, link);
|
||||
ESP_LOGD(TAG, "Adding subs kv %s -> %s", new_subst_pair->key, new_subst_pair->subst);
|
||||
new_subst_pair = NULL;
|
||||
|
||||
kv_start = t + 1; // go past the pipe
|
||||
state = P_KEY;
|
||||
|
||||
if (t == closebr) {
|
||||
break; // found the ending brace, so let's quit the kv parse loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t++;
|
||||
}
|
||||
// clean up after a messed up subs kv pairs syntax
|
||||
if (new_subst_pair != NULL) {
|
||||
free(new_subst_pair);
|
||||
}
|
||||
}
|
||||
|
||||
if (!token_valid) {
|
||||
// false match, include it in the block to send before the next token
|
||||
searchpos = openbr + 1;
|
||||
ESP_LOGD(TAG, "Skip invalid token near %10s", openbr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// now we know it looks like a substitution token
|
||||
|
||||
// flush data before the token
|
||||
if (pos != openbr) ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (openbr - pos)));
|
||||
|
||||
const char *token_start = openbr;
|
||||
|
||||
tpl_escape_t escape = TPL_ESCAPE_NONE;
|
||||
|
||||
// extract and terminate the token
|
||||
size_t token_len = MIN(MAX_TOKEN_LEN-1, closebr - openbr - 1);
|
||||
if (token_end) {
|
||||
token_len = MIN(token_len, token_end - openbr - 1);
|
||||
}
|
||||
|
||||
if (is_include) {
|
||||
token_start += 1; // skip the @
|
||||
token_len -= 1;
|
||||
} else {
|
||||
if (0 == strncmp("h:", openbr + 1, 2)) {
|
||||
escape = TPL_ESCAPE_HTML;
|
||||
token_start += 2;
|
||||
token_len -= 2;
|
||||
}
|
||||
else if (0 == strncmp("j:", openbr + 1, 2)) {
|
||||
escape = TPL_ESCAPE_JS;
|
||||
token_start += 2;
|
||||
token_len -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
strncpy(token_buf, token_start+1, token_len);
|
||||
token_buf[token_len] = 0;
|
||||
|
||||
ESP_LOGD(TAG, "Token: %s", token_buf);
|
||||
|
||||
esp_err_t rv;
|
||||
|
||||
if (is_include) {
|
||||
ESP_LOGD(TAG, "Trying to include a sub-file");
|
||||
|
||||
const struct embedded_file_info *file = NULL;
|
||||
rv = www_get_static_file(token_buf, FILE_ACCESS_PROTECTED, &file);
|
||||
if (rv != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to statically include \"%s\" in a template - %s", token_buf, esp_err_to_name(rv));
|
||||
// this will cause the token to be emitted verbatim
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Descending...");
|
||||
|
||||
// combine the two replacers
|
||||
struct stacked_replacer_context combo = {
|
||||
.replacer0 = replacer,
|
||||
.context0 = context,
|
||||
.replacer1 = tpl_kv_replacer,
|
||||
.context1 = &substitutions_head
|
||||
};
|
||||
|
||||
rv = httpd_send_template_file_struct(r, file, stacked_replacer, &combo, HTOPT_INCLUDE);
|
||||
ESP_LOGD(TAG, "...back in parent");
|
||||
}
|
||||
|
||||
// tear down the list
|
||||
tpl_kv_free(&substitutions_head);
|
||||
|
||||
if (rv != ESP_OK) {
|
||||
// just send it verbatim...
|
||||
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
|
||||
}
|
||||
} else {
|
||||
if (replacer) {
|
||||
ESP_LOGD(TAG, "Running replacer for \"%s\" with escape %d", token_buf, escape);
|
||||
rv = replacer(r, context, token_buf, escape);
|
||||
|
||||
if (rv != ESP_OK) {
|
||||
if (rv == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGD(TAG, "Token rejected");
|
||||
// optional token becomes empty string if not replaced
|
||||
if (!token_is_optional) {
|
||||
ESP_LOGD(TAG, "Not optional, keeping verbatim");
|
||||
// replacer rejected the token, keep it verbatim
|
||||
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
ESP_LOGE(TAG, "Unexpected error from replacer func: 0x%02x - %s", rv, esp_err_to_name(rv));
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Not replacer");
|
||||
// no replacer, only includes - used for 'static' files
|
||||
if (!token_is_optional) {
|
||||
ESP_LOGD(TAG, "Token not optional, keeping verbatim");
|
||||
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchpos = pos = closebr + 1;
|
||||
}
|
||||
|
||||
if (0 == (opts & HTOPT_NO_CLOSE)) {
|
||||
ESP_TRY(httpd_resp_send_chunk(r, NULL, 0));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num)
|
||||
{
|
||||
char buf[12];
|
||||
itoa(num, buf, 10);
|
||||
return tpl_kv_add(head, key, buf);
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num)
|
||||
{
|
||||
char buf[21];
|
||||
sprintf(buf, "%"PRIi64, num);
|
||||
return tpl_kv_add(head, key, buf);
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h)
|
||||
{
|
||||
char buf[IP4ADDR_STRLEN_MAX];
|
||||
ip4_addr_t addr;
|
||||
addr.addr = lwip_htonl(ip4h);
|
||||
ip4addr_ntoa_r(&addr, buf, IP4ADDR_STRLEN_MAX);
|
||||
|
||||
return tpl_kv_add(head, key, buf);
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst)
|
||||
{
|
||||
ESP_LOGD(TAG, "kv add subs %s := %s", key, subst);
|
||||
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
|
||||
if (entry == NULL) return ESP_ERR_NO_MEM;
|
||||
|
||||
assert(strlen(key) < TPL_KV_KEY_LEN);
|
||||
assert(strlen(subst) < TPL_KV_SUBST_LEN);
|
||||
|
||||
strncpy(entry->key, key, TPL_KV_KEY_LEN);
|
||||
entry->key[TPL_KV_KEY_LEN - 1] = 0;
|
||||
|
||||
strncpy(entry->subst, subst, TPL_KV_SUBST_LEN - 1);
|
||||
entry->subst[TPL_KV_KEY_LEN - 1] = 0;
|
||||
|
||||
SLIST_INSERT_HEAD(head, entry, link);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst)
|
||||
{
|
||||
ESP_LOGD(TAG, "kv add subs %s := (heap str)", key);
|
||||
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
|
||||
if (entry == NULL) return ESP_ERR_NO_MEM;
|
||||
|
||||
assert(strlen(key) < TPL_KV_KEY_LEN);
|
||||
|
||||
strncpy(entry->key, key, TPL_KV_KEY_LEN);
|
||||
entry->key[TPL_KV_KEY_LEN - 1] = 0;
|
||||
|
||||
entry->subst_heap = subst;
|
||||
|
||||
SLIST_INSERT_HEAD(head, entry, link);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...)
|
||||
{
|
||||
ESP_LOGD(TAG, "kv printf %s := %s", key, format);
|
||||
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
|
||||
if (entry == NULL) return ESP_ERR_NO_MEM;
|
||||
|
||||
assert(strlen(key) < TPL_KV_KEY_LEN);
|
||||
|
||||
strncpy(entry->key, key, TPL_KV_KEY_LEN);
|
||||
entry->key[TPL_KV_KEY_LEN - 1] = 0;
|
||||
|
||||
va_list list;
|
||||
va_start(list, format);
|
||||
vsnprintf(entry->subst, TPL_KV_SUBST_LEN, format, list);
|
||||
va_end(list);
|
||||
entry->subst[TPL_KV_KEY_LEN - 1] = 0;
|
||||
|
||||
SLIST_INSERT_HEAD(head, entry, link);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void tpl_kv_free(struct tpl_kv_list *head)
|
||||
{
|
||||
struct tpl_kv_entry *item, *next;
|
||||
SLIST_FOREACH_SAFE(item, head, link, next) {
|
||||
if (item->subst_heap) {
|
||||
free(item->subst_heap);
|
||||
item->subst_heap = NULL;
|
||||
}
|
||||
|
||||
free(item);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
||||
|
||||
#define BUF_CAP 512
|
||||
char *buf_head = malloc(BUF_CAP);
|
||||
if (!buf_head) {
|
||||
ESP_LOGE(TAG, "Malloc err");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
char *buf = buf_head;
|
||||
size_t cap = BUF_CAP;
|
||||
struct tpl_kv_entry *entry;
|
||||
|
||||
// GCC nested function
|
||||
esp_err_t send_part() {
|
||||
esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap);
|
||||
buf = buf_head; // buf is assigned to buf head
|
||||
cap = BUF_CAP;
|
||||
if (suc != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error sending buffer");
|
||||
free(buf_head);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
}
|
||||
return suc;
|
||||
}
|
||||
|
||||
SLIST_FOREACH(entry, head, link) {
|
||||
while(NULL == (buf = append(buf, entry->key, &cap))) ESP_TRY(send_part());
|
||||
while(NULL == (buf = append(buf, "\x1f", &cap))) ESP_TRY(send_part());
|
||||
|
||||
if (entry->subst_heap) {
|
||||
if (strlen(entry->subst_heap) >= BUF_CAP) {
|
||||
// send what we have
|
||||
ESP_TRY(send_part());
|
||||
esp_err_t suc = httpd_resp_send_chunk(req, entry->subst_heap, -1);
|
||||
if (suc != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error sending buffer");
|
||||
free(buf_head);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
}
|
||||
} else {
|
||||
while (NULL == (buf = append(buf, entry->subst_heap, &cap))) ESP_TRY(send_part());
|
||||
}
|
||||
} else {
|
||||
while(NULL == (buf = append(buf, entry->subst, &cap))) ESP_TRY(send_part());
|
||||
}
|
||||
|
||||
if (entry->link.sle_next) {
|
||||
while(NULL == (buf = append(buf, "\x1e", &cap))) ESP_TRY(send_part());
|
||||
}
|
||||
}
|
||||
// send leftovers
|
||||
if (buf != buf_head) {
|
||||
esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap);
|
||||
if (suc != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error sending buffer");
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
free(buf_head);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
set(COMPONENT_ADD_INCLUDEDIRS
|
||||
"include")
|
||||
|
||||
set(COMPONENT_SRCDIRS
|
||||
"src")
|
||||
|
||||
set(COMPONENT_REQUIRES tcpip_adapter esp_http_server common_utils)
|
||||
|
||||
register_component()
|
||||
@@ -0,0 +1,4 @@
|
||||
Functions enriching the HTTP server bundled with ESP-IDF.
|
||||
This package includes HTTP-related utilities, captive
|
||||
portal implementation, and a cookie-based session system with a
|
||||
key-value store and expirations.
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
COMPONENT_SRCDIRS := src
|
||||
COMPONENT_ADD_INCLUDEDIRS := include
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef HTTPD_UTILS_CAPTIVE_H
|
||||
#define HTTPD_UTILS_CAPTIVE_H
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
/**
|
||||
* Redirect if needed when a captive portal capture is detected
|
||||
*
|
||||
* @param r
|
||||
* @return ESP_OK on redirect, ESP_ERR_NOT_FOUND if not needed, other error on failure
|
||||
*/
|
||||
esp_err_t httpd_captive_redirect(httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Get URL to redirect to. WEAK.
|
||||
*
|
||||
* @param r
|
||||
* @param buf
|
||||
* @param maxlen
|
||||
* @return http(s)://foo.bar/
|
||||
*/
|
||||
esp_err_t httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen);
|
||||
|
||||
/**
|
||||
* Get captive portal domain. WEAK.
|
||||
*
|
||||
* @return foo.bar
|
||||
*/
|
||||
const char * httpd_captive_redirect_get_domain();
|
||||
|
||||
#endif //HTTPD_UTILS_CAPTIVE_H
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef HTTPD_FDIPV4_H
|
||||
#define HTTPD_FDIPV4_H
|
||||
|
||||
/**
|
||||
* Get IP address for a FD
|
||||
*
|
||||
* @param fd
|
||||
* @param[out] ipv4
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4);
|
||||
|
||||
#endif //HTTPD_FDIPV4_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef HTTPD_UTILS_REDIRECT_H
|
||||
#define HTTPD_UTILS_REDIRECT_H
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
/**
|
||||
* Redirect to other URI - sends a HTTP response from the http server
|
||||
*
|
||||
* @param r - request
|
||||
* @param uri - target uri
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri);
|
||||
|
||||
#endif // HTTPD_UTILS_REDIRECT_H
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Session store system main include file
|
||||
*
|
||||
* Created on 2019/07/13.
|
||||
*/
|
||||
|
||||
#ifndef HTTPD_UTILS_SESSION_H
|
||||
#define HTTPD_UTILS_SESSION_H
|
||||
|
||||
#include "session_kvmap.h"
|
||||
#include "session_store.h"
|
||||
|
||||
// Customary keys
|
||||
|
||||
/** Session key for OK flash message */
|
||||
#define SESS_FLASH_OK "flash_ok"
|
||||
/** Session key for error flash message */
|
||||
#define SESS_FLASH_ERR "flash_err"
|
||||
/** Session key for a "logged in" flag. Value is 1 if logged in. */
|
||||
#define SESS_AUTHED "authed"
|
||||
|
||||
// ..
|
||||
|
||||
/**
|
||||
* Redirect to /login form if unauthed,
|
||||
* but also retrieve the session key-value store for further use
|
||||
*/
|
||||
#define HTTP_GET_AUTHED_SESSION(kvstore, r) do { \
|
||||
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA); \
|
||||
if (NULL == kvstore || NULL == sess_kv_map_get(kvstore, SESS_AUTHED)) { \
|
||||
return httpd_redirect_to((r), "/login"); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* Start or resume a session without checking for authed status.
|
||||
* When started, the session cookie is added to the response immediately.
|
||||
*
|
||||
* @param[out] kvstore - a place to store the allocated or retrieved session kvmap
|
||||
* @param[in] r - request
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **kvstore, httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Redirect to the login form if unauthed.
|
||||
* This is the same as `HTTP_GET_AUTHED_SESSION`, except the kvstore variable is
|
||||
* not needed in the uri handler calling this, so it is declared internally.
|
||||
*/
|
||||
#define HTTP_REDIRECT_IF_UNAUTHED(r) do { \
|
||||
sess_kv_map_t *_kvstore; \
|
||||
HTTP_GET_AUTHED_SESSION(_kvstore, r); \
|
||||
} while(0)
|
||||
|
||||
#endif // HTTPD_UTILS_SESSION_H
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Simple key-value map for session data storage.
|
||||
* Takes care of dynamic allocation and cleanup.
|
||||
*
|
||||
* Created on 2019/01/28.
|
||||
*/
|
||||
|
||||
#ifndef SESSION_KVMAP_H
|
||||
#define SESSION_KVMAP_H
|
||||
|
||||
/**
|
||||
* Prototype for a free() func to clean up session-held objects
|
||||
*/
|
||||
typedef void (*sess_kv_free_func_t)(void *obj);
|
||||
|
||||
typedef struct sess_kv_map sess_kv_map_t;
|
||||
|
||||
#define SESS_KVMAP_KEY_LEN 16
|
||||
|
||||
/**
|
||||
* Allocate a new session key-value store
|
||||
*
|
||||
* @return the store, NULL on error
|
||||
*/
|
||||
sess_kv_map_t *sess_kv_map_alloc(void);
|
||||
|
||||
/**
|
||||
* Free the session kv store.
|
||||
*
|
||||
* @param head - store head
|
||||
*/
|
||||
void sess_kv_map_free(void *head);
|
||||
|
||||
/**
|
||||
* Get a value from the session kv store.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to get a value for
|
||||
* @return the value, or NULL if not found
|
||||
*/
|
||||
void *sess_kv_map_get(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Get and remove a value from the session store.
|
||||
*
|
||||
* The free function is not called in this case and the recipient is
|
||||
* responsible for cleaning it up correctly.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to get a value for
|
||||
* @return the value, or NULL if not found
|
||||
*/
|
||||
void * sess_kv_map_take(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Remove an entry from the session by its key name.
|
||||
* The slot is not free'd yet, but is made available for reuse.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to remove
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t sess_kv_map_remove(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Set a key value. If there is an old value stored, it will be freed by its free function and replaced by the new one.
|
||||
* Otherwise a new slot is allocated for it, or a previously released one is reused.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to assign to
|
||||
* @param value - new value
|
||||
* @param free_fn - value free func
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t sess_kv_map_set(sess_kv_map_t *head, const char *key, void *value, sess_kv_free_func_t free_fn);
|
||||
|
||||
#endif //SESSION_KVMAP_H
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Cookie-based session store
|
||||
*/
|
||||
|
||||
#ifndef SESSION_STORE_H
|
||||
#define SESSION_STORE_H
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#define SESSION_EXPIRY_TIME_S 60*30
|
||||
#define SESSION_COOKIE_NAME "SESSID"
|
||||
|
||||
/** function that frees a session data object */
|
||||
typedef void (*sess_data_free_fn_t)(void *);
|
||||
|
||||
enum session_find_action {
|
||||
SESS_DROP, SESS_GET_DATA
|
||||
};
|
||||
|
||||
/**
|
||||
* Find session and either get data, or drop it.
|
||||
*
|
||||
* @param cookie
|
||||
* @param action
|
||||
* @return
|
||||
*/
|
||||
void *session_find_and(const char *cookie, enum session_find_action action);
|
||||
|
||||
/**
|
||||
* Initialize the session store.
|
||||
* Safely empty it if initialized
|
||||
*/
|
||||
void session_store_init(void);
|
||||
|
||||
// placeholder for when no data is stored
|
||||
#define SESSION_DUMMY ((void *) 1)
|
||||
|
||||
/**
|
||||
* Create a new session. Data must not be NULL, because it wouldn't be possible
|
||||
* to distinguish between NULL value and session not found in return values.
|
||||
* It can be e.g. 1 if no data storage is needed.
|
||||
*
|
||||
* @param data - data object to attach to the session
|
||||
* @param free_fn - function that disposes of the data when the session expires
|
||||
* @return NULL on error, or the new session ID. This is a live pointer into the session structure,
|
||||
* must be copied if stored, as it can become invalid at any time
|
||||
*/
|
||||
const char *session_new(void *data, sess_data_free_fn_t free_fn);
|
||||
|
||||
/**
|
||||
* Find a session by it's ID (from a cookie)
|
||||
*
|
||||
* @param cookie - session ID string
|
||||
* @return session data (void*), or NULL
|
||||
*/
|
||||
void *session_find(const char *cookie);
|
||||
|
||||
/**
|
||||
* Loop through all sessions and drop these that expired.
|
||||
*/
|
||||
void session_drop_expired(void);
|
||||
|
||||
/**
|
||||
* Drop a session by its ID. Does nothing if not found.
|
||||
*
|
||||
* @param cookie - session ID string
|
||||
*/
|
||||
void session_drop(const char *cookie);
|
||||
|
||||
/**
|
||||
* Parse the Cookie header from a request, and do something with the corresponding session.
|
||||
*
|
||||
* To also delete the cookie, use req_delete_session_cookie(r)
|
||||
*
|
||||
* @param r - request
|
||||
* @param action - what to do with the session
|
||||
* @return session data, NULL if removed or not found
|
||||
*/
|
||||
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action);
|
||||
|
||||
/**
|
||||
* Add a header that deletes the session cookie
|
||||
*
|
||||
* @param r - request
|
||||
*/
|
||||
void httpd_resp_delete_session_cookie(httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Add a header that sets the session cookie.
|
||||
*
|
||||
* This must be called after creating a session (e.g. user logged in) to make it persistent.
|
||||
*
|
||||
* @attention NOT RE-ENTRANT, CAN'T BE USED AGAIN UNTIL THE REQUEST IT WAS CALLED FOR IS DISPATCHED.
|
||||
*
|
||||
* @param r - request
|
||||
* @param cookie - cookie ID
|
||||
*/
|
||||
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie);
|
||||
|
||||
#endif //SESSION_STORE_H
|
||||
@@ -0,0 +1,106 @@
|
||||
#include <esp_wifi_types.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#include "httpd_utils/captive.h"
|
||||
#include "httpd_utils/fd_to_ipv4.h"
|
||||
#include "httpd_utils/redirect.h"
|
||||
#include <common_utils/utils.h>
|
||||
|
||||
static const char *TAG="captive";
|
||||
|
||||
const char * __attribute__((weak))
|
||||
httpd_captive_redirect_get_domain(void)
|
||||
{
|
||||
return "fb_node.captive";
|
||||
}
|
||||
|
||||
esp_err_t __attribute__((weak))
|
||||
httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen)
|
||||
{
|
||||
buf = append(buf, "http://", &maxlen);
|
||||
buf = append(buf, httpd_captive_redirect_get_domain(), &maxlen);
|
||||
append(buf, "/", &maxlen);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t httpd_captive_redirect(httpd_req_t *r)
|
||||
{
|
||||
// must be static to survive being used in the redirect header
|
||||
static char s_buf[64];
|
||||
|
||||
wifi_mode_t mode = 0;
|
||||
esp_wifi_get_mode(&mode);
|
||||
|
||||
// Check if we have an softap interface. No point checking IPs and hostnames if the client can't be on AP.
|
||||
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_NULL) {
|
||||
goto no_redirect;
|
||||
}
|
||||
|
||||
int fd = httpd_req_to_sockfd(r);
|
||||
|
||||
tcpip_adapter_ip_info_t apip;
|
||||
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &apip);
|
||||
|
||||
u32_t client_addr;
|
||||
if(ESP_OK != fd_to_ipv4(fd, &client_addr)) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "[captive] Client addr = 0x%08x, ap addr 0x%08x, ap nmask 0x%08x",
|
||||
client_addr,
|
||||
apip.ip.addr,
|
||||
apip.netmask.addr
|
||||
);
|
||||
|
||||
// Check if client IP looks like from our AP dhcps
|
||||
if ((client_addr & apip.netmask.addr) != (apip.ip.addr & apip.netmask.addr)) {
|
||||
ESP_LOGD(TAG, "[captive] Client not in AP IP range");
|
||||
goto no_redirect;
|
||||
}
|
||||
|
||||
// Get requested hostname from the header
|
||||
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Host", s_buf, 64);
|
||||
if (rv != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[captive] No host in request?");
|
||||
goto no_redirect;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "[captive] Candidate for redirect: %s%s", s_buf, r->uri);
|
||||
|
||||
// Never redirect if host is an IP
|
||||
if (strlen(s_buf)>7) {
|
||||
bool isIP = 1;
|
||||
for (int x = 0; x < strlen(s_buf); x++) {
|
||||
if (s_buf[x] != '.' && (s_buf[x] < '0' || s_buf[x] > '9')) {
|
||||
isIP = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isIP) {
|
||||
ESP_LOGD(TAG, "[captive] Access via IP, no redirect needed");
|
||||
goto no_redirect;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect if host differs
|
||||
// - this can be e.g. connectivitycheck.gstatic.com or the equivalent for ios
|
||||
|
||||
if (0 != strcmp(s_buf, httpd_captive_redirect_get_domain())) {
|
||||
ESP_LOGD(TAG, "[captive] Host differs, redirecting...");
|
||||
|
||||
httpd_captive_redirect_get_url(r, s_buf, 64);
|
||||
return httpd_redirect_to(r, s_buf);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[captive] Host is OK");
|
||||
goto no_redirect;
|
||||
}
|
||||
|
||||
no_redirect:
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <esp_wifi_types.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "httpd_utils/fd_to_ipv4.h"
|
||||
|
||||
static const char *TAG = "fd2ipv4";
|
||||
|
||||
/**
|
||||
* Get IP address for a FD
|
||||
*
|
||||
* @param fd
|
||||
* @param[out] ipv4
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4)
|
||||
{
|
||||
struct sockaddr_in6 addr;
|
||||
size_t len = sizeof(addr);
|
||||
int rv = getpeername(fd, (struct sockaddr *) &addr, &len);
|
||||
if (rv != 0) {
|
||||
ESP_LOGE(TAG, "Failed to get IP addr for fd %d", fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint32_t client_addr = 0;
|
||||
if (addr.sin6_family == AF_INET6) {
|
||||
// this would fail in a real ipv6 network
|
||||
// with ipv4 the addr is simply in the last ipv6 byte
|
||||
struct sockaddr_in6 *s = &addr;
|
||||
client_addr = s->sin6_addr.un.u32_addr[3];
|
||||
}
|
||||
else {
|
||||
struct sockaddr_in *s = (struct sockaddr_in *) &addr;
|
||||
client_addr = s->sin_addr.s_addr;
|
||||
}
|
||||
|
||||
*ipv4 = client_addr;
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#include <esp_wifi_types.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "httpd_utils/redirect.h"
|
||||
|
||||
static const char *TAG="redirect";
|
||||
|
||||
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri)
|
||||
{
|
||||
ESP_LOGD(TAG, "Redirect to %s", uri);
|
||||
|
||||
httpd_resp_set_hdr(r, "Location", uri);
|
||||
httpd_resp_set_status(r, "303 See Other");
|
||||
httpd_resp_set_type(r, HTTPD_TYPE_TEXT);
|
||||
const char *msg = "Redirect";
|
||||
return httpd_resp_send(r, msg, -1);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
||||
|
||||
#include <rom/queue.h>
|
||||
#include <malloc.h>
|
||||
#include <assert.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <string.h>
|
||||
#include "httpd_utils/session_kvmap.h"
|
||||
|
||||
static const char *TAG = "sess_kvmap";
|
||||
|
||||
// this struct is opaque, a stub like this is sufficient for the head pointer.
|
||||
struct sess_kv_entry;
|
||||
|
||||
/** Session head structure, dynamically allocated */
|
||||
SLIST_HEAD(sess_kv_map, sess_kv_entry);
|
||||
|
||||
struct sess_kv_entry {
|
||||
SLIST_ENTRY(sess_kv_entry) link;
|
||||
char key[SESS_KVMAP_KEY_LEN];
|
||||
void *value;
|
||||
sess_kv_free_func_t free_fn;
|
||||
};
|
||||
|
||||
struct sess_kv_map *sess_kv_map_alloc(void)
|
||||
{
|
||||
ESP_LOGD(TAG, "kv store alloc");
|
||||
struct sess_kv_map *map = malloc(sizeof(struct sess_kv_map));
|
||||
assert(map);
|
||||
SLIST_INIT(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
void sess_kv_map_free(void *head_v)
|
||||
{
|
||||
struct sess_kv_map* head = head_v;
|
||||
|
||||
ESP_LOGD(TAG, "kv store free");
|
||||
assert(head);
|
||||
struct sess_kv_entry *item, *tmp;
|
||||
SLIST_FOREACH_SAFE(item, head, link, tmp) {
|
||||
if (item->free_fn) {
|
||||
item->free_fn(item->value);
|
||||
free(item);
|
||||
}
|
||||
}
|
||||
free(head);
|
||||
}
|
||||
|
||||
|
||||
void * sess_kv_map_get(struct sess_kv_map *head, const char *key)
|
||||
{
|
||||
assert(head);
|
||||
assert(key);
|
||||
ESP_LOGD(TAG, "kv store get %s", key);
|
||||
|
||||
struct sess_kv_entry *item;
|
||||
SLIST_FOREACH(item, head, link) {
|
||||
if (0==strcmp(item->key, key)) {
|
||||
ESP_LOGD(TAG, "got ok");
|
||||
return item->value;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "not found in store");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void * sess_kv_map_take(struct sess_kv_map *head, const char *key)
|
||||
{
|
||||
assert(head);
|
||||
assert(key);
|
||||
ESP_LOGD(TAG, "kv store take %s", key);
|
||||
|
||||
struct sess_kv_entry *item;
|
||||
SLIST_FOREACH(item, head, link) {
|
||||
if (0==strcmp(item->key, key)) {
|
||||
item->key[0] = 0;
|
||||
item->free_fn = NULL;
|
||||
ESP_LOGD(TAG, "taken ok");
|
||||
return item->value;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "not found in store");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_err_t sess_kv_map_remove(struct sess_kv_map *head, const char *key)
|
||||
{
|
||||
assert(head);
|
||||
assert(key);
|
||||
ESP_LOGD(TAG, "kv store remove %s", key);
|
||||
|
||||
struct sess_kv_entry *item;
|
||||
SLIST_FOREACH(item, head, link) {
|
||||
if (0==strcmp(item->key, key)) {
|
||||
if (item->free_fn) {
|
||||
item->free_fn(item->value);
|
||||
}
|
||||
item->key[0] = 0;
|
||||
item->value = NULL;
|
||||
item->free_fn = NULL;
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "couldn't remove, not found: %s", key);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t sess_kv_map_set(struct sess_kv_map *head, const char *key, void *value, sess_kv_free_func_t free_fn)
|
||||
{
|
||||
assert(head);
|
||||
assert(key);
|
||||
ESP_LOGD(TAG, "kv set value for key %s", key);
|
||||
|
||||
size_t key_len = strlen(key);
|
||||
if (key_len > SESS_KVMAP_KEY_LEN-1) {
|
||||
ESP_LOGE(TAG, "Key too long: %s", key);
|
||||
// discard illegal key
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (key_len == 0) {
|
||||
ESP_LOGE(TAG, "Key too short: \"%s\"", key);
|
||||
// discard illegal key
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
struct sess_kv_entry *item = NULL;
|
||||
struct sess_kv_entry *empty_item = NULL; // found item with no content
|
||||
SLIST_FOREACH(item, head, link) {
|
||||
ESP_LOGD(TAG, "test item with key %s, ptr %p > %p", item->key, item, item->link.sle_next);
|
||||
if (0 == item->key[0]) {
|
||||
ESP_LOGD(TAG, "found an empty slot");
|
||||
empty_item = item;
|
||||
}
|
||||
else if (0==strcmp(item->key, key)) {
|
||||
ESP_LOGD(TAG, "old value replaced");
|
||||
if (item->free_fn) {
|
||||
item->free_fn(item->value);
|
||||
}
|
||||
item->value = value;
|
||||
item->free_fn = free_fn;
|
||||
return ESP_OK;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "skip this one");
|
||||
}
|
||||
}
|
||||
|
||||
struct sess_kv_entry *new_item = NULL;
|
||||
|
||||
// insert new or reuse an empty item
|
||||
if (empty_item) {
|
||||
new_item = empty_item;
|
||||
ESP_LOGD(TAG, "empty item reused (%p)", new_item);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "alloc new item");
|
||||
// key not found, add a new entry.
|
||||
new_item = malloc(sizeof(struct sess_kv_entry));
|
||||
if (!new_item) {
|
||||
ESP_LOGE(TAG, "New entry alloc failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
strcpy(new_item->key, key);
|
||||
new_item->free_fn = free_fn;
|
||||
new_item->value = value;
|
||||
|
||||
if (!empty_item) {
|
||||
ESP_LOGD(TAG, "insert new item into list");
|
||||
// this item was malloc'd
|
||||
SLIST_INSERT_HEAD(head, new_item, link);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
||||
|
||||
#include <malloc.h>
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <rom/queue.h>
|
||||
|
||||
#include "httpd_utils/session_store.h"
|
||||
|
||||
// TODO add a limit on simultaneously open sessions (can cause memory exhaustion DoS)
|
||||
|
||||
#define COOKIE_LEN 32
|
||||
static const char *TAG = "session";
|
||||
|
||||
struct session {
|
||||
char cookie[COOKIE_LEN + 1];
|
||||
void *data;
|
||||
TickType_t last_activity_time;
|
||||
LIST_ENTRY(session) link;
|
||||
sess_data_free_fn_t free_fn;
|
||||
};
|
||||
|
||||
static LIST_HEAD(sessions_, session) s_store;
|
||||
|
||||
static SemaphoreHandle_t sess_store_lock = NULL;
|
||||
static bool sess_store_inited = false;
|
||||
|
||||
|
||||
void session_store_init(void)
|
||||
{
|
||||
if (sess_store_inited) {
|
||||
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
|
||||
{
|
||||
struct session *it, *tit;
|
||||
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
|
||||
ESP_LOGW(TAG, "Session cookie expired: \"%s\"", it->cookie);
|
||||
if (it->free_fn) it->free_fn(it->data);
|
||||
// no relink, we dont care if the list breaks after this - we're removing all of it
|
||||
free(it);
|
||||
}
|
||||
}
|
||||
LIST_INIT(&s_store);
|
||||
xSemaphoreGive(sess_store_lock);
|
||||
} else {
|
||||
LIST_INIT(&s_store);
|
||||
sess_store_lock = xSemaphoreCreateMutex();
|
||||
sess_store_inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill buffer with base64 symbols. Does not add a trailing null byte
|
||||
*
|
||||
* @param buf
|
||||
* @param len
|
||||
*/
|
||||
static void esp_fill_random_alnum(char *buf, size_t len)
|
||||
{
|
||||
#define alphabet_len 64
|
||||
static const char alphabet[alphabet_len] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
|
||||
|
||||
unsigned int seed = xTaskGetTickCount();
|
||||
|
||||
assert(buf != NULL);
|
||||
for (int i = 0; i < len; i++) {
|
||||
int index = rand_r(&seed) % alphabet_len;
|
||||
*buf++ = (uint8_t) alphabet[index];
|
||||
}
|
||||
}
|
||||
|
||||
const char *session_new(void *data, sess_data_free_fn_t free_fn)
|
||||
{
|
||||
assert(data != NULL);
|
||||
|
||||
struct session *item = malloc(sizeof(struct session));
|
||||
if (item == NULL) return NULL;
|
||||
|
||||
item->data = data;
|
||||
item->free_fn = free_fn;
|
||||
esp_fill_random_alnum(item->cookie, COOKIE_LEN);
|
||||
item->cookie[COOKIE_LEN] = 0; // add the terminator
|
||||
|
||||
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
|
||||
{
|
||||
item->last_activity_time = xTaskGetTickCount();
|
||||
|
||||
LIST_INSERT_HEAD(&s_store, item, link);
|
||||
}
|
||||
xSemaphoreGive(sess_store_lock);
|
||||
|
||||
ESP_LOGD(TAG, "New HTTP session: %s", item->cookie);
|
||||
|
||||
return item->cookie;
|
||||
}
|
||||
|
||||
void *session_find_and(const char *cookie, enum session_find_action action)
|
||||
{
|
||||
// no point in searching if the length is wrong
|
||||
if (strlen(cookie) != COOKIE_LEN) {
|
||||
ESP_LOGW(TAG, "Wrong session cookie length: \"%s\"", cookie);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct session *it = NULL;
|
||||
|
||||
bool found = false;
|
||||
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
|
||||
{
|
||||
LIST_FOREACH(it, &s_store, link) {
|
||||
if (0==strcmp(it->cookie, cookie)) {
|
||||
ESP_LOGD(TAG, "Session cookie matched: \"%s\"", cookie);
|
||||
|
||||
it->last_activity_time = xTaskGetTickCount();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found && action == SESS_DROP) {
|
||||
if (it->free_fn) it->free_fn(it->data);
|
||||
LIST_REMOVE(it, link);
|
||||
free(it);
|
||||
ESP_LOGD(TAG, "Dropped session: \"%s\"", cookie);
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(sess_store_lock);
|
||||
if (found) {
|
||||
if (action == SESS_DROP) {
|
||||
// it was dropped inside the guarded block
|
||||
// the return value is not used with DROP
|
||||
return NULL;
|
||||
}
|
||||
else if(action == SESS_GET_DATA) {
|
||||
return it->data;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Session cookie not found: \"%s\"", cookie);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *session_find(const char *cookie)
|
||||
{
|
||||
return session_find_and(cookie, SESS_GET_DATA);
|
||||
}
|
||||
|
||||
void session_drop(const char *cookie)
|
||||
{
|
||||
session_find_and(cookie, SESS_DROP);
|
||||
}
|
||||
|
||||
void session_drop_expired(void)
|
||||
{
|
||||
struct session *it;
|
||||
struct session *tit;
|
||||
|
||||
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
|
||||
{
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
|
||||
TickType_t elapsed = now - it->last_activity_time;
|
||||
if (elapsed > pdMS_TO_TICKS(SESSION_EXPIRY_TIME_S*1000)) {
|
||||
ESP_LOGD(TAG, "Session cookie expired: \"%s\"", it->cookie);
|
||||
if (it->free_fn) it->free_fn(it->data);
|
||||
LIST_REMOVE(it, link);
|
||||
free(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(sess_store_lock);
|
||||
}
|
||||
|
||||
|
||||
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action)
|
||||
{
|
||||
// this could be called periodically, but it's sufficient to run it at each request
|
||||
// it won't slow anything down unless there are hundreds of sessions
|
||||
session_drop_expired();
|
||||
|
||||
static char buf[256];
|
||||
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Cookie", buf, 256);
|
||||
if (rv == ESP_OK || rv == ESP_ERR_HTTPD_RESULT_TRUNC) {
|
||||
ESP_LOGD(TAG, "Cookie header: %s", buf);
|
||||
|
||||
// probably OK, see if we have a cookie
|
||||
char *start = strstr(buf, SESSION_COOKIE_NAME"=");
|
||||
if (start != 0) {
|
||||
start += strlen(SESSION_COOKIE_NAME"=");
|
||||
char *end = strchr(start, ';');
|
||||
if (end != NULL) *end = 0;
|
||||
|
||||
ESP_LOGD(TAG, "Cookie is: %s", start);
|
||||
return session_find_and(start, action);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "No cookie.");
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void httpd_resp_delete_session_cookie(httpd_req_t *r)
|
||||
{
|
||||
httpd_resp_set_hdr(r, "Set-Cookie", SESSION_COOKIE_NAME"=");
|
||||
}
|
||||
|
||||
|
||||
// Static because the value is passed and stored by reference, so it wouldn't live long enough if it was on stack,
|
||||
// and there also isn't any hook for freeing it if we used malloc(). This is an SDK bug.
|
||||
static char cookie_hdr_buf[COOKIE_LEN + 10];
|
||||
|
||||
// !!! this must not be called concurrently from a different thread.
|
||||
// That is no problem so long as the server stays single-threaded
|
||||
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie)
|
||||
{
|
||||
snprintf(cookie_hdr_buf, COOKIE_LEN + 10, "SESSID=%s", cookie);
|
||||
httpd_resp_set_hdr(r, "Set-Cookie", cookie_hdr_buf);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2019/07/13.
|
||||
*/
|
||||
|
||||
#ifndef SESSION_UTILS_C_H
|
||||
#define SESSION_UTILS_C_H
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_server.h>
|
||||
#include "httpd_utils/session_kvmap.h"
|
||||
#include "httpd_utils/session_store.h"
|
||||
|
||||
/**
|
||||
* Start or resume a session.
|
||||
*/
|
||||
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **ppkvstore, httpd_req_t *r)
|
||||
{
|
||||
sess_kv_map_t *kvstore;
|
||||
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA);
|
||||
if (NULL == kvstore) {
|
||||
kvstore = sess_kv_map_alloc();
|
||||
if (!kvstore) return ESP_ERR_NO_MEM;
|
||||
|
||||
const char *cookie = session_new(kvstore, sess_kv_map_free);
|
||||
if (cookie == NULL) {
|
||||
// session alloc failed
|
||||
sess_kv_map_free(kvstore);
|
||||
*ppkvstore = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
httpd_resp_set_session_cookie(r, cookie);
|
||||
}
|
||||
|
||||
*ppkvstore = kvstore;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
#endif //SESSION_UTILS_C_H
|
||||
@@ -1,4 +1,34 @@
|
||||
set(COMPONENT_SRCS "app_main.c" "nokia.c" "knob.c" "gui.c")
|
||||
set(COMPONENT_ADD_INCLUDEDIRS "")
|
||||
set(COMPONENT_SRCS
|
||||
"app_main.c"
|
||||
"knob.c"
|
||||
"liquid/gui.c"
|
||||
"analog.c"
|
||||
"liquid/liquid.c"
|
||||
"scenes/scene_root.c"
|
||||
"scenes/scene_bootanim.c"
|
||||
"scenes/scene_menu.c"
|
||||
"scenes/scene_number.c"
|
||||
"scenes/scene_manual_menu.c"
|
||||
"scenes/scene_popup_menu.c"
|
||||
"scenes/scene_popup_test.c"
|
||||
"graphics/nokia.c"
|
||||
"graphics/utf8.c"
|
||||
"graphics/font.c"
|
||||
"graphics/drawing.c"
|
||||
"graphics/bitmaps.c"
|
||||
"web/websrv.c"
|
||||
"files/files_enum.c"
|
||||
"utils.c"
|
||||
"arduinopid.c"
|
||||
"firehazard.c"
|
||||
)
|
||||
set(COMPONENT_ADD_INCLUDEDIRS "." "liquid" "graphics")
|
||||
|
||||
#begin staticfiles
|
||||
# generated by rebuild_file_tables
|
||||
set(COMPONENT_EMBED_FILES
|
||||
"files/embed/favicon.ico"
|
||||
"files/embed/index.html")
|
||||
#end staticfiles
|
||||
|
||||
register_component()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include <stdint.h>
|
||||
#include <stdatomic.h>
|
||||
#include "analog.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/adc.h"
|
||||
#include "esp_adc_cal.h"
|
||||
#include "firehazard.h"
|
||||
|
||||
static esp_adc_cal_characteristics_t *adc_chars;
|
||||
|
||||
// 32-39
|
||||
static const adc1_channel_t channel = ADC1_CHANNEL_4; //GPIO34 if ADC1, GPIO14 if ADC2
|
||||
|
||||
static float measurement_celsius;
|
||||
static const adc_atten_t atten = ADC_ATTEN_DB_0;
|
||||
static const adc_unit_t unit = ADC_UNIT_1;
|
||||
|
||||
float reg_meas_history[REG_HISTORY_LEN] = {};
|
||||
float reg_tset_history[REG_HISTORY_LEN] = {};
|
||||
uint32_t history_counter = 0;
|
||||
|
||||
static void analog_service(void *arg);
|
||||
|
||||
static TaskHandle_t hAnalog;
|
||||
|
||||
#define DEFAULT_VREF 1095
|
||||
#define NO_OF_SAMPLES 128
|
||||
|
||||
void analog_init() {
|
||||
printf("Analog init\n");
|
||||
|
||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
||||
adc1_config_channel_atten(channel, atten);
|
||||
adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
|
||||
esp_adc_cal_characterize(unit, atten, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
|
||||
|
||||
int rv = xTaskCreate(analog_service, "analog", 4096, NULL, 6, &hAnalog);
|
||||
assert (rv == pdPASS);
|
||||
}
|
||||
|
||||
#define TSENSE_LOOKUP_LEN 81
|
||||
#define TSENSE_T_STEP 5
|
||||
#define TSENSE_T_MIN 0
|
||||
#define TSENSE_T_MAX 400
|
||||
static const float TSENSE_LOOKUP[TSENSE_LOOKUP_LEN] = {
|
||||
// 4k7
|
||||
//0.067859346082665f, 0.069156572911158f, 0.070450833857595f, 0.07174213479836f, 0.073030481589859f, 0.074315880068592f, 0.075598336051229f, 0.076877855334685f, 0.078154443696192f, 0.079428106893372f, 0.080698850664312f, 0.081966680727637f, 0.083231602782579f, 0.084493622509052f, 0.085752745567722f, 0.087008977600079f, 0.088262324228509f, 0.089512791056363f, 0.090760383668026f, 0.092005107628991f, 0.093246968485926f, 0.094485971766743f, 0.095722122980667f, 0.096955427618307f, 0.098185891151722f, 0.099413519034488f, 0.10063831670177f, 0.101860289570385f, 0.10307944303887f, 0.10429578248755f, 0.105509313278605f, 0.106720040756132f, 0.107927970246218f, 0.109133107056997f, 0.110335456478721f, 0.111535023783824f, 0.112731814226983f, 0.113925833045189f, 0.115117085457804f, 0.116305576666627f, 0.117491311855962f, 0.118674296192672f, 0.119854534826251f, 0.12103203288888f, 0.122206795495492f, 0.123378827743833f, 0.124548134714525f, 0.125714721471126f, 0.12687859306019f, 0.128039754511331f, 0.129198210837281f, 0.13035396703395f, 0.131507028080486f, 0.132657398939339f, 0.133805084556313f, 0.134950089860629f, 0.136092419764986f, 0.137232079165615f, 0.138369072942339f, 0.139503405958634f, 0.14063508306168f, 0.141764109082427f, 0.142890488835645f, 0.144014227119983f, 0.145135328718029f, 0.146253798396361f, 0.147369640905609f, 0.148482860980504f, 0.14959346333994f, 0.150701452687026f, 0.151806833709142f, 0.152909611077994f, 0.154009789449667f, 0.155107373464683f, 0.156202367748052f, 0.157294776909324f, 0.15838460554265f, 0.159471858226827f, 0.160556539525357f, 0.161638653986497f, 0.162718206143312f
|
||||
// 2k7
|
||||
//0.118961788031723f,0.121199278149888f,0.123430305551104f,0.125654890818048f,0.12787305443543f,0.130084816790549f,0.132290198173841f,0.134489218779432f,0.13668189870568f,0.138868257955715f,0.141048316437981f,0.143222093966762f,0.145389610262718f,0.147550884953408f,0.149705937573814f,0.151854787566858f,0.153997454283918f,0.156133956985341f,0.158264314840951f,0.160388546930552f,0.162506672244433f,0.164618709683859f,0.166724678061576f,0.168824596102292f,0.170918482443171f,0.173006355634316f,0.17508823413925f,0.177164136335394f,0.179234080514542f,0.181298084883333f,0.183356167563718f,0.185408346593427f,0.187454639926429f,0.189495065433395f,0.191529640902147f,0.19355838403812f,0.195581312464802f,0.197598443724189f,0.199609795277224f,0.20161538450424f,0.203615228705396f,0.205609345101115f,0.207597750832512f,0.209580462961826f,0.211557498472848f,0.213528874271338f,0.215494607185455f,0.217454713966165f,0.219409211287665f,0.221358115747788f,0.223301443868418f,0.225239212095895f,0.227171436801418f,0.229098134281449f,0.231019320758111f,0.232935012379582f,0.234845225220494f,0.236749975282318f,0.238649278493758f,0.240543150711133f,0.242431607718764f,0.24431466522935f,0.246192338884353f,0.248064644254368f,0.249931596839502f,0.25179321206974f,0.253649505305317f,0.255500491837083f,0.257346186886869f,0.259186605607846f,0.261021763084885f,0.262851674334914f,0.264676354307276f,0.266495817884073f,0.268310079880525f,0.270119155045314f,0.271923058060928f,0.273721803544007f,0.275515406045682f,0.277303880051916f,0.27908723998384f
|
||||
0.118709444844989f,0.120942188771995f,0.123168483690844f,0.125388350140555f,0.127601808562385f,0.129808879300387f,0.132009582601957f,0.134203938618385f,0.136391967405395f,0.138573688923688f,0.140749123039476f,0.142918289525014f,0.145081208059131f,0.14723789822775f,0.149388379524415f,0.151532671350807f,0.153670793017255f,0.155802763743251f,0.157928602657955f,0.1600483288007f,0.16216196112149f,0.1642695184815f,0.166371019653566f,0.168466483322681f,0.170555928086473f,0.172639372455698f,0.174716834854712f,0.176788333621955f,0.178853887010421f,0.180913513188126f,0.182967230238583f,0.185015056161259f,0.18705700887204f,0.189093106203687f,0.191123365906294f,0.193147805647736f,0.195166443014119f,0.197179295510229f,0.19918638055997f,0.201187715506807f,0.203183317614203f,0.205173204066052f,0.207157391967109f,0.209135898343422f,0.211108740142754f,0.213075934235005f,0.215037497412637f,0.216993446391085f,0.218943797809176f,0.220888568229535f,0.222827774139f,0.224761431949025f,0.226689557996082f,0.228612168542064f,0.230529279774684f,0.232440907807868f,0.234347068682147f,0.236247778365053f,0.238143052751499f,0.24003290766417f,0.241917358853906f,0.243796422000076f,0.245670112710962f,0.247538446524132f,0.249401438906812f,0.251259105256259f,0.253111460900123f,0.254958521096822f,0.256800301035897f,0.258636815838374f,0.260468080557129f,0.262294110177234f,0.264114919616321f,0.265930523724925f,0.267740937286839f,0.269546175019461f,0.271346251574132f,0.273141181536489f,0.274930979426797f,0.276715659700291f,0.278495236747511f
|
||||
};
|
||||
|
||||
static float v_to_c(float v){
|
||||
// TODO use binary search.. lol
|
||||
for (int i = 1; i < TSENSE_LOOKUP_LEN; i++) {
|
||||
float cur = TSENSE_LOOKUP[i];
|
||||
if (cur >= v) {
|
||||
float prev = TSENSE_LOOKUP[i-1];
|
||||
|
||||
float ratio = (v - prev) / (cur - prev);
|
||||
return TSENSE_T_MIN + ((float) i + ratio) * TSENSE_T_STEP;
|
||||
}
|
||||
}
|
||||
|
||||
return TSENSE_T_MAX;
|
||||
}
|
||||
|
||||
static void __attribute__((noreturn)) analog_service(void *arg) {
|
||||
while (1) {
|
||||
uint32_t adc_reading = 0;
|
||||
//Multisampling
|
||||
for (int i = 0; i < NO_OF_SAMPLES; i++) {
|
||||
adc_reading += adc1_get_raw(channel);
|
||||
}
|
||||
adc_reading /= NO_OF_SAMPLES;
|
||||
|
||||
//Convert adc_reading to voltage in mV
|
||||
uint32_t mv = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);
|
||||
|
||||
#define CORRECT -10;
|
||||
mv += CORRECT;
|
||||
|
||||
printf("Raw: %d ... Voltage: %dmV ...", adc_reading, mv);
|
||||
|
||||
float volts = mv * 0.001f;
|
||||
|
||||
float celsius = v_to_c(volts);
|
||||
printf("Celsius: %.1f°C\n", celsius);
|
||||
|
||||
measurement_celsius = celsius;
|
||||
|
||||
for (int i = 0; i < REG_HISTORY_LEN-1; i++) {
|
||||
reg_meas_history[i] = reg_meas_history[i+1];
|
||||
reg_tset_history[i] = reg_tset_history[i+1];
|
||||
}
|
||||
reg_meas_history[REG_HISTORY_LEN-1] = celsius;
|
||||
reg_tset_history[REG_HISTORY_LEN-1] = fire_get_setpoint(true);
|
||||
history_counter = (history_counter + 1) % 20;
|
||||
|
||||
fire_regulate(celsius);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(ANALOG_SAMPLE_TIME_MS));
|
||||
}
|
||||
}
|
||||
|
||||
float analog_read() {
|
||||
return measurement_celsius;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2020/01/03.
|
||||
*/
|
||||
|
||||
#ifndef REFLOWER_ANALOG_H
|
||||
#define REFLOWER_ANALOG_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define ANALOG_SAMPLE_TIME_MS 1000
|
||||
|
||||
#define REG_HISTORY_LEN 241
|
||||
extern float reg_meas_history[REG_HISTORY_LEN];
|
||||
extern float reg_tset_history[REG_HISTORY_LEN];
|
||||
extern uint32_t history_counter;
|
||||
|
||||
void analog_init();
|
||||
|
||||
float analog_read();
|
||||
|
||||
#endif //REFLOWER_ANALOG_H
|
||||
@@ -7,18 +7,90 @@
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <web/websrv.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_log.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_spi_flash.h"
|
||||
#include "nokia.h"
|
||||
#include "graphics/nokia.h"
|
||||
#include "knob.h"
|
||||
#include "gui.h"
|
||||
#include "analog.h"
|
||||
#include "utils.h"
|
||||
#include "esp_event_loop.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "firehazard.h"
|
||||
|
||||
/**
|
||||
* Application event handler
|
||||
*
|
||||
* @param ctx - ignored, context passed to esp_event_loop_init()
|
||||
* @param event
|
||||
* @return success
|
||||
*/
|
||||
static esp_err_t event_handler(void *ctx, system_event_t *event)
|
||||
{
|
||||
switch (event->event_id) {
|
||||
case SYSTEM_EVENT_STA_START:
|
||||
try_reconn_if_have_wifi_creds();
|
||||
break;
|
||||
|
||||
case SYSTEM_EVENT_STA_CONNECTED:
|
||||
// we should get an IP address soon
|
||||
break;
|
||||
|
||||
case SYSTEM_EVENT_STA_GOT_IP:
|
||||
// xEventGroupSetBits(g_wifi_event_group, EG_WIFI_CONNECTED_BIT);
|
||||
break;
|
||||
|
||||
case SYSTEM_EVENT_STA_DISCONNECTED:
|
||||
// xEventGroupClearBits(g_wifi_event_group, EG_WIFI_CONNECTED_BIT);
|
||||
try_reconn_if_have_wifi_creds();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// dhcp_watchdog_notify(dhcp_wd, event->event_id);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize WiFi in station mode, try to connect if settings are stored.
|
||||
* Set up WiFi event group & start related tasks
|
||||
*/
|
||||
static void initialise_wifi(void)
|
||||
{
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
}
|
||||
|
||||
|
||||
void __attribute__((noreturn)) app_main()
|
||||
{
|
||||
printf("Hello world!\n");
|
||||
// Initialize NVS
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
// NVS partition was truncated and needs to be erased
|
||||
// Retry nvs_flash_init
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK( err );
|
||||
|
||||
|
||||
//ESP_ERROR_CHECK(esp_register_shutdown_handler(cspemu_run_shutdown_handlers));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));
|
||||
|
||||
gpio_config_t output = {
|
||||
.pin_bit_mask = (1<<22),
|
||||
@@ -26,10 +98,31 @@ void __attribute__((noreturn)) app_main()
|
||||
};
|
||||
gpio_config(&output);
|
||||
|
||||
tcpip_adapter_init();
|
||||
initialise_wifi();
|
||||
|
||||
#if 0
|
||||
// TODO add proper join procedure
|
||||
const char *ssid = "Chlivek_2.4G";
|
||||
const char *pass = "slepice123";
|
||||
wifi_config_t wifi_config = {};
|
||||
strncpy((char*) wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
|
||||
if (pass) {
|
||||
strncpy((char*) wifi_config.sta.password, pass, sizeof(wifi_config.sta.password));
|
||||
}
|
||||
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
|
||||
ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) );
|
||||
ESP_ERROR_CHECK( esp_wifi_disconnect() );
|
||||
ESP_ERROR_CHECK( esp_wifi_connect() );
|
||||
#endif
|
||||
|
||||
gui_init();
|
||||
|
||||
knob_init();
|
||||
analog_init();
|
||||
|
||||
try_reconn_if_have_wifi_creds();
|
||||
websrv_init();
|
||||
fire_init();
|
||||
|
||||
bool level = 0;
|
||||
while (1) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "arduinopid.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
static void clampOutput(struct PID *self) {
|
||||
if (self->Output > self->outMax) self->Output = self->outMax;
|
||||
else if (self->Output < self->outMin) self->Output = self->outMin;
|
||||
}
|
||||
|
||||
static void clampIterm(struct PID *self) {
|
||||
if (self->ITerm > self->outMax) self->ITerm = self->outMax;
|
||||
else if (self->ITerm < self->outMin) self->ITerm = self->outMin;
|
||||
}
|
||||
|
||||
void PID_Compute(struct PID *self, float Input)
|
||||
{
|
||||
if (!self->ctlMode) return;
|
||||
self->Input = Input;
|
||||
|
||||
uint32_t now = xTaskGetTickCount();
|
||||
int32_t timeChange = (now - self->lastTime);
|
||||
if (timeChange >= self->SampleTime) {
|
||||
/*Compute all the working error variables*/
|
||||
float error = self->Setpoint - Input;
|
||||
self->ITerm += (self->ki * error);
|
||||
|
||||
clampIterm(self);
|
||||
|
||||
float dInput = (Input - self->lastInput);
|
||||
|
||||
/*Compute PID Output*/
|
||||
self->Output = self->kp * error + self->ITerm - self->kd * dInput;
|
||||
|
||||
clampOutput(self);
|
||||
|
||||
/*Remember some variables for next time*/
|
||||
self->lastInput = Input;
|
||||
self->lastTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
void PID_SetSetpoint(struct PID *self, float Setpoint)
|
||||
{
|
||||
self->Setpoint = Setpoint;
|
||||
}
|
||||
|
||||
void PID_SetTunings(struct PID *self, float Kp, float Ki, float Kd)
|
||||
{
|
||||
if (Kp < 0 || Ki < 0 || Kd < 0) return;
|
||||
|
||||
float SampleTimeInSec = ((float) self->SampleTime) / 1000;
|
||||
self->kp = Kp;
|
||||
self->ki = Ki * SampleTimeInSec;
|
||||
self->kd = Kd / SampleTimeInSec;
|
||||
|
||||
if (self->controllerDirection == PID_REVERSE) {
|
||||
self->kp = -self->kp;
|
||||
self->ki = -self->ki;
|
||||
self->kd = -self->kd;
|
||||
}
|
||||
}
|
||||
|
||||
void PID_SetSampleTime(struct PID *self, uint32_t NewSampleTime)
|
||||
{
|
||||
if (NewSampleTime > 0) {
|
||||
float ratio = (float) NewSampleTime
|
||||
/ (float) self->SampleTime;
|
||||
self->ki *= ratio;
|
||||
self->kd /= ratio;
|
||||
self->SampleTime = (uint32_t) NewSampleTime;
|
||||
}
|
||||
}
|
||||
|
||||
void PID_SetOutputLimits(struct PID *self, float Min, float Max)
|
||||
{
|
||||
if (Min > Max) return;
|
||||
self->outMin = Min;
|
||||
self->outMax = Max;
|
||||
|
||||
clampOutput(self);
|
||||
clampIterm(self);
|
||||
}
|
||||
|
||||
void PID_SetCtlMode(struct PID *self, enum PIDCtlMode Mode)
|
||||
{
|
||||
bool newAuto = (Mode == PID_AUTOMATIC);
|
||||
if (newAuto == !self->ctlMode) { /*we just went from manual to auto*/
|
||||
PID_Initialize(self);
|
||||
}
|
||||
self->ctlMode = newAuto;
|
||||
}
|
||||
|
||||
void PID_Initialize(struct PID *self)
|
||||
{
|
||||
self->lastInput = self->Input;
|
||||
self->ITerm = self->Output;
|
||||
|
||||
clampIterm(self);
|
||||
}
|
||||
|
||||
void PID_SetControllerDirection(struct PID *self, enum PIDDirection Direction)
|
||||
{
|
||||
self->controllerDirection = Direction;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* adapted from the Arduino PID library
|
||||
*
|
||||
* Created on 2020/01/08.
|
||||
*/
|
||||
|
||||
#ifndef ARDUINOPID_H
|
||||
#define ARDUINOPID_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum PIDCtlMode {
|
||||
PID_MANUAL = 0,
|
||||
PID_AUTOMATIC = 1,
|
||||
};
|
||||
|
||||
enum PIDDirection {
|
||||
PID_DIRECT = 0,
|
||||
PID_REVERSE = 1,
|
||||
};
|
||||
|
||||
struct PID {
|
||||
/*working variables*/
|
||||
uint32_t lastTime;
|
||||
float Input, Output, Setpoint;
|
||||
float ITerm, lastInput;
|
||||
float kp, ki, kd;
|
||||
uint32_t SampleTime; //1 sec = 1000
|
||||
float outMin, outMax;
|
||||
enum PIDCtlMode ctlMode; // false
|
||||
enum PIDDirection controllerDirection;
|
||||
};
|
||||
|
||||
#define PID_DEFAULT() { .SampleTime = 1000, .ctlMode=PID_MANUAL, .controllerDirection=PID_DIRECT }
|
||||
|
||||
void PID_Compute(struct PID *self, float Input);
|
||||
|
||||
void PID_SetTunings(struct PID *self, float Kp, float Ki, float Kd);
|
||||
|
||||
void PID_SetSampleTime(struct PID *self, uint32_t NewSampleTime);
|
||||
|
||||
void PID_SetOutputLimits(struct PID *self, float Min, float Max);
|
||||
|
||||
void PID_SetCtlMode(struct PID *self, enum PIDCtlMode Mode);
|
||||
|
||||
void PID_Initialize(struct PID *self);
|
||||
|
||||
void PID_SetSetpoint(struct PID *self, float Setpoint);
|
||||
|
||||
void PID_SetControllerDirection(struct PID *self, enum PIDDirection Direction);
|
||||
|
||||
#endif //ARDUINOPID_H
|
||||
@@ -0,0 +1,54 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 440">
|
||||
<style>
|
||||
.ticks, .frame {
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
path.major {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.ylabels text {
|
||||
font-size: 10px;
|
||||
text-anchor: end;
|
||||
font-family: Droid Sans, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.grid {
|
||||
stroke-dasharray: 4;
|
||||
stroke: #999;
|
||||
}
|
||||
|
||||
.series {
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(50,15)">
|
||||
<path d="M100,0 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400"
|
||||
class="grid" transform="translate(0,0)" id="grid-v" />
|
||||
<path d="M0,100 h600m-600,100 h600m-600,100 h600m-600,100"
|
||||
class="grid" stroke-dashoffset="0" id="grid-h" />
|
||||
<path d="M-10,0 h10m-10,100 h10m-10,100 h10m-10,100 h10m-10,100 h10" class="ticks" />
|
||||
<path d="M-5,10
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5" class="ticks" />
|
||||
<g class="series">
|
||||
<path d="M0,400L600,0" stroke="blue" id="ser-set" />
|
||||
<path d="M0,0L300,100L600,400" stroke="red" id="ser-actual" />
|
||||
</g>
|
||||
<path d="M0,0h600v400h-600Z" class="frame" />
|
||||
<g class="ylabels" transform="translate(0,3)">
|
||||
<text x="-15" y="0">400 °C</text>
|
||||
<text x="-15" y="100">300 °C</text>
|
||||
<text x="-15" y="200">200 °C</text>
|
||||
<text x="-15" y="300">100 °C</text>
|
||||
<text x="-15" y="400">0 °C</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Breadflow Web Control</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#ctab {
|
||||
width: 1400px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#td-side {
|
||||
border-left: 1px solid #ccc;
|
||||
width: 340px;
|
||||
padding: 15px;
|
||||
vertical-align: top;
|
||||
padding-top: 22px;
|
||||
}
|
||||
#td-img {
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Breadflow {version}</h1>
|
||||
|
||||
<table id="ctab">
|
||||
<tr>
|
||||
<td id="td-img">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 430">
|
||||
<style>
|
||||
.ticks, .frame {
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.ylabels text {
|
||||
font-size: 10px;
|
||||
text-anchor: start;
|
||||
font-family: Droid Sans, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.grid {
|
||||
stroke-dasharray: 2;
|
||||
stroke: #dbdbdb;
|
||||
}
|
||||
|
||||
.series {
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(10,15)">
|
||||
<path d="M100,0 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400"
|
||||
class="grid" transform="translate(0,0)" id="grid-v" />
|
||||
<path d="M0,100 h600m-600,100 h600m-600,100 h600m-600,100"
|
||||
class="grid" stroke-dashoffset="0" id="grid-h" />
|
||||
<path d="M600,0 h10m-10,100 h10m-10,100 h10m-10,100 h10m-10,100 h10" class="ticks" />
|
||||
<path d="M600,10
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
|
||||
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5" class="ticks" />
|
||||
<g class="series">
|
||||
<path d="{ser-set}" stroke="blue" id="ser-set" /><!--M0,400L600,0-->
|
||||
<path d="{ser-act}" stroke="red" id="ser-act" /><!--M0,0L300,100L600,400-->
|
||||
</g>
|
||||
<path d="M0,0h600v400h-600Z" class="frame" />
|
||||
<g class="ylabels" transform="translate(630,3)">
|
||||
<text x="-15" y="0">400 °C</text>
|
||||
<text x="-15" y="100">300 °C</text>
|
||||
<text x="-15" y="200">200 °C</text>
|
||||
<text x="-15" y="300">100 °C</text>
|
||||
<text x="-15" y="400">0 °C</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</td>
|
||||
<td id="td-side">
|
||||
t<sub>sens</sub> = <span id="temp">{temp}</span>°C<br>
|
||||
|
||||
<form action="/set" method="POST">
|
||||
<h3>Oven Control</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Heater:</th>
|
||||
<td><select name="fire">
|
||||
<option value="0" {fire_dis_ck}>Disable</option>
|
||||
<option value="1" {fire_en_ck}>Enable</option>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>t<sub>set</sub> =</th>
|
||||
<td><input type="number" step="1" name="tset" value="{tset}"></td>
|
||||
</tr>
|
||||
<tr><td></td><td><input type="submit" value="Set"></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<form action="/set" method="POST">
|
||||
<h3>PID tuning</h3>
|
||||
<table>
|
||||
<tr><th>Kp = </th><td><input type="number" step="any" name="kp" value="{kp}"></td></tr>
|
||||
<tr><th>Ki = </th><td><input type="number" step="any" name="ki" value="{ki}"></td></tr>
|
||||
<tr><th>Kd = </th><td><input type="number" step="any" name="kd" value="{kd}"></td></tr>
|
||||
<tr><td></td><td><input type="submit" value="Set"></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
let Qi = function (x) { return document.getElementById(x) };
|
||||
const REFR_TIME = 1000;
|
||||
function update(data) {
|
||||
if (data) {
|
||||
let rows = data.split('\x1e');
|
||||
rows.forEach(function (v) {
|
||||
let [k, va] = v.split('\x1f');
|
||||
switch (k) {
|
||||
case 'ser-set':
|
||||
Qi('ser-set').setAttribute('d', va);
|
||||
break;
|
||||
case 'ser-act':
|
||||
Qi('ser-act').setAttribute('d', va);
|
||||
break;
|
||||
case 'timeshift':
|
||||
Qi('grid-v').setAttribute('transform', 'translate(-'+(va*5)+',0)');
|
||||
Qi('grid-h').setAttribute('stroke-dashoffset', -va * 5);
|
||||
break;
|
||||
case 'temp':
|
||||
Qi('temp').innerHTML = va;
|
||||
break;
|
||||
// form fields are not live updated.
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let xhr=new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState===4){
|
||||
if (xhr.status===200) {
|
||||
update(xhr.responseText);
|
||||
}
|
||||
setTimeout(update, REFR_TIME);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
setTimeout(update, REFR_TIME);
|
||||
};
|
||||
xhr.open('GET', '/data');
|
||||
xhr.send();
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(update, REFR_TIME);
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
// Generated by 'rebuild_file_tables'
|
||||
#include <stdint.h>
|
||||
#include "files_enum.h"
|
||||
|
||||
extern const uint8_t _binary_favicon_ico_start[];
|
||||
extern const uint8_t _binary_favicon_ico_end[];
|
||||
extern const uint8_t _binary_index_html_start[];
|
||||
extern const uint8_t _binary_index_html_end[];
|
||||
|
||||
const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
|
||||
[FILE_FAVICON_ICO] = {_binary_favicon_ico_start, _binary_favicon_ico_end, "favicon.ico", "image/vnd.microsoft.icon"},
|
||||
[FILE_INDEX_HTML] = {_binary_index_html_start, _binary_index_html_end, "index.html", "text/html"},
|
||||
};
|
||||
|
||||
const size_t EMBEDDED_FILE_LOOKUP_LEN = 2;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Generated by 'rebuild_file_tables'
|
||||
|
||||
#ifndef _EMBEDDED_FILES_ENUM_H
|
||||
#define _EMBEDDED_FILES_ENUM_H
|
||||
|
||||
#include "fileserver/embedded_files.h"
|
||||
|
||||
enum embedded_file_id {
|
||||
FILE_FAVICON_ICO = 0,
|
||||
FILE_INDEX_HTML = 1
|
||||
};
|
||||
|
||||
#endif // _EMBEDDED_FILES_ENUM_H
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
|
||||
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
|
||||
|
||||
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
|
||||
|
||||
|
||||
// List all files
|
||||
$files = scandir(__DIR__.'/embed');
|
||||
|
||||
$files = array_filter(array_map(function ($f) {
|
||||
if (!is_file(__DIR__.'/embed/'.$f)) return null;
|
||||
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh|\.ignore\..*$/', $f)) return null;
|
||||
|
||||
echo "Found: $f\n";
|
||||
return $f;
|
||||
}, $files));
|
||||
|
||||
sort($files);
|
||||
|
||||
$formatted = array_filter(array_map(function ($f) {
|
||||
return "\"files/embed/$f\"";
|
||||
}, $files));
|
||||
|
||||
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
|
||||
|
||||
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
|
||||
"#begin staticfiles\n".
|
||||
"# generated by rebuild_file_tables\n".
|
||||
"set(COMPONENT_EMBED_FILES\n ".
|
||||
implode("\n ", $formatted) . ")\n".
|
||||
"#end staticfiles",
|
||||
$cmake);
|
||||
|
||||
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
|
||||
|
||||
|
||||
// Generate a list of files
|
||||
|
||||
$num = 0;
|
||||
$enum_keys = array_map(function ($f) use(&$num) {
|
||||
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
|
||||
return 'FILE_'. $a.' = '.($num++);
|
||||
}, $files);
|
||||
|
||||
$keylist = implode(",\n ", $enum_keys);
|
||||
|
||||
$struct_array = [];
|
||||
|
||||
$externs = array_map(function ($f) use (&$struct_array) {
|
||||
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
|
||||
|
||||
$start = '_binary_'. strtolower($a).'_start';
|
||||
$end = '_binary_'. strtolower($a).'_end';
|
||||
|
||||
static $mimes = array(
|
||||
'txt' => 'text/plain',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'php' => 'text/html',
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'xml' => 'application/xml',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'flv' => 'video/x-flv',
|
||||
|
||||
'pem' => 'application/x-pem-file',
|
||||
|
||||
// images
|
||||
'png' => 'image/png',
|
||||
'jpe' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'bmp' => 'image/bmp',
|
||||
'ico' => 'image/vnd.microsoft.icon',
|
||||
'tiff' => 'image/tiff',
|
||||
'tif' => 'image/tiff',
|
||||
'svg' => 'image/svg+xml',
|
||||
'svgz' => 'image/svg+xml',
|
||||
|
||||
// archives
|
||||
'zip' => 'application/zip',
|
||||
'rar' => 'application/x-rar-compressed',
|
||||
'exe' => 'application/x-msdownload',
|
||||
'msi' => 'application/x-msdownload',
|
||||
'cab' => 'application/vnd.ms-cab-compressed',
|
||||
|
||||
// audio/video
|
||||
'mp3' => 'audio/mpeg',
|
||||
'qt' => 'video/quicktime',
|
||||
'mov' => 'video/quicktime',
|
||||
|
||||
// adobe
|
||||
'pdf' => 'application/pdf',
|
||||
'psd' => 'image/vnd.adobe.photoshop',
|
||||
'ai' => 'application/postscript',
|
||||
'eps' => 'application/postscript',
|
||||
'ps' => 'application/postscript',
|
||||
|
||||
// ms office
|
||||
'doc' => 'application/msword',
|
||||
'rtf' => 'application/rtf',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
|
||||
// open office
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
);
|
||||
|
||||
$parts = explode('.', $f);
|
||||
$suffix = end($parts);
|
||||
$mime = $mimes[$suffix] ?? 'application/octet-stream';
|
||||
|
||||
$len = filesize('embed/'.$f);
|
||||
|
||||
$struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},";
|
||||
|
||||
return
|
||||
'extern const uint8_t '.$start.'[];'."\n".
|
||||
'extern const uint8_t '.$end.'[];';
|
||||
}, $files);
|
||||
|
||||
$externlist = implode("\n", $externs);
|
||||
$structlist = implode("\n ", $struct_array);
|
||||
|
||||
|
||||
file_put_contents('files_enum.h', <<<FILE
|
||||
// Generated by 'rebuild_file_tables'
|
||||
|
||||
#ifndef _EMBEDDED_FILES_ENUM_H
|
||||
#define _EMBEDDED_FILES_ENUM_H
|
||||
|
||||
#include "fileserver/embedded_files.h"
|
||||
|
||||
enum embedded_file_id {
|
||||
$keylist
|
||||
};
|
||||
|
||||
#endif // _EMBEDDED_FILES_ENUM_H
|
||||
|
||||
FILE
|
||||
);
|
||||
|
||||
$files_count = count($struct_array);
|
||||
file_put_contents("files_enum.c", <<<FILE
|
||||
// Generated by 'rebuild_file_tables'
|
||||
#include <stdint.h>
|
||||
#include "files_enum.h"
|
||||
|
||||
$externlist
|
||||
|
||||
const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
|
||||
$structlist
|
||||
};
|
||||
|
||||
const size_t EMBEDDED_FILE_LOOKUP_LEN = $files_count;
|
||||
|
||||
FILE
|
||||
);
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "firehazard.h"
|
||||
#include "arduinopid.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <math.h>
|
||||
#include <esp_log.h>
|
||||
#include <nvs.h>
|
||||
#include "driver/ledc.h"
|
||||
#include "esp_err.h"
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "fire";
|
||||
|
||||
static struct PID pid = PID_DEFAULT();
|
||||
|
||||
static void pwm_init(void);
|
||||
static void pwm_set(float duty);
|
||||
|
||||
void fire_get_tuning(float *kp, float *ki, float *kd) {
|
||||
*kp = pid.kp;
|
||||
*ki = pid.ki;
|
||||
*kd = pid.kd;
|
||||
}
|
||||
|
||||
void fire_init() {
|
||||
printf("Regulator init");
|
||||
|
||||
PID_Initialize(&pid);
|
||||
PID_SetOutputLimits(&pid, 0, 1);
|
||||
PID_SetCtlMode(&pid, PID_MANUAL);
|
||||
|
||||
nvs_handle nvs;
|
||||
ESP_ERROR_CHECK(nvs_open("config", NVS_READWRITE, &nvs)); // must use RW to allow opening nonexistent
|
||||
union uf32 kp, ki, kd;
|
||||
kp.f = 0.4f;
|
||||
ki.f = 0.5f;
|
||||
kd.f = 0.0f;
|
||||
nvs_get_u32(nvs, "kp", &kp.u);
|
||||
nvs_get_u32(nvs, "ki", &ki.u);
|
||||
nvs_get_u32(nvs, "kd", &kd.u);
|
||||
PID_SetTunings(&pid, kp.f, ki.f, kd.f);
|
||||
nvs_close(nvs);
|
||||
|
||||
pwm_init();
|
||||
|
||||
fire_setlevel(20);
|
||||
// fire_enable(true);
|
||||
}
|
||||
|
||||
void fire_set_tuning(float kp, float ki, float kd) {
|
||||
ESP_LOGI(TAG, "PID set tuning Kp=%.3f, Ki=%.3f, Kd=%.3f", kp, ki, kd);
|
||||
PID_SetTunings(&pid, kp, ki, kd);
|
||||
}
|
||||
|
||||
float fire_get_setpoint(bool off_is_zero) {
|
||||
if (off_is_zero) {
|
||||
return pid.ctlMode == PID_MANUAL ? 0 : pid.Setpoint;
|
||||
} else {
|
||||
return pid.Setpoint;
|
||||
}
|
||||
}
|
||||
|
||||
void fire_setlevel(float cels) {
|
||||
ESP_LOGI(TAG, "PID set target %.3f°C", cels);
|
||||
|
||||
if (cels < 0) cels = 0;
|
||||
if (cels > MAX_SETPOINT) cels = MAX_SETPOINT;
|
||||
PID_SetSetpoint(&pid, cels);
|
||||
}
|
||||
|
||||
void fire_enable(bool enable) {
|
||||
ESP_LOGI(TAG, "Heater %s", enable ? "enable" : "disable");
|
||||
|
||||
PID_SetCtlMode(&pid, enable ? PID_AUTOMATIC : PID_MANUAL);
|
||||
}
|
||||
|
||||
bool fire_enabled() {
|
||||
return pid.ctlMode == PID_AUTOMATIC;
|
||||
}
|
||||
|
||||
void fire_regulate(float cels) {
|
||||
PID_Compute(&pid, cels);
|
||||
|
||||
if (cels > MAX_TSENSE || cels < MIN_TSENSE) {
|
||||
ESP_LOGE(TAG, "Tsense out of bounds! Stopping.");
|
||||
fire_enable(false);
|
||||
}
|
||||
|
||||
if (pid.ctlMode == PID_MANUAL) {
|
||||
pwm_set(0);
|
||||
} else {
|
||||
printf("PID in %.2f°C, out %.3f, I %.3f\n", cels, pid.Output, pid.ITerm);
|
||||
pwm_set(pid.Output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#define PWM_CHANNEL LEDC_CHANNEL_1
|
||||
#define PWM_TIMER LEDC_TIMER_1
|
||||
#define PWM_BIT_NUM LEDC_TIMER_12_BIT
|
||||
|
||||
#define PWM_PIN GPIO_NUM_14
|
||||
|
||||
static void pwm_init(void)
|
||||
{
|
||||
ledc_channel_config_t ledc_channel_left = {0};
|
||||
ledc_channel_left.gpio_num = PWM_PIN;
|
||||
ledc_channel_left.speed_mode = LEDC_HIGH_SPEED_MODE;
|
||||
ledc_channel_left.channel = PWM_CHANNEL;
|
||||
ledc_channel_left.intr_type = LEDC_INTR_DISABLE;
|
||||
ledc_channel_left.timer_sel = PWM_TIMER;
|
||||
ledc_channel_left.duty = 0;
|
||||
|
||||
ledc_timer_config_t ledc_timer = {0};
|
||||
ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE;
|
||||
ledc_timer.duty_resolution = PWM_BIT_NUM;
|
||||
ledc_timer.timer_num = PWM_TIMER;
|
||||
ledc_timer.freq_hz = 1; // TODO ??
|
||||
|
||||
ESP_ERROR_CHECK( ledc_channel_config(&ledc_channel_left) );
|
||||
ESP_ERROR_CHECK( ledc_timer_config(&ledc_timer) );
|
||||
}
|
||||
|
||||
static void pwm_set(float duty)
|
||||
{
|
||||
uint32_t max_duty = (1 << PWM_BIT_NUM);// - 1
|
||||
uint32_t dutycycle = lroundf((duty) * (float)max_duty);
|
||||
printf("Dutycycle %d\n", dutycycle);
|
||||
|
||||
ESP_ERROR_CHECK( ledc_set_duty(LEDC_HIGH_SPEED_MODE, PWM_CHANNEL, dutycycle) );
|
||||
ESP_ERROR_CHECK( ledc_update_duty(LEDC_HIGH_SPEED_MODE, PWM_CHANNEL) );
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2020/01/08.
|
||||
*/
|
||||
|
||||
#ifndef REFLOWER_FIREHAZARD_H
|
||||
#define REFLOWER_FIREHAZARD_H
|
||||
#include <stdbool.h>
|
||||
|
||||
#define MAX_SETPOINT 350
|
||||
#define MAX_TSENSE 400
|
||||
#define MIN_TSENSE 0
|
||||
|
||||
void fire_regulate(float cels);
|
||||
void fire_init();
|
||||
void fire_setlevel(float cels);
|
||||
void fire_enable(bool enable);
|
||||
|
||||
float fire_get_setpoint(bool off_is_zero);
|
||||
void fire_get_tuning(float *kp, float *ki, float *kd);
|
||||
bool fire_enabled();
|
||||
void fire_set_tuning(float kp, float ki, float kd);
|
||||
|
||||
#endif //REFLOWER_FIREHAZARD_H
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "bitmaps.h"
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
/* 21x22: no_flame */
|
||||
// ········██····█······
|
||||
// ·········██···██·····
|
||||
// ··········██·········
|
||||
// ··········███········
|
||||
// ·····█····███···█····
|
||||
// ···██····████··███···
|
||||
// ··██···█████··███····
|
||||
// ··█···█████··███·····
|
||||
// ·██··█████··███······
|
||||
// ·█··█████··███·······
|
||||
// ·█··████··███··█·····
|
||||
// ····███··███··███····
|
||||
// ·····█··███···███··█·
|
||||
// ·······███··█████··██
|
||||
// ·█····███··███████··█
|
||||
// █····███···███████···
|
||||
// ██··███··█··██████···
|
||||
// █··███··██···█████···
|
||||
// ··███··███···█████···
|
||||
// ·███··██·····█·███···
|
||||
// ███··█·········██····
|
||||
// ·█··███·······██·····
|
||||
static const uint8_t G_NO_FLAME_BYTES[] = { 0x00, 0x00, 0xc0, 0x60, 0x20, 0x10, 0x80, 0xc0, 0xc1, 0xe3, 0xfe, 0x7c, 0x38, 0x80, 0xc3, 0xe2, 0x70, 0x20, 0x00, 0x00, 0x00, 0x80, 0x47, 0x01, 0x00, 0x0e, 0x9f, 0xcf, 0xe7, 0x73, 0x39, 0x1c, 0xce, 0xe7, 0xe3, 0xf9, 0xfc, 0xf8, 0xc0, 0x00, 0x30, 0x60, 0x13, 0x39, 0x1c, 0x0e, 0x27, 0x33, 0x29, 0x0c, 0x06, 0x07, 0x00, 0x00, 0x01, 0x0f, 0x27, 0x3f, 0x1f, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
/* 21x22: flame */
|
||||
// ········██····█······
|
||||
// ·········██···██·····
|
||||
// ··········██·········
|
||||
// ··········███········
|
||||
// ·····█····███········
|
||||
// ···██····█████···█···
|
||||
// ··██···███████··█····
|
||||
// ··█···███████··██····
|
||||
// ·██··████████·██·····
|
||||
// ·█··████████··██·····
|
||||
// ·█··████████·███·····
|
||||
// ····████████·████····
|
||||
// ·····████·███·███··█·
|
||||
// ······███·███████··██
|
||||
// ·█·····██··███████··█
|
||||
// █·······██·███████···
|
||||
// ██·····███··██████···
|
||||
// █████·████···█████···
|
||||
// ·█████████···█████···
|
||||
// ··██████·····█·███···
|
||||
// ···███·········██····
|
||||
// ····███·······██·····
|
||||
static const uint8_t G_FLAME_BYTES[] = { 0x00, 0x00, 0xc0, 0x60, 0x20, 0x10, 0x80, 0xc0, 0xc1, 0xe3, 0xfe, 0xfc, 0xf8, 0x60, 0x03, 0x82, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x80, 0x47, 0x01, 0x00, 0x0e, 0x1f, 0x3f, 0x7f, 0xff, 0x8f, 0x3f, 0xff, 0xf1, 0xec, 0xff, 0xff, 0xf8, 0xc0, 0x00, 0x30, 0x60, 0x03, 0x07, 0x0e, 0x1e, 0x3e, 0x3c, 0x2e, 0x0f, 0x07, 0x07, 0x00, 0x00, 0x01, 0x0f, 0x27, 0x3f, 0x1f, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
/* 84x48: boot_logo */
|
||||
// ····················································································
|
||||
// ·····································································████████·······
|
||||
// ························································█████████████····█···██·····
|
||||
// ···················█·······························█████····█·███·····███······█····
|
||||
// ······█·█····································██████···██·█████··███████·········█···
|
||||
// ·······█······························███████····██·█████·█·······███··········█····
|
||||
// ······█·█······················███████·····███·███████··██···················██·····
|
||||
// ···························██████·██·███·███·███████████·████·············███·······
|
||||
// ······················████████████·██··███··█████████·█████·············██·██·······
|
||||
// ··················█████·██·████···█·████···███████·███·············█████···██·······
|
||||
// ·············███████████·█████████████···██·······█············█████··█·█·█·██······
|
||||
// ············██████████·██████████··███████·················█████··█·██·█···███······
|
||||
// ·········██████··█··████·██·███···████·█···············████···█··███··█······██·····
|
||||
// ······███·████████████████████··██················█████··█···█·█·█···········█·█····
|
||||
// ···███████····················██··············████·█····█·····█················█····
|
||||
// ····██··························█·············█···█····██······················█····
|
||||
// ··██·····························█·······█·█·█···█····██·······················█····
|
||||
// ·█······························█·······█·█·█···█·█····························█····
|
||||
// ·█····························██·······█·█·█·····█····················█········█····
|
||||
// ··██······················████·█·█·█··█·······························█······█··█···
|
||||
// ····████···················█··█·█·█··█································█······█··█···
|
||||
// ·······█···················██·█·██··························█·········█······█··█···
|
||||
// ·······█····················█·██······█·····················█··········█·····█··█···
|
||||
// ·······█····················█·█········█··········█··········█·········█·····███····
|
||||
// ········█···················█··········█···········█·········█·····██████████·······
|
||||
// ········█····················█·········█············█·········█·███·················
|
||||
// ········█····················█··········█···········█·······████····················
|
||||
// ········█·····················█·········█············█·█████························
|
||||
// ········█······················█·········█·········████·····························
|
||||
// ········█·······················█········█····█████···██················█·█·········
|
||||
// ·········█······················█·········█·██···························█··········
|
||||
// ·········█······················█·······████····························█·█·········
|
||||
// ·········█······················█····███·····················█······················
|
||||
// ·········█·······················████·······················███·····················
|
||||
// ·········█·····················███···························█······················
|
||||
// ·········█········██████████████····················································
|
||||
// ··········█·██████·····························································█····
|
||||
// ···········█····························································█······█····
|
||||
// ··························································█············█·······█····
|
||||
// ··████·································█··················█······██····█··█····█····
|
||||
// ··█···█···██████···█████████····█·····███████···███████···█·····█··█····█·██····█···
|
||||
// ··█···█··██·····█·█·█··········█·█······█····█·█·█·······█·····█····█···█··█····█···
|
||||
// ··████····█·····██··█··········█·█······█····█···██████··█·····█····█···█··█····█···
|
||||
// ··█···█···██████··███████·····█···█·····█···█··███·······█·····█···█·····█·██···█···
|
||||
// ··█····█··██········█········███████····█··█·····█·······█······█·█······█·█·█·█····
|
||||
// ··█···█···█·██······█·····██··█····█··█████······█········█████··█········█···█·····
|
||||
// ██████····█···███····█████···█·····██············█··································
|
||||
// ··········█·····█···································································
|
||||
static const uint8_t G_BOOT_LOGO_BYTES[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x20, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0xc0, 0xc0, 0x40, 0xc0, 0xc0, 0x40, 0xc0, 0xa0, 0xa0, 0x20, 0xa0, 0xa0, 0xe0, 0x60, 0xd0, 0x90, 0xd0, 0xd0, 0xf0, 0xf0, 0xc8, 0xe8, 0xe8, 0xb8, 0xb8, 0x64, 0xd4, 0xb4, 0x94, 0x9c, 0x14, 0x0c, 0x0c, 0x1c, 0x14, 0x34, 0x34, 0x34, 0x12, 0x1a, 0x0a, 0x0a, 0x06, 0x82, 0x82, 0x82, 0x44, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xc0, 0xc0, 0x60, 0x60, 0x60, 0x50, 0x30, 0x30, 0x38, 0x3c, 0x3c, 0x2c, 0x2c, 0x3c, 0x2e, 0x2e, 0x3e, 0x3e, 0x37, 0x3d, 0x2b, 0x3f, 0x3d, 0x2f, 0x3f, 0x3f, 0x5f, 0x4d, 0xad, 0x25, 0x16, 0x1d, 0x1f, 0x1e, 0x0a, 0x1b, 0x09, 0x0d, 0x04, 0x02, 0x03, 0x03, 0xc3, 0x43, 0x43, 0x43, 0xa5, 0x63, 0x23, 0x22, 0x21, 0x91, 0xd1, 0x31, 0x11, 0x08, 0x08, 0x28, 0x58, 0x2c, 0x04, 0x34, 0x1c, 0x16, 0x0a, 0x0a, 0x16, 0x0a, 0x05, 0x01, 0x04, 0x0b, 0x0f, 0x3c, 0x10, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x09, 0x09, 0x10, 0x10, 0x10, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x38, 0xe8, 0x08, 0xf4, 0x4c, 0x32, 0x29, 0x10, 0x08, 0x00, 0x10, 0x48, 0x84, 0x02, 0x05, 0x02, 0x05, 0x02, 0x01, 0x00, 0x00, 0x02, 0x05, 0x82, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x60, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x80, 0x87, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x08, 0x10, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8c, 0xb0, 0xc0, 0x80, 0x40, 0x40, 0x20, 0x20, 0x20, 0x20, 0x20, 0x11, 0x16, 0x18, 0x30, 0x28, 0x08, 0x08, 0x08, 0x08, 0x04, 0x05, 0x06, 0x04, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0xa1, 0x41, 0xa1, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x0f, 0x10, 0x20, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0c, 0x05, 0x06, 0x02, 0x02, 0x02, 0x01, 0x01, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x07, 0x02, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x7f, 0x44, 0x44, 0x44, 0x2b, 0x10, 0x00, 0x02, 0xff, 0x19, 0x29, 0x29, 0x49, 0x49, 0xc6, 0x04, 0x0a, 0x09, 0x3f, 0x49, 0x49, 0x49, 0x49, 0x41, 0x21, 0x21, 0x00, 0x50, 0x38, 0x16, 0x11, 0x16, 0x18, 0x70, 0x40, 0x00, 0x21, 0x21, 0x3f, 0x21, 0x21, 0x11, 0x09, 0x06, 0x00, 0x0a, 0x09, 0x7f, 0x05, 0x05, 0x05, 0x05, 0x05, 0x00, 0x00, 0x1e, 0x21, 0x20, 0x20, 0x20, 0x20, 0x0e, 0x11, 0x20, 0x10, 0x09, 0x06, 0x00, 0x00, 0x00, 0x07, 0x18, 0x21, 0x1f, 0x08, 0x10, 0x20, 0x10, 0x0f, 0x00, 0x00, 0x00 };
|
||||
|
||||
static const struct BitmapImage bitmaps[] = {
|
||||
{ .name="no_flame", .width=21, .height=22, .bytes=G_NO_FLAME_BYTES },
|
||||
{ .name="flame", .width=21, .height=22, .bytes=G_FLAME_BYTES },
|
||||
{ .name="boot_logo", .width=84, .height=48, .bytes=G_BOOT_LOGO_BYTES },
|
||||
{}
|
||||
};
|
||||
|
||||
const struct BitmapImage *Bitmap_Get(const char *name) {
|
||||
const struct BitmapImage *ptr = &bitmaps[0];
|
||||
while (ptr->name) {
|
||||
if (0 == strcmp(ptr->name, name)) {
|
||||
return ptr;
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef BITMAPS_H
|
||||
#define BITMAPS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct BitmapImage {
|
||||
const char *name;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
const uint8_t* bytes;
|
||||
};
|
||||
|
||||
const struct BitmapImage *Bitmap_Get(const char *name);
|
||||
|
||||
#endif //BITMAPS_H
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef REFLOWER_DISPLAY_SPEC_H
|
||||
#define REFLOWER_DISPLAY_SPEC_H
|
||||
|
||||
#define LCD_WIDTH 84 // Note: x-coordinates go wide
|
||||
#define LCD_HEIGHT 48 // Note: y-coordinates go high
|
||||
|
||||
#endif //REFLOWER_DISPLAY_SPEC_H
|
||||
@@ -0,0 +1,369 @@
|
||||
#include "drawing.h"
|
||||
#include "display_spec.h"
|
||||
#include "font.h"
|
||||
#include "bitmaps.h"
|
||||
|
||||
extern uint8_t LCD_displayMap[LCD_WIDTH * LCD_HEIGHT / 8];
|
||||
|
||||
// This function sets a pixel on displayMap to your preferred
|
||||
// color. 1=Black, 0= white.
|
||||
void LCD_setPixel(int x, int y, enum Color bw)
|
||||
{
|
||||
// First, double check that the coordinate is in range.
|
||||
if ((x >= 0) && (x < LCD_WIDTH) && (y >= 0) && (y < LCD_HEIGHT)) {
|
||||
uint8_t shift = y % 8;
|
||||
|
||||
if (bw) // If black, set the bit.
|
||||
LCD_displayMap[x + (y / 8) * LCD_WIDTH] |= 1 << shift;
|
||||
else // If white clear the bit.
|
||||
LCD_displayMap[x + (y / 8) * LCD_WIDTH] &= ~(1 << shift);
|
||||
}
|
||||
}
|
||||
|
||||
// setLine draws a line from x0,y0 to x1,y1 with the set color.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setLine(int x0, int y0, int x1, int y1, enum Color bw)
|
||||
{
|
||||
int dy = y1 - y0; // Difference between y0 and y1
|
||||
int dx = x1 - x0; // Difference between x0 and x1
|
||||
int stepx, stepy;
|
||||
|
||||
if (dy < 0) {
|
||||
dy = -dy;
|
||||
stepy = -1;
|
||||
}
|
||||
else
|
||||
stepy = 1;
|
||||
|
||||
if (dx < 0) {
|
||||
dx = -dx;
|
||||
stepx = -1;
|
||||
}
|
||||
else
|
||||
stepx = 1;
|
||||
|
||||
dy <<= 1; // dy is now 2*dy
|
||||
dx <<= 1; // dx is now 2*dx
|
||||
LCD_setPixel(x0, y0, bw); // Draw the first pixel.
|
||||
|
||||
if (dx > dy) {
|
||||
int fraction = dy - (dx >> 1);
|
||||
while (x0 != x1) {
|
||||
if (fraction >= 0) {
|
||||
y0 += stepy;
|
||||
fraction -= dx;
|
||||
}
|
||||
x0 += stepx;
|
||||
fraction += dy;
|
||||
LCD_setPixel(x0, y0, bw);
|
||||
}
|
||||
}
|
||||
else {
|
||||
int fraction = dx - (dy >> 1);
|
||||
while (y0 != y1) {
|
||||
if (fraction >= 0) {
|
||||
x0 += stepx;
|
||||
fraction -= dy;
|
||||
}
|
||||
y0 += stepy;
|
||||
fraction += dx;
|
||||
LCD_setPixel(x0, y0, bw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setRect will draw a rectangle from x0,y0 top-left corner to
|
||||
// a x1,y1 bottom-right corner. Can be filled with the fill
|
||||
// parameter, and colored with bw.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setRect(int x0, int y0, int x1, int y1, bool fill, enum Color bw)
|
||||
{
|
||||
// check if the rectangle is to be filled
|
||||
if (fill == 1) {
|
||||
int xDiff;
|
||||
|
||||
if (x0 > x1)
|
||||
xDiff = x0 - x1; //Find the difference between the x vars
|
||||
else
|
||||
xDiff = x1 - x0;
|
||||
|
||||
while (xDiff >= 0) {
|
||||
LCD_setLine(x0, y0, x0, y1, bw);
|
||||
|
||||
if (x0 > x1)
|
||||
x0--;
|
||||
else
|
||||
x0++;
|
||||
|
||||
xDiff--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// best way to draw an unfilled rectangle is to draw four lines
|
||||
LCD_setLine(x0, y0, x1, y0, bw);
|
||||
LCD_setLine(x0, y1, x1, y1, bw);
|
||||
LCD_setLine(x0, y0, x0, y1, bw);
|
||||
LCD_setLine(x1, y0, x1, y1, bw);
|
||||
}
|
||||
}
|
||||
|
||||
// setCircle draws a circle centered around x0,y0 with a defined
|
||||
// radius. The circle can be black or white. And have a line
|
||||
// thickness ranging from 1 to the radius of the circle.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setCircle(int x0, int y0, int radius, enum Color bw, int lineThickness)
|
||||
{
|
||||
for (int r = 0; r < lineThickness; r++) {
|
||||
int f = 1 - radius;
|
||||
int ddF_x = 0;
|
||||
int ddF_y = -2 * radius;
|
||||
int x = 0;
|
||||
int y = radius;
|
||||
|
||||
LCD_setPixel(x0, y0 + radius, bw);
|
||||
LCD_setPixel(x0, y0 - radius, bw);
|
||||
LCD_setPixel(x0 + radius, y0, bw);
|
||||
LCD_setPixel(x0 - radius, y0, bw);
|
||||
|
||||
while (x < y) {
|
||||
if (f >= 0) {
|
||||
y--;
|
||||
ddF_y += 2;
|
||||
f += ddF_y;
|
||||
}
|
||||
x++;
|
||||
ddF_x += 2;
|
||||
f += ddF_x + 1;
|
||||
|
||||
LCD_setPixel(x0 + x, y0 + y, bw);
|
||||
LCD_setPixel(x0 - x, y0 + y, bw);
|
||||
LCD_setPixel(x0 + x, y0 - y, bw);
|
||||
LCD_setPixel(x0 - x, y0 - y, bw);
|
||||
LCD_setPixel(x0 + y, y0 + x, bw);
|
||||
LCD_setPixel(x0 - y, y0 + x, bw);
|
||||
LCD_setPixel(x0 + y, y0 - x, bw);
|
||||
LCD_setPixel(x0 - y, y0 - x, bw);
|
||||
}
|
||||
radius--;
|
||||
}
|
||||
}
|
||||
|
||||
static void LCD_setCharEx(struct Utf8Char character, int x, int y, enum Color color, struct TextStyle style)
|
||||
{
|
||||
const struct FontGraphic *symbol = Font_GetSymbol(character);
|
||||
|
||||
uint8_t column; // temp byte to store character's column bitmap
|
||||
for (int i = 0; i < 5; i++) // 5 columns (x) per character
|
||||
{
|
||||
column = symbol->columns[i];
|
||||
for (int j = 0; j < 8; j++) // 8 rows (y) per character
|
||||
{
|
||||
bool bit = column & (1 << j);
|
||||
|
||||
if (style.size == FONT_NORMAL) {
|
||||
if (bit) {// test bits to set pixels
|
||||
LCD_setPixel(x + i, y + j, color);
|
||||
}
|
||||
else if (style.bg) {
|
||||
LCD_setPixel(x + i, y + j, !color);
|
||||
}
|
||||
}
|
||||
else if (style.size == FONT_DOUBLE) {
|
||||
if (bit) {// test bits to set pixels
|
||||
LCD_setPixel(x + i * 2, y + j * 2, color);
|
||||
LCD_setPixel(x + i * 2 + 1, y + j * 2, color);
|
||||
LCD_setPixel(x + i * 2, y + j * 2 + 1, color);
|
||||
LCD_setPixel(x + i * 2 + 1, y + j * 2 + 1, color);
|
||||
}
|
||||
else if (style.bg) {
|
||||
LCD_setPixel(x + i * 2, y + j * 2, !color);
|
||||
LCD_setPixel(x + i * 2 + 1, y + j * 2, !color);
|
||||
LCD_setPixel(x + i * 2, y + j * 2 + 1, !color);
|
||||
LCD_setPixel(x + i * 2 + 1, y + j * 2 + 1, !color);
|
||||
}
|
||||
}
|
||||
else if (style.size == FONT_BOLD) {
|
||||
if (bit) {// test bits to set pixels
|
||||
LCD_setPixel(x + i, y + j, color);
|
||||
LCD_setPixel(x + i + 1, y + j, color);
|
||||
}
|
||||
else if (style.bg) {
|
||||
LCD_setPixel(x + i, y + j, !color);
|
||||
LCD_setPixel(x + i + 1, y + j, !color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setStr draws a string of characters, calling setChar with
|
||||
// progressive coordinates until it's done.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setStr(const char *dString, int x, int y, enum Color bw)
|
||||
{
|
||||
LCD_setStrEx(dString, x, y, bw, (struct TextStyle) {});
|
||||
}
|
||||
|
||||
static const int char_widths[] = {
|
||||
[FONT_NORMAL] = 5,
|
||||
[FONT_DOUBLE] = 10,
|
||||
[FONT_BOLD] = 6
|
||||
};
|
||||
|
||||
static const int char_spacings[] = {
|
||||
[FONT_NORMAL] = 1,
|
||||
[FONT_DOUBLE] = 2,
|
||||
[FONT_BOLD] = 1
|
||||
};
|
||||
|
||||
static const int char_heights[] = {
|
||||
[FONT_NORMAL] = 8,
|
||||
[FONT_DOUBLE] = 16,
|
||||
[FONT_BOLD] = 8
|
||||
};
|
||||
|
||||
// setStr draws a string of characters, calling setChar with
|
||||
// progressive coordinates until it's done.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library. AND HEAVILY MODIFIED
|
||||
void LCD_setStrEx(const char *dString, int x, int y, enum Color color, struct TextStyle style)
|
||||
{
|
||||
struct Utf8Char uchar;
|
||||
struct Utf8Iterator iter;
|
||||
Utf8Iterator_Init(&iter, dString);
|
||||
|
||||
int charw = char_widths[style.size];
|
||||
int charh = char_heights[style.size];
|
||||
int spacingx = char_spacings[style.size] + style.spacing_x;
|
||||
int spacingy = style.spacing_y;
|
||||
|
||||
if (style.align != ALIGN_LEFT) {
|
||||
int line_len = 0;
|
||||
int skip = style.skip;
|
||||
while ((uchar = Utf8Iterator_Next(&iter)).uint) {
|
||||
if (skip > 0) {
|
||||
skip -= 1;
|
||||
continue;
|
||||
}
|
||||
if (uchar.bytes[0] == '\n') {
|
||||
break;
|
||||
}
|
||||
line_len++;
|
||||
}
|
||||
if (style.align == ALIGN_CENTER) {
|
||||
x -= ((charw + spacingx) * line_len) / 2 - spacingx/2;
|
||||
} else {
|
||||
// right
|
||||
x -= ((charw + spacingx) * line_len) - spacingx;
|
||||
}
|
||||
}
|
||||
|
||||
const int x0 = x;
|
||||
Utf8Iterator_Init(&iter, dString);
|
||||
|
||||
int limit = style.limit;
|
||||
int skip = style.skip;
|
||||
bool wrap;
|
||||
while ((uchar = Utf8Iterator_Next(&iter)).uint) {
|
||||
if (skip > 0) {
|
||||
skip -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
wrap = uchar.bytes[0] == '\n';
|
||||
|
||||
if (!wrap) LCD_setCharEx(uchar, x, y, color, style);
|
||||
|
||||
if (limit > 0) {
|
||||
limit -= 1;
|
||||
if (limit == 0) return;
|
||||
}
|
||||
|
||||
if (!wrap) {
|
||||
x += charw;
|
||||
if (style.bg) {
|
||||
for (int i = y; i < y + charh; i++) {
|
||||
for (int j = x; j < x + spacingx; j++) {
|
||||
LCD_setPixel(j, i, !color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x += spacingx;
|
||||
if (wrap || x > (LCD_WIDTH - charw)) {
|
||||
if (style.nowrap) break;
|
||||
if (style.wrap_to_0) {
|
||||
x = 0;
|
||||
} else {
|
||||
x = x0;
|
||||
}
|
||||
y += charh + spacingy;
|
||||
}
|
||||
|
||||
// no more characters could be seen
|
||||
if (y >= LCD_HEIGHT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function will draw an array over the screen. (For now) the
|
||||
// array must be the same size as the screen, covering the entirety
|
||||
// of the display.
|
||||
void LCD_setBitmapFullScreen(const uint8_t *bitArray)
|
||||
{
|
||||
for (int i = 0; i < sizeof(LCD_displayMap); i++) {
|
||||
LCD_displayMap[i] = bitArray[i];
|
||||
}
|
||||
}
|
||||
|
||||
void LCD_setBitmap(const struct BitmapImage *image, int x, int y, bool bg, enum Color color)
|
||||
{
|
||||
// int byte_count = image->width * ((image->height + 7) / 8);
|
||||
|
||||
for (int xi = 0; xi < image->width; xi++) {
|
||||
for (int yi = 0; yi < image->height; yi++) {
|
||||
int idx = xi + (yi / 8) * image->width;
|
||||
// if (idx > byte_count) continue;
|
||||
bool bit = image->bytes[idx] & (1 << (yi % 8));
|
||||
if (bit) {
|
||||
LCD_setPixel(x + xi, y + yi, color);
|
||||
}
|
||||
else if (bg) {
|
||||
LCD_setPixel(x + xi, y + yi, !color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function clears the entire display either white (0) or
|
||||
// black (1).
|
||||
// The screen won't actually clear until you call updateDisplay()!
|
||||
void LCD_clearDisplay(enum Color bw)
|
||||
{
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
if (bw)
|
||||
LCD_displayMap[i] = 0xFF;
|
||||
else
|
||||
LCD_displayMap[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void LCD_invertDisplayData()
|
||||
{
|
||||
/* Indirect, swap bits in displayMap option: */
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
LCD_displayMap[i] ^= 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
void LCD_setShadeOverlay() {
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
LCD_displayMap[i] |= i & 1 ? 0x88 : 0x22;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* TODO file description
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef LCD_DRAWING_H
|
||||
#define LCD_DRAWING_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "utf8.h"
|
||||
#include "bitmaps.h"
|
||||
|
||||
enum Color {
|
||||
WHITE = 0,
|
||||
BLACK = 1,
|
||||
};
|
||||
|
||||
// This function sets a pixel on displayMap to your preferred
|
||||
// color. 1=Black, 0= white.
|
||||
void LCD_setPixel(int x, int y, enum Color bw);
|
||||
|
||||
// setLine draws a line from x0,y0 to x1,y1 with the set color
|
||||
void LCD_setLine(int x0, int y0, int x1, int y1, enum Color bw);
|
||||
|
||||
// setRect will draw a rectangle from x0,y0 top-left corner to
|
||||
// a x1,y1 bottom-right corner. Can be filled with the fill
|
||||
// parameter, and colored with bw.
|
||||
void LCD_setRect(int x0, int y0, int x1, int y1, bool fill, enum Color bw);
|
||||
|
||||
// setCircle draws a circle centered around x0,y0 with a defined
|
||||
// radius. The circle can be black or white. And have a line
|
||||
// thickness ranging from 1 to the radius of the circle.
|
||||
void LCD_setCircle (int x0, int y0, int radius, enum Color bw, int lineThickness);
|
||||
|
||||
void LCD_setBitmapFullScreen(const uint8_t *bitArray);
|
||||
|
||||
/*
|
||||
FONT FUNCTIONS
|
||||
size = 1 ... normal
|
||||
size = 2 ... 2x
|
||||
size = 3 ... normal bold
|
||||
size | 0x80 ... clear background behind characters
|
||||
*/
|
||||
|
||||
enum TextSize {
|
||||
FONT_NORMAL = 0,
|
||||
FONT_DOUBLE = 1,
|
||||
FONT_BOLD = 2,
|
||||
};
|
||||
|
||||
enum TextAlign {
|
||||
ALIGN_LEFT = 0,
|
||||
ALIGN_CENTER = 1,
|
||||
ALIGN_RIGHT = 2,
|
||||
};
|
||||
|
||||
struct TextStyle {
|
||||
bool bg; //!< Fill the characters background with the opposite color
|
||||
enum TextSize size; //!< Character size
|
||||
enum TextAlign align; //!< Alignment (works only for single line)
|
||||
int limit; //!< Number of characters to print from the string, if > 0
|
||||
int skip; //!< Characters to skip before printing
|
||||
int spacing_x; //!< Additional X spacing
|
||||
int spacing_y; //!< Additional Y spacing
|
||||
bool nowrap; //!< Stop painting when the right edge of the screen is reached
|
||||
bool wrap_to_0; //!< wrap to 0 instead of the initial X coordinate
|
||||
};
|
||||
|
||||
// setStr draws a string of characters, calling setChar with
|
||||
// progressive coordinates until it's done.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setStr(const char * dString, int x, int y, enum Color bw);
|
||||
|
||||
void LCD_setStrEx(const char *dString, int x, int y, enum Color color, struct TextStyle style);
|
||||
|
||||
// This function clears the entire display either white (0) or
|
||||
// black (1).
|
||||
// The screen won't actually clear until you call updateDisplay()!
|
||||
void LCD_clearDisplay(enum Color bw);
|
||||
|
||||
/* Invert colors (hard change in data; does NOT send to display immediately) */
|
||||
void LCD_invertDisplayData();
|
||||
|
||||
void LCD_setBitmap(const struct BitmapImage *image, int x, int y, bool bg, enum Color color);
|
||||
|
||||
/** draw dotted shadow over current content */
|
||||
void LCD_setShadeOverlay();
|
||||
|
||||
#endif //LCD_DRAWING_H
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "font.h"
|
||||
#include "utf8.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#define FONT_EXTRAS_START (0x7e - 0x20 + 1)
|
||||
|
||||
struct Utf8Symbol {
|
||||
union {
|
||||
const char symbol[4];
|
||||
const uint32_t uint;
|
||||
};
|
||||
struct FontGraphic graphic;
|
||||
};
|
||||
|
||||
// ASCII symbols are stored as bare graphic to reduce ROM size
|
||||
const struct FontGraphic ascii_glyphs[95] = {
|
||||
{{0x00, 0x00, 0x00, 0x00, 0x00}}, // " 0x20
|
||||
{{0x00, 0x00, 0x5f, 0x00, 0x00}}, // ! 0x21
|
||||
{{0x00, 0x07, 0x00, 0x07, 0x00}}, // \" 0x22
|
||||
{{0x14, 0x7f, 0x14, 0x7f, 0x14}}, // # 0x23
|
||||
{{0x24, 0x2a, 0x7f, 0x2a, 0x12}}, // $ 0x24
|
||||
{{0x23, 0x13, 0x08, 0x64, 0x62}}, // % 0x25
|
||||
{{0x36, 0x49, 0x55, 0x22, 0x50}}, // & 0x26
|
||||
{{0x00, 0x05, 0x03, 0x00, 0x00}}, // ' 0x27
|
||||
{{0x00, 0x1c, 0x22, 0x41, 0x00}}, // ( 0x28
|
||||
{{0x00, 0x41, 0x22, 0x1c, 0x00}}, // ) 0x29
|
||||
{{0x14, 0x08, 0x3e, 0x08, 0x14}}, // * 0x2a
|
||||
{{0x08, 0x08, 0x3e, 0x08, 0x08}}, // + 0x2b
|
||||
{{0x00, 0x50, 0x30, 0x00, 0x00}}, // , 0x2c
|
||||
{{0x08, 0x08, 0x08, 0x08, 0x08}}, // - 0x2d
|
||||
{{0x00, 0x60, 0x60, 0x00, 0x00}}, // . 0x2e
|
||||
{{0x20, 0x10, 0x08, 0x04, 0x02}}, // / 0x2f
|
||||
{{0x3e, 0x51, 0x49, 0x45, 0x3e}}, // 0 0x30
|
||||
{{0x00, 0x42, 0x7f, 0x40, 0x00}}, // 1 0x31
|
||||
{{0x42, 0x61, 0x51, 0x49, 0x46}}, // 2 0x32
|
||||
{{0x21, 0x41, 0x45, 0x4b, 0x31}}, // 3 0x33
|
||||
{{0x18, 0x14, 0x12, 0x7f, 0x10}}, // 4 0x34
|
||||
{{0x27, 0x45, 0x45, 0x45, 0x39}}, // 5 0x35
|
||||
{{0x3c, 0x4a, 0x49, 0x49, 0x30}}, // 6 0x36
|
||||
{{0x01, 0x71, 0x09, 0x05, 0x03}}, // 7 0x37
|
||||
{{0x36, 0x49, 0x49, 0x49, 0x36}}, // 8 0x38
|
||||
{{0x06, 0x49, 0x49, 0x29, 0x1e}}, // 9 0x39
|
||||
{{0x00, 0x36, 0x36, 0x00, 0x00}}, // : 0x3a
|
||||
{{0x00, 0x56, 0x36, 0x00, 0x00}}, // ; 0x3b
|
||||
{{0x08, 0x14, 0x22, 0x41, 0x00}}, // < 0x3c
|
||||
{{0x14, 0x14, 0x14, 0x14, 0x14}}, // = 0x3d
|
||||
{{0x00, 0x41, 0x22, 0x14, 0x08}}, // > 0x3e
|
||||
{{0x02, 0x01, 0x51, 0x09, 0x06}}, // ? 0x3f
|
||||
{{0x32, 0x49, 0x79, 0x41, 0x3e}}, // @ 0x40
|
||||
{{0x7e, 0x11, 0x11, 0x11, 0x7e}}, // A 0x41
|
||||
{{0x7f, 0x49, 0x49, 0x49, 0x36}}, // B 0x42
|
||||
{{0x3e, 0x41, 0x41, 0x41, 0x22}}, // C 0x43
|
||||
{{0x7f, 0x41, 0x41, 0x22, 0x1c}}, // D 0x44
|
||||
{{0x7f, 0x49, 0x49, 0x49, 0x41}}, // E 0x45
|
||||
{{0x7f, 0x09, 0x09, 0x09, 0x01}}, // F 0x46
|
||||
{{0x3e, 0x41, 0x49, 0x49, 0x7a}}, // G 0x47
|
||||
{{0x7f, 0x08, 0x08, 0x08, 0x7f}}, // H 0x48
|
||||
{{0x00, 0x41, 0x7f, 0x41, 0x00}}, // I 0x49
|
||||
{{0x20, 0x40, 0x41, 0x3f, 0x01}}, // J 0x4a
|
||||
{{0x7f, 0x08, 0x14, 0x22, 0x41}}, // K 0x4b
|
||||
{{0x7f, 0x40, 0x40, 0x40, 0x40}}, // L 0x4c
|
||||
{{0x7f, 0x02, 0x0c, 0x02, 0x7f}}, // M 0x4d
|
||||
{{0x7f, 0x04, 0x08, 0x10, 0x7f}}, // N 0x4e
|
||||
{{0x3e, 0x41, 0x41, 0x41, 0x3e}}, // O 0x4f
|
||||
{{0x7f, 0x09, 0x09, 0x09, 0x06}}, // P 0x50
|
||||
{{0x3e, 0x41, 0x51, 0x21, 0x5e}}, // Q 0x51
|
||||
{{0x7f, 0x09, 0x19, 0x29, 0x46}}, // R 0x52
|
||||
{{0x46, 0x49, 0x49, 0x49, 0x31}}, // S 0x53
|
||||
{{0x01, 0x01, 0x7f, 0x01, 0x01}}, // T 0x54
|
||||
{{0x3f, 0x40, 0x40, 0x40, 0x3f}}, // U 0x55
|
||||
{{0x1f, 0x20, 0x40, 0x20, 0x1f}}, // V 0x56
|
||||
{{0x3f, 0x40, 0x38, 0x40, 0x3f}}, // W 0x57
|
||||
{{0x63, 0x14, 0x08, 0x14, 0x63}}, // X 0x58
|
||||
{{0x07, 0x08, 0x70, 0x08, 0x07}}, // Y 0x59
|
||||
{{0x61, 0x51, 0x49, 0x45, 0x43}}, // Z 0x5a
|
||||
{{0x00, 0x7f, 0x41, 0x41, 0x00}}, // [ 0x5b
|
||||
{{0x02, 0x04, 0x08, 0x10, 0x20}}, // \\ 0x5c
|
||||
{{0x00, 0x41, 0x41, 0x7f, 0x00}}, // ] 0x5d
|
||||
{{0x04, 0x02, 0x01, 0x02, 0x04}}, // ^ 0x5e
|
||||
{{0x40, 0x40, 0x40, 0x40, 0x40}}, // _ 0x5f
|
||||
{{0x00, 0x01, 0x02, 0x04, 0x00}}, // ` 0x60
|
||||
{{0x20, 0x54, 0x54, 0x54, 0x78}}, // a 0x61
|
||||
{{0x7f, 0x48, 0x44, 0x44, 0x38}}, // b 0x62
|
||||
{{0x38, 0x44, 0x44, 0x44, 0x20}}, // c 0x63
|
||||
{{0x38, 0x44, 0x44, 0x48, 0x7f}}, // d 0x64
|
||||
{{0x38, 0x54, 0x54, 0x54, 0x18}}, // e 0x65
|
||||
{{0x08, 0x7e, 0x09, 0x01, 0x02}}, // f 0x66
|
||||
{{0x0c, 0x52, 0x52, 0x52, 0x3e}}, // g 0x67
|
||||
{{0x7f, 0x08, 0x04, 0x04, 0x78}}, // h 0x68
|
||||
{{0x00, 0x44, 0x7d, 0x40, 0x00}}, // i 0x69
|
||||
{{0x20, 0x40, 0x44, 0x3d, 0x00}}, // j 0x6a
|
||||
{{0x7f, 0x10, 0x28, 0x44, 0x00}}, // k 0x6b
|
||||
{{0x00, 0x41, 0x7f, 0x40, 0x00}}, // l 0x6c
|
||||
{{0x7c, 0x04, 0x18, 0x04, 0x78}}, // m 0x6d
|
||||
{{0x7c, 0x08, 0x04, 0x04, 0x78}}, // n 0x6e
|
||||
{{0x38, 0x44, 0x44, 0x44, 0x38}}, // o 0x6f
|
||||
{{0x7c, 0x14, 0x14, 0x14, 0x08}}, // p 0x70
|
||||
{{0x08, 0x14, 0x14, 0x18, 0x7c}}, // q 0x71
|
||||
{{0x7c, 0x08, 0x04, 0x04, 0x08}}, // r 0x72
|
||||
{{0x48, 0x54, 0x54, 0x54, 0x20}}, // s 0x73
|
||||
{{0x04, 0x3f, 0x44, 0x40, 0x20}}, // t 0x74
|
||||
{{0x3c, 0x40, 0x40, 0x20, 0x7c}}, // u 0x75
|
||||
{{0x1c, 0x20, 0x40, 0x20, 0x1c}}, // v 0x76
|
||||
{{0x3c, 0x40, 0x30, 0x40, 0x3c}}, // w 0x77
|
||||
{{0x44, 0x28, 0x10, 0x28, 0x44}}, // x 0x78
|
||||
{{0x0c, 0x50, 0x50, 0x50, 0x3c}}, // y 0x79
|
||||
{{0x44, 0x64, 0x54, 0x4c, 0x44}}, // z 0x7a
|
||||
{{0x00, 0x08, 0x36, 0x41, 0x00}}, // { 0x7b
|
||||
{{0x00, 0x00, 0x7f, 0x00, 0x00}}, // | 0x7c
|
||||
{{0x00, 0x41, 0x36, 0x08, 0x00}}, // } 0x7d
|
||||
{{0x10, 0x08, 0x08, 0x10, 0x08}}, // ~ 0x7e
|
||||
};
|
||||
|
||||
// utf8 characters list ending with empty struct
|
||||
const struct Utf8Symbol utf8_glyphs[] = {
|
||||
{ .symbol="�", {{0xFE, 0x82, 0x82, 0x82, 0xFE}} }, // box
|
||||
{ .symbol="×", {{0x22, 0x14, 0x08, 0x14, 0x22}} }, // cross
|
||||
{ .symbol="↑", {{0x08, 0x04, 0x3e, 0x04, 0x08}} }, // arrow_up
|
||||
{ .symbol="↓", {{0x08, 0x10, 0x3e, 0x10, 0x08}} }, // arrow_down
|
||||
{ .symbol="←", {{0x08, 0x1c, 0x2a, 0x08, 0x08}} }, // arrow_left
|
||||
{ .symbol="→", {{0x08, 0x08, 0x2a, 0x1c, 0x08}} }, // arrow_right
|
||||
{ .symbol="⏰", {{0x1c, 0x22, 0x2e, 0x2a, 0x1c}} }, // clock
|
||||
{ .symbol="⌛", {{0x63, 0x55, 0x4d, 0x55, 0x63}} }, // hourglass
|
||||
{ .symbol="☸", {{0x1c, 0x22, 0x2a, 0x22, 0x1c}} }, // wheel
|
||||
{ .symbol="⏎", {{0x10, 0x38, 0x54, 0x10, 0x1e}} }, // return
|
||||
{ .symbol="🌡", {{0x60, 0x9e, 0x81, 0x9e, 0x6a}} }, // thermometer
|
||||
{ .symbol="°", {{0x00, 0x07, 0x05, 0x07, 0x00}} }, // degree
|
||||
{ .symbol="🔙", {{0x04, 0x4e, 0x55, 0x44, 0x38}} }, // back
|
||||
{ .symbol="▶", {{0x7f, 0x3e, 0x1c, 0x08, 0x00}} }, // tri_right
|
||||
{ .symbol="◀", {{0x00, 0x08, 0x1c, 0x3e, 0x7f}} }, // tri_left
|
||||
|
||||
// End of the list
|
||||
{},
|
||||
};
|
||||
|
||||
const struct FontGraphic *Font_GetSymbol(struct Utf8Char ch) {
|
||||
if (ch.bytes[0] < 32) {
|
||||
// low ASCII is not supported.
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (ch.bytes[0] < 127) {
|
||||
// valid ASCII at hard positions.
|
||||
// In this case only the first byte is significant.
|
||||
return &ascii_glyphs[ch.bytes[0] - 32];
|
||||
}
|
||||
|
||||
// search UTF8
|
||||
const struct Utf8Symbol *sym = &utf8_glyphs[0];
|
||||
while (sym->symbol[0]) {
|
||||
if (sym->uint == ch.uint) {
|
||||
return &sym->graphic;
|
||||
}
|
||||
sym++;
|
||||
}
|
||||
|
||||
fail:
|
||||
return &utf8_glyphs[0].graphic; // replacement character
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* UTF-8 capable bitmap font
|
||||
*
|
||||
* Created on 2020/01/04.
|
||||
*/
|
||||
|
||||
#ifndef GFX_FONT_H
|
||||
#define GFX_FONT_H
|
||||
|
||||
#include "font.h"
|
||||
#include "utf8.h"
|
||||
#include <stdint.h>
|
||||
|
||||
struct FontGraphic {
|
||||
uint8_t columns[5];
|
||||
};
|
||||
|
||||
const struct FontGraphic *Font_GetSymbol(struct Utf8Char ch);
|
||||
|
||||
#endif //GFX_FONT_H
|
||||
@@ -0,0 +1,178 @@
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
#include "nokia.h"
|
||||
#include <string.h>
|
||||
|
||||
/* Pin definitions:
|
||||
Most of these pins can be moved to any digital or analog pin.
|
||||
DN(MOSI)and SCLK should be left where they are (SPI pins). The
|
||||
LED (backlight) pin should remain on a PWM-capable pin. */
|
||||
static const int scePin = 17; // SCE - Chip select, pin 3 on LCD.
|
||||
static const int rstPin = 16; // RST - Reset, pin 4 on LCD.
|
||||
static const int dcPin = 4; // DC - Data/Command, pin 5 on LCD.
|
||||
static const int sdinPin = 15; // DN(MOSI) - Serial data, pin 6 on LCD.
|
||||
static const int sclkPin = 13; // SCLK - Serial clock, pin 7 on LCD.
|
||||
|
||||
/* PCD8544-specific defines: */
|
||||
#define LCD_COMMAND 0
|
||||
#define LCD_DATA 1
|
||||
|
||||
#define DEFAULT_CONTRAST 48
|
||||
|
||||
static spi_device_handle_t hSPI;
|
||||
|
||||
/* The displayMap variable stores a buffer representation of the
|
||||
pixels on our display. There are 504 total bits in this array,
|
||||
same as how many pixels there are on a 84 x 48 display.
|
||||
|
||||
Each byte in this array covers a 8-pixel vertical block on the
|
||||
display. Each successive byte covers the next 8-pixel column over
|
||||
until you reach the right-edge of the display and step down 8 rows.
|
||||
|
||||
To update the display, we first have to write to this array, then
|
||||
call the updateDisplay() function, which sends this whole array
|
||||
to the PCD8544.
|
||||
|
||||
Because the PCD8544 won't let us write individual pixels at a
|
||||
time, this is how we can make targeted changes to the display. */
|
||||
uint8_t LCD_displayMap[LCD_WIDTH * LCD_HEIGHT / 8] = {};
|
||||
|
||||
// There are two memory banks in the LCD, data/RAM and commands.
|
||||
// This function sets the DC pin high or low depending, and then
|
||||
// sends the data byte
|
||||
static void LCD_SendBytes(bool data_or_command, const uint8_t *data, size_t len)
|
||||
{
|
||||
esp_err_t ret;
|
||||
spi_transaction_t t;
|
||||
if (len == 0) return; //no need to send anything
|
||||
memset(&t, 0, sizeof(t)); //Zero out the transaction
|
||||
t.length = len * 8; //Len is in bytes, transaction length is in bits.
|
||||
t.tx_buffer = data; //Data
|
||||
t.user = (void *) data_or_command; //D/C
|
||||
ret = spi_device_polling_transmit(hSPI, &t); //Transmit!
|
||||
assert(ret == ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
// There are two memory banks in the LCD, data/RAM and commands.
|
||||
// This function sets the DC pin high or low depending, and then
|
||||
// sends the data byte
|
||||
static void LCD_SendByte(bool data_or_command, uint8_t data)
|
||||
{
|
||||
esp_err_t ret;
|
||||
spi_transaction_t t;
|
||||
memset(&t, 0, sizeof(t)); //Zero out the transaction
|
||||
t.length = 8; // transaction length is in bits.
|
||||
t.tx_data[0] = data; //Data
|
||||
t.flags = SPI_TRANS_USE_TXDATA;
|
||||
t.user = (void *) data_or_command; //D/C
|
||||
ret = spi_device_polling_transmit(hSPI, &t); //Transmit!
|
||||
assert(ret == ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
|
||||
// Helpful function to directly command the LCD to go to a
|
||||
// specific x,y coordinate.
|
||||
static void gotoXY(int x, int y)
|
||||
{
|
||||
const uint8_t cmd[2] = {
|
||||
0x80 | x,
|
||||
0x40 | y,
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, cmd, 2);
|
||||
}
|
||||
|
||||
// This will actually draw on the display, whatever is currently
|
||||
// in the displayMap array.
|
||||
void LCD_updateDisplay()
|
||||
{
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
gotoXY(0, 0);
|
||||
LCD_SendBytes(LCD_DATA, &LCD_displayMap[0], LCD_WIDTH * (LCD_HEIGHT / 8));
|
||||
spi_device_release_bus(hSPI);
|
||||
}
|
||||
|
||||
// Set contrast can set the LCD Vop to a value between 0 and 127.
|
||||
// 40-60 is usually a pretty good range.
|
||||
void LCD_setContrast(uint8_t contrast)
|
||||
{
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
const uint8_t cmd[3] = {
|
||||
0x21, //Tell LCD that extended commands follow
|
||||
0x80 | contrast,//Set LCD Vop (Contrast): Try 0xB1(good @ 3.3V) or 0xBF if your display is too dark
|
||||
0x20,//Set display mode
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, cmd, 3);
|
||||
spi_device_release_bus(hSPI);
|
||||
}
|
||||
|
||||
void LCD_invertDisplay(bool invert)
|
||||
{
|
||||
LCD_SendByte(LCD_COMMAND, 0x0C | invert);
|
||||
}
|
||||
|
||||
//This function is called (in irq context!) just before a transmission starts. It will
|
||||
//set the D/C line to the value indicated in the user field.
|
||||
void lcd_spi_pre_transfer_callback(spi_transaction_t *t)
|
||||
{
|
||||
int dc = (int) t->user;
|
||||
gpio_set_level(dcPin, dc);
|
||||
}
|
||||
|
||||
//This sends the magical commands to the PCD8544
|
||||
void LCD_setup(void)
|
||||
{
|
||||
gpio_config_t output = {
|
||||
.pin_bit_mask = (1 << scePin) | (1 << rstPin) | (1 << dcPin) | (1 << sdinPin) | (1 << sclkPin),
|
||||
.mode = GPIO_MODE_OUTPUT
|
||||
};
|
||||
gpio_config(&output);
|
||||
|
||||
esp_err_t ret;
|
||||
spi_bus_config_t buscfg = {
|
||||
.miso_io_num=-1,
|
||||
.mosi_io_num=sdinPin,
|
||||
.sclk_io_num=sclkPin,
|
||||
.quadwp_io_num=-1,
|
||||
.quadhd_io_num=-1,
|
||||
.max_transfer_sz=LCD_WIDTH * LCD_HEIGHT, // ??? this doesnt seem to do anything in the driver
|
||||
};
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz=4 * 1000 * 1000, // Hz
|
||||
// enable signal lead/lag
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.mode=0, //SPI mode 0
|
||||
.spics_io_num=scePin, //CS pin
|
||||
.queue_size=1, // we use the polling mode, so this does not matter
|
||||
.pre_cb=lcd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
|
||||
};
|
||||
|
||||
//Initialize the SPI bus
|
||||
ret = spi_bus_initialize(HSPI_HOST, &buscfg, 1);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
//Attach the LCD to the SPI bus
|
||||
ret = spi_bus_add_device(HSPI_HOST, &devcfg, &hSPI);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
{
|
||||
//Reset the LCD to a known state
|
||||
gpio_set_level(rstPin, 0);
|
||||
gpio_set_level(rstPin, 1);
|
||||
|
||||
const uint8_t magic[6] = {
|
||||
0x21, // Tell LCD extended commands follow
|
||||
0x80 + DEFAULT_CONTRAST, // Set LCD Vop (Contrast)
|
||||
0x04, // Set Temp coefficent
|
||||
0x14, // LCD bias mode 1:48 (try 0x13)
|
||||
//We must send 0x20 before modifying the display control mode
|
||||
0x20, // Clear extended option
|
||||
0x0C, // Set display control, normal mode.
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, &magic[0], 6);
|
||||
}
|
||||
spi_device_release_bus(hSPI);
|
||||
|
||||
// show the blank screen (display is in an undefined state after boot)
|
||||
LCD_updateDisplay();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Driver for Nokia 5110 SPI LCD display
|
||||
*/
|
||||
|
||||
#ifndef GFX_NOKIA_H
|
||||
#define GFX_NOKIA_H
|
||||
|
||||
/* Pin definitions:
|
||||
Most of these pins can be moved to any digital or analog pin.
|
||||
DN(MOSI)and SCLK should be left where they are (SPI pins). The
|
||||
LED (backlight) pin should remain on a PWM-capable pin. */
|
||||
// SCE - Chip select, pin 3 on LCD.
|
||||
// RST - Reset, pin 4 on LCD.
|
||||
// DC - Data/Command, pin 5 on LCD.
|
||||
// DN(MOSI) - Serial data, pin 6 on LCD.
|
||||
// SCLK - Serial clock, pin 7 on LCD.
|
||||
// LED - Backlight LED, pin 8 on LCD.
|
||||
|
||||
/* 84x48 LCD Defines: */
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "utf8.h"
|
||||
#include "display_spec.h"
|
||||
|
||||
// This will actually draw on the display, whatever is currently
|
||||
// in the displayMap array.
|
||||
void LCD_updateDisplay();
|
||||
|
||||
// Set contrast can set the LCD Vop to a value between 0 and 127.
|
||||
// 40-60 is usually a pretty good range.
|
||||
void LCD_setContrast(uint8_t contrast);
|
||||
|
||||
/* Invert colors */
|
||||
void LCD_invertDisplay(bool invert);
|
||||
|
||||
//This sends the magical commands to the PCD8544
|
||||
void LCD_setup(void);
|
||||
|
||||
#endif // GFX_NOKIA_H
|
||||
@@ -0,0 +1,122 @@
|
||||
#include <stdint.h>
|
||||
#include "utf8.h"
|
||||
|
||||
//
|
||||
// Created by MightyPork on 2017/08/20.
|
||||
//
|
||||
// UTF-8 parser - collects bytes of a code point before writing them
|
||||
// into a screen cell.
|
||||
//
|
||||
|
||||
const struct Utf8Char EMPTY_CHAR = (struct Utf8Char) {.uint = 0};
|
||||
|
||||
// Code Points First Byte Second Byte Third Byte Fourth Byte
|
||||
// U+0000 - U+007F 00 - 7F
|
||||
// U+0080 - U+07FF C2 - DF 80 - BF
|
||||
// U+0800 - U+0FFF E0 *A0 - BF 80 - BF
|
||||
// U+1000 - U+CFFF E1 - EC 80 - BF 80 - BF
|
||||
// U+D000 - U+D7FF ED 80 - *9F 80 - BF
|
||||
// U+E000 - U+FFFF EE - EF 80 - BF 80 - BF
|
||||
// U+10000 - U+3FFFF F0 *90 - BF 80 - BF 80 - BF
|
||||
// U+40000 - U+FFFFF F1 - F3 80 - BF 80 - BF 80 - BF
|
||||
// U+100000 - U+10FFFF F4 80 - *8F 80 - BF 80 - BF
|
||||
|
||||
size_t utf8_strlen(const char *text)
|
||||
{
|
||||
// TODO optimize
|
||||
struct Utf8Iterator iter;
|
||||
Utf8Iterator_Init(&iter, text);
|
||||
size_t num = 0;
|
||||
while ((Utf8Iterator_Next(&iter)).uint) {
|
||||
num++;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a received character
|
||||
*/
|
||||
struct Utf8Char Utf8Parser_Handle(struct Utf8Parser *self, char c)
|
||||
{
|
||||
uint8_t *bytes = self->buffer.bytes;
|
||||
|
||||
uint8_t uc = (uint8_t) c;
|
||||
// collecting unicode glyphs...
|
||||
if (uc & 0x80) {
|
||||
if (self->utf_len == 0) {
|
||||
bytes[0] = uc;
|
||||
self->utf_j = 1;
|
||||
|
||||
// start
|
||||
if (uc == 0xC0 || uc == 0xC1 || uc > 0xF4) {
|
||||
// forbidden start codes
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if ((uc & 0xE0) == 0xC0) {
|
||||
self->utf_len = 2;
|
||||
}
|
||||
else if ((uc & 0xF0) == 0xE0) {
|
||||
self->utf_len = 3;
|
||||
}
|
||||
else if ((uc & 0xF8) == 0xF0) {
|
||||
self->utf_len = 4;
|
||||
}
|
||||
else {
|
||||
// chars over 127 that don't start unicode sequences
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ((uc & 0xC0) != 0x80) {
|
||||
bytes[self->utf_j++] = uc;
|
||||
goto fail;
|
||||
}
|
||||
else {
|
||||
bytes[self->utf_j++] = uc;
|
||||
if (self->utf_j >= self->utf_len) {
|
||||
// check for bad sequences - overlong or some other problem
|
||||
if (bytes[0] == 0xF4 && bytes[1] > 0x8F) goto fail;
|
||||
if (bytes[0] == 0xF0 && bytes[1] < 0x90) goto fail;
|
||||
if (bytes[0] == 0xED && bytes[1] > 0x9F) goto fail;
|
||||
if (bytes[0] == 0xE0 && bytes[1] < 0xA0) goto fail;
|
||||
|
||||
// trap for surrogates - those break javascript
|
||||
if (bytes[0] == 0xED && bytes[1] >= 0xA0 && bytes[1] <= 0xBF) goto fail;
|
||||
|
||||
goto success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
bytes[0] = uc;
|
||||
goto success;
|
||||
}
|
||||
|
||||
return EMPTY_CHAR;
|
||||
|
||||
success:;
|
||||
struct Utf8Char result = self->buffer;
|
||||
self->buffer.uint = 0; // erase the buffer
|
||||
self->utf_len = 0;
|
||||
return result;
|
||||
|
||||
fail:
|
||||
self->buffer.uint = 0; // erase the buffer
|
||||
self->utf_len = 0;
|
||||
return EMPTY_CHAR;
|
||||
}
|
||||
|
||||
struct Utf8Char Utf8Iterator_Next(struct Utf8Iterator *self)
|
||||
{
|
||||
char c;
|
||||
struct Utf8Char uchar;
|
||||
while ((c = *self->source++) != 0) {
|
||||
uchar = Utf8Parser_Handle(&self->parser, c);
|
||||
if (uchar.uint) {
|
||||
return uchar;
|
||||
}
|
||||
}
|
||||
return EMPTY_CHAR;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* UTF-8 string parsing and character iteration
|
||||
*
|
||||
* Created on 2020/01/04.
|
||||
*/
|
||||
|
||||
#ifndef LIQUIDTYPE_UTF8_H
|
||||
#define LIQUIDTYPE_UTF8_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* UTF-8 encoded character.
|
||||
*/
|
||||
struct Utf8Char {
|
||||
union {
|
||||
/** character bytes; padded by zero bytes if shorter than 4 */
|
||||
uint8_t bytes[4];
|
||||
/** u32 view of the bytes */
|
||||
uint32_t uint;
|
||||
};
|
||||
};
|
||||
|
||||
/** UTF8 string parser internal state */
|
||||
struct Utf8Parser {
|
||||
/** UTF-8 bytes buffer */
|
||||
struct Utf8Char buffer;
|
||||
/** Currently collected UTF-8 character length */
|
||||
uint8_t utf_len;
|
||||
/** Position in the current character */
|
||||
uint8_t utf_j;
|
||||
};
|
||||
|
||||
static inline void Utf8Parser_Clear(struct Utf8Parser *self) {
|
||||
self->buffer.uint = 0;
|
||||
self->utf_j = 0;
|
||||
self->utf_len = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utf8 character iterator.
|
||||
*
|
||||
* Usage:
|
||||
* struct Utf8Iterator iter;
|
||||
* Utf8Iterator_Init(&iter, myString);
|
||||
*
|
||||
* union Utf8Char uchar;
|
||||
* while ((uchar = Utf8Iterator_Next(&iter)).uint) {
|
||||
* // do something with the char
|
||||
* }
|
||||
*
|
||||
* // Free myString if needed, it is not mutated.
|
||||
*/
|
||||
struct Utf8Iterator {
|
||||
/* Characters to parse. The pointer is advanced as the iterator progresses. */
|
||||
const char *source;
|
||||
struct Utf8Parser parser;
|
||||
};
|
||||
|
||||
static inline void Utf8Iterator_Init(struct Utf8Iterator *self, const char *source) {
|
||||
Utf8Parser_Clear(&self->parser);
|
||||
self->source = source;
|
||||
}
|
||||
|
||||
size_t utf8_strlen(const char *text);
|
||||
|
||||
/**
|
||||
* Get the next character from the iterator; Returns empty character if there are no more characters to parse.
|
||||
*
|
||||
* Invalid characters are skipped.
|
||||
*/
|
||||
struct Utf8Char Utf8Iterator_Next(struct Utf8Iterator *self);
|
||||
|
||||
/**
|
||||
* Parse a character.
|
||||
*
|
||||
* The returned struct contains NIL (uint == 0) if no character is yet available.
|
||||
*
|
||||
* ASCII is passed through, utf-8 is collected and returned in one piece.
|
||||
*/
|
||||
struct Utf8Char Utf8Parser_Handle(struct Utf8Parser *self, char c);
|
||||
|
||||
#endif //LIQUIDTYPE_UTF8_H
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "gui.h"
|
||||
#include "nokia.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
static void gui_thread(void *arg);
|
||||
|
||||
TaskHandle_t hGuiThread = NULL;
|
||||
|
||||
void gui_init() {
|
||||
printf("GUI init\n");
|
||||
LCD_setup();
|
||||
|
||||
LCD_setContrast(60);
|
||||
|
||||
LCD_clearDisplay(0);
|
||||
LCD_setStr("Hello World", 0, 0, 1);
|
||||
LCD_updateDisplay();
|
||||
|
||||
int rv = xTaskCreate(gui_thread, "gui", 4096, NULL, 6, &hGuiThread);
|
||||
assert (rv == pdPASS);
|
||||
}
|
||||
|
||||
static void __attribute__((noreturn)) gui_thread(void *arg) {
|
||||
uint32_t pos = 20;
|
||||
bool btn = 0;
|
||||
#define NMAX 60
|
||||
while (1) {
|
||||
uint32_t value = 0;
|
||||
xTaskNotifyWait(0, ULONG_MAX, &value, portMAX_DELAY);
|
||||
// printf("Knob event 0x%02x ", value);
|
||||
|
||||
if (value & 0b1000) {
|
||||
// printf("PUSH ");
|
||||
btn = 1;
|
||||
} else if (value & 0b100) {
|
||||
btn = 0;
|
||||
}
|
||||
|
||||
if (value & 0b01) {
|
||||
// printf("FWD ");
|
||||
if (pos == NMAX) {
|
||||
pos = 0;
|
||||
}
|
||||
else {
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (value & 0b10) {
|
||||
// printf("BACK ");
|
||||
if (pos == 0) {
|
||||
pos = NMAX;
|
||||
}
|
||||
else {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
|
||||
LCD_setRect(0, 15, 83, 35, 1, 1);
|
||||
char buf[10];
|
||||
sprintf(buf, "%3d %s", pos, btn?"BTN":"");
|
||||
LCD_setStr(buf, 2, 17, 0);
|
||||
LCD_updateDisplay();
|
||||
|
||||
// printf(">");
|
||||
// for (int i=0; i<pos; i++) {
|
||||
// printf(" ");
|
||||
// }
|
||||
// printf("#");
|
||||
// for (int i=pos; i<NMAX; i++) {
|
||||
// printf(" ");
|
||||
// }
|
||||
// printf("<\n");
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,8 @@ const int pushPin = 5;
|
||||
const int wheelPin1 = 18;
|
||||
const int wheelPin2 = 23;
|
||||
|
||||
//const int sigPin1 = 12;
|
||||
//const int sigPin2 = 14;
|
||||
|
||||
#define DEBOUNCE_WHEEL_MS 3
|
||||
#define DEBOUNCE_BTN_MS 10
|
||||
|
||||
static void handle_pushbtn(void *arg);
|
||||
static void handle_wheel(void *arg);
|
||||
|
||||
|
||||
#define DEBOUNCE_WHEEL_MS 2
|
||||
#define DEBOUNCE_BTN_MS 5
|
||||
|
||||
static void debounce_service(void *arg);
|
||||
static TaskHandle_t hDebouncer;
|
||||
@@ -25,330 +17,92 @@ static TaskHandle_t hDebouncer;
|
||||
void knob_init() {
|
||||
printf("Knob init\n");
|
||||
|
||||
// gpio_config_t cfgWheel = {
|
||||
// .pin_bit_mask = (1 << wheelPin1) /*| (1 << wheelPin2)*/,
|
||||
// .mode = GPIO_MODE_INPUT,
|
||||
// .pull_up_en = 1,
|
||||
// .intr_type = GPIO_INTR_NEGEDGE, // neg means active
|
||||
// };
|
||||
// gpio_config(&cfgWheel);
|
||||
|
||||
gpio_config_t cfgPush = {
|
||||
.pin_bit_mask = (1 << pushPin) | (1 << wheelPin1),
|
||||
.pin_bit_mask = (1 << pushPin) | (1 << wheelPin1) | (1 << wheelPin2),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = 1,
|
||||
.intr_type = GPIO_INTR_ANYEDGE, // neg means active
|
||||
.pull_up_en = 1
|
||||
};
|
||||
gpio_config(&cfgPush);
|
||||
|
||||
// wheel2 without itr
|
||||
cfgPush.intr_type = GPIO_INTR_DISABLE;
|
||||
cfgPush.pin_bit_mask = (1 << wheelPin2);
|
||||
gpio_config(&cfgPush);
|
||||
|
||||
// gpio_config_t output = {
|
||||
// .pin_bit_mask = (1<<sigPin1)|(1<<sigPin2),
|
||||
// .mode = GPIO_MODE_OUTPUT
|
||||
// };
|
||||
// gpio_config(&output);
|
||||
|
||||
gpio_install_isr_service(0);
|
||||
gpio_intr_enable(pushPin);
|
||||
gpio_intr_enable(wheelPin1);
|
||||
// gpio_intr_enable(wheelPin2);
|
||||
|
||||
gpio_isr_handler_add(pushPin, handle_pushbtn, (void *)0);
|
||||
gpio_isr_handler_add(wheelPin1, handle_wheel, (void *)0);
|
||||
//gpio_isr_handler_add(wheelPin2, handle_wheel, (void *)1);
|
||||
|
||||
|
||||
int rv = xTaskCreate(debounce_service, "debo", 2048, NULL, 6, &hDebouncer);
|
||||
int rv = xTaskCreate(debounce_service, "debo", 4096, NULL, 6, &hDebouncer);
|
||||
assert (rv == pdPASS);
|
||||
}
|
||||
|
||||
#if 0
|
||||
enum state {
|
||||
S_11 = 0,
|
||||
|
||||
S_FWD_1_01 = 1,
|
||||
S_FWD_2_00 = 2,
|
||||
S_FWD_3_10 = 3,
|
||||
|
||||
S_BCK_1_10 = 4,
|
||||
S_BCK_2_00 = 5,
|
||||
S_BCK_3_01 = 6,
|
||||
|
||||
S_ILLEGAL = 7,
|
||||
};
|
||||
|
||||
#define SW_FWD 0b10000000
|
||||
#define SW_BCK 0b01000000
|
||||
|
||||
static enum state wheelState = S_11;
|
||||
|
||||
const uint8_t switch_table[28] = {
|
||||
/* -- S_11 -- */
|
||||
S_ILLEGAL, /* 00 ILLEGAL */
|
||||
S_FWD_1_01, /* 01 ADVANCE */
|
||||
S_BCK_1_10, /* 10 BACK */
|
||||
S_11, /* 11 */
|
||||
|
||||
/* -- S_FWD_1_01 -- */
|
||||
S_FWD_2_00, /* 00 ADVANCE */
|
||||
S_FWD_1_01, /* 01 */
|
||||
S_FWD_3_10, /* 10 ILLEGAL */
|
||||
S_11, /* 11 ABORT */
|
||||
|
||||
/* -- S_FWD_2_00 -- */
|
||||
S_FWD_2_00, /* 00 */
|
||||
S_FWD_1_01, /* 01 ABORT */
|
||||
S_FWD_3_10, /* 10 ADVANCE */
|
||||
S_ILLEGAL, /* 11 ILLEGAL */
|
||||
|
||||
/* -- S_FWD_3_10 -- */
|
||||
S_FWD_2_00, /* 00 ABORT */
|
||||
S_FWD_1_01, /* 01 ILLEGAL */
|
||||
S_FWD_3_10, /* 10 */
|
||||
SW_FWD | S_11, /* 11 ADVANCE */
|
||||
|
||||
/* -- S_BCK_1_10 -- */
|
||||
S_BCK_2_00, /* 00 ADVANCE */
|
||||
S_BCK_3_01, /* 01 ILLEGAL */
|
||||
S_BCK_1_10, /* 10 */
|
||||
S_11, /* 11 ABORT */
|
||||
|
||||
/* -- S_BCK_2_00 -- */
|
||||
S_BCK_2_00, /* 00 */
|
||||
S_BCK_3_01, /* 01 */
|
||||
S_BCK_1_10, /* 10 ABORT */
|
||||
S_ILLEGAL, /* 11 */
|
||||
|
||||
/* -- S_BCK_3_01 -- */
|
||||
S_BCK_2_00, /* 00 */
|
||||
S_BCK_3_01, /* 01 */
|
||||
S_BCK_1_10, /* 10 */
|
||||
SW_BCK | S_11, /* 11 */
|
||||
};
|
||||
|
||||
static uint8_t OldEnc = 0b00;
|
||||
#endif
|
||||
|
||||
static void handle_wheel(void *arg) {
|
||||
static uint32_t negedge_time = 0;
|
||||
static uint32_t posedge_time = 0;
|
||||
static bool last_edge_level = 1;
|
||||
static bool flipflop = 0;
|
||||
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
const bool a = 0 != (inputs & (1 << wheelPin1)); // neg = active
|
||||
const bool b = 0 != (inputs & (1 << wheelPin2));
|
||||
|
||||
// This discards "false rising edge" when we get triggered by a rising edge
|
||||
//
|
||||
if (a == last_edge_level) {
|
||||
return;
|
||||
}
|
||||
last_edge_level = a;
|
||||
|
||||
const uint32_t time = xTaskGetTickCount();
|
||||
|
||||
flipflop ^= 1;
|
||||
// gpio_set_level(sigPin2, flipflop);
|
||||
|
||||
int dir = 0;
|
||||
if (!a) {
|
||||
if ((time - posedge_time) > pdMS_TO_TICKS(DEBOUNCE_WHEEL_MS)) {
|
||||
// negedge
|
||||
negedge_time = time;
|
||||
// gpio_set_level(sigPin1, 0);
|
||||
}
|
||||
} else {
|
||||
// posedge
|
||||
if ((time - negedge_time) > pdMS_TO_TICKS(DEBOUNCE_WHEEL_MS)) {
|
||||
posedge_time = time;
|
||||
if (b) {
|
||||
dir = -1;
|
||||
} else {
|
||||
dir = 1;
|
||||
}
|
||||
// gpio_set_level(sigPin1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
bool a = 0 == (inputs & (1 << wheelPin1)); // neg = active
|
||||
bool b = 0 == (inputs & (1 << wheelPin2));
|
||||
|
||||
#define ENCODER_POS1 1
|
||||
#define ENCODER_POS2 2
|
||||
#define ENCODER_POS3 3
|
||||
#define ENCODER_POS0 0
|
||||
|
||||
uint8_t NewEnc = a | (b<<1);
|
||||
if (NewEnc ^ OldEnc) { // Encoder value changed???
|
||||
switch(NewEnc) {
|
||||
case ENCODER_POS1 : // 01
|
||||
if (OldEnc == ENCODER_POS0) // 00
|
||||
dir--;
|
||||
else
|
||||
dir++;
|
||||
break;
|
||||
case ENCODER_POS3 : // 11
|
||||
if (OldEnc == ENCODER_POS1) // 01
|
||||
dir--;
|
||||
else
|
||||
dir++;
|
||||
break;
|
||||
case ENCODER_POS2 : // 10
|
||||
if (OldEnc == ENCODER_POS3) // 11
|
||||
dir--;
|
||||
else
|
||||
dir++;
|
||||
break;
|
||||
case ENCODER_POS0 : // 00
|
||||
if (OldEnc == ENCODER_POS2) // 10
|
||||
dir--;
|
||||
else
|
||||
dir++;
|
||||
break;
|
||||
};
|
||||
OldEnc = NewEnc;
|
||||
}; // end if encoder value changed.
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
// Omron version
|
||||
int which = (int)arg;
|
||||
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
|
||||
bool a = 0 == (inputs & (1 << wheelPin1)); // neg = active
|
||||
bool b = 0 == (inputs & (1 << wheelPin2));
|
||||
|
||||
if (which == 0) { // itr from A
|
||||
if (a) {
|
||||
if (b) {
|
||||
dir = -1;
|
||||
}
|
||||
else {
|
||||
dir = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // itr from B
|
||||
if (b) {
|
||||
if (a) {
|
||||
dir = 1;
|
||||
}
|
||||
else {
|
||||
dir = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
// Naive version
|
||||
int which = arg;
|
||||
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
|
||||
bool a = 0 == (inputs & (1 << wheelPin1)); // neg = active
|
||||
bool b = 0 == (inputs & (1 << wheelPin2));
|
||||
|
||||
if (which == 0) { // itr from A
|
||||
if (b) {
|
||||
dir = -1;
|
||||
} /*else {
|
||||
dir = 1;
|
||||
}*/
|
||||
}
|
||||
else { // itr from B
|
||||
if (a) {
|
||||
dir = 1;
|
||||
} /*else {
|
||||
dir = -1;
|
||||
}*/
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
// More complex version
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
const uint8_t ab =
|
||||
(((inputs >> wheelPin2) & 1) << 1) |
|
||||
((inputs >> wheelPin1) & 1);
|
||||
|
||||
if (wheelState == S_ILLEGAL) {
|
||||
if (ab != 0b11) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t next = switch_table[wheelState*4 + ab];
|
||||
|
||||
if (next & SW_FWD) dir = 1;
|
||||
if (next & SW_BCK) dir = -1;
|
||||
next &= ~(SW_FWD | SW_BCK);
|
||||
|
||||
wheelState = next;
|
||||
#endif
|
||||
|
||||
if (dir != 0) {
|
||||
BaseType_t higherWoken = 0;
|
||||
xTaskNotifyFromISR(hGuiThread, dir == 1 ? 0b10 : 0b01, eSetBits, &higherWoken);
|
||||
if (higherWoken) portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
static void __attribute__((noreturn)) debounce_service(void *arg) {
|
||||
bool push_state = 0;
|
||||
uint32_t push_change_time = 0;
|
||||
struct debo {
|
||||
bool state;
|
||||
uint32_t start_time;
|
||||
uint32_t debo_time;
|
||||
};
|
||||
|
||||
struct debo state[3] = {
|
||||
{.debo_time = pdMS_TO_TICKS(DEBOUNCE_BTN_MS)}, // push
|
||||
{.debo_time = pdMS_TO_TICKS(DEBOUNCE_WHEEL_MS), .state=1}, // wheel input 1
|
||||
{.debo_time = pdMS_TO_TICKS(DEBOUNCE_WHEEL_MS), .state=1} // wheel input 2
|
||||
};
|
||||
|
||||
while (1) {
|
||||
// uint32_t value = 0;
|
||||
// xTaskNotifyWait(0, ULONG_MAX, &value, pdMS_TO_TICKS(1));
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
|
||||
uint32_t now = xTaskGetTickCount();
|
||||
|
||||
const uint32_t inputs = gpio_input_get();
|
||||
bool pushed = (inputs & (1 << pushPin)) == 0;
|
||||
|
||||
if (pushed) { // event "PUSHED"
|
||||
if (!push_state) {
|
||||
if (push_change_time == 0) {
|
||||
push_change_time = now;
|
||||
bool input[3] = {
|
||||
(inputs & (1 << pushPin)) == 0,
|
||||
(inputs & (1 << wheelPin1)) != 0,
|
||||
(inputs & (1 << wheelPin2)) != 0,
|
||||
};
|
||||
|
||||
enum change {
|
||||
CHG_NONE = 2,
|
||||
CHG_SET = 1,
|
||||
CHG_CLEAR = 0,
|
||||
};
|
||||
|
||||
enum change changes[3] = {CHG_NONE, CHG_NONE, CHG_NONE};
|
||||
|
||||
for (int i=0; i<3;i++) {
|
||||
if (input[i] == !state[i].state) {
|
||||
// Change detected
|
||||
if (state[i].start_time == 0) {
|
||||
// start counting
|
||||
state[i].start_time = now;
|
||||
}
|
||||
|
||||
if ((state[i].start_time != 0) && (now - state[i].start_time > state[i].debo_time)) {
|
||||
state[i].state = !state[i].state;
|
||||
changes[i] = state[i].state ? CHG_SET : CHG_CLEAR;
|
||||
}
|
||||
} else {
|
||||
push_change_time = 0;
|
||||
}
|
||||
}
|
||||
else { // event "RELEASED"
|
||||
if (push_state) {
|
||||
if (push_change_time == 0) {
|
||||
push_change_time = now;
|
||||
}
|
||||
} else {
|
||||
push_change_time = 0;
|
||||
else {
|
||||
// returned back to original, reset counter
|
||||
state[i].start_time = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ((push_change_time != 0) && (now - push_change_time > pdMS_TO_TICKS(DEBOUNCE_BTN_MS))) {
|
||||
push_state = !push_state;
|
||||
uint32_t signal = 0;
|
||||
|
||||
// Wheel logic
|
||||
if (changes[1] == CHG_SET) {
|
||||
if (state[2].state) {
|
||||
signal |= 0b01;
|
||||
}
|
||||
else {
|
||||
signal |= 0b10;
|
||||
}
|
||||
}
|
||||
|
||||
// knob
|
||||
if (changes[0] != CHG_NONE) {
|
||||
signal |= 0b100 << changes[0];
|
||||
}
|
||||
|
||||
// send event to GUI
|
||||
if (signal != 0) {
|
||||
BaseType_t higherWoken = 0;
|
||||
xTaskNotifyFromISR(hGuiThread, 0b100<<push_state, eSetBits, &higherWoken);
|
||||
xTaskNotifyFromISR(hGuiThread, signal, eSetBits, &higherWoken);
|
||||
if (higherWoken) portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_pushbtn(void *arg) {
|
||||
// const uint32_t inputs = gpio_input_get();
|
||||
// bool pushed = (inputs & (1 << pushPin)) == 0;
|
||||
//
|
||||
// BaseType_t higherWoken = 0;
|
||||
// xTaskNotifyFromISR(hDebouncer, 1<<pushed, eSetBits, &higherWoken);
|
||||
// if (higherWoken) portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "nokia.h"
|
||||
#include "liquid.h"
|
||||
#include "drawing.h"
|
||||
|
||||
static void gui_thread(void *arg);
|
||||
|
||||
TaskHandle_t hGuiThread = NULL;
|
||||
|
||||
void gui_init() {
|
||||
printf("GUI init\n");
|
||||
LCD_setup();
|
||||
LCD_setContrast(57);
|
||||
|
||||
int rv = xTaskCreate(gui_thread, "gui", 4096, NULL, 6, &hGuiThread);
|
||||
assert (rv == pdPASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification API:
|
||||
*
|
||||
* 0b1 - knob CW
|
||||
* 0b10 - knowb CCW
|
||||
* 0b100 - button released
|
||||
* 0b1000 - button pressed
|
||||
*
|
||||
* @param arg
|
||||
*/
|
||||
static void __attribute__((noreturn)) gui_thread(void *arg) {
|
||||
struct Liquid *liquid = Liquid_start();
|
||||
|
||||
uint32_t last_wheel_time = 0;
|
||||
|
||||
uint32_t last_time = xTaskGetTickCount();
|
||||
while (1) {
|
||||
bool want_repaint = false;
|
||||
uint32_t value = 0;
|
||||
xTaskNotifyWait(0, ULONG_MAX, &value, pdMS_TO_TICKS(10));
|
||||
uint32_t time = xTaskGetTickCount();
|
||||
|
||||
if (time - last_time >= pdMS_TO_TICKS(2)) {
|
||||
want_repaint |= Liquid_handleTick(liquid, time - last_time);
|
||||
last_time = time;
|
||||
}
|
||||
|
||||
if (value & 0b1000) {
|
||||
want_repaint |= Liquid_handleInput(liquid, InputEvent_Button(1));
|
||||
} else if (value & 0b100) {
|
||||
want_repaint |= Liquid_handleInput(liquid, InputEvent_Button(0));
|
||||
}
|
||||
|
||||
if (value & 0b11) {
|
||||
uint32_t increment = 1;
|
||||
// wheel delta changes with speed
|
||||
if (last_wheel_time != 0) {
|
||||
uint32_t ela = time - last_wheel_time;
|
||||
if (ela < pdMS_TO_TICKS(20)) {
|
||||
increment = 25;
|
||||
} else if (ela < pdMS_TO_TICKS(35)) {
|
||||
increment = 10;
|
||||
} else if (ela < pdMS_TO_TICKS(75)) {
|
||||
increment = 5;
|
||||
}
|
||||
}
|
||||
last_wheel_time = time;
|
||||
|
||||
if (value & 0b01) {
|
||||
want_repaint |= Liquid_handleInput(liquid, InputEvent_Wheel(increment));
|
||||
}
|
||||
|
||||
if (value & 0b10) {
|
||||
want_repaint |= Liquid_handleInput(liquid, InputEvent_Wheel(-increment));
|
||||
}
|
||||
}
|
||||
|
||||
if (want_repaint) {
|
||||
Liquid_paint(liquid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Input event structural enum
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef LIQUID_INPUT_EVENT_H
|
||||
#define LIQUID_INPUT_EVENT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum InputEvent_Kind {
|
||||
InputEventKind_Wheel,
|
||||
InputEventKind_Button,
|
||||
};
|
||||
|
||||
struct InputEvent {
|
||||
enum InputEvent_Kind kind;
|
||||
union {
|
||||
struct {
|
||||
// Wheel delta
|
||||
int32_t delta;
|
||||
int8_t direction;
|
||||
} wheel;
|
||||
struct {
|
||||
// Button state
|
||||
bool state;
|
||||
} button;
|
||||
};
|
||||
};
|
||||
|
||||
static inline struct InputEvent InputEvent_Wheel(int32_t delta)
|
||||
{
|
||||
return (struct InputEvent) {
|
||||
.kind = InputEventKind_Wheel,
|
||||
.wheel = {
|
||||
.delta = delta,
|
||||
.direction = delta > 0 ? 1 : -1,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Button event (push, release)
|
||||
*
|
||||
* @param state - pushed
|
||||
* @return event
|
||||
*/
|
||||
static inline struct InputEvent InputEvent_Button(bool state)
|
||||
{
|
||||
return (struct InputEvent) {
|
||||
.kind = InputEventKind_Button,
|
||||
.button = {.state = state},
|
||||
};
|
||||
}
|
||||
|
||||
#endif //LIQUID_INPUT_EVENT_H
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <malloc.h>
|
||||
#include <assert.h>
|
||||
#include <esp_log.h>
|
||||
#include <rom/queue.h>
|
||||
|
||||
#include "liquid.h"
|
||||
#include "nokia.h"
|
||||
|
||||
static const char *TAG = "Liquid";
|
||||
|
||||
extern struct Scene *NewScene_Root(void);
|
||||
static struct SceneEvent Liquid_initTopScene(struct Liquid *container);
|
||||
static bool processReturnValue(struct Liquid *container, struct SceneEvent result);
|
||||
|
||||
struct RunningScene {
|
||||
struct Scene *scene;
|
||||
SLIST_ENTRY(RunningScene) next;
|
||||
} cmd_item_t;
|
||||
|
||||
struct Liquid {
|
||||
SLIST_HEAD(, RunningScene) stack; // stack with the topmost scene as the first element / head
|
||||
};
|
||||
|
||||
static struct SceneEvent Default_onChildReturn(struct Scene *scene, uint32_t tag, int32_t status, void *data) {
|
||||
if (data) free(data);
|
||||
return SceneEvent_Repaint();
|
||||
}
|
||||
|
||||
static void addChild(struct Liquid *container, struct Scene *child) {
|
||||
struct RunningScene *elm = calloc(1, sizeof(struct RunningScene));
|
||||
if (!child->onChildReturn) child->onChildReturn = Default_onChildReturn;
|
||||
elm->scene = child;
|
||||
SLIST_INSERT_HEAD(&container->stack, elm, next);
|
||||
}
|
||||
|
||||
struct Liquid *Liquid_start(void) {
|
||||
struct Liquid *container = calloc(1, sizeof(struct Liquid));
|
||||
|
||||
addChild(container, NewScene_Root());
|
||||
|
||||
struct SceneEvent result = Liquid_initTopScene(container);
|
||||
|
||||
if (processReturnValue(container, result)) {
|
||||
Liquid_paint(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
static struct SceneEvent handleSceneEvent(struct Liquid *container, struct SceneEvent event) {
|
||||
struct RunningScene *topmost = SLIST_FIRST(&container->stack);
|
||||
struct Scene *newScene;
|
||||
uint32_t tag;
|
||||
switch (event.kind) {
|
||||
case SceneEventKind_Close:
|
||||
assert(SLIST_NEXT(topmost, next) != NULL);
|
||||
SLIST_REMOVE_HEAD(&container->stack, next);
|
||||
tag = topmost->scene->tag;
|
||||
|
||||
if (topmost->scene->deinit) {
|
||||
topmost->scene->deinit(topmost->scene);
|
||||
}
|
||||
free(topmost->scene);
|
||||
free(topmost);
|
||||
|
||||
topmost = SLIST_FIRST(&container->stack);
|
||||
|
||||
// this is always set.
|
||||
return topmost->scene->onChildReturn(topmost->scene, tag, event.close.status, event.close.data);
|
||||
|
||||
case SceneEventKind_OpenChild:
|
||||
if (!event.open.scene) {
|
||||
ESP_LOGE(TAG, "Attempt to open NULL scene!");
|
||||
return SceneEvent_None();
|
||||
}
|
||||
newScene = event.open.scene;
|
||||
newScene->tag = event.open.tag;
|
||||
addChild(container, newScene);
|
||||
return Liquid_initTopScene(container);
|
||||
|
||||
case SceneEventKind_RequestRepaint:
|
||||
case SceneEventKind_None:
|
||||
default:
|
||||
// this shouldn't get here
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if repaint is requested
|
||||
*/
|
||||
static bool processReturnValue(struct Liquid *container, struct SceneEvent result) {
|
||||
bool repaint = false;
|
||||
while (result.kind != SceneEventKind_None) {
|
||||
if (result.kind == SceneEventKind_RequestRepaint) {
|
||||
// Repaint explicitly requested, there's no more chained events.
|
||||
repaint = true;
|
||||
break;
|
||||
}
|
||||
if (result.kind == SceneEventKind_Close) {
|
||||
// child close always triggers repaint, but the event handler can return
|
||||
// another event (e.g. close itself, or open a new child).
|
||||
repaint = true;
|
||||
}
|
||||
// one event can lead to another...
|
||||
result = handleSceneEvent(container, result);
|
||||
}
|
||||
return repaint;
|
||||
}
|
||||
|
||||
bool Liquid_handleInput(struct Liquid *container, struct InputEvent event) {
|
||||
struct RunningScene *topmost = SLIST_FIRST(&container->stack);
|
||||
assert(topmost != NULL);
|
||||
assert(topmost->scene != NULL);
|
||||
if (topmost->scene->onInput) {
|
||||
struct SceneEvent result = topmost->scene->onInput(topmost->scene, event);
|
||||
return processReturnValue(container, result);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Liquid_handleTick(struct Liquid *container, uint32_t elapsed_millis) {
|
||||
struct RunningScene *topmost = SLIST_FIRST(&container->stack);
|
||||
assert(topmost != NULL);
|
||||
assert(topmost->scene != NULL);
|
||||
if (topmost->scene->onTick) {
|
||||
struct SceneEvent result = topmost->scene->onTick(topmost->scene, elapsed_millis);
|
||||
return processReturnValue(container, result);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static struct SceneEvent Liquid_initTopScene(struct Liquid *container) {
|
||||
struct RunningScene *topmost = SLIST_FIRST(&container->stack);
|
||||
assert(topmost != NULL);
|
||||
assert(topmost->scene != NULL);
|
||||
if (topmost->scene->init) {
|
||||
return topmost->scene->init(topmost->scene);
|
||||
} else {
|
||||
return SceneEvent_Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void Liquid_paint(struct Liquid *container) {
|
||||
struct RunningScene *topmost = SLIST_FIRST(&container->stack);
|
||||
assert(topmost != NULL);
|
||||
if (topmost->scene->paint) {
|
||||
topmost->scene->paint(topmost->scene);
|
||||
}
|
||||
|
||||
LCD_updateDisplay();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* GUI framework
|
||||
*
|
||||
* Created on 2020/01/03.
|
||||
*/
|
||||
|
||||
#ifndef LIQUID_H
|
||||
#define LIQUID_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "input_event.h"
|
||||
#include "scene_event.h"
|
||||
#include "scene_type.h"
|
||||
|
||||
struct Liquid;
|
||||
|
||||
/** return 1 if repaint requested */
|
||||
bool Liquid_handleInput(struct Liquid *container, struct InputEvent event);
|
||||
|
||||
/** return 1 if repaint requested */
|
||||
bool Liquid_handleTick(struct Liquid *container, uint32_t elapsed_millis);
|
||||
|
||||
/** render the active scene */
|
||||
void Liquid_paint(struct Liquid *container);
|
||||
|
||||
/** Initialize the GUI system with a root scene */
|
||||
struct Liquid *Liquid_start(void);
|
||||
|
||||
#endif //LIQUID_H
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Scene event structural enum
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef LIQUID_SCENE_EVENT_H
|
||||
#define LIQUID_SCENE_EVENT_H
|
||||
|
||||
// forward declaration
|
||||
struct Scene;
|
||||
|
||||
enum SceneEvent_Kind {
|
||||
SceneEventKind_Close,
|
||||
SceneEventKind_OpenChild,
|
||||
SceneEventKind_RequestRepaint,
|
||||
SceneEventKind_None,
|
||||
};
|
||||
|
||||
/**
|
||||
* Scene event, returned from some scene methods.
|
||||
*
|
||||
* Use the constructor functions to create this struct.
|
||||
*/
|
||||
struct SceneEvent {
|
||||
/** Event kind enum */
|
||||
enum SceneEvent_Kind kind;
|
||||
union {
|
||||
/* data for Close event kind */
|
||||
struct {
|
||||
// Status code
|
||||
int32_t status;
|
||||
// Return data on heap
|
||||
void *data;
|
||||
} close;
|
||||
/* Data for Open event kind */
|
||||
struct {
|
||||
// Scene (initialized, with options loaded in through the constructor)
|
||||
struct Scene *scene;
|
||||
// Tag used by parent to identify the open child
|
||||
uint32_t tag;
|
||||
} open;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create empty (null object) scene event.
|
||||
*
|
||||
* @return event
|
||||
*/
|
||||
static inline struct SceneEvent SceneEvent_None(void)
|
||||
{
|
||||
return (struct SceneEvent) {
|
||||
.kind = SceneEventKind_None,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Request scene repaint
|
||||
*
|
||||
* @return event
|
||||
*/
|
||||
static inline struct SceneEvent SceneEvent_Repaint(void)
|
||||
{
|
||||
return (struct SceneEvent) {
|
||||
.kind = SceneEventKind_RequestRepaint
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a sub-scene to be opened
|
||||
*
|
||||
* @param child - child scene
|
||||
* @param tag - scene tag
|
||||
* @return event
|
||||
*/
|
||||
static inline struct SceneEvent SceneEvent_OpenChild(struct Scene *child, uint32_t tag) {
|
||||
return (struct SceneEvent) {
|
||||
.kind = SceneEventKind_OpenChild,
|
||||
.open = { .scene = child, .tag=tag },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this scene, returning to parent.
|
||||
*
|
||||
* @param status - status number for the parent
|
||||
* @param data - heap-allocated data for parent, can be NULL; e.g. user input as string.
|
||||
* @return event
|
||||
*/
|
||||
static inline struct SceneEvent SceneEvent_Close(int32_t status, void *data)
|
||||
{
|
||||
return (struct SceneEvent) {
|
||||
.kind = SceneEventKind_Close,
|
||||
.close = {.status = status, .data=data},
|
||||
};
|
||||
}
|
||||
|
||||
#endif //LIQUID_SCENE_EVENT_H
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Scene struct
|
||||
*
|
||||
* Created on 2020/01/05.
|
||||
*/
|
||||
|
||||
#ifndef LIQUID_SCENE_TYPE_H
|
||||
#define LIQUID_SCENE_TYPE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct Scene;
|
||||
struct InputEvent;
|
||||
|
||||
/**
|
||||
* Scene::onInput fp type - handle user input
|
||||
*
|
||||
* @param scene - self
|
||||
* @param event - the input event
|
||||
* @return follow-up scene event, can be SceneEvent_None() if not used.
|
||||
*/
|
||||
typedef struct SceneEvent (*Scene_onInput_t)(struct Scene *scene, struct InputEvent event);
|
||||
|
||||
/**
|
||||
* Scene::onChildReturn fp type
|
||||
*
|
||||
* @param scene - self
|
||||
* @param tag - child's tag
|
||||
* @param status - status code returned from the child
|
||||
* @param data - data returned from the child, must be heap-allocated if not NULL.
|
||||
* @return follow-up scene event, can be SceneEvent_None() if not used.
|
||||
*/
|
||||
typedef struct SceneEvent (*Scene_onChildReturn_t)(struct Scene *scene, uint32_t tag, int32_t status, void *data);
|
||||
|
||||
/**
|
||||
* Scene::onTick fp type
|
||||
*
|
||||
* @param scene - self
|
||||
* @return follow-up scene event, can be SceneEvent_None() if not used.
|
||||
*/
|
||||
typedef struct SceneEvent (*Scene_onTick_t)(struct Scene *scene, uint32_t millis);
|
||||
|
||||
/**
|
||||
* Scene::init fp type
|
||||
*
|
||||
* @param scene - self
|
||||
* @return follow-up scene event, can be SceneEvent_None() if not used.
|
||||
*/
|
||||
typedef struct SceneEvent (*Scene_init_t)(struct Scene *scene);
|
||||
|
||||
/**
|
||||
* Scene::paint fp type
|
||||
*
|
||||
* @param scene - self
|
||||
*/
|
||||
typedef void (*Scene_paint_t)(struct Scene *scene);
|
||||
|
||||
/**
|
||||
* Scene::deinit fp type.
|
||||
* Release internally allocated resources.
|
||||
* This is called by the GUI engine when the scene is closed.
|
||||
*
|
||||
* @param scene - self
|
||||
*/
|
||||
typedef void (*Scene_deinit_t)(struct Scene *scene);
|
||||
|
||||
/**
|
||||
* Scene instance in the framework
|
||||
*/
|
||||
struct Scene {
|
||||
/**
|
||||
* Tag given to the scene by its parent to identify its close event.
|
||||
*/
|
||||
uint32_t tag;
|
||||
|
||||
/**
|
||||
* Init function, called once after the scene has been opened.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_init_t init;
|
||||
|
||||
/**
|
||||
* Handle input event.
|
||||
* Can return a follow-up scene event, e.g. repaint request.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_onInput_t onInput;
|
||||
|
||||
/**
|
||||
* Child scene closed, handle its return value and data, if any.
|
||||
* Can return a follow-up scene event.
|
||||
* In any case, the scene will be re-painted, if it stays open and on top.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_onChildReturn_t onChildReturn;
|
||||
|
||||
/**
|
||||
* Handle a periodic tick (10ms).
|
||||
* Can return a follow-up scene event, e.g. repaint request.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_onTick_t onTick;
|
||||
|
||||
/**
|
||||
* Draw the scene to the LCD buffer.
|
||||
* DO NOT write to the display yet, it will be done by the GUI engine.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_paint_t paint;
|
||||
|
||||
/**
|
||||
* Release internally allocated resources, if any. Called on close.
|
||||
* Nullable field.
|
||||
*/
|
||||
Scene_deinit_t deinit;
|
||||
};
|
||||
|
||||
#endif //LIQUID_SCENE_TYPE_H
|
||||
@@ -1,509 +0,0 @@
|
||||
#include <driver/gpio.h>
|
||||
#include <arch/cc.h>
|
||||
#include <driver/spi_master.h>
|
||||
#include "nokia.h"
|
||||
#include <string.h>
|
||||
|
||||
/* Pin definitions:
|
||||
Most of these pins can be moved to any digital or analog pin.
|
||||
DN(MOSI)and SCLK should be left where they are (SPI pins). The
|
||||
LED (backlight) pin should remain on a PWM-capable pin. */
|
||||
static const int scePin = 17; // SCE - Chip select, pin 3 on LCD.
|
||||
static const int rstPin = 16; // RST - Reset, pin 4 on LCD.
|
||||
static const int dcPin = 4; // DC - Data/Command, pin 5 on LCD.
|
||||
static const int sdinPin = 15; // DN(MOSI) - Serial data, pin 6 on LCD.
|
||||
static const int sclkPin = 13; // SCLK - Serial clock, pin 7 on LCD.
|
||||
|
||||
/* PCD8544-specific defines: */
|
||||
#define LCD_COMMAND 0
|
||||
#define LCD_DATA 1
|
||||
|
||||
#define DEFAULT_CONTRAST 48
|
||||
|
||||
static spi_device_handle_t hSPI;
|
||||
|
||||
/* Font table:
|
||||
This table contains the hex values that represent pixels for a
|
||||
font that is 5 pixels wide and 8 pixels high. Each byte in a row
|
||||
represents one, 8-pixel, vertical column of a character. 5 bytes
|
||||
per character. */
|
||||
static const uint8_t ASCII[][5] = {
|
||||
// First 32 characters (0x00-0x19) are ignored. These are
|
||||
// non-displayable, control characters.
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00}, // 0x20
|
||||
{0x00, 0x00, 0x5f, 0x00, 0x00}, // 0x21 !
|
||||
{0x00, 0x07, 0x00, 0x07, 0x00}, // 0x22 "
|
||||
{0x14, 0x7f, 0x14, 0x7f, 0x14}, // 0x23 #
|
||||
{0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 0x24 $
|
||||
{0x23, 0x13, 0x08, 0x64, 0x62}, // 0x25 %
|
||||
{0x36, 0x49, 0x55, 0x22, 0x50}, // 0x26 &
|
||||
{0x00, 0x05, 0x03, 0x00, 0x00}, // 0x27 '
|
||||
{0x00, 0x1c, 0x22, 0x41, 0x00}, // 0x28 (
|
||||
{0x00, 0x41, 0x22, 0x1c, 0x00}, // 0x29 )
|
||||
{0x14, 0x08, 0x3e, 0x08, 0x14}, // 0x2a *
|
||||
{0x08, 0x08, 0x3e, 0x08, 0x08}, // 0x2b +
|
||||
{0x00, 0x50, 0x30, 0x00, 0x00}, // 0x2c ,
|
||||
{0x08, 0x08, 0x08, 0x08, 0x08}, // 0x2d -
|
||||
{0x00, 0x60, 0x60, 0x00, 0x00}, // 0x2e .
|
||||
{0x20, 0x10, 0x08, 0x04, 0x02}, // 0x2f /
|
||||
{0x3e, 0x51, 0x49, 0x45, 0x3e}, // 0x30 0
|
||||
{0x00, 0x42, 0x7f, 0x40, 0x00}, // 0x31 1
|
||||
{0x42, 0x61, 0x51, 0x49, 0x46}, // 0x32 2
|
||||
{0x21, 0x41, 0x45, 0x4b, 0x31}, // 0x33 3
|
||||
{0x18, 0x14, 0x12, 0x7f, 0x10}, // 0x34 4
|
||||
{0x27, 0x45, 0x45, 0x45, 0x39}, // 0x35 5
|
||||
{0x3c, 0x4a, 0x49, 0x49, 0x30}, // 0x36 6
|
||||
{0x01, 0x71, 0x09, 0x05, 0x03}, // 0x37 7
|
||||
{0x36, 0x49, 0x49, 0x49, 0x36}, // 0x38 8
|
||||
{0x06, 0x49, 0x49, 0x29, 0x1e}, // 0x39 9
|
||||
{0x00, 0x36, 0x36, 0x00, 0x00}, // 0x3a :
|
||||
{0x00, 0x56, 0x36, 0x00, 0x00}, // 0x3b ;
|
||||
{0x08, 0x14, 0x22, 0x41, 0x00}, // 0x3c <
|
||||
{0x14, 0x14, 0x14, 0x14, 0x14}, // 0x3d =
|
||||
{0x00, 0x41, 0x22, 0x14, 0x08}, // 0x3e >
|
||||
{0x02, 0x01, 0x51, 0x09, 0x06}, // 0x3f ?
|
||||
{0x32, 0x49, 0x79, 0x41, 0x3e}, // 0x40 @
|
||||
{0x7e, 0x11, 0x11, 0x11, 0x7e}, // 0x41 A
|
||||
{0x7f, 0x49, 0x49, 0x49, 0x36}, // 0x42 B
|
||||
{0x3e, 0x41, 0x41, 0x41, 0x22}, // 0x43 C
|
||||
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 0x44 D
|
||||
{0x7f, 0x49, 0x49, 0x49, 0x41}, // 0x45 E
|
||||
{0x7f, 0x09, 0x09, 0x09, 0x01}, // 0x46 F
|
||||
{0x3e, 0x41, 0x49, 0x49, 0x7a}, // 0x47 G
|
||||
{0x7f, 0x08, 0x08, 0x08, 0x7f}, // 0x48 H
|
||||
{0x00, 0x41, 0x7f, 0x41, 0x00}, // 0x49 I
|
||||
{0x20, 0x40, 0x41, 0x3f, 0x01}, // 0x4a J
|
||||
{0x7f, 0x08, 0x14, 0x22, 0x41}, // 0x4b K
|
||||
{0x7f, 0x40, 0x40, 0x40, 0x40}, // 0x4c L
|
||||
{0x7f, 0x02, 0x0c, 0x02, 0x7f}, // 0x4d M
|
||||
{0x7f, 0x04, 0x08, 0x10, 0x7f}, // 0x4e N
|
||||
{0x3e, 0x41, 0x41, 0x41, 0x3e}, // 0x4f O
|
||||
{0x7f, 0x09, 0x09, 0x09, 0x06}, // 0x50 P
|
||||
{0x3e, 0x41, 0x51, 0x21, 0x5e}, // 0x51 Q
|
||||
{0x7f, 0x09, 0x19, 0x29, 0x46}, // 0x52 R
|
||||
{0x46, 0x49, 0x49, 0x49, 0x31}, // 0x53 S
|
||||
{0x01, 0x01, 0x7f, 0x01, 0x01}, // 0x54 T
|
||||
{0x3f, 0x40, 0x40, 0x40, 0x3f}, // 0x55 U
|
||||
{0x1f, 0x20, 0x40, 0x20, 0x1f}, // 0x56 V
|
||||
{0x3f, 0x40, 0x38, 0x40, 0x3f}, // 0x57 W
|
||||
{0x63, 0x14, 0x08, 0x14, 0x63}, // 0x58 X
|
||||
{0x07, 0x08, 0x70, 0x08, 0x07}, // 0x59 Y
|
||||
{0x61, 0x51, 0x49, 0x45, 0x43}, // 0x5a Z
|
||||
{0x00, 0x7f, 0x41, 0x41, 0x00}, // 0x5b [
|
||||
{0x02, 0x04, 0x08, 0x10, 0x20}, // 0x5c \ (keep this to escape the backslash)
|
||||
{0x00, 0x41, 0x41, 0x7f, 0x00}, // 0x5d ]
|
||||
{0x04, 0x02, 0x01, 0x02, 0x04}, // 0x5e ^
|
||||
{0x40, 0x40, 0x40, 0x40, 0x40}, // 0x5f _
|
||||
{0x00, 0x01, 0x02, 0x04, 0x00}, // 0x60 `
|
||||
{0x20, 0x54, 0x54, 0x54, 0x78}, // 0x61 a
|
||||
{0x7f, 0x48, 0x44, 0x44, 0x38}, // 0x62 b
|
||||
{0x38, 0x44, 0x44, 0x44, 0x20}, // 0x63 c
|
||||
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 0x64 d
|
||||
{0x38, 0x54, 0x54, 0x54, 0x18}, // 0x65 e
|
||||
{0x08, 0x7e, 0x09, 0x01, 0x02}, // 0x66 f
|
||||
{0x0c, 0x52, 0x52, 0x52, 0x3e}, // 0x67 g
|
||||
{0x7f, 0x08, 0x04, 0x04, 0x78}, // 0x68 h
|
||||
{0x00, 0x44, 0x7d, 0x40, 0x00}, // 0x69 i
|
||||
{0x20, 0x40, 0x44, 0x3d, 0x00}, // 0x6a j
|
||||
{0x7f, 0x10, 0x28, 0x44, 0x00}, // 0x6b k
|
||||
{0x00, 0x41, 0x7f, 0x40, 0x00}, // 0x6c l
|
||||
{0x7c, 0x04, 0x18, 0x04, 0x78}, // 0x6d m
|
||||
{0x7c, 0x08, 0x04, 0x04, 0x78}, // 0x6e n
|
||||
{0x38, 0x44, 0x44, 0x44, 0x38}, // 0x6f o
|
||||
{0x7c, 0x14, 0x14, 0x14, 0x08}, // 0x70 p
|
||||
{0x08, 0x14, 0x14, 0x18, 0x7c}, // 0x71 q
|
||||
{0x7c, 0x08, 0x04, 0x04, 0x08}, // 0x72 r
|
||||
{0x48, 0x54, 0x54, 0x54, 0x20}, // 0x73 s
|
||||
{0x04, 0x3f, 0x44, 0x40, 0x20}, // 0x74 t
|
||||
{0x3c, 0x40, 0x40, 0x20, 0x7c}, // 0x75 u
|
||||
{0x1c, 0x20, 0x40, 0x20, 0x1c}, // 0x76 v
|
||||
{0x3c, 0x40, 0x30, 0x40, 0x3c}, // 0x77 w
|
||||
{0x44, 0x28, 0x10, 0x28, 0x44}, // 0x78 x
|
||||
{0x0c, 0x50, 0x50, 0x50, 0x3c}, // 0x79 y
|
||||
{0x44, 0x64, 0x54, 0x4c, 0x44}, // 0x7a z
|
||||
{0x00, 0x08, 0x36, 0x41, 0x00}, // 0x7b {
|
||||
{0x00, 0x00, 0x7f, 0x00, 0x00}, // 0x7c |
|
||||
{0x00, 0x41, 0x36, 0x08, 0x00}, // 0x7d }
|
||||
{0x10, 0x08, 0x08, 0x10, 0x08}, // 0x7e ~
|
||||
{0x78, 0x46, 0x41, 0x46, 0x78}, // 0x7f DEL
|
||||
};
|
||||
|
||||
/* The displayMap variable stores a buffer representation of the
|
||||
pixels on our display. There are 504 total bits in this array,
|
||||
same as how many pixels there are on a 84 x 48 display.
|
||||
|
||||
Each byte in this array covers a 8-pixel vertical block on the
|
||||
display. Each successive byte covers the next 8-pixel column over
|
||||
until you reach the right-edge of the display and step down 8 rows.
|
||||
|
||||
To update the display, we first have to write to this array, then
|
||||
call the updateDisplay() function, which sends this whole array
|
||||
to the PCD8544.
|
||||
|
||||
Because the PCD8544 won't let us write individual pixels at a
|
||||
time, this is how we can make targeted changes to the display. */
|
||||
static uint8_t displayMap[LCD_WIDTH * LCD_HEIGHT / 8];
|
||||
|
||||
// There are two memory banks in the LCD, data/RAM and commands.
|
||||
// This function sets the DC pin high or low depending, and then
|
||||
// sends the data byte
|
||||
static void LCD_SendBytes(bool data_or_command, const uint8_t *data, size_t len)
|
||||
{
|
||||
esp_err_t ret;
|
||||
spi_transaction_t t;
|
||||
if (len == 0) return; //no need to send anything
|
||||
memset(&t, 0, sizeof(t)); //Zero out the transaction
|
||||
t.length = len * 8; //Len is in bytes, transaction length is in bits.
|
||||
t.tx_buffer = data; //Data
|
||||
t.user = (void *) data_or_command; //D/C
|
||||
ret = spi_device_polling_transmit(hSPI, &t); //Transmit!
|
||||
assert(ret == ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
// There are two memory banks in the LCD, data/RAM and commands.
|
||||
// This function sets the DC pin high or low depending, and then
|
||||
// sends the data byte
|
||||
static void LCD_SendByte(bool data_or_command, uint8_t data)
|
||||
{
|
||||
esp_err_t ret;
|
||||
spi_transaction_t t;
|
||||
memset(&t, 0, sizeof(t)); //Zero out the transaction
|
||||
t.length = 8; // transaction length is in bits.
|
||||
t.tx_data[0] = data; //Data
|
||||
t.flags = SPI_TRANS_USE_TXDATA;
|
||||
t.user = (void *) data_or_command; //D/C
|
||||
ret = spi_device_polling_transmit(hSPI, &t); //Transmit!
|
||||
assert(ret == ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
// This function sets a pixel on displayMap to your preferred
|
||||
// color. 1=Black, 0= white.
|
||||
void LCD_setPixel(int x, int y, bool bw)
|
||||
{
|
||||
// First, double check that the coordinate is in range.
|
||||
if ((x >= 0) && (x < LCD_WIDTH) && (y >= 0) && (y < LCD_HEIGHT)) {
|
||||
uint8_t shift = y % 8;
|
||||
|
||||
if (bw) // If black, set the bit.
|
||||
displayMap[x + (y / 8) * LCD_WIDTH] |= 1 << shift;
|
||||
else // If white clear the bit.
|
||||
displayMap[x + (y / 8) * LCD_WIDTH] &= ~(1 << shift);
|
||||
}
|
||||
}
|
||||
|
||||
// setLine draws a line from x0,y0 to x1,y1 with the set color.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setLine(int x0, int y0, int x1, int y1, bool bw)
|
||||
{
|
||||
int dy = y1 - y0; // Difference between y0 and y1
|
||||
int dx = x1 - x0; // Difference between x0 and x1
|
||||
int stepx, stepy;
|
||||
|
||||
if (dy < 0) {
|
||||
dy = -dy;
|
||||
stepy = -1;
|
||||
}
|
||||
else
|
||||
stepy = 1;
|
||||
|
||||
if (dx < 0) {
|
||||
dx = -dx;
|
||||
stepx = -1;
|
||||
}
|
||||
else
|
||||
stepx = 1;
|
||||
|
||||
dy <<= 1; // dy is now 2*dy
|
||||
dx <<= 1; // dx is now 2*dx
|
||||
LCD_setPixel(x0, y0, bw); // Draw the first pixel.
|
||||
|
||||
if (dx > dy) {
|
||||
int fraction = dy - (dx >> 1);
|
||||
while (x0 != x1) {
|
||||
if (fraction >= 0) {
|
||||
y0 += stepy;
|
||||
fraction -= dx;
|
||||
}
|
||||
x0 += stepx;
|
||||
fraction += dy;
|
||||
LCD_setPixel(x0, y0, bw);
|
||||
}
|
||||
}
|
||||
else {
|
||||
int fraction = dx - (dy >> 1);
|
||||
while (y0 != y1) {
|
||||
if (fraction >= 0) {
|
||||
x0 += stepx;
|
||||
fraction -= dy;
|
||||
}
|
||||
y0 += stepy;
|
||||
fraction += dx;
|
||||
LCD_setPixel(x0, y0, bw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setRect will draw a rectangle from x0,y0 top-left corner to
|
||||
// a x1,y1 bottom-right corner. Can be filled with the fill
|
||||
// parameter, and colored with bw.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setRect(int x0, int y0, int x1, int y1, bool fill, bool bw)
|
||||
{
|
||||
// check if the rectangle is to be filled
|
||||
if (fill == 1) {
|
||||
int xDiff;
|
||||
|
||||
if (x0 > x1)
|
||||
xDiff = x0 - x1; //Find the difference between the x vars
|
||||
else
|
||||
xDiff = x1 - x0;
|
||||
|
||||
while (xDiff > 0) {
|
||||
LCD_setLine(x0, y0, x0, y1, bw);
|
||||
|
||||
if (x0 > x1)
|
||||
x0--;
|
||||
else
|
||||
x0++;
|
||||
|
||||
xDiff--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// best way to draw an unfilled rectangle is to draw four lines
|
||||
LCD_setLine(x0, y0, x1, y0, bw);
|
||||
LCD_setLine(x0, y1, x1, y1, bw);
|
||||
LCD_setLine(x0, y0, x0, y1, bw);
|
||||
LCD_setLine(x1, y0, x1, y1, bw);
|
||||
}
|
||||
}
|
||||
|
||||
// setCircle draws a circle centered around x0,y0 with a defined
|
||||
// radius. The circle can be black or white. And have a line
|
||||
// thickness ranging from 1 to the radius of the circle.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setCircle(int x0, int y0, int radius, bool bw, int lineThickness)
|
||||
{
|
||||
for (int r = 0; r < lineThickness; r++) {
|
||||
int f = 1 - radius;
|
||||
int ddF_x = 0;
|
||||
int ddF_y = -2 * radius;
|
||||
int x = 0;
|
||||
int y = radius;
|
||||
|
||||
LCD_setPixel(x0, y0 + radius, bw);
|
||||
LCD_setPixel(x0, y0 - radius, bw);
|
||||
LCD_setPixel(x0 + radius, y0, bw);
|
||||
LCD_setPixel(x0 - radius, y0, bw);
|
||||
|
||||
while (x < y) {
|
||||
if (f >= 0) {
|
||||
y--;
|
||||
ddF_y += 2;
|
||||
f += ddF_y;
|
||||
}
|
||||
x++;
|
||||
ddF_x += 2;
|
||||
f += ddF_x + 1;
|
||||
|
||||
LCD_setPixel(x0 + x, y0 + y, bw);
|
||||
LCD_setPixel(x0 - x, y0 + y, bw);
|
||||
LCD_setPixel(x0 + x, y0 - y, bw);
|
||||
LCD_setPixel(x0 - x, y0 - y, bw);
|
||||
LCD_setPixel(x0 + y, y0 + x, bw);
|
||||
LCD_setPixel(x0 - y, y0 + x, bw);
|
||||
LCD_setPixel(x0 + y, y0 - x, bw);
|
||||
LCD_setPixel(x0 - y, y0 - x, bw);
|
||||
}
|
||||
radius--;
|
||||
}
|
||||
}
|
||||
|
||||
// This function will draw a char (defined in the ASCII table
|
||||
// near the beginning of this sketch) at a defined x and y).
|
||||
// The color can be either black (1) or white (0).
|
||||
void LCD_setChar(char character, int x, int y, bool bw)
|
||||
{
|
||||
uint8_t column; // temp byte to store character's column bitmap
|
||||
for (int i = 0; i < 5; i++) // 5 columns (x) per character
|
||||
{
|
||||
column = ASCII[character - 0x20][i];
|
||||
for (int j = 0; j < 8; j++) // 8 rows (y) per character
|
||||
{
|
||||
if (column & (0x01 << j)) // test bits to set pixels
|
||||
LCD_setPixel(x + i, y + j, bw);
|
||||
else
|
||||
LCD_setPixel(x + i, y + j, !bw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setStr draws a string of characters, calling setChar with
|
||||
// progressive coordinates until it's done.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setStr(char *dString, int x, int y, bool bw)
|
||||
{
|
||||
while (*dString != 0x00) // loop until null terminator
|
||||
{
|
||||
LCD_setChar(*dString++, x, y, bw);
|
||||
x += 5;
|
||||
for (int i = y; i < y + 8; i++) {
|
||||
LCD_setPixel(x, i, !bw);
|
||||
}
|
||||
x++;
|
||||
if (x > (LCD_WIDTH - 5)) // Enables wrap around
|
||||
{
|
||||
x = 0;
|
||||
y += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function will draw an array over the screen. (For now) the
|
||||
// array must be the same size as the screen, covering the entirety
|
||||
// of the display.
|
||||
// Also, the array must reside in FLASH and declared with PROGMEM.
|
||||
void LCD_setBitmap(const char *bitArray)
|
||||
{
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
char c = bitArray[i];
|
||||
displayMap[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
// This function clears the entire display either white (0) or
|
||||
// black (1).
|
||||
// The screen won't actually clear until you call updateDisplay()!
|
||||
void LCD_clearDisplay(bool bw)
|
||||
{
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
if (bw)
|
||||
displayMap[i] = 0xFF;
|
||||
else
|
||||
displayMap[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpful function to directly command the LCD to go to a
|
||||
// specific x,y coordinate.
|
||||
static void gotoXY(int x, int y)
|
||||
{
|
||||
const uint8_t cmd[2] = {
|
||||
0x80 | x,
|
||||
0x40 | y,
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, cmd, 2);
|
||||
}
|
||||
|
||||
// This will actually draw on the display, whatever is currently
|
||||
// in the displayMap array.
|
||||
void LCD_updateDisplay()
|
||||
{
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
gotoXY(0, 0);
|
||||
LCD_SendBytes(LCD_DATA, &displayMap[0], LCD_WIDTH * (LCD_HEIGHT / 8));
|
||||
spi_device_release_bus(hSPI);
|
||||
}
|
||||
|
||||
// Set contrast can set the LCD Vop to a value between 0 and 127.
|
||||
// 40-60 is usually a pretty good range.
|
||||
void LCD_setContrast(uint8_t contrast)
|
||||
{
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
const uint8_t cmd[3] = {
|
||||
0x21, //Tell LCD that extended commands follow
|
||||
0x80 | contrast,//Set LCD Vop (Contrast): Try 0xB1(good @ 3.3V) or 0xBF if your display is too dark
|
||||
0x20,//Set display mode
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, cmd, 3);
|
||||
spi_device_release_bus(hSPI);
|
||||
}
|
||||
|
||||
void LCD_invertDisplay(bool invert)
|
||||
{
|
||||
LCD_SendByte(LCD_COMMAND, 0x0C | invert);
|
||||
}
|
||||
|
||||
void LCD_invertDisplayData()
|
||||
{
|
||||
/* Indirect, swap bits in displayMap option: */
|
||||
for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
|
||||
displayMap[i] ^= 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
//This function is called (in irq context!) just before a transmission starts. It will
|
||||
//set the D/C line to the value indicated in the user field.
|
||||
void lcd_spi_pre_transfer_callback(spi_transaction_t *t)
|
||||
{
|
||||
int dc = (int) t->user;
|
||||
gpio_set_level(dcPin, dc);
|
||||
}
|
||||
|
||||
//This sends the magical commands to the PCD8544
|
||||
void LCD_setup(void)
|
||||
{
|
||||
// clear the display array
|
||||
LCD_clearDisplay(0);
|
||||
|
||||
gpio_config_t output = {
|
||||
.pin_bit_mask = (1<<scePin) | (1<<rstPin) | (1<<dcPin) | (1<<sdinPin) | (1<<sclkPin),
|
||||
.mode = GPIO_MODE_OUTPUT
|
||||
};
|
||||
gpio_config(&output);
|
||||
|
||||
esp_err_t ret;
|
||||
spi_bus_config_t buscfg = {
|
||||
.miso_io_num=-1,
|
||||
.mosi_io_num=sdinPin,
|
||||
.sclk_io_num=sclkPin,
|
||||
.quadwp_io_num=-1,
|
||||
.quadhd_io_num=-1,
|
||||
.max_transfer_sz=LCD_WIDTH * LCD_HEIGHT, // ??? this doesnt seem to do anything in the driver
|
||||
};
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz=4 * 1000 * 1000, // Hz
|
||||
// enable signal lead/lag
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.mode=0, //SPI mode 0
|
||||
.spics_io_num=scePin, //CS pin
|
||||
.queue_size=1, // we use the polling mode, so this does not matter
|
||||
.pre_cb=lcd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
|
||||
};
|
||||
|
||||
//Initialize the SPI bus
|
||||
ret = spi_bus_initialize(HSPI_HOST, &buscfg, 1);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
//Attach the LCD to the SPI bus
|
||||
ret = spi_bus_add_device(HSPI_HOST, &devcfg, &hSPI);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
spi_device_acquire_bus(hSPI, portMAX_DELAY);
|
||||
{
|
||||
//Reset the LCD to a known state
|
||||
gpio_set_level(rstPin, 0);
|
||||
gpio_set_level(rstPin, 1);
|
||||
|
||||
const uint8_t magic[6] = {
|
||||
0x21, // Tell LCD extended commands follow
|
||||
0x80 + DEFAULT_CONTRAST, // Set LCD Vop (Contrast)
|
||||
0x04, // Set Temp coefficent
|
||||
0x14, // LCD bias mode 1:48 (try 0x13)
|
||||
//We must send 0x20 before modifying the display control mode
|
||||
0x20, // Clear extended option
|
||||
0x0C, // Set display control, normal mode.
|
||||
};
|
||||
LCD_SendBytes(LCD_COMMAND, &magic[0], 6);
|
||||
}
|
||||
spi_device_release_bus(hSPI);
|
||||
|
||||
// show the blank screen (display is in indefined state after boot)
|
||||
LCD_updateDisplay();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/* Pin definitions:
|
||||
Most of these pins can be moved to any digital or analog pin.
|
||||
DN(MOSI)and SCLK should be left where they are (SPI pins). The
|
||||
LED (backlight) pin should remain on a PWM-capable pin. */
|
||||
// SCE - Chip select, pin 3 on LCD.
|
||||
// RST - Reset, pin 4 on LCD.
|
||||
// DC - Data/Command, pin 5 on LCD.
|
||||
// DN(MOSI) - Serial data, pin 6 on LCD.
|
||||
// SCLK - Serial clock, pin 7 on LCD.
|
||||
// LED - Backlight LED, pin 8 on LCD.
|
||||
|
||||
/* 84x48 LCD Defines: */
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define LCD_WIDTH 84 // Note: x-coordinates go wide
|
||||
#define LCD_HEIGHT 48 // Note: y-coordinates go high
|
||||
#define WHITE 0 // For drawing pixels. A 0 draws white.
|
||||
#define BLACK 1 // A 1 draws black.
|
||||
|
||||
// This function sets a pixel on displayMap to your preferred
|
||||
// color. 1=Black, 0= white.
|
||||
void LCD_setPixel(int x, int y, bool bw);
|
||||
|
||||
// setLine draws a line from x0,y0 to x1,y1 with the set color
|
||||
void LCD_setLine(int x0, int y0, int x1, int y1, bool bw);
|
||||
|
||||
// setRect will draw a rectangle from x0,y0 top-left corner to
|
||||
// a x1,y1 bottom-right corner. Can be filled with the fill
|
||||
// parameter, and colored with bw.
|
||||
void LCD_setRect(int x0, int y0, int x1, int y1, bool fill, bool bw);
|
||||
|
||||
// setCircle draws a circle centered around x0,y0 with a defined
|
||||
// radius. The circle can be black or white. And have a line
|
||||
// thickness ranging from 1 to the radius of the circle.
|
||||
void LCD_setCircle (int x0, int y0, int radius, bool bw, int lineThickness);
|
||||
|
||||
// This function will draw a char (defined in the ASCII table
|
||||
// near the beginning of this sketch) at a defined x and y).
|
||||
// The color can be either black (1) or white (0).
|
||||
void LCD_setChar(char character, int x, int y, bool bw);
|
||||
|
||||
// setStr draws a string of characters, calling setChar with
|
||||
// progressive coordinates until it's done.
|
||||
// This function was grabbed from the SparkFun ColorLCDShield
|
||||
// library.
|
||||
void LCD_setStr(char * dString, int x, int y, bool bw);
|
||||
|
||||
// This function clears the entire display either white (0) or
|
||||
// black (1).
|
||||
// The screen won't actually clear until you call updateDisplay()!
|
||||
void LCD_clearDisplay(bool bw);
|
||||
|
||||
// This will actually draw on the display, whatever is currently
|
||||
// in the displayMap array.
|
||||
void LCD_updateDisplay();
|
||||
|
||||
// Set contrast can set the LCD Vop to a value between 0 and 127.
|
||||
// 40-60 is usually a pretty good range.
|
||||
void LCD_setContrast(uint8_t contrast);
|
||||
|
||||
/* Invert colors */
|
||||
void LCD_invertDisplay(bool in_data);
|
||||
|
||||
/* Invert colors (hard change in data; does NOT send to display immediately) */
|
||||
void LCD_invertDisplayData();
|
||||
|
||||
//This sends the magical commands to the PCD8544
|
||||
void LCD_setup(void);
|
||||