Compare commits

..

23 Commits

Author SHA1 Message Date
Ondřej Hruška e22d4fb9e2
add a "popup" menu variant (for the reflow profile editor) 5 years ago
Ondřej Hruška 7240773581
better icon, bugfix in bitmaps generator script 5 years ago
Ondřej Hruška 9b604aab4e
fix bitmap drawing, align number picker screen to center 5 years ago
Ondřej Hruška 8a2fe2a18b
add new bitmap sources 5 years ago
Ondřej Hruška 6839a46358
add new bitmaps, bitmap drawing func (broken) 5 years ago
Ondřej Hruška 4e2992f244
add font centering, minor fixes 5 years ago
Ondřej Hruška 09d2c5760a
better vref constant 5 years ago
Ondřej Hruška 55e921712f
move constants nvs load to firehazard module 5 years ago
Ondřej Hruška ad3eb59d58
add wrap-around option to the menu 5 years ago
Ondřej Hruška 202b9e49f7
add manual controls menu, remove demu screens 5 years ago
Ondřej Hruška 17a228faa7
improve font drawing api 5 years ago
Ondřej Hruška 2e28f71d48
better readme 5 years ago
Ondřej Hruška d0d82ee8fb
add firehazard.c 5 years ago
Ondřej Hruška ef7866a29f
add webserver components, some webserver fices and improvements in templating, add real time history chart 5 years ago
Ondřej Hruška ed53611e0f
fixes, improve menu extending, comments 5 years ago
Ondřej Hruška 4587e1d1ab
implement number input 5 years ago
Ondřej Hruška 2b92a6832d
add elapsed time to tick handler params, add generic scrollable menu 5 years ago
Ondřej Hruška d7cd0e9cfa
add bitmap conversion from aseprite ico, add boot animation 5 years ago
Ondřej Hruška 6db58e14a7
code splitting & refactors; more efficient font drawing 5 years ago
Ondřej Hruška 1a2cc21fbb
doc comments, clarify data ownership in scene, use subtyping for scene struct 5 years ago
Ondřej Hruška bdb344752f
fixes in the demo screen 5 years ago
Ondřej Hruška 12f9d2ec2c
add utf8, some glyphs, and more font rendering options 5 years ago
Ondřej Hruška 7299521ab1
Merge branch 'gui-framework' 5 years ago
  1. 1
      .gitignore
  2. 6
      README.md
  3. BIN
      aseprite_c/font/arrow_down.ico
  4. BIN
      aseprite_c/font/arrow_left.ico
  5. BIN
      aseprite_c/font/arrow_right.ico
  6. BIN
      aseprite_c/font/arrow_up.ico
  7. BIN
      aseprite_c/font/back.ico
  8. BIN
      aseprite_c/font/clock.ico
  9. BIN
      aseprite_c/font/cross.ico
  10. BIN
      aseprite_c/font/degree.ico
  11. BIN
      aseprite_c/font/hourglass.ico
  12. BIN
      aseprite_c/font/ico2h
  13. 56
      aseprite_c/font/ico2h.rs
  14. BIN
      aseprite_c/font/return.ico
  15. BIN
      aseprite_c/font/thermometer.ico
  16. BIN
      aseprite_c/font/tri_left.ico
  17. BIN
      aseprite_c/font/tri_right.ico
  18. BIN
      aseprite_c/font/wheel.ico
  19. 124
      aseprite_c/graphics/bitmaps.c
  20. BIN
      aseprite_c/graphics/boot_logo.ico
  21. BIN
      aseprite_c/graphics/flame.ico
  22. BIN
      aseprite_c/graphics/gfx2h
  23. 172
      aseprite_c/graphics/gfx2h.rs
  24. BIN
      aseprite_c/graphics/no_flame.ase
  25. BIN
      aseprite_c/graphics/no_flame.ico
  26. BIN
      aseprite_c/graphics/unittests/12x8.ico
  27. BIN
      aseprite_c/graphics/unittests/3x3.ico
  28. BIN
      aseprite_c/graphics/unittests/8x12.ico
  29. BIN
      aseprite_c/graphics/unittests/8x8.ico
  30. 8
      components/common_utils/CMakeLists.txt
  31. 2
      components/common_utils/README.txt
  32. 3
      components/common_utils/component.mk
  33. 75
      components/common_utils/include/common_utils/base16.h
  34. 131
      components/common_utils/include/common_utils/datetime.h
  35. 19
      components/common_utils/include/common_utils/hexdump.h
  36. 101
      components/common_utils/include/common_utils/utils.h
  37. 62
      components/common_utils/src/base16.c
  38. 85
      components/common_utils/src/common_utils.c
  39. 110
      components/common_utils/src/datetime.c
  40. 72
      components/common_utils/src/hexdump.c
  41. 9
      components/fileserver/CMakeLists.txt
  42. 2
      components/fileserver/README.txt
  43. 3
      components/fileserver/component.mk
  44. 51
      components/fileserver/include/fileserver/embedded_files.h
  45. 227
      components/fileserver/include/fileserver/token_subs.h
  46. 29
      components/fileserver/readme/README.md
  47. 170
      components/fileserver/readme/rebuild_file_tables.php
  48. 22
      components/fileserver/src/embedded_files.c
  49. 625
      components/fileserver/src/token_subs.c
  50. 9
      components/httpd_utils/CMakeLists.txt
  51. 4
      components/httpd_utils/README.txt
  52. 3
      components/httpd_utils/component.mk
  53. 32
      components/httpd_utils/include/httpd_utils/captive.h
  54. 13
      components/httpd_utils/include/httpd_utils/fd_to_ipv4.h
  55. 16
      components/httpd_utils/include/httpd_utils/redirect.h
  56. 55
      components/httpd_utils/include/httpd_utils/session.h
  57. 77
      components/httpd_utils/include/httpd_utils/session_kvmap.h
  58. 100
      components/httpd_utils/include/httpd_utils/session_store.h
  59. 106
      components/httpd_utils/src/captive.c
  60. 42
      components/httpd_utils/src/fd_to_ipv4.c
  61. 20
      components/httpd_utils/src/redirect.c
  62. 181
      components/httpd_utils/src/session_kvmap.c
  63. 220
      components/httpd_utils/src/session_store.c
  64. 41
      components/httpd_utils/src/session_utils.c
  65. 31
      main/CMakeLists.txt
  66. 62
      main/analog.c
  67. 9
      main/analog.h
  68. 96
      main/app_main.c
  69. 106
      main/arduinopid.c
  70. 52
      main/arduinopid.h
  71. 54
      main/files/embed/chart.ignore.svg
  72. BIN
      main/files/embed/favicon.ico
  73. 166
      main/files/embed/index.html
  74. 15
      main/files/files_enum.c
  75. 13
      main/files/files_enum.h
  76. 163
      main/files/rebuild_file_tables.php
  77. 133
      main/firehazard.c
  78. 25
      main/firehazard.h
  79. 124
      main/graphics/bitmaps.c
  80. 21
      main/graphics/bitmaps.h
  81. 13
      main/graphics/display_spec.h
  82. 369
      main/graphics/drawing.c
  83. 93
      main/graphics/drawing.h
  84. 159
      main/graphics/font.c
  85. 20
      main/graphics/font.h
  86. 178
      main/graphics/nokia.c
  87. 39
      main/graphics/nokia.h
  88. 122
      main/graphics/utf8.c
  89. 84
      main/graphics/utf8.h
  90. 49
      main/liquid/gui.c
  91. 0
      main/liquid/gui.h
  92. 57
      main/liquid/input_event.h
  93. 72
      main/liquid/liquid.c
  94. 129
      main/liquid/liquid.h
  95. 44
      main/liquid/scene_car.c
  96. 99
      main/liquid/scene_event.h
  97. 85
      main/liquid/scene_root.c
  98. 118
      main/liquid/scene_type.h
  99. 13
      main/liquid/scenes.h
  100. 509
      main/nokia.c
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitignore vendored

@ -3,3 +3,4 @@ cmake-build-debug
build/ build/
.idea/ .idea/
sdkconfig.old 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

@ -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");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

@ -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");
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

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, "&#34;", 5));
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "&#39;", 5));
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "&lt;", 4));
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "&gt;", 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,13 +1,34 @@
set(COMPONENT_SRCS set(COMPONENT_SRCS
"app_main.c" "app_main.c"
"nokia.c"
"knob.c" "knob.c"
"gui.c" "liquid/gui.c"
"analog.c" "analog.c"
"liquid/liquid.c" "liquid/liquid.c"
"liquid/scene_root.c" "scenes/scene_root.c"
"liquid/scene_car.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") 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() register_component()

@ -6,6 +6,7 @@
#include "driver/gpio.h" #include "driver/gpio.h"
#include "driver/adc.h" #include "driver/adc.h"
#include "esp_adc_cal.h" #include "esp_adc_cal.h"
#include "firehazard.h"
static esp_adc_cal_characteristics_t *adc_chars; static esp_adc_cal_characteristics_t *adc_chars;
@ -16,12 +17,16 @@ static float measurement_celsius;
static const adc_atten_t atten = ADC_ATTEN_DB_0; static const adc_atten_t atten = ADC_ATTEN_DB_0;
static const adc_unit_t unit = ADC_UNIT_1; 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 void analog_service(void *arg);
static TaskHandle_t hAnalog; static TaskHandle_t hAnalog;
#define DEFAULT_VREF 1100 #define DEFAULT_VREF 1095
#define NO_OF_SAMPLES 64 #define NO_OF_SAMPLES 128
void analog_init() { void analog_init() {
printf("Analog init\n"); printf("Analog init\n");
@ -35,6 +40,32 @@ void analog_init() {
assert (rv == pdPASS); 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) { static void __attribute__((noreturn)) analog_service(void *arg) {
while (1) { while (1) {
@ -46,24 +77,31 @@ static void __attribute__((noreturn)) analog_service(void *arg) {
adc_reading /= NO_OF_SAMPLES; adc_reading /= NO_OF_SAMPLES;
//Convert adc_reading to voltage in mV //Convert adc_reading to voltage in mV
uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars); uint32_t mv = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);
#define CORRECT -10; #define CORRECT -10;
voltage += CORRECT; mv += CORRECT;
// printf("Raw: %d ... Voltage: %dmV ...", adc_reading, voltage); printf("Raw: %d ... Voltage: %dmV ...", adc_reading, mv);
float volts = voltage * 0.001f; float volts = mv * 0.001f;
#define R1 4750 float celsius = v_to_c(volts);
#define V1 3.3f printf("Celsius: %.1f°C\n", celsius);
float r_pt100 = (volts * R1)/(V1 - volts);
float celsius = (r_pt100/100.0f - 1.0f) / 3.9083E-3f;
// printf("Rpt %.3f, Celsius: %.1f\n", r_pt100, celsius);
measurement_celsius = celsius; measurement_celsius = celsius;
vTaskDelay(pdMS_TO_TICKS(100)); 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));
} }
} }

@ -7,6 +7,15 @@
#ifndef REFLOWER_ANALOG_H #ifndef REFLOWER_ANALOG_H
#define 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(); void analog_init();
float analog_read(); float analog_read();

@ -7,19 +7,90 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
#include <stdio.h> #include <stdio.h>
#include <web/websrv.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_system.h" #include "esp_system.h"
#include "esp_spi_flash.h" #include "esp_spi_flash.h"
#include "nokia.h" #include "graphics/nokia.h"
#include "knob.h" #include "knob.h"
#include "gui.h" #include "gui.h"
#include "analog.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() 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 = { gpio_config_t output = {
.pin_bit_mask = (1<<22), .pin_bit_mask = (1<<22),
@ -27,11 +98,32 @@ void __attribute__((noreturn)) app_main()
}; };
gpio_config(&output); 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(); gui_init();
knob_init(); knob_init();
analog_init(); analog_init();
try_reconn_if_have_wifi_creds();
websrv_init();
fire_init();
bool level = 0; bool level = 0;
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));

@ -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

Binary file not shown.

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="<EFBFBD>", {{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,44 +1,21 @@
#include "gui.h"
#include "nokia.h"
#include "analog.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <liquid.h>
#include <freertos/timers.h> #include "nokia.h"
#include "liquid.h"
#include "drawing.h"
static void gui_thread(void *arg); static void gui_thread(void *arg);
static void gui_tick(TimerHandle_t xTimer);
TaskHandle_t hGuiThread = NULL; TaskHandle_t hGuiThread = NULL;
TimerHandle_t hTicker = NULL;
void gui_init() { void gui_init() {
printf("GUI init\n"); printf("GUI init\n");
LCD_setup(); LCD_setup();
LCD_setContrast(57);
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); int rv = xTaskCreate(gui_thread, "gui", 4096, NULL, 6, &hGuiThread);
assert (rv == pdPASS); assert (rv == pdPASS);
hTicker = xTimerCreate(
"gui_tick", /* name */
pdMS_TO_TICKS(10), /* period/time */
pdTRUE, /* auto reload */
(void*)0, /* timer ID */
gui_tick); /* callback */
assert(hTicker != NULL);
xTimerStart(hTicker, portMAX_DELAY);
}
static void gui_tick(TimerHandle_t xTimer) {
xTaskNotify(hGuiThread, 0b10000, eSetBits);
} }
/** /**
@ -48,7 +25,6 @@ static void gui_tick(TimerHandle_t xTimer) {
* 0b10 - knowb CCW * 0b10 - knowb CCW
* 0b100 - button released * 0b100 - button released
* 0b1000 - button pressed * 0b1000 - button pressed
* 0b10000 - ticker event
* *
* @param arg * @param arg
*/ */
@ -57,17 +33,16 @@ static void __attribute__((noreturn)) gui_thread(void *arg) {
uint32_t last_wheel_time = 0; uint32_t last_wheel_time = 0;
uint32_t last_time = xTaskGetTickCount();
while (1) { while (1) {
bool want_repaint = false;
uint32_t value = 0; uint32_t value = 0;
xTaskNotifyWait(0, ULONG_MAX, &value, pdMS_TO_TICKS(10)); xTaskNotifyWait(0, ULONG_MAX, &value, pdMS_TO_TICKS(10));
uint32_t time = xTaskGetTickCount();
bool want_repaint = false; if (time - last_time >= pdMS_TO_TICKS(2)) {
want_repaint |= Liquid_handleTick(liquid, time - last_time);
// printf("Knob event 0x%02x ", value); last_time = time;
if (value & 0b10000) {
// TICK
want_repaint |= Liquid_handleTick(liquid);
} }
if (value & 0b1000) { if (value & 0b1000) {
@ -77,8 +52,8 @@ static void __attribute__((noreturn)) gui_thread(void *arg) {
} }
if (value & 0b11) { if (value & 0b11) {
uint32_t time = xTaskGetTickCount();
uint32_t increment = 1; uint32_t increment = 1;
// wheel delta changes with speed
if (last_wheel_time != 0) { if (last_wheel_time != 0) {
uint32_t ela = time - last_wheel_time; uint32_t ela = time - last_wheel_time;
if (ela < pdMS_TO_TICKS(20)) { if (ela < pdMS_TO_TICKS(20)) {

@ -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

@ -1,12 +1,17 @@
#include "liquid.h"
#include "rom/queue.h"
#include "scenes.h"
#include <malloc.h> #include <malloc.h>
#include <assert.h> #include <assert.h>
#include <esp_log.h> #include <esp_log.h>
#include <rom/queue.h>
#include "liquid.h"
#include "nokia.h"
static const char *TAG = "Liquid"; 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 RunningScene {
struct Scene *scene; struct Scene *scene;
SLIST_ENTRY(RunningScene) next; SLIST_ENTRY(RunningScene) next;
@ -16,7 +21,7 @@ struct Liquid {
SLIST_HEAD(, RunningScene) stack; // stack with the topmost scene as the first element / head 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, uint32_t status, void *data) { static struct SceneEvent Default_onChildReturn(struct Scene *scene, uint32_t tag, int32_t status, void *data) {
if (data) free(data); if (data) free(data);
return SceneEvent_Repaint(); return SceneEvent_Repaint();
} }
@ -24,29 +29,35 @@ static struct SceneEvent Default_onChildReturn(struct Scene *scene, uint32_t tag
static void addChild(struct Liquid *container, struct Scene *child) { static void addChild(struct Liquid *container, struct Scene *child) {
struct RunningScene *elm = calloc(1, sizeof(struct RunningScene)); struct RunningScene *elm = calloc(1, sizeof(struct RunningScene));
if (!child->onChildReturn) child->onChildReturn = Default_onChildReturn; if (!child->onChildReturn) child->onChildReturn = Default_onChildReturn;
assert(child->paint != NULL);
elm->scene = child; elm->scene = child;
SLIST_INSERT_HEAD(&container->stack, elm, next); SLIST_INSERT_HEAD(&container->stack, elm, next);
} }
struct Liquid *Liquid_start() { struct Liquid *Liquid_start(void) {
struct Liquid *container = calloc(1, sizeof(struct Liquid)); struct Liquid *container = calloc(1, sizeof(struct Liquid));
addChild(container, NewScene_Root()); addChild(container, NewScene_Root());
struct SceneEvent result = Liquid_initTopScene(container);
if (processReturnValue(container, result)) {
Liquid_paint(container); Liquid_paint(container);
}
return container; return container;
} }
static struct SceneEvent handleSceneEvent(struct Liquid *container, struct SceneEvent event) { static struct SceneEvent handleSceneEvent(struct Liquid *container, struct SceneEvent event) {
struct RunningScene *topmost = SLIST_FIRST(&container->stack); struct RunningScene *topmost = SLIST_FIRST(&container->stack);
struct Scene *newScene;
uint32_t tag;
switch (event.kind) { switch (event.kind) {
case SceneEventKind_Close: case SceneEventKind_Close:
assert(SLIST_NEXT(topmost, next) != NULL); assert(SLIST_NEXT(topmost, next) != NULL);
SLIST_REMOVE_HEAD(&container->stack, next); SLIST_REMOVE_HEAD(&container->stack, next);
uint32_t tag = topmost->scene->tag; tag = topmost->scene->tag;
if (topmost->scene->options) { if (topmost->scene->deinit) {
free(topmost->scene->options); topmost->scene->deinit(topmost->scene);
} }
free(topmost->scene); free(topmost->scene);
free(topmost); free(topmost);
@ -56,15 +67,15 @@ static struct SceneEvent handleSceneEvent(struct Liquid *container, struct Scene
// this is always set. // this is always set.
return topmost->scene->onChildReturn(topmost->scene, tag, event.close.status, event.close.data); return topmost->scene->onChildReturn(topmost->scene, tag, event.close.status, event.close.data);
case SceneEventKind_OpenChild:; case SceneEventKind_OpenChild:
if (!event.open.scene) { if (!event.open.scene) {
ESP_LOGE(TAG, "Attempt to open NULL scene!"); ESP_LOGE(TAG, "Attempt to open NULL scene!");
return SceneEvent_None(); return SceneEvent_None();
} }
struct Scene *newScene = event.open.scene; newScene = event.open.scene;
newScene->tag = event.open.tag; newScene->tag = event.open.tag;
addChild(container, newScene); addChild(container, newScene);
return SceneEvent_Repaint(); return Liquid_initTopScene(container);
case SceneEventKind_RequestRepaint: case SceneEventKind_RequestRepaint:
case SceneEventKind_None: case SceneEventKind_None:
@ -74,14 +85,26 @@ static struct SceneEvent handleSceneEvent(struct Liquid *container, struct Scene
} }
} }
bool processReturnValue(struct Liquid *container, struct SceneEvent result) { /**
* returns true if repaint is requested
*/
static bool processReturnValue(struct Liquid *container, struct SceneEvent result) {
bool repaint = false;
while (result.kind != SceneEventKind_None) { while (result.kind != SceneEventKind_None) {
if (result.kind == SceneEventKind_RequestRepaint) { if (result.kind == SceneEventKind_RequestRepaint) {
return 1; // repaint // 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); result = handleSceneEvent(container, result);
} }
return 0; return repaint;
} }
bool Liquid_handleInput(struct Liquid *container, struct InputEvent event) { bool Liquid_handleInput(struct Liquid *container, struct InputEvent event) {
@ -96,20 +119,35 @@ bool Liquid_handleInput(struct Liquid *container, struct InputEvent event) {
} }
} }
bool Liquid_handleTick(struct Liquid *container) { bool Liquid_handleTick(struct Liquid *container, uint32_t elapsed_millis) {
struct RunningScene *topmost = SLIST_FIRST(&container->stack); struct RunningScene *topmost = SLIST_FIRST(&container->stack);
assert(topmost != NULL); assert(topmost != NULL);
assert(topmost->scene != NULL); assert(topmost->scene != NULL);
if (topmost->scene->onTick) { if (topmost->scene->onTick) {
struct SceneEvent result = topmost->scene->onTick(topmost->scene); struct SceneEvent result = topmost->scene->onTick(topmost->scene, elapsed_millis);
return processReturnValue(container, result); return processReturnValue(container, result);
} else { } else {
return false; 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) { void Liquid_paint(struct Liquid *container) {
struct RunningScene *topmost = SLIST_FIRST(&container->stack); struct RunningScene *topmost = SLIST_FIRST(&container->stack);
assert(topmost != NULL); assert(topmost != NULL);
if (topmost->scene->paint) {
topmost->scene->paint(topmost->scene); topmost->scene->paint(topmost->scene);
}
LCD_updateDisplay();
} }

@ -4,134 +4,27 @@
* Created on 2020/01/03. * Created on 2020/01/03.
*/ */
#ifndef REFLOWER_LIQUID_H #ifndef LIQUID_H
#define REFLOWER_LIQUID_H #define LIQUID_H
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include "input_event.h"
#include "scene_event.h"
#include "scene_type.h"
struct Liquid; struct Liquid;
enum InputEvent_Kind {
InputEventKind_Wheel,
InputEventKind_Button,
};
struct InputEvent {
enum InputEvent_Kind kind;
union {
struct {
// Wheel delta
int8_t delta;
} wheel;
struct {
// Button state
bool state;
} button;
};
};
#define InputEvent_Wheel(delta_) ((struct InputEvent) { \
.kind = InputEventKind_Wheel, \
.wheel = { .delta = delta_ } \
})
#define InputEvent_Button(state_) ((struct InputEvent) { \
.kind = InputEventKind_Button, \
.button = { .state = state_ }, \
})
enum SceneEvent_Kind {
// Close this scene.
// The scene is responsible for cleaning up 'private'.
// 'options' and the scene itself will be freed by the GUI engine.
SceneEventKind_Close,
SceneEventKind_OpenChild,
SceneEventKind_RequestRepaint,
SceneEventKind_None,
};
struct SceneEvent {
enum SceneEvent_Kind kind;
union {
struct {
// Status code
uint32_t status;
// Return data on heap
void *data;
} close;
struct {
// Scene (initialized, with options loaded in through the constructor)
void *scene;
// Tag used by parent to identify the open child
uint32_t tag;
} open;
};
};
struct Scene;
#define SceneEvent_None() ((struct SceneEvent) { \
.kind = SceneEventKind_None\
})
#define SceneEvent_Repaint() ((struct SceneEvent) { \
.kind = SceneEventKind_RequestRepaint\
})
#define SceneEvent_OpenChild(child_, tag_) ((struct SceneEvent) { \
.kind = SceneEventKind_OpenChild,\
.open = { .scene = (child_), .tag=tag_ }, \
})
#define SceneEvent_Close(status_, data_) ((struct SceneEvent) { \
.kind = SceneEventKind_Close,\
.close = { .status = (status_), .data=(data_) }, \
})
typedef struct SceneEvent (*Scene_onInput_t)(struct Scene *scene, struct InputEvent event);
typedef struct SceneEvent (*Scene_onChildReturn_t)(struct Scene *scene, uint32_t tag, uint32_t status, void *data);
typedef struct SceneEvent (*Scene_onTick_t)(struct Scene *scene);
typedef void (*Scene_repaint_t)(struct Scene *scene);
struct Scene {
uint32_t tag;
void *options;
void *private;
/** Handle input */
Scene_onInput_t onInput;
/** Handle child scene return */
Scene_onChildReturn_t onChildReturn;
/** Periodic tick */
Scene_onTick_t onTick;
/** Periodic tick */
Scene_repaint_t paint;
};
/**
* Allocate scene and the inner private type.
*
* Requires <malloc.h>
*
* Must be used like:
*
* struct Scene *scene = SCENE_SAFE_ALLOC(struct private);
* scene->onInput = ...
* return scene;
*/
#define SCENE_SAFE_ALLOC(private_type) \
calloc(1, sizeof(struct Scene)); \
if (!scene) return NULL; \
scene->private = calloc(1, sizeof(private_type)); \
if (!scene->private) return NULL;
/** return 1 if repaint requested */ /** return 1 if repaint requested */
bool Liquid_handleInput(struct Liquid *container, struct InputEvent event); bool Liquid_handleInput(struct Liquid *container, struct InputEvent event);
/** return 1 if repaint requested */ /** return 1 if repaint requested */
bool Liquid_handleTick(struct Liquid *container); bool Liquid_handleTick(struct Liquid *container, uint32_t elapsed_millis);
/** render the active scene */
void Liquid_paint(struct Liquid *container); void Liquid_paint(struct Liquid *container);
struct Liquid *Liquid_start();
#endif //REFLOWER_LIQUID_H /** Initialize the GUI system with a root scene */
struct Liquid *Liquid_start(void);
#endif //LIQUID_H

@ -1,44 +0,0 @@
#include "scenes.h"
#include "liquid.h"
#include "../nokia.h"
#include <malloc.h>
struct private {
int32_t pos;
};
static struct SceneEvent Car_onInput(struct Scene *scene, struct InputEvent event) {
struct private *priv = scene->private;
switch (event.kind) {
case InputEventKind_Wheel:
priv->pos += event.wheel.delta;
if (priv->pos < 0) priv->pos = 0;
if (priv->pos > LCD_WIDTH-21) priv->pos = LCD_WIDTH-21;
return SceneEvent_Repaint();
case InputEventKind_Button:
if (event.button.state) {
return SceneEvent_Close(0, NULL);
}
// fall through
default:
return SceneEvent_None();
}
}
static void Car_paint(struct Scene *scene)
{
struct private *priv = scene->private;
LCD_clearDisplay(0);
LCD_setRect(priv->pos, LCD_HEIGHT/2-10, priv->pos+20,LCD_HEIGHT/2+10,0,1);
LCD_updateDisplay();
}
struct Scene *NewScene_Car(void) {
struct Scene *scene = SCENE_SAFE_ALLOC(struct private);
scene->onInput = Car_onInput;
scene->paint = Car_paint;
return scene;
}

@ -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

@ -1,85 +0,0 @@
#include "scenes.h"
#include "liquid.h"
#include "../nokia.h"
#include "../analog.h"
#include <malloc.h>
#include <stdio.h>
struct private {
int32_t pos;
uint32_t timer_phase;
uint32_t timer_prescale;
};
static struct SceneEvent Root_onInput(struct Scene *scene, struct InputEvent event) {
struct private *priv = scene->private;
switch (event.kind) {
case InputEventKind_Wheel:
priv->pos += event.wheel.delta;
break;
case InputEventKind_Button:
if (event.button.state) {
return SceneEvent_OpenChild(NewScene_Car(), 0);
}
break;
}
return SceneEvent_Repaint();
}
static struct SceneEvent Root_onTick(struct Scene *scene) {
struct private *priv = scene->private;
priv->timer_prescale += 1;
if (priv->timer_prescale == 100) {
priv->timer_prescale = 0;
priv->timer_phase += 1;
priv->timer_phase = priv->timer_phase & 0b11; // 0..3
return SceneEvent_Repaint();
}
return SceneEvent_None();
}
static void Root_paint(struct Scene *scene)
{
struct private *priv = scene->private;
LCD_clearDisplay(0);
const char *header = "???";
switch (priv->timer_phase) {
case 0:
header = "ICE";
break;
case 1:
header = " COLD";
break;
case 2:
header = "COCA";
break;
case 3:
header = " COLA";
break;
}
LCD_setStr(header, 20, 3, 1);
LCD_setRect(0, 15, 83, 35, 1, 1);
char buf[10];
sprintf(buf, "%3d", priv->pos);
LCD_setStr(buf, 2, 17, 0);
sprintf(buf, "%.0f C", analog_read());
LCD_setStr(buf, 2, 26, 0);
LCD_updateDisplay();
}
struct Scene *NewScene_Root(void) {
struct Scene *scene = SCENE_SAFE_ALLOC(struct private);
scene->onInput = Root_onInput;
scene->paint = Root_paint;
scene->onTick = Root_onTick;
return scene;
}

@ -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,13 +0,0 @@
/**
* TODO file description
*
* Created on 2020/01/03.
*/
#ifndef REFLOWER_SCENES_H
#define REFLOWER_SCENES_H
struct Scene *NewScene_Root(void);
struct Scene *NewScene_Car(void);
#endif //REFLOWER_SCENES_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(const 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();
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save