mirror of
https://github.com/Spritetm/picframe_colepd.git
synced 2025-04-18 16:44:40 -04:00
Squash all commits in order to remove personal information. Sorry, you don't get to see the history on this project...
This commit is contained in:
commit
2165990cfd
40 changed files with 99165 additions and 0 deletions
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[submodule "www/cropperjs"]
|
||||
path = "www/cropperjs"
|
||||
url = https://github.com/fengyuanchen/cropperjs.git
|
||||
[submodule "firmware/components/esp32-wifi-manager"]
|
||||
path = firmware/components/esp32-wifi-manager
|
||||
url = "https://github.com/Spritetm/esp32-wifi-manager.git"
|
37
README.md
Normal file
37
README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
|
||||
* Workflow
|
||||
Uploading:
|
||||
- html/js: select image, crop, resize, upload to server
|
||||
- upload php: use C program to convert to EPD.
|
||||
Spit out preview (?), add epd to database w/ timestamp
|
||||
|
||||
epd:
|
||||
- Wake up
|
||||
If Internet:
|
||||
* fetch index JSON file (contains OTA plus timestamps of last 10 images)
|
||||
* Update files to make internal store match
|
||||
* Update list of image shows: set to 0 for new images
|
||||
- Take list of image shows; get lowest show count; show newest image with that count.
|
||||
|
||||
Flash:
|
||||
Other shit: 0x10000
|
||||
OTA1: 0x150000
|
||||
OTA2: 0x150000
|
||||
left: 0x150000
|
||||
|
||||
img size 134464
|
||||
11 images -> 10 150000
|
||||
|
||||
|
||||
Case uses M3 inserts: height 4mm dia 5mm
|
||||
Corresponding screws: M3*6mm
|
||||
|
||||
|
||||
Colors:
|
||||
- Grab picture in 'normal' situation (daylight, curtains, ...)
|
||||
- Open in Gimp
|
||||
- Image -> precision: 16-bit, linear
|
||||
- Filters -> blur (to get rid of grain)
|
||||
- Curve -> make darker colors a bit more dark (so it mixes more white in there, making end image lighter)
|
||||
- Filters -> levels, select black level, select white level
|
||||
|
1
case/.gitignore
vendored
Normal file
1
case/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.stl
|
303
case/case.scad
Normal file
303
case/case.scad
Normal file
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
$fs=0.2;
|
||||
|
||||
pcb_size=[155.5,130,1.6];
|
||||
holes=[
|
||||
[7.5,7.5,0],
|
||||
[7.5,65,0],
|
||||
[7.5,130-7.5,0],
|
||||
[155.5-40.1,7.5,0],
|
||||
[155.5-7.5,65,0],
|
||||
[155.5-7.5,130-7.5,0],
|
||||
[155.5/2,130+5.3,0] //battery compartment screw
|
||||
];
|
||||
btns=[
|
||||
[64, 92.25, 0],
|
||||
[91.5, 92.25, 0]
|
||||
];
|
||||
btn_d=10;
|
||||
|
||||
//location of the battery terminals
|
||||
bat_plus_pos=[24.5, 122, 0];
|
||||
bat_min_pos=[131, 122, 0];
|
||||
//Size of the pocket for the battery terminal solder points
|
||||
bat_term_stickout=[3, 6, 1.3];
|
||||
bat_term_size=[0.65, 12, 12.5];
|
||||
bat_dia=15; //aa battery
|
||||
eink_pcb_offset=[16.2, 16.9, 0];
|
||||
eink_size=[125.4, 99.5, 0.95];
|
||||
eink_vis_size=[114.9, 85.8, 0]; //visible area size
|
||||
eink_vis_offset=[5.25, eink_size.y-eink_vis_size.y-4.6, 0];
|
||||
|
||||
pcb_center=eink_pcb_offset+eink_vis_offset+eink_vis_size/2;
|
||||
|
||||
mat_size=[145,120,3]; //size of actual mat
|
||||
mat_cutout=[eink_vis_size.x+15, eink_vis_size.y+15,0]; //cutout for mat in case
|
||||
mat_vis_extra=1; //how much mm more to show than the visible eink area
|
||||
|
||||
case_size=[170,143,24];
|
||||
case_th=1.5;
|
||||
|
||||
screw_insert_d=5;
|
||||
screw_insert_h=4;
|
||||
screw_insert_post_d=7;
|
||||
screw_hole_d=8.5;
|
||||
screw_thread_hole_d=3.5;
|
||||
|
||||
tolerance=0.2;
|
||||
|
||||
support_d=2.0+tolerance;
|
||||
support_pos=[0,-30,-6];
|
||||
support_angle=[30,0,0];
|
||||
|
||||
render=2;
|
||||
|
||||
if (render==1) {
|
||||
intersection() {
|
||||
rotate([0,0,00]) union() {
|
||||
translate(-pcb_center) pcb();
|
||||
translate([0,0,pcb_size.z]) color("beige") mat();
|
||||
case_top();
|
||||
case_bottom();
|
||||
battery_lid();
|
||||
}
|
||||
translate([0,55,0])cube([200,1,40], center=true);
|
||||
}
|
||||
} else if (render==2) {
|
||||
//translate(-pcb_center) pcb();
|
||||
//translate([0,0,pcb_size.z]) color("beige") mat();
|
||||
//case_top();
|
||||
case_bottom();
|
||||
//battery_lid();
|
||||
} else if (render==3) {
|
||||
button("R");
|
||||
translate([10,10,0]) button("C");
|
||||
}
|
||||
|
||||
|
||||
module button(txt) {
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(h=8, d=btn_d-tolerance*2);
|
||||
cylinder(h=2, d=btn_d+case_th*2);
|
||||
translate([-(case_th-tolerance*2)/2,0,0]) cube([(case_th-tolerance*2),btn_d/2+case_th,5]);
|
||||
}
|
||||
translate([0,0,-1]) cylinder(h=3.5, d=4.5);
|
||||
translate([-3,-3,7.5])linear_extrude(height = 1) {
|
||||
text(txt, font = "Liberation Sans:style=Bold",size=6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module pcb() {
|
||||
difference() {
|
||||
cube(pcb_size);
|
||||
for (pos=holes) {
|
||||
translate(pos+[0,0,-1]) cylinder(h=pcb_size.z+2, d=3.2);
|
||||
}
|
||||
}
|
||||
//Eink
|
||||
translate(eink_pcb_offset) color("white") cube(eink_size+[0,0,pcb_size.z]);
|
||||
//eink visible area
|
||||
translate(eink_pcb_offset+eink_vis_offset) color("magenta") cube(eink_vis_size+[0,0,pcb_size.z+eink_size.z+0.1]);
|
||||
translate(bat_plus_pos-[bat_term_size.x/2, bat_term_size.y/2, bat_term_size.z]) cube(bat_term_size);
|
||||
translate(bat_min_pos-[bat_term_size.x/2, bat_term_size.y/2, bat_term_size.z]) cube(bat_term_size);
|
||||
translate(bat_plus_pos-[-2, 0, bat_dia/2]) rotate([0,90,0]) cylinder(d=bat_dia, h=100);
|
||||
}
|
||||
|
||||
module chamfered_cutout(size) {
|
||||
difference() {
|
||||
translate([0,0,size.z/2]) cube(size+[size.z*2,size.z*2,tolerance*2], center=true);
|
||||
translate([-size.x/2,-size.y,0]) rotate([0,-45,0]) translate([-size.z*2,0,0]) cube([size.z*2, size.y*2, size.z*2]);
|
||||
translate([size.x/2,-size.y,0]) rotate([0,45,0]) translate([0,0,-tolerance*2])cube([size.z*2, size.y*2, size.z*2]);
|
||||
translate([-size.x,-size.y/2,0]) rotate([45,0,0]) translate([0,-size.z*2,0])cube([size.x*2, size.z*2, size.z*2]);
|
||||
translate([-size.x,size.y/2,0]) rotate([-45,0,0]) translate([0,0,-tolerance*2])cube([size.x*2, size.z*2, size.z*2]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//note: assuming the eink visible middle is at [0,0] and the top of the pcb at z=0
|
||||
module mat() {
|
||||
difference() {
|
||||
translate([0,0,mat_size.z/2]) cube(mat_size, center=true);
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,-1]) cylinder(h=mat_size.z+2, d=screw_insert_post_d+tolerance*2);
|
||||
}
|
||||
//beveled hole
|
||||
translate([0,0,eink_size.z-tolerance])chamfered_cutout(eink_vis_size+[mat_vis_extra*2,mat_vis_extra*2,mat_size.z]);
|
||||
//eink plate hole
|
||||
translate(-pcb_center+eink_pcb_offset-[1, 1, tolerance]) cube(eink_size+[1, 1, tolerance]*2);
|
||||
//cutouts for battery contact solder things
|
||||
translate(-pcb_center+bat_plus_pos-[bat_term_stickout.x/2, bat_term_stickout.y/2, pcb_size.z]) cube(bat_term_stickout+[0,0,pcb_size.z]);
|
||||
translate(-pcb_center+bat_min_pos-[bat_term_stickout.x/2, bat_term_stickout.y/2, tolerance]) cube(bat_term_stickout+[0,0,tolerance]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module case_top() {
|
||||
difference() {
|
||||
intersection() {
|
||||
case();
|
||||
translate([-100,-100,-case_th+tolerance*2]) cube([200,200,20]);
|
||||
}
|
||||
//lip
|
||||
translate([-case_size.x/2+case_th/2-tolerance/2, -case_size.y/2+case_th/2-tolerance/2, -case_size.z+tolerance*2]) cube(case_size-[case_th-tolerance,case_th-tolerance,0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module case_bot_fancy(pullback) {
|
||||
edge_off=case_th*2;
|
||||
h=bat_dia+case_th*2;
|
||||
difference() {
|
||||
//square case
|
||||
translate([0,0,bat_dia/2+case_th-pullback]) cube([case_size.x-pullback*2,case_size.y-pullback*2,bat_dia+edge_off], center=true);
|
||||
//beveled main
|
||||
translate([-100, pcb_center.y-bat_plus_pos.y+bat_dia/2, h-pullback]) rotate([-5,0,0]) cube([200,200,20]);
|
||||
//beveled at battery
|
||||
translate([-100, pcb_center.y-bat_plus_pos.y-bat_dia/2, h-pullback]) rotate([30,0,0]) translate([0,-200,0]) cube([200,200,20]);
|
||||
//beveled edges
|
||||
translate([case_size.x/2,-case_size.y/2-tolerance,edge_off-pullback]) rotate([0,50,0]) translate([-100,-tolerance*2,0]) cube([100,case_size.y+tolerance*4,case_size.z,]);
|
||||
translate([-case_size.x/2,case_size.y/2-tolerance,edge_off-pullback]) rotate([0,50,180]) translate([-100,-tolerance*2,0]) cube([100,case_size.y+tolerance*4,case_size.z,]);
|
||||
|
||||
//lip
|
||||
if (pullback==0) difference() {
|
||||
translate([0,0,case_th/2+tolerance/2]) cube([case_size.x,case_size.y,case_th+tolerance], center=true);
|
||||
translate([0,0,case_th/2+tolerance/2]) cube([case_size.x-case_th,case_size.y-case_th,case_th+tolerance], center=true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module battery_lid_cutout(pullback) {
|
||||
translate(-pcb_center) {
|
||||
translate([bat_plus_pos.x-bat_term_size.x/2-pullback,bat_min_pos.y-bat_dia/2-pullback,-20+tolerance*(case_th-pullback)]) cube((bat_min_pos-bat_plus_pos)+[bat_term_size.x+pullback*2,50,20]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module battery_lid() {
|
||||
difference() {
|
||||
intersection() {
|
||||
difference() {
|
||||
//outer case
|
||||
rotate([180,0,0]) case_bot_fancy(0);
|
||||
//cut out screwholes
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,-50-case_th]) cylinder(h=50, d=screw_hole_d);
|
||||
translate(-pcb_center+pos+[0,0,-50+tolerance]) cylinder(h=50, d=screw_thread_hole_d);
|
||||
}
|
||||
}
|
||||
//cut out battery lid itself and half of the inner wall
|
||||
battery_lid_cutout(case_th/2-tolerance);
|
||||
}
|
||||
difference() {
|
||||
rotate([180,0,0]) case_bot_fancy(case_th/2-tolerance);
|
||||
battery_lid_cutout(-tolerance);
|
||||
}
|
||||
difference() {
|
||||
//inner case
|
||||
rotate([180,0,0]) case_bot_fancy(case_th);
|
||||
//cut out screwholes
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,-50]) cylinder(h=50, d=screw_hole_d+case_th*2);
|
||||
}
|
||||
translate(-pcb_center+[bat_plus_pos.x-bat_term_size.x,bat_min_pos.y+bat_dia/2,-20+tolerance]) cube((bat_min_pos-bat_plus_pos)+[bat_term_size.x*2,case_th,20]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module support(pullback) {
|
||||
translate(support_pos) rotate(-support_angle+[270,0,0]) translate([0,0,-pullback]) cylinder(h=90,d=support_d+pullback*2);
|
||||
}
|
||||
|
||||
//support(0);
|
||||
|
||||
module case_bottom() {
|
||||
difference() {
|
||||
union() {
|
||||
difference() {
|
||||
//outer case
|
||||
rotate([180,0,0]) case_bot_fancy(0);
|
||||
//cut out screwholes
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,-50-case_th]) cylinder(h=50, d=screw_hole_d);
|
||||
translate(-pcb_center+pos+[0,0,-50+tolerance]) cylinder(h=50, d=screw_thread_hole_d);
|
||||
}
|
||||
//cut out battery lid hole and half of the inner wall
|
||||
battery_lid_cutout(case_th/2);
|
||||
support(0);
|
||||
}
|
||||
//add other half of the inner wall
|
||||
difference() {
|
||||
intersection() {
|
||||
rotate([180,0,0]) case_bot_fancy(case_th/2);
|
||||
battery_lid_cutout(case_th);
|
||||
}
|
||||
battery_lid_cutout(0);
|
||||
}
|
||||
}
|
||||
difference() {
|
||||
//inner case
|
||||
rotate([180,0,0]) case_bot_fancy(case_th);
|
||||
//cut out screwholes
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,-50]) cylinder(h=50, d=screw_hole_d+case_th*2);
|
||||
}
|
||||
battery_lid_cutout(case_th);
|
||||
hull() {
|
||||
support(case_th);
|
||||
translate([0,0,-10]) support(case_th);
|
||||
}
|
||||
for (pos=btns) {
|
||||
translate(pos-pcb_center-[0,0,20]) difference() {
|
||||
//button hole
|
||||
cylinder(d=btn_d+case_th*2, h=10);
|
||||
//slot
|
||||
rotate([0,0,-90]) translate([0,-case_th/2,0]) cube([btn_d+case_th*3, case_th,11]);
|
||||
}
|
||||
}
|
||||
//support in center of case back
|
||||
translate([0,0,-20]) cylinder(h=20,d=8);
|
||||
}
|
||||
for (pos=btns) {
|
||||
translate(pos-pcb_center-[0,0,20]) cylinder(d=btn_d+tolerance*2, h=20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module case() {
|
||||
hpos=pcb_size.z+mat_size.z+case_th+tolerance;
|
||||
difference() {
|
||||
union(){
|
||||
difference() {
|
||||
//outer cube
|
||||
translate([0,0,-case_size.z/2+hpos]) cube(case_size, center=true);
|
||||
//inner cube cutout
|
||||
translate([0,0,-case_size.z/2+hpos]) cube(case_size-[case_th, case_th, case_th]*2, center=true);
|
||||
}
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,pcb_size.z]) cylinder(h=screw_insert_h, d=screw_insert_post_d);
|
||||
}
|
||||
}
|
||||
//mat cutout
|
||||
translate([0,0,hpos-case_th-tolerance*2]) chamfered_cutout(mat_cutout+[0,0,case_th+tolerance*2]);
|
||||
for (pos=holes) {
|
||||
translate(-pcb_center+pos+[0,0,pcb_size.z-tolerance]) cylinder(h=screw_insert_h+tolerance, d=screw_insert_d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
firmware/.gitignore
vendored
Normal file
5
firmware/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
build
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
managed_components
|
||||
dependencies.lock
|
6
firmware/CMakeLists.txt
Normal file
6
firmware/CMakeLists.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(picframe)
|
1
firmware/components/esp32-wifi-manager
Submodule
1
firmware/components/esp32-wifi-manager
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e855f6949de7a2038a50addae05f4f4b7bcaf204
|
5
firmware/main/CMakeLists.txt
Normal file
5
firmware/main/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
set(srcs "main.c" "epd.c" "sync.c" "io.c")
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_FILES icons.bmp)
|
10
firmware/main/Kconfig.projbuild
Normal file
10
firmware/main/Kconfig.projbuild
Normal file
|
@ -0,0 +1,10 @@
|
|||
menu "Photoframe Configuration"
|
||||
|
||||
config PHOTOFRAME_BASE_URL
|
||||
string "Base URL"
|
||||
default "http://example.com/"
|
||||
help
|
||||
Base URL. Points to a http (not https) URL where the epd-info.php etc files can be found.
|
||||
Note that this MUST end with a / character!
|
||||
|
||||
endmenu
|
238
firmware/main/epd.c
Normal file
238
firmware/main/epd.c
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#ifdef HSPI_HOST
|
||||
//Waveshare ESP32 board
|
||||
#define EPD_HOST HSPI_HOST
|
||||
#define PIN_NUM_BUSY 25
|
||||
#define PIN_NUM_MOSI 14
|
||||
#define PIN_NUM_CLK 13
|
||||
#define PIN_NUM_CS 15
|
||||
#define PIN_NUM_RST 26
|
||||
#define PIN_NUM_DC 27
|
||||
#else
|
||||
//Actual picframe_epd board.
|
||||
#define EPD_HOST SPI2_HOST
|
||||
#define PIN_NUM_BUSY 4
|
||||
#define PIN_NUM_MOSI 9
|
||||
#define PIN_NUM_CLK 8
|
||||
#define PIN_NUM_CS 7
|
||||
#define PIN_NUM_RST 5
|
||||
#define PIN_NUM_DC 6
|
||||
#endif
|
||||
|
||||
|
||||
//To speed up transfers, every SPI transfer sends a bunch of lines. This define specifies how many. More means more memory use,
|
||||
//but less overhead for setting up / finishing transfers. Make sure 240 is dividable by this.
|
||||
#define PARALLEL_LINES 16
|
||||
|
||||
static const char *TAG="epd";
|
||||
|
||||
/*
|
||||
The EPD needs a bunch of command/argument values to be initialized. They are stored in this struct.
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t cmd;
|
||||
uint8_t data[16];
|
||||
uint8_t databytes; //No of data in data; bit 7 = delay after set; 0xFF = end of cmds.
|
||||
} epd_init_cmd_t;
|
||||
#define INIT_DATA_WAIT 0x80
|
||||
|
||||
static const epd_init_cmd_t epd_init_cmds[]={
|
||||
{0x00, {0xef, 0x08}, 2},
|
||||
{0x01, {0x37, 0x00, 0x23, 0x23}, 4},
|
||||
{0x03, {0x00}, 1},
|
||||
{0x06, {0xC7, 0xC7, 0x1D}, 3},
|
||||
{0x30, {0x39}, 1},
|
||||
{0x41, {0x00}, 1},
|
||||
{0x50, {0x37}, 1},
|
||||
{0x60, {0x22}, 1},
|
||||
{0x61, {0x02, 0x58, 0x01, 0xC0}, 4},
|
||||
{0xE3, {0xAA}, 1 | INIT_DATA_WAIT},
|
||||
{0x50, {0x37}, 1},
|
||||
{0, {0}, 0xFF}
|
||||
};
|
||||
|
||||
/* Send a command to the EPD. Uses spi_device_polling_transmit, which waits
|
||||
* until the transfer is complete.
|
||||
*/
|
||||
static void epd_cmd(spi_device_handle_t spi, const uint8_t cmd) {
|
||||
esp_err_t ret;
|
||||
spi_transaction_t t;
|
||||
memset(&t, 0, sizeof(t)); //Zero out the transaction
|
||||
t.length=8; //Command is 8 bits
|
||||
t.tx_buffer=&cmd; //The data is the cmd itself
|
||||
t.user=(void*)0; //D/C needs to be set to 0
|
||||
ret=spi_device_polling_transmit(spi, &t); //Transmit!
|
||||
assert(ret==ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
/* Send data to the EPD. Uses spi_device_polling_transmit, which waits until the
|
||||
* transfer is complete.
|
||||
*/
|
||||
static void epd_data(spi_device_handle_t spi, const uint8_t *data, int 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*)1; //D/C needs to be set to 1
|
||||
ret=spi_device_polling_transmit(spi, &t); //Transmit!
|
||||
assert(ret==ESP_OK); //Should have had no issues.
|
||||
}
|
||||
|
||||
//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.
|
||||
static void epd_spi_pre_transfer_callback(spi_transaction_t *t) {
|
||||
int dc=(int)t->user;
|
||||
gpio_set_level(PIN_NUM_DC, dc);
|
||||
}
|
||||
|
||||
//Wait for the EPD to not be busy anymore
|
||||
static void wait_busy(int val, int timeout_ms) {
|
||||
int64_t tout=esp_timer_get_time()+timeout_ms*1000;
|
||||
while(gpio_get_level(PIN_NUM_BUSY)!=val) {
|
||||
vTaskDelay(2);
|
||||
if (esp_timer_get_time()>tout) {
|
||||
ESP_LOGE(TAG, "Timeout on waiting for busy to go %s!", val?"high":"low");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Initialize the display
|
||||
static void epd_init(spi_device_handle_t spi) {
|
||||
int cmd=0;
|
||||
|
||||
const gpio_config_t cfg[2]={
|
||||
{
|
||||
.pin_bit_mask=(1<<PIN_NUM_DC)|(1<<PIN_NUM_RST),
|
||||
.mode=GPIO_MODE_OUTPUT
|
||||
}, {
|
||||
.pin_bit_mask=(1<<PIN_NUM_BUSY),
|
||||
.mode=GPIO_MODE_INPUT,
|
||||
.pull_up_en=GPIO_PULLUP_ENABLE
|
||||
}
|
||||
};
|
||||
gpio_config(&cfg[0]);
|
||||
gpio_config(&cfg[1]);
|
||||
|
||||
//Initialize non-SPI GPIOs
|
||||
gpio_set_direction(PIN_NUM_DC, GPIO_MODE_OUTPUT);
|
||||
gpio_set_direction(PIN_NUM_RST, GPIO_MODE_OUTPUT);
|
||||
gpio_set_direction(PIN_NUM_BUSY, GPIO_MODE_INPUT);
|
||||
|
||||
//Reset the display
|
||||
gpio_set_level(PIN_NUM_RST, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_set_level(PIN_NUM_RST, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
//wait for not busy
|
||||
wait_busy(1, 1000);
|
||||
|
||||
//Send all the commands
|
||||
while (epd_init_cmds[cmd].databytes!=0xff) {
|
||||
epd_cmd(spi, epd_init_cmds[cmd].cmd);
|
||||
uint8_t data[16];
|
||||
memcpy(data, epd_init_cmds[cmd].data, 16);
|
||||
epd_data(spi, data, epd_init_cmds[cmd].databytes&0x1F);
|
||||
if (epd_init_cmds[cmd].databytes&0x80) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
cmd++;
|
||||
}
|
||||
}
|
||||
|
||||
extern const uint8_t icons_bmp_start[] asm("_binary_icons_bmp_start");
|
||||
|
||||
spi_device_handle_t spi;
|
||||
|
||||
void epd_send(const uint8_t *epddata, int icon) {
|
||||
gpio_hold_dis(PIN_NUM_CS);
|
||||
gpio_hold_dis(PIN_NUM_RST);
|
||||
|
||||
/* initialize epd */
|
||||
esp_err_t ret;
|
||||
spi_bus_config_t buscfg={
|
||||
.miso_io_num=-1,
|
||||
.mosi_io_num=PIN_NUM_MOSI,
|
||||
.sclk_io_num=PIN_NUM_CLK,
|
||||
.quadwp_io_num=-1,
|
||||
.quadhd_io_num=-1,
|
||||
.max_transfer_sz=PARALLEL_LINES*600/2+8
|
||||
};
|
||||
spi_device_interface_config_t devcfg={
|
||||
.clock_speed_hz=1*1000*1000, //Clock out at 10 MHz
|
||||
.mode=0, //SPI mode 0
|
||||
.spics_io_num=PIN_NUM_CS, //CS pin
|
||||
.queue_size=7, //We want to be able to queue 7 transactions at a time
|
||||
.pre_cb=epd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
|
||||
};
|
||||
//Initialize the SPI bus
|
||||
ret=spi_bus_initialize(EPD_HOST, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
//Attach the EPD to the SPI bus
|
||||
ret=spi_bus_add_device(EPD_HOST, &devcfg, &spi);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
//Initialize the EPD
|
||||
epd_init(spi);
|
||||
|
||||
epd_cmd(spi, 0x61);
|
||||
uint8_t data[4]={0x02, 0x58, 0x01, 0xC0};
|
||||
epd_data(spi, data, 4);
|
||||
epd_cmd(spi, 0x10);
|
||||
int bmp_pix_start=icons_bmp_start[0xa]+(icons_bmp_start[0xb]<<8); //actually header is 32-bit... care.
|
||||
//ESP_LOGI(TAG, "bmp starts at 0x%X", bmp_pix_start);
|
||||
for (int y=0; y<448; y++) {
|
||||
uint8_t buf[300];
|
||||
memcpy(buf, &epddata[y*300], 300);
|
||||
if (icon!=0 && y<32) {
|
||||
//the bmp is a file with 4-bit info. Each icon is 32x32 pixels (aka 32x16 bytes)
|
||||
memcpy(buf, &icons_bmp_start[bmp_pix_start+y*16+(icon-1)*(16*32)], 16);
|
||||
}
|
||||
epd_data(spi, buf, 300);
|
||||
}
|
||||
epd_cmd(spi, 0x4);
|
||||
wait_busy(1, 30000);
|
||||
epd_cmd(spi, 0x12);
|
||||
wait_busy(1, 30000);
|
||||
epd_cmd(spi, 0x2);
|
||||
wait_busy(1, 30000);
|
||||
ESP_LOGI(TAG, "Displayed image.");
|
||||
}
|
||||
|
||||
void epd_shutdown() {
|
||||
//deep sleep
|
||||
epd_cmd(spi, 0x7);
|
||||
uint8_t sdata=0xA5;
|
||||
epd_data(spi, &sdata, 1);
|
||||
const gpio_config_t cfg={
|
||||
.pin_bit_mask=(1<<PIN_NUM_DC)|(1<<PIN_NUM_RST)|(1<<PIN_NUM_MOSI)|(1<<PIN_NUM_CS)|(1<<PIN_NUM_CLK),
|
||||
.mode=GPIO_MODE_OUTPUT
|
||||
};
|
||||
//not sure if CS survives deep sleep, as it's GPIO15... RST surely does.
|
||||
gpio_set_level(PIN_NUM_CS, 0);
|
||||
gpio_set_level(PIN_NUM_RST, 1);
|
||||
gpio_config(&cfg);
|
||||
gpio_hold_en(PIN_NUM_CS);
|
||||
gpio_hold_en(PIN_NUM_RST);
|
||||
}
|
9
firmware/main/epd.h
Normal file
9
firmware/main/epd.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
#define ICON_NONE 0
|
||||
//note: icons are bottom to top in bmp
|
||||
#define ICON_BAT_EMPTY 1
|
||||
#define ICON_WIFI 2
|
||||
#define ICON_SERVER 3
|
||||
|
||||
void epd_send(const uint8_t *epddata, int icon);
|
||||
void epd_shutdown();
|
20
firmware/main/epd_flash_image.h
Normal file
20
firmware/main/epd_flash_image.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t id;
|
||||
uint64_t timestamp;
|
||||
uint8_t unused[64-12];
|
||||
} flash_image_hdr_t;
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
flash_image_hdr_t hdr;
|
||||
uint8_t data[600*448/2];
|
||||
uint8_t padding[768-sizeof(flash_image_hdr_t)];
|
||||
} flash_image_t;
|
||||
|
||||
#define IMG_SLOT_COUNT 10
|
||||
#define IMG_SIZE_BYTES 0x21000
|
||||
|
||||
static inline int img_valid(const flash_image_hdr_t *img) {
|
||||
return (img->id==0xfafa1a1a);
|
||||
}
|
BIN
firmware/main/icons.bmp
Normal file
BIN
firmware/main/icons.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
17
firmware/main/idf_component.yml
Normal file
17
firmware/main/idf_component.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/mdns: "*"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
77
firmware/main/io.c
Normal file
77
firmware/main/io.c
Normal file
|
@ -0,0 +1,77 @@
|
|||
//Handles the simple IO things, like the button and battery measuring
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
#include <esp_adc/adc_oneshot.h>
|
||||
#include <esp_adc/adc_cali_scheme.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
|
||||
#define PIN_NUM_BTN 10
|
||||
|
||||
static adc_oneshot_unit_handle_t adc1_handle;
|
||||
static adc_cali_handle_t cal_handle;
|
||||
|
||||
//For the battery, we record the minimum voltage. Given that WiFi startup loads the battery,
|
||||
//this gives a better indication of the state of the thing.
|
||||
int min_bat=9999;
|
||||
|
||||
void adc_callback(void *arg) {
|
||||
int raw, mv;
|
||||
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, ADC_CHANNEL_0, &raw));
|
||||
adc_cali_raw_to_voltage(cal_handle, raw, &mv);
|
||||
if (min_bat>mv) min_bat=mv;
|
||||
}
|
||||
|
||||
void io_init() {
|
||||
const gpio_config_t gpio_cfg={
|
||||
.pin_bit_mask=(1<<PIN_NUM_BTN),
|
||||
.mode=GPIO_MODE_INPUT,
|
||||
.pull_up_en=GPIO_PULLUP_ENABLE
|
||||
};
|
||||
gpio_config(&gpio_cfg);
|
||||
esp_timer_create_args_t config={
|
||||
.callback=adc_callback,
|
||||
.name="adc",
|
||||
.skip_unhandled_events=true
|
||||
};
|
||||
|
||||
adc_oneshot_unit_init_cfg_t adc1_cfg = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.ulp_mode = false,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_oneshot_new_unit(&adc1_cfg, &adc1_handle));
|
||||
adc_oneshot_chan_cfg_t chan_cfg = {
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
.atten = ADC_ATTEN_DB_11,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_0, &chan_cfg));
|
||||
adc_cali_curve_fitting_config_t cali_config = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.atten = ADC_ATTEN_DB_11,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(&cali_config, &cal_handle));
|
||||
|
||||
adc_callback(NULL); //first callback is manual
|
||||
esp_timer_handle_t handle;
|
||||
esp_timer_create(&config, &handle);
|
||||
esp_timer_start_periodic(handle, 50*1000);
|
||||
}
|
||||
|
||||
int io_get_btn() {
|
||||
return !gpio_get_level(PIN_NUM_BTN);
|
||||
}
|
||||
|
||||
int io_get_battery_mv() {
|
||||
return min_bat;
|
||||
}
|
3
firmware/main/io.h
Normal file
3
firmware/main/io.h
Normal file
|
@ -0,0 +1,3 @@
|
|||
void io_init();
|
||||
int io_get_btn();
|
||||
int io_get_battery_mv();
|
224
firmware/main/main.c
Normal file
224
firmware/main/main.c
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "wifi_manager.h"
|
||||
#include <assert.h>
|
||||
#include "esp_partition.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "nvs.h"
|
||||
#include "epd.h"
|
||||
#include "epd_flash_image.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "esp_timer.h"
|
||||
#include "sync.h"
|
||||
#include "io.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_ota_ops.h"
|
||||
|
||||
static const char *TAG="main";
|
||||
|
||||
SemaphoreHandle_t connect_sema;
|
||||
|
||||
void cb_connection_ok(void *pvParameter){
|
||||
ip_event_got_ip_t* param = (ip_event_got_ip_t*)pvParameter;
|
||||
|
||||
/* transform IP to human readable string */
|
||||
char str_ip[16];
|
||||
esp_ip4addr_ntoa(¶m->ip_info.ip, str_ip, IP4ADDR_STRLEN_MAX);
|
||||
|
||||
ESP_LOGI(TAG, "I have a connection and my IP is %s!", str_ip);
|
||||
xSemaphoreGive(connect_sema);
|
||||
}
|
||||
|
||||
void shutdown_callback(void *arg) {
|
||||
ESP_LOGW(TAG, "Alive for too long! Sleeping.");
|
||||
esp_sleep_enable_timer_wakeup(60*60*1000ULL*1000ULL);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
|
||||
void app_main(void) {
|
||||
_Static_assert((sizeof(flash_image_t) == IMG_SIZE_BYTES), "flash_image_t not right size");
|
||||
connect_sema=xSemaphoreCreateBinary();
|
||||
io_init();
|
||||
int icon=ICON_NONE;
|
||||
|
||||
//make sure we shut down after 10 mins guaranteed
|
||||
esp_timer_create_args_t config={
|
||||
.callback=shutdown_callback,
|
||||
.name="shutdown",
|
||||
.skip_unhandled_events=true
|
||||
};
|
||||
esp_timer_handle_t handle;
|
||||
esp_timer_create(&config, &handle);
|
||||
esp_timer_start_periodic(handle, 10*60*1000ULL*1000ULL);
|
||||
|
||||
//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.
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init(); //Retry nvs_flash_init
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
//get nvs handle
|
||||
nvs_handle_t nvs;
|
||||
nvs_open("epd", NVS_READWRITE, &nvs);
|
||||
|
||||
//map image partition, we'll need it later
|
||||
const flash_image_t *images=NULL;
|
||||
spi_flash_mmap_handle_t mmap_handle;
|
||||
const esp_partition_t *part=esp_partition_find_first(123, 0, NULL);
|
||||
esp_partition_mmap(part, 0, IMG_SIZE_BYTES*IMG_SLOT_COUNT, SPI_FLASH_MMAP_DATA, (const void**)&images, &mmap_handle);
|
||||
|
||||
//see if we still have enough juice to go online
|
||||
int bat=io_get_battery_mv();
|
||||
int32_t bat_empty_thr=2250;
|
||||
ESP_LOGI(TAG, "Battery at %d mV before going online...", bat);
|
||||
|
||||
if (bat<bat_empty_thr) {
|
||||
icon=ICON_BAT_EMPTY;
|
||||
} else {
|
||||
//start the wifi manager
|
||||
wifi_manager_start();
|
||||
wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &cb_connection_ok);
|
||||
|
||||
if (io_get_btn()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
wifi_manager_send_message(WM_ORDER_DISCONNECT_STA, NULL);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
wifi_manager_send_message(WM_ORDER_START_AP, NULL);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
wifi_manager_send_message(WM_ORDER_START_HTTP_SERVER, NULL);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
wifi_manager_send_message(WM_ORDER_START_DNS_SERVICE, NULL);
|
||||
}
|
||||
//wait for connection
|
||||
ESP_LOGI(TAG, "Waiting for connection...x");
|
||||
int can_connect=xSemaphoreTake(connect_sema, pdMS_TO_TICKS(30*1000));
|
||||
int bat=io_get_battery_mv();
|
||||
ESP_LOGI(TAG, "Battery at %d mV after WiFi init", bat);
|
||||
if (bat<bat_empty_thr) {
|
||||
icon=ICON_BAT_EMPTY;
|
||||
} else {
|
||||
if (can_connect) {
|
||||
//We have a WiFi connection.
|
||||
err=picframe_sync(images, part);
|
||||
if (err==ESP_OK) esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (err!=ESP_OK) icon=ICON_SERVER;
|
||||
} else {
|
||||
icon=ICON_WIFI;
|
||||
//No connection, mayhaps we're in config mode?
|
||||
//If so, wait for a few minutes for the user to do its thing in the
|
||||
//wifi manager UI. We break out either when we have a sta connection or
|
||||
//if the mode is not apsta/ap anymore.
|
||||
int wifi_timeout_sec=3*60;
|
||||
do {
|
||||
wifi_mode_t mode;
|
||||
esp_wifi_get_mode(&mode);
|
||||
if (mode==WIFI_MODE_STA) break;
|
||||
|
||||
can_connect=xSemaphoreTake(connect_sema, pdMS_TO_TICKS(1*1000));
|
||||
if (can_connect) break;
|
||||
|
||||
wifi_timeout_sec--;
|
||||
if (wifi_timeout_sec==0) break;
|
||||
ESP_LOGI(TAG, "Delaying shutdown as we're in AP/APSTA mode...");
|
||||
} while(1);
|
||||
|
||||
if (can_connect) {
|
||||
err=picframe_sync(images, part);
|
||||
if (err==ESP_OK) esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (err!=ESP_OK) icon=ICON_SERVER; else icon=ICON_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Assume we're entirely done with WiFi.
|
||||
wifi_manager_destroy();
|
||||
vTaskDelay(pdMS_TO_TICKS(200)); //needed?
|
||||
esp_wifi_stop();
|
||||
}
|
||||
|
||||
//Grab cur_img and img_shows data from NVS
|
||||
int16_t curr_img[IMG_SLOT_COUNT];
|
||||
int16_t img_shows[IMG_SLOT_COUNT]={0};
|
||||
for (int i=0; i<IMG_SLOT_COUNT; i++) curr_img[i]=-1; //default to invalid
|
||||
size_t len=IMG_SLOT_COUNT*sizeof(uint16_t);
|
||||
nvs_get_blob(nvs, "curr_img", curr_img, &len);
|
||||
len=IMG_SLOT_COUNT*sizeof(uint16_t);
|
||||
nvs_get_blob(nvs, "img_shows", img_shows, &len);
|
||||
|
||||
//Find the image that is valid with the highest ID and lowest count
|
||||
int img=0;
|
||||
for (int i=1; i<IMG_SLOT_COUNT; i++) {
|
||||
if (curr_img[i]==-1) continue; //invalid
|
||||
if (img_shows[i]<img_shows[img]) {
|
||||
img=i;
|
||||
} else if (img_shows[i]==img_shows[img]) {
|
||||
if (curr_img[i]>curr_img[img]) img=i;
|
||||
}
|
||||
}
|
||||
|
||||
//Show image we found, if any
|
||||
if (curr_img[img]!=-1) {
|
||||
img_shows[img]++;
|
||||
nvs_set_blob(nvs, "img_shows", img_shows, IMG_SLOT_COUNT*sizeof(uint16_t));
|
||||
|
||||
ESP_LOGI(TAG, "Displaying img id %d from slot %d", curr_img[img], img);
|
||||
epd_send(images[img].data, icon);
|
||||
epd_shutdown();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Going to sleep.");
|
||||
//Set timezone
|
||||
char tz[32];
|
||||
len=sizeof(tz);
|
||||
if (nvs_get_str(nvs, "tz", tz, &len)==ESP_OK) {
|
||||
setenv("TZ", tz, 1);
|
||||
tzset();
|
||||
}
|
||||
//Figure out when to wake.
|
||||
int32_t updhour=0;
|
||||
nvs_get_i32(nvs, "upd_hour", &updhour);
|
||||
struct tm tm;
|
||||
time_t now=time(NULL);
|
||||
localtime_r(&now, &tm);
|
||||
ESP_LOGI(TAG, "Now is %d:%02d, sleeping till %ld:00", tm.tm_hour, tm.tm_min, updhour);
|
||||
tm.tm_hour=updhour;
|
||||
tm.tm_min=0;
|
||||
time_t wake=mktime(&tm);
|
||||
int64_t sleep_sec=wake-time(NULL);
|
||||
//Make sure this is in the future. We can wake up a bit before the wake time, and without
|
||||
//this, it would sleep only a short while until the actual wake time.
|
||||
while (sleep_sec<(10*60)) sleep_sec+=(24*60*60);
|
||||
|
||||
if (icon==ICON_BAT_EMPTY) {
|
||||
//Sleep forever.
|
||||
ESP_LOGI(TAG, "Low battery, so sleeping forever.");
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
//Zzzzz....
|
||||
ESP_LOGI(TAG, "Sleeping %lld sec", sleep_sec);
|
||||
esp_sleep_enable_timer_wakeup(sleep_sec*1000ULL*1000ULL);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
esp_deep_sleep_start();
|
||||
}
|
301
firmware/main/sync.c
Normal file
301
firmware/main/sync.c
Normal file
|
@ -0,0 +1,301 @@
|
|||
|
||||
/*
|
||||
This code syncs the state of the picture frame (firmware, images in memory, config) to
|
||||
what the server thinks it should be.
|
||||
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include <assert.h>
|
||||
#include "esp_partition.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_mac.h"
|
||||
#include "cJSON.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_flash_partitions.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_tls_crypto.h"
|
||||
#include "mbedtls/base64.h"
|
||||
#include "sync.h"
|
||||
#include "io.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
static const char *TAG="sync";
|
||||
|
||||
#define BASE_URL CONFIG_PHOTOFRAME_BASE_URL
|
||||
#define INFO_PATH "epd-info.php"
|
||||
#define IMG_PATH "epd-img.php"
|
||||
|
||||
#define CHECKFW_OK 0
|
||||
#define CHECKFW_NEED_UPDATE 1
|
||||
#define CHECKFW_ERR 2
|
||||
|
||||
static void get_app_sha(uint8_t *sha, int for_ota_part) {
|
||||
const esp_partition_t *runpart;
|
||||
if (!for_ota_part) {
|
||||
runpart=esp_ota_get_running_partition();
|
||||
} else {
|
||||
runpart=esp_ota_get_next_update_partition(NULL);
|
||||
}
|
||||
esp_app_desc_t runappinfo;
|
||||
esp_ota_get_partition_description(runpart, &runappinfo);
|
||||
memcpy(sha, runappinfo.app_elf_sha256, 32);
|
||||
}
|
||||
|
||||
static int check_fw_update(cJSON *json) {
|
||||
char sha[32];
|
||||
cJSON *js_fwsha=cJSON_GetObjectItem(json, "fw_sha");
|
||||
if (!js_fwsha) return CHECKFW_ERR;
|
||||
const char *sha_txt=cJSON_GetStringValue(js_fwsha);
|
||||
if (!sha_txt) return CHECKFW_ERR;
|
||||
size_t olen;
|
||||
mbedtls_base64_decode((unsigned char*)sha, 32, &olen, (const unsigned char*)sha_txt, strlen(sha_txt));
|
||||
if (olen!=32) return CHECKFW_ERR;
|
||||
|
||||
uint8_t cur_sha[32];
|
||||
get_app_sha(cur_sha, 0);
|
||||
|
||||
if (memcmp(cur_sha, sha, 32)==0) {
|
||||
ESP_LOGI(TAG, "Firmware still up to date.");
|
||||
return CHECKFW_OK;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Firmware needs update.");
|
||||
get_app_sha(cur_sha, 1);
|
||||
if (memcmp(cur_sha, sha, 32)==0) {
|
||||
ESP_LOGI(TAG, "...but the update was previously attempted and failed");
|
||||
return CHECKFW_OK;
|
||||
}
|
||||
return CHECKFW_NEED_UPDATE;
|
||||
}
|
||||
}
|
||||
|
||||
static void do_fw_update(esp_http_client_handle_t http) {
|
||||
esp_ota_handle_t ota;
|
||||
const esp_partition_t *runpart=esp_ota_get_running_partition();
|
||||
const esp_partition_t *updpart=esp_ota_get_next_update_partition(runpart);
|
||||
esp_err_t err=esp_ota_begin(updpart, OTA_SIZE_UNKNOWN, &ota);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Could not initialize ota: %s", esp_err_to_name(err));
|
||||
goto err_ota;
|
||||
}
|
||||
//Data read/write loop
|
||||
char buf[1024];
|
||||
while(1) {
|
||||
int len=esp_http_client_read(http, buf, sizeof(buf));
|
||||
if (len==0) {
|
||||
if (esp_http_client_is_complete_data_received(http)) {
|
||||
err=esp_ota_end(ota);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "OTA finalize failed: %s", esp_err_to_name(err));
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "HTTP reading image failed");
|
||||
goto err_ota;
|
||||
}
|
||||
}
|
||||
err=esp_ota_write(ota, buf, len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "OTA write of len %d failed: %s", len, esp_err_to_name(err));
|
||||
goto err_ota;
|
||||
}
|
||||
}
|
||||
err=esp_ota_set_boot_partition(updpart);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "OTA set boot part failed: %s", esp_err_to_name(err));
|
||||
goto err;
|
||||
}
|
||||
ESP_LOGW(TAG, "Update succesful! Booting into new app version.");
|
||||
esp_restart();
|
||||
while(1); //never reached
|
||||
err_ota:
|
||||
esp_ota_abort(ota);
|
||||
err:
|
||||
}
|
||||
|
||||
const char *json_get_string(cJSON *json, const char *name) {
|
||||
cJSON *jsnode=cJSON_GetObjectItem(json, name);
|
||||
if (!jsnode) return NULL;
|
||||
const char *ret=cJSON_GetStringValue(jsnode);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t picframe_sync(const flash_image_t *images, const esp_partition_t *part) {
|
||||
esp_err_t ret=ESP_OK;
|
||||
const esp_http_client_config_t config={
|
||||
.url=BASE_URL,
|
||||
.timeout_ms=16000,
|
||||
};
|
||||
esp_http_client_handle_t http=esp_http_client_init(&config);
|
||||
ESP_GOTO_ON_FALSE(http, ESP_ERR_NO_MEM, err_client_alloc, TAG, "couldn't init http client");
|
||||
|
||||
//Generate info retrieve URL
|
||||
unsigned char mac[6];
|
||||
char url[192];
|
||||
esp_base_mac_addr_get(mac);
|
||||
int bat_pwr=io_get_battery_mv();
|
||||
uint8_t cur_sha[32];
|
||||
get_app_sha(cur_sha, 0);
|
||||
char cur_sha_text[16];
|
||||
for (int i=0; i<8; i++) sprintf(&cur_sha_text[i*2], "%02X", cur_sha[i]);
|
||||
sprintf(url, "%s%s?mac=%02X%02X%02X%02X%02X%02X&bat=%d&fw=%s", BASE_URL, INFO_PATH,
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], bat_pwr, cur_sha_text);
|
||||
|
||||
//Fetch info
|
||||
esp_err_t err;
|
||||
err=esp_http_client_set_url(http, url);
|
||||
ESP_GOTO_ON_ERROR(err, err_http, TAG, "esp_http_client_url to info url failed");
|
||||
err=esp_http_client_open(http, 0);
|
||||
ESP_GOTO_ON_ERROR(err, err_http, TAG, "esp_http_client_open for info url failed");
|
||||
esp_http_client_fetch_headers(http); //note: error ignored
|
||||
char resp[512];
|
||||
size_t len=esp_http_client_read(http, resp, sizeof(resp));
|
||||
ESP_GOTO_ON_FALSE(len>0, ESP_FAIL, err_http, TAG, "couldn't read info url");
|
||||
err=esp_http_client_close(http);
|
||||
ESP_GOTO_ON_ERROR(err, err_http, TAG, "esp_http_client_open for info url failed");
|
||||
|
||||
//Parse info
|
||||
cJSON *json=cJSON_Parse(resp);
|
||||
ESP_GOTO_ON_FALSE(json!=NULL, ESP_FAIL, err_http, TAG, "couldn't parse info");
|
||||
|
||||
//First, see if there's a software update.
|
||||
int needs_update=check_fw_update(json);
|
||||
if (needs_update==CHECKFW_NEED_UPDATE) {
|
||||
const char *upd_path=json_get_string(json, "fw_upd");
|
||||
ESP_GOTO_ON_FALSE(upd_path!=NULL, ESP_FAIL, err_http, TAG, "couldn't parse json from info");
|
||||
sprintf(url, "%s%s", BASE_URL, upd_path);
|
||||
ESP_LOGI(TAG, "Doing OTA from %s", url);
|
||||
err=esp_http_client_set_url(http, url);
|
||||
ESP_GOTO_ON_ERROR(err, err_httpjs, TAG, "esp_http_client_url to update url failed");
|
||||
err=esp_http_client_open(http, 0);
|
||||
ESP_GOTO_ON_ERROR(err, err_httpjs, TAG, "esp_http_client_open for update url failed");
|
||||
esp_http_client_fetch_headers(http);
|
||||
do_fw_update(http);
|
||||
err=esp_http_client_close(http);
|
||||
ESP_GOTO_ON_ERROR(err, err_httpjs, TAG, "esp_http_client_close for update url failed");
|
||||
}
|
||||
|
||||
//Next, save the things we received to flash
|
||||
nvs_handle_t nvs;
|
||||
nvs_open("epd", NVS_READWRITE, &nvs);
|
||||
const char *tz=json_get_string(json, "tz");
|
||||
if (tz) nvs_set_str(nvs, "tz", tz);
|
||||
cJSON *j_updhour=cJSON_GetObjectItem(json, "update_hour");
|
||||
if (j_updhour) {
|
||||
int32_t updhour=cJSON_GetNumberValue(j_updhour);
|
||||
nvs_set_i32(nvs, "upd_hour", updhour);
|
||||
}
|
||||
|
||||
//Set time. We set timezone in the main app from the nvs value.
|
||||
cJSON *js_time=cJSON_GetObjectItem(json, "time");
|
||||
if (js_time && tz) {
|
||||
setenv("TZ", "GMT+0", 1);
|
||||
tzset();
|
||||
//set time
|
||||
struct timeval tv={0};
|
||||
tv.tv_sec=cJSON_GetNumberValue(js_time);
|
||||
settimeofday(&tv, NULL);
|
||||
ESP_LOGI(TAG, "Time set.");
|
||||
}
|
||||
|
||||
int16_t curr_img[IMG_SLOT_COUNT];
|
||||
int16_t server_img[IMG_SLOT_COUNT];
|
||||
int16_t img_shows[IMG_SLOT_COUNT]={0};
|
||||
for (int i=0; i<IMG_SLOT_COUNT; i++) {
|
||||
curr_img[i]=-1; //default to invalid
|
||||
server_img[i]=-1;
|
||||
}
|
||||
len=IMG_SLOT_COUNT*sizeof(uint16_t);
|
||||
nvs_get_blob(nvs, "curr_img", curr_img, &len);
|
||||
len=IMG_SLOT_COUNT*sizeof(uint16_t);
|
||||
nvs_get_blob(nvs, "img_shows", img_shows, &len);
|
||||
|
||||
//Parse info we got from server
|
||||
cJSON *js_ids=cJSON_GetObjectItem(json, "images");
|
||||
ESP_GOTO_ON_FALSE(js_ids, ESP_FAIL, err_httpjs, TAG, "no image array in info");
|
||||
for (int i=0; i<IMG_SLOT_COUNT; i++) {
|
||||
cJSON *js_id=cJSON_GetArrayItem(js_ids, i);
|
||||
if (!js_id) continue;
|
||||
server_img[i]=cJSON_GetNumberValue(js_id);
|
||||
}
|
||||
//See if there's anything we need to download
|
||||
for (int i=0; i<IMG_SLOT_COUNT; i++) {
|
||||
int found=0;
|
||||
for (int j=0; j<IMG_SLOT_COUNT; j++) {
|
||||
if (server_img[i]==curr_img[j]) found=1;
|
||||
}
|
||||
if (found) {
|
||||
ESP_LOGI(TAG, "Image ID %d: already have that", server_img[i]);
|
||||
} else {
|
||||
//Need to find a slot to download this to. We can use a slot that contains an
|
||||
//image that is stale, as in, not on the list the server gave us.
|
||||
int download_slot=-1;
|
||||
for (int j=0; j<IMG_SLOT_COUNT; j++) {
|
||||
int slot_available=1;
|
||||
for (int k=0; k<IMG_SLOT_COUNT; k++) {
|
||||
if (curr_img[j]==server_img[k]) slot_available=0;
|
||||
}
|
||||
if (slot_available) {
|
||||
download_slot=j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//Mark slot as invalid, in case the download fails
|
||||
curr_img[download_slot]=-1; //invalid
|
||||
nvs_set_blob(nvs, "curr_img", curr_img, IMG_SLOT_COUNT*sizeof(uint16_t));
|
||||
//Download image to flash partition.
|
||||
ESP_LOGI(TAG, "Image ID %d: need to download to slot %d, overwriting image id %d", server_img[i], download_slot, curr_img[download_slot]);
|
||||
esp_partition_erase_range(part, download_slot*IMG_SIZE_BYTES, IMG_SIZE_BYTES);
|
||||
sprintf(url, "%s%s?id=%d", BASE_URL, IMG_PATH, server_img[i]);
|
||||
err=esp_http_client_set_url(http, url);
|
||||
ESP_GOTO_ON_ERROR(err, err_httpjs, TAG, "esp_http_client_url to image url failed");
|
||||
err=esp_http_client_open(http, 0);
|
||||
ESP_GOTO_ON_ERROR(err, err_httpjs, TAG, "esp_http_client_open for image url failed");
|
||||
esp_http_client_fetch_headers(http);
|
||||
char buf[1024];
|
||||
int p=download_slot*IMG_SIZE_BYTES;
|
||||
int len;
|
||||
int recved=0;
|
||||
while ((len=esp_http_client_read(http, buf, sizeof(buf)))>0) {
|
||||
esp_partition_write(part, p, buf, len);
|
||||
p+=len;
|
||||
recved+=len;
|
||||
}
|
||||
ESP_GOTO_ON_FALSE(len>=0, ESP_FAIL, err_http, TAG, "couldn't read image");
|
||||
esp_http_client_close(http);
|
||||
if (recved<sizeof(flash_image_hdr_t)+(600*448/2)) {
|
||||
//Not sure what happened here... download succeeded but was too small. Server error?
|
||||
ESP_LOGW(TAG, "Image data too short. Not marking image as valid.");
|
||||
} else {
|
||||
//update curr_img to reflect download
|
||||
curr_img[download_slot]=server_img[i];
|
||||
img_shows[download_slot]=0;
|
||||
nvs_set_blob(nvs, "curr_img", curr_img, IMG_SLOT_COUNT*sizeof(uint16_t));
|
||||
nvs_set_blob(nvs, "img_shows", img_shows, IMG_SLOT_COUNT*sizeof(uint16_t));
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Sync done.");
|
||||
err_httpjs:
|
||||
cJSON_Delete(json);
|
||||
err_http:
|
||||
esp_http_client_cleanup(http);
|
||||
err_client_alloc:
|
||||
return ret;
|
||||
}
|
3
firmware/main/sync.h
Normal file
3
firmware/main/sync.h
Normal file
|
@ -0,0 +1,3 @@
|
|||
#include "epd_flash_image.h"
|
||||
|
||||
esp_err_t picframe_sync(const flash_image_t *images, const esp_partition_t *part);
|
7
firmware/partitions.csv
Normal file
7
firmware/partitions.csv
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
ota_0, app, ota_0, 0x10000, 0x150000
|
||||
ota_1, app, ota_1, , 0x150000
|
||||
images, 123, 00, , 0x150000
|
|
20
firmware/sdkconfig.defaults
Normal file
20
firmware/sdkconfig.defaults
Normal file
|
@ -0,0 +1,20 @@
|
|||
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PHOTOFRAME_BASE_URL="http://example.com/please_configure_this_url_in_menuconfig/"
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048
|
||||
CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y
|
||||
# CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND is not set
|
||||
CONFIG_ESP_PHY_REDUCE_TX_POWER=y
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
CONFIG_ESP_TASK_WDT_PANIC=y
|
||||
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10
|
||||
CONFIG_ESP_PANIC_HANDLER_IRAM=y
|
||||
# CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set
|
||||
# CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2 is not set
|
||||
CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION=y
|
||||
CONFIG_WIFI_MANAGER_MAX_RETRY_START_AP=9999999
|
||||
CONFIG_DEFAULT_AP_SSID="photoframe"
|
||||
CONFIG_DEFAULT_AP_PASSWORD=""
|
19
notes.txt
Normal file
19
notes.txt
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
For PCB:
|
||||
XR1151 - 800'ish mA, 65uA quiescent -> 3.5 year
|
||||
ESP32C3 - need to test if works with same code, otherwise NP
|
||||
|
||||
|
||||
Article:
|
||||
Color Floyd-Steinberg is a PITA. Difference colors? LAB colors, euclidian thingy. That sucks,
|
||||
pink white and blue-ish white is the same difference as red and blue. Initial modification:
|
||||
multiply A and B differences (=hue difference) with sin(L). This makes those differences less
|
||||
important. Great results. But WtF is up with the shitty 'official' way? Seems there's multiple
|
||||
ways: latest and greatest is E2000. Actually not that much different than my hacky solution.
|
||||
|
||||
Non-linear color space: one pixel of (0,0,0) and one of (64,64,64) don't look the same
|
||||
(are not as bright) as two of (32,32,32). Luckily (linearized) RGB works.
|
||||
|
||||
Stucki dithering?
|
||||
|
||||
https://mathematica.stackexchange.com/questions/7483/how-to-calculate-mix-of-4-colors-defined-in-cielab-lab-model
|
2
pcb/.gitignore
vendored
Normal file
2
pcb/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
gerber
|
||||
einkpicframe-backups
|
5423
pcb/einkpicframe.kicad_pcb
Normal file
5423
pcb/einkpicframe.kicad_pcb
Normal file
File diff suppressed because it is too large
Load diff
77
pcb/einkpicframe.kicad_prl
Normal file
77
pcb/einkpicframe.kicad_prl
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": false,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"ratsnest_display_mode": 0,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": true,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36
|
||||
],
|
||||
"visible_layers": "ffcffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"meta": {
|
||||
"filename": "einkpicframe.kicad_prl",
|
||||
"version": 3
|
||||
},
|
||||
"project": {
|
||||
"files": []
|
||||
}
|
||||
}
|
552
pcb/einkpicframe.kicad_pro
Normal file
552
pcb/einkpicframe.kicad_pro
Normal file
|
@ -0,0 +1,552 @@
|
|||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"board_outline_line_width": 0.09999999999999999,
|
||||
"copper_line_width": 0.19999999999999998,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.049999999999999996,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.09999999999999999,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.15,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.762,
|
||||
"height": 1.524,
|
||||
"width": 1.524
|
||||
},
|
||||
"silk_line_width": 0.15,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.15,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"45_degree_only": false,
|
||||
"min_clearance": 0.508
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
{
|
||||
"gap": 0.0,
|
||||
"via_gap": 0.0,
|
||||
"width": 0.0
|
||||
}
|
||||
],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_type_mismatch": "error",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "warning",
|
||||
"lib_footprint_mismatch": "warning",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"padstack": "error",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "warning",
|
||||
"silk_over_copper": "warning",
|
||||
"silk_overlap": "warning",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"text_height": "warning",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"allow_blind_buried_vias": false,
|
||||
"allow_microvias": false,
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.0,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.0,
|
||||
"min_hole_clearance": 0.25,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.19999999999999998,
|
||||
"min_microvia_drill": 0.09999999999999999,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.7999999999999999,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.19999999999999998,
|
||||
"min_via_annular_width": 0.049999999999999996,
|
||||
"min_via_diameter": 0.39999999999999997,
|
||||
"solder_mask_clearance": 0.0,
|
||||
"solder_mask_min_width": 0.0,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 5,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_onpadsmd": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_ontrackend": false,
|
||||
"td_onviapad": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [
|
||||
0.0,
|
||||
0.25,
|
||||
0.45
|
||||
],
|
||||
"via_dimensions": [
|
||||
{
|
||||
"diameter": 0.0,
|
||||
"drill": 0.0
|
||||
},
|
||||
{
|
||||
"diameter": 0.8,
|
||||
"drill": 0.4
|
||||
}
|
||||
],
|
||||
"zones_allow_external_fillets": false,
|
||||
"zones_use_no_outline": true
|
||||
},
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"lib_symbol_issues": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"similar_labels": "warning",
|
||||
"simulation_model_issue": "error",
|
||||
"unannotated": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "einkpicframe.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.25,
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Pwr",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.45,
|
||||
"via_diameter": 1.2,
|
||||
"via_drill": 0.6,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 3
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": [
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "+3V3"
|
||||
},
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "/VBAT"
|
||||
},
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "/VBAT_F"
|
||||
},
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "/VBAT_FP"
|
||||
},
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "/VBAT_IND"
|
||||
},
|
||||
{
|
||||
"netclass": "Pwr",
|
||||
"pattern": "GND"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"ngspice": {
|
||||
"fix_include_paths": true,
|
||||
"fix_passive_vals": false,
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"model_mode": 0,
|
||||
"workbook_filename": ""
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"spice_adjust_passive_values": false,
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"6e57f210-b0e1-4f15-9b02-871a474fe8e8",
|
||||
""
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
5027
pcb/einkpicframe.kicad_sch
Normal file
5027
pcb/einkpicframe.kicad_sch
Normal file
File diff suppressed because it is too large
Load diff
86017
pcb/fp-info-cache
Normal file
86017
pcb/fp-info-cache
Normal file
File diff suppressed because it is too large
Load diff
1
www/.gitignore
vendored
Normal file
1
www/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
config.php
|
17
www/README.md
Normal file
17
www/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
This requires a MySQL or MariaDB database to work.
|
||||
|
||||
How to do this:
|
||||
- Make sure your server is running a MySQL/Mariadb database you have access to.
|
||||
- Create user 'epd' in the database. Assign a password.
|
||||
- Copy config.php.example to config.php. Edit the $pass variable to be the actual password.
|
||||
- Create database 'epd', grant user 'epd' all permissions
|
||||
- Create the structure:
|
||||
mysql -u epd -p epd < create-database.sql
|
||||
|
||||
This also needs a image -> EPD binary program that is written in C to work. This
|
||||
needs to be compiled. To do so:
|
||||
- Make sure gcc and make are installed on the host
|
||||
- Install libgd-dev (the development package for libgd)
|
||||
- cd conv; make
|
||||
- Make sure php is allowed to run unix executables
|
||||
|
11
www/config.php.example
Normal file
11
www/config.php.example
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
//Database configuration. Replace with your actual values
|
||||
|
||||
//Database name
|
||||
$db="epd";
|
||||
//Username/password
|
||||
$username="epd";
|
||||
$pass="mypassword";
|
||||
|
||||
?>
|
5
www/conv/.gitignore
vendored
Normal file
5
www/conv/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
conv
|
||||
*.o
|
||||
*.jpg
|
||||
*.png
|
||||
*.bin
|
6
www/conv/Makefile
Normal file
6
www/conv/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
CFLAGS=-ggdb -O2
|
||||
LDFLAGS=-lgd -lm
|
||||
|
||||
conv: conv.o
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
370
www/conv/conv.c
Normal file
370
www/conv/conv.c
Normal file
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "gd.h"
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#define EPD_W 600
|
||||
#define EPD_H 448
|
||||
|
||||
#define EPD_UPSIDE_DOWN 1
|
||||
|
||||
//This loads a png/jpg file and if needed converts it to truecolor, crops the center to the
|
||||
//EPD aspect ratio and scales to EPD_W/EPD_H.
|
||||
gdImagePtr load_scaled(char *filename) {
|
||||
FILE *f;
|
||||
f=fopen(filename, "r");
|
||||
if (f==NULL) {
|
||||
perror(filename);
|
||||
return NULL;
|
||||
}
|
||||
//We check the first 8 bytes of the file to check if it's PNG.
|
||||
char pnghdr[8]={0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
|
||||
char buf[8];
|
||||
fread(buf, 8, 1, f);
|
||||
rewind(f);
|
||||
//If match, we load it as PNG, if not we load it as JPEG.
|
||||
gdImagePtr oim;
|
||||
if (memcmp(pnghdr, buf, 8)==0) {
|
||||
oim=gdImageCreateFromPng(f);
|
||||
} else {
|
||||
oim=gdImageCreateFromJpegEx(f, 1);
|
||||
}
|
||||
if (oim==NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gdImagePtr nim=NULL;
|
||||
if (gdImageSY(oim)==EPD_H && gdImageSX(oim)==EPD_W && gdImageTrueColor(oim)) {
|
||||
//no scaling needed
|
||||
nim=oim;
|
||||
} else {
|
||||
//Need scaling and/or converting to truecolor
|
||||
nim=gdImageCreateTrueColor(EPD_W, EPD_H);
|
||||
if (nim==NULL) {
|
||||
gdImageDestroy(oim);
|
||||
return NULL;
|
||||
}
|
||||
int bgnd = gdImageColorAllocate(nim, 255,255,255);
|
||||
gdImageFilledRectangle(nim, 0, 0, EPD_W, EPD_H, bgnd);
|
||||
int nw=EPD_W;
|
||||
int nh=(gdImageSY(oim)*EPD_W)/gdImageSX(oim);
|
||||
if (nh>EPD_H) {
|
||||
nh=EPD_H;
|
||||
nw=(gdImageSX(oim)*EPD_H)/gdImageSY(oim);
|
||||
}
|
||||
gdImageCopyResampled(nim, oim, (EPD_W-nw)/2, (EPD_H-nh)/2,
|
||||
0, 0, nw, nh, gdImageSX(oim), gdImageSY(oim));
|
||||
gdImageDestroy(oim);
|
||||
}
|
||||
return nim;
|
||||
}
|
||||
|
||||
//The two typedefs define what the epd binary image looks like.
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t id;
|
||||
uint64_t timestamp;
|
||||
uint8_t unused[64-12];
|
||||
} flash_image_hdr_t;
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
flash_image_hdr_t hdr;
|
||||
uint8_t data[600*448/2];
|
||||
uint8_t padding[768-sizeof(flash_image_hdr_t)];
|
||||
} flash_image_t;
|
||||
|
||||
//Convert SRGB to linear.
|
||||
//note: takes srgb [0..1], outputs linear rgb [0..1]
|
||||
float gamma_linear(float in) {
|
||||
return (in > 0.04045) ? pow((in + 0.055) / (1.0 + 0.055), 2.4) : (in / 12.92);
|
||||
}
|
||||
|
||||
//Converts a RGB float to a CIE-LAB float
|
||||
//note: takes linearized RGB [0..1], outputs LAB [0..1]
|
||||
void rgb_to_lab(float *rgb, float *lab) {
|
||||
float var_R = rgb[0] * 100;
|
||||
float var_G = rgb[1] * 100;
|
||||
float var_B = rgb[2] * 100;
|
||||
if (var_R>100) var_R=100;
|
||||
if (var_G>100) var_G=100;
|
||||
if (var_B>100) var_B=100;
|
||||
if (var_R<0) var_R=0;
|
||||
if (var_G<0) var_G=0;
|
||||
if (var_B<0) var_B=0;
|
||||
|
||||
//Observer. = 2°, Illuminant = D65
|
||||
float inX = var_R * 0.4124f + var_G * 0.3576f + var_B * 0.1805f;
|
||||
float inY = var_R * 0.2126f + var_G * 0.7152f + var_B * 0.0722f;
|
||||
float inZ = var_R * 0.0193f + var_G * 0.1192f + var_B * 0.9505f;
|
||||
|
||||
float var_X = (inX / 95.047);
|
||||
float var_Y = (inY / 100.0);
|
||||
float var_Z = (inZ / 108.883);
|
||||
|
||||
if ( var_X > 0.008856 )
|
||||
var_X = powf(var_X , ( 1.0f/3 ));
|
||||
else
|
||||
var_X = ( 7.787 * var_X ) + ( 16.0f/116 );
|
||||
|
||||
if ( var_Y > 0.008856 )
|
||||
var_Y = powf(var_Y , ( 1.0f/3 ));
|
||||
else
|
||||
var_Y = ( 7.787 * var_Y ) + ( 16.0f/116 );
|
||||
|
||||
if ( var_Z > 0.008856 )
|
||||
var_Z = powf(var_Z , ( 1.0f/3 ));
|
||||
else
|
||||
var_Z = ( 7.787 * var_Z ) + ( 16.0f/116 );
|
||||
|
||||
lab[0] = ( 116 * var_Y ) - 16;
|
||||
lab[1] = 500 * ( var_X - var_Y );
|
||||
lab[2] = 200 * ( var_Y - var_Z );
|
||||
}
|
||||
|
||||
//Returns the input value clamped between min and max
|
||||
float clamp(float val, float min, float max) {
|
||||
if (val<min) val=min;
|
||||
if (val>max) val=max;
|
||||
return val;
|
||||
}
|
||||
|
||||
//Given an array of [rgb] floats, adds the difference diff to the
|
||||
//pixel at [x,y], with i indicating the color (0=r, 1=g, 2=b)
|
||||
void dist_diff(float *pixels, int x, int y, int i, float dif) {
|
||||
if (x<0 || x>=EPD_W) return;
|
||||
if (y<0 || y>=EPD_H) return;
|
||||
float new_val=pixels[(x+y*EPD_W)*3+i]+dif;
|
||||
new_val=clamp(new_val, 0, 1);
|
||||
pixels[(x+y*EPD_W)*3+i]=new_val;
|
||||
}
|
||||
|
||||
static inline float deg2rad(float deg) {
|
||||
return (2 * M_PI * deg) / 360.0;
|
||||
}
|
||||
|
||||
static inline float rad2deg(float rad) {
|
||||
return 360.0 * rad / (2 * M_PI);
|
||||
}
|
||||
|
||||
//This, when fed two RGB values in range [0..1], returns the deltaE00 difference between the two.
|
||||
//The higher the return value, the more perceptually different the two RGB values are.
|
||||
float col_diff(float *rgb1, float *rgb2) {
|
||||
float lab1[3], lab2[3];
|
||||
rgb_to_lab(rgb1, lab1);
|
||||
rgb_to_lab(rgb2, lab2);
|
||||
|
||||
//deltaE00 code stolen from https://github.com/hamada147/IsThisColourSimilar/blob/master/Colour.js
|
||||
// Start Equation
|
||||
// Equation exist on the following URL http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html
|
||||
float avgL=(lab1[0]+lab2[0])/2;
|
||||
float c1 = sqrtf(powf(lab1[1], 2) + powf(lab1[2], 2));
|
||||
float c2 = sqrtf(powf(lab2[1], 2) + powf(lab2[2], 2));
|
||||
float avgC = (c1 + c2) / 2;
|
||||
float g = (1 - sqrtf(powf(avgC, 7) / (powf(avgC, 7) + powf(25, 7)))) / 2;
|
||||
|
||||
float a1p = lab1[1] * (1 + g);
|
||||
float a2p = lab2[1] * (1 + g);
|
||||
|
||||
float c1p = sqrtf(powf(a1p, 2) + powf(lab1[2], 2));
|
||||
float c2p = sqrtf(powf(a2p, 2) + powf(lab2[2], 2));
|
||||
|
||||
float avgCp = (c1p + c2p) / 2;
|
||||
|
||||
float h1p = rad2deg(atan2f(lab1[2], a1p));
|
||||
if (h1p < 0) h1p = h1p + 360;
|
||||
float h2p = rad2deg(atan2f(lab2[2], a2p));
|
||||
if (h2p < 0) h2p = h2p + 360;
|
||||
|
||||
float avghp = fabsf(h1p - h2p) > 180 ? (h1p + h2p + 360) / 2 : (h1p + h2p) / 2;
|
||||
float t = 1 - 0.17 * cosf(deg2rad(avghp - 30)) + 0.24 * cosf(deg2rad(2 * avghp)) + 0.32 * cosf(deg2rad(3 * avghp + 6)) - 0.2 * cosf(deg2rad(4 * avghp - 63));
|
||||
float deltahp = h2p - h1p;
|
||||
if (fabsf(deltahp) > 180) {
|
||||
if (h2p <= h1p) {
|
||||
deltahp += 360;
|
||||
} else {
|
||||
deltahp -= 360;
|
||||
}
|
||||
}
|
||||
|
||||
float deltalp = lab2[0] - lab1[0];
|
||||
float deltacp = c2p - c1p;
|
||||
|
||||
deltahp = 2 * sqrtf(c1p * c2p) * sinf(deg2rad(deltahp) / 2);
|
||||
|
||||
float sl = 1 + ((0.015 * powf(avgL - 50, 2)) / sqrtf(20 + powf(avgL - 50, 2)));
|
||||
float sc = 1 + 0.045 * avgCp;
|
||||
float sh = 1 + 0.015 * avgCp * t;
|
||||
|
||||
float deltaro = 30 * expf(-(powf((avghp - 275) / 25, 2)));
|
||||
float rc = 2 * sqrtf(powf(avgCp, 7) / (powf(avgCp, 7) + powf(25, 7)));
|
||||
float rt = -rc * sinf(2 * deg2rad(deltaro));
|
||||
|
||||
float kl = 1;
|
||||
float kc = 1;
|
||||
float kh = 1;
|
||||
|
||||
float deltaE = sqrtf(powf(deltalp / (kl * sl), 2) + powf(deltacp / (kc * sc), 2) + powf(deltahp / (kh * sh), 2) + rt * (deltacp / (kc * sc)) * (deltahp / (kh * sh)));
|
||||
return deltaE;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char *im_in="";
|
||||
char *im_out="";
|
||||
char *bin_out="";
|
||||
int error=0;
|
||||
//Find & parse command line arguments
|
||||
for (int i=1; i<argc; i++) {
|
||||
if (strcmp(argv[i], "-p")==0 && i<argc-1) {
|
||||
if (im_out[0]!=0) error=1;
|
||||
i++;
|
||||
im_out=argv[i];
|
||||
} else if (strcmp(argv[i], "-o")==0 && i<argc-1) {
|
||||
if (bin_out[0]!=0) error=1;
|
||||
i++;
|
||||
bin_out=argv[i];
|
||||
} else {
|
||||
if (im_in[0]!=0) error=1;
|
||||
im_in=argv[i];
|
||||
}
|
||||
}
|
||||
if (im_in[0]==0 || error) {
|
||||
printf("Usage: %s [-o outfile.bin] [-p preview.png] infile.[jpg|png]\n", argv[0]);
|
||||
printf("infile can be png or jpeg; if no outfile is given, output will go to stdout\n");
|
||||
exit(error);
|
||||
}
|
||||
|
||||
//Load image
|
||||
gdImagePtr im=load_scaled(im_in);
|
||||
if (!im) {
|
||||
fprintf(stderr, "Could not load image %s\n", im_in);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
flash_image_t *bin=calloc(sizeof(flash_image_t), 1);
|
||||
assert(bin);
|
||||
bin->hdr.id=0xfafa1a1a; //magic header
|
||||
bin->hdr.timestamp=time(NULL);
|
||||
|
||||
//RGB colors as displayed on the screen
|
||||
//These are calculated by grabbing a test pattern which shows all colors, taking a picture,
|
||||
//then using an image editor to max out the levels for max contrast & saturation, then taking
|
||||
//the linear (not SRGB) RGB values and putting them here.
|
||||
float epd_colors[7][3]={ //rgb
|
||||
{0,0,0},
|
||||
{1,1,1},
|
||||
{0.059, 0.329, 0.119},
|
||||
{0.061, 0.147, 0.336},
|
||||
{0.574, 0.066, 0.010},
|
||||
{0.982, 0.756, 0.004},
|
||||
{0.795, 0.255, 0.018},
|
||||
};
|
||||
|
||||
//Convert float RGB colors to int colors for putting on the preview png
|
||||
int epd_colors_int[7];
|
||||
for (int i=0; i<7; i++) {
|
||||
int c=0;
|
||||
for (int j=0; j<3; j++) {
|
||||
int pc=epd_colors[i][j]*255;
|
||||
c=(c<<8)|pc;
|
||||
}
|
||||
epd_colors_int[i]=c;
|
||||
}
|
||||
|
||||
//Convert image to an array of floats so we can Floyd-Steinberg without limiting ourselves
|
||||
//to the range of ints
|
||||
float *pixels=calloc(EPD_W*EPD_H*3, sizeof(float));
|
||||
assert(pixels);
|
||||
float *pixelp=pixels;
|
||||
for (int y=0; y<EPD_H; y++) {
|
||||
for (int x=0; x<EPD_W; x++) {
|
||||
int c=gdImageGetPixel(im,x,y);
|
||||
*pixelp++=gamma_linear(((c>>16)&0xff)/255.0);
|
||||
*pixelp++=gamma_linear(((c>>8)&0xff)/255.0);
|
||||
*pixelp++=gamma_linear(((c>>0)&0xff)/255.0);
|
||||
}
|
||||
}
|
||||
|
||||
//Create preview image
|
||||
gdImagePtr tim=gdImageCreateTrueColor(EPD_W, EPD_H);
|
||||
assert(tim);
|
||||
for (int y=0; y<EPD_H; y++) {
|
||||
int ob=0;
|
||||
for (int x=0; x<EPD_W; x++) {
|
||||
//Find closest color for this pixel from the palette the epd can display
|
||||
int best=0;
|
||||
float best_dif=999999999;
|
||||
for (int i=0; i<7; i++) {
|
||||
float d=col_diff(&pixels[(x+y*EPD_W)*3], epd_colors[i]);
|
||||
if (d<best_dif) {
|
||||
best_dif=d;
|
||||
best=i;
|
||||
}
|
||||
}
|
||||
|
||||
//Distribute difference between chosen and ideal color using Floyd-Steinberg
|
||||
for (int i=0; i<3; i++) {
|
||||
float dif=pixels[(x+y*EPD_W)*3+i] - epd_colors[best][i];
|
||||
dist_diff(pixels, x+1, y, i, (dif/16.0)*7.0);
|
||||
dist_diff(pixels, x-1, y+1, i, (dif/16.0)*3.0);
|
||||
dist_diff(pixels, x, y+1, i, (dif/16.0)*5.0);
|
||||
dist_diff(pixels, x+1, y+1, i, (dif/16.0)*1.0);
|
||||
}
|
||||
// best=((x*14)/448)%7; //uncomment for test image
|
||||
//Set byte in output EPD binary data
|
||||
#if EPD_UPSIDE_DOWN
|
||||
if (x&1) {
|
||||
bin->data[((EPD_H-1-y)*EPD_W+(EPD_W-1-x))/2]=ob|(best<<4);
|
||||
} else {
|
||||
ob=best;
|
||||
}
|
||||
#else
|
||||
if (x&1) {
|
||||
bin->data[(y*EPD_W+x)/2]=(ob<<4)|best;
|
||||
} else {
|
||||
ob=best;
|
||||
}
|
||||
#endif
|
||||
//Also set pixel in output
|
||||
gdImageSetPixel(tim, x, y, epd_colors_int[best]);
|
||||
}
|
||||
}
|
||||
|
||||
if (im_out[0]) {
|
||||
//Write preview image
|
||||
FILE *of=fopen(im_out, "w");
|
||||
if (!of) {
|
||||
perror(im_out);
|
||||
exit(1);
|
||||
}
|
||||
gdImagePng(tim, of);
|
||||
fclose(of);
|
||||
}
|
||||
gdImageDestroy(tim);
|
||||
if (bin_out[0]) {
|
||||
//Write binary output to file
|
||||
FILE *of=fopen(bin_out, "wb");
|
||||
if (!of) {
|
||||
perror(bin_out);
|
||||
exit(1);
|
||||
}
|
||||
fwrite(bin, sizeof(flash_image_t)-sizeof(bin->padding), 1, of);
|
||||
fclose(of);
|
||||
} else {
|
||||
//Write binary output to stdout
|
||||
fwrite(bin, sizeof(flash_image_t)-sizeof(bin->padding), 1, stdout);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
79
www/create-database.sql
Normal file
79
www/create-database.sql
Normal file
|
@ -0,0 +1,79 @@
|
|||
-- MariaDB dump 10.19 Distrib 10.5.15-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: epd
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.5.15-MariaDB-0+deb11u1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `checkins`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `checkins`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `checkins` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`device_id` int(11) NOT NULL,
|
||||
`timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`battery_mv` int(11) NOT NULL,
|
||||
`ip` text NOT NULL,
|
||||
`fw` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `device_id` (`device_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=143 DEFAULT CHARSET=utf8mb4;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `devices`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `devices`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `devices` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`mac` tinytext NOT NULL,
|
||||
`tz` tinytext NOT NULL,
|
||||
`update_hour` int(11) NOT NULL,
|
||||
`fw_upd` tinytext NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `images`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `images`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `images` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`orig_name` text NOT NULL,
|
||||
`epd_bin` mediumblob NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=utf8mb4;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2022-08-04 9:54:18
|
1
www/cropperjs
Submodule
1
www/cropperjs
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 133b8b07d00a68a01063af2f57f0fab183050fcf
|
28
www/epd-img.php
Normal file
28
www/epd-img.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/*
|
||||
Return an EPD binary. If no argument, return latest EPD binary. If GET argument 'id' is given,
|
||||
return EPD binary with that ID.
|
||||
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
require("config.php");
|
||||
|
||||
$mysqli = mysqli_connect("localhost",$username, $pass, $db);
|
||||
|
||||
if (isset($_GET["id"])) {
|
||||
$id=intval($_GET["id"]);
|
||||
$result = $mysqli->query("SELECT epd_bin FROM images WHERE `id`='$id' ORDER BY timestamp DESC LIMIT 1");
|
||||
} else {
|
||||
//get latest
|
||||
$result = $mysqli->query("SELECT epd_bin FROM images ORDER BY timestamp DESC LIMIT 1");
|
||||
}
|
||||
header("Content-Type: image/epd");
|
||||
$row=$result->fetch_assoc();
|
||||
echo $row["epd_bin"];
|
||||
|
||||
?>
|
92
www/epd-info.php
Normal file
92
www/epd-info.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
/*
|
||||
Return JSON with the state we expect the EPD controller to be in: what firmware it's supposed
|
||||
to have and what images are supposed to be in its memory. We also record the battery voltage.
|
||||
|
||||
Called using GET: epd-info.php?mac=01234567&bat=2901
|
||||
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
|
||||
* this notice you can do whatever you want with this stuff. If we meet some day,
|
||||
* and you think this stuff is worth it, you can buy me a beer in return.
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
require("config.php");
|
||||
|
||||
//This returns the app_info SHA hash. If the image is different, this is different.
|
||||
//Makes the assumption the appinfo magic doesn't appear earlier in the image.
|
||||
function get_app_image_data($file) {
|
||||
$f=fopen(__DIR__."/".$file, "r");
|
||||
if (!$f) return "";
|
||||
$data=fread($f, 1024*4);
|
||||
fclose($f);
|
||||
$magicpos=strpos($data, "\x32\x54\xCD\xAB"); //0xABCD5432 little-endian
|
||||
if (!$magicpos) return "";
|
||||
$shapos=$magicpos+4+4+8+32+32+16+16+32;
|
||||
return base64_encode(substr($data,$shapos,32));
|
||||
}
|
||||
|
||||
$mysqli = mysqli_connect("localhost",$username, $pass, $db);
|
||||
|
||||
//Sanitize MAC
|
||||
$mac="000000000000";
|
||||
if (isset($_GET["mac"])) $mac=$_GET["mac"];
|
||||
if (!preg_match("/^[0-9a-fA-F]{12}$/", $mac)) {
|
||||
$mac="000000000000";
|
||||
}
|
||||
|
||||
$fw="unknown";
|
||||
if (isset($_GET["fw"])) $fw=$_GET["fw"];
|
||||
|
||||
//Find device based on MAC
|
||||
$result = $mysqli->query("SELECT id,tz,fw_upd,update_hour FROM devices WHERE `mac`='$mac'");
|
||||
$dev_info=$result->fetch_assoc();
|
||||
if (!$dev_info) {
|
||||
//No data... make a new one
|
||||
$stmt = $mysqli->prepare("INSERT INTO devices (mac,tz,fw_upd,update_hour) VALUES (?,?,?,?)");
|
||||
$tz="CST-8";
|
||||
$fw_upd="picframe.bin";
|
||||
$update_hour=3;
|
||||
$stmt->bind_param("sssi", $mac, $tz, $fw_upd, $update_hour);
|
||||
$stmt->execute() || die($stmt->error);
|
||||
//re-run query to fetch the data... lazy, I know
|
||||
$result = $mysqli->query("SELECT id,tz,fw_upd,update_hour FROM devices WHERE `mac`='$mac'");
|
||||
$dev_info=$result->fetch_assoc();
|
||||
}
|
||||
|
||||
//Insert checkin data into db
|
||||
$stmt = $mysqli->prepare("INSERT INTO checkins (device_id,battery_mv,ip,fw) VALUES (?,?,?,?)");
|
||||
$battery_mv=0;
|
||||
$ip="";
|
||||
$fw="unknown";
|
||||
if (isset($_GET["fw"])) $fw=$_GET["fw"];
|
||||
if (isset($_GET["bat"])) $battery_mv=$_GET["bat"];
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) $ip=$_SERVER['REMOTE_ADDR'];
|
||||
$stmt->bind_param("iiss", $dev_info["id"], $battery_mv, $ip, $fw);
|
||||
$stmt->execute() || die($stmt->error);
|
||||
|
||||
//Grab latest 10 images
|
||||
$image_ids=array();
|
||||
$result = $mysqli->query("SELECT id FROM images ORDER BY timestamp DESC LIMIT 10");
|
||||
|
||||
//Get IDs of last 10 images
|
||||
for ($i=0; $i<10; $i++) {
|
||||
$row=$result->fetch_assoc();
|
||||
$image_ids[$i]=intval($row["id"]);
|
||||
}
|
||||
|
||||
//Dump all info in json
|
||||
$ret=array();
|
||||
$ret["time"]=time();
|
||||
$ret["fw_sha"]=get_app_image_data($dev_info["fw_upd"]);
|
||||
$ret["fw_upd"]=$dev_info["fw_upd"];
|
||||
$ret["tz"]=$dev_info["tz"];
|
||||
$ret["update_hour"]=intval($dev_info["update_hour"]);
|
||||
$ret["images"]=$image_ids;
|
||||
|
||||
//send
|
||||
header("Content-Type: text/json");
|
||||
echo json_encode($ret);
|
||||
|
||||
?>
|
104
www/index.html
Normal file
104
www/index.html
Normal file
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Cropper.js</title>
|
||||
<link rel="stylesheet" href="cropperjs/dist//cropper.css">
|
||||
<style>
|
||||
.container {
|
||||
margin: 20px auto;
|
||||
max-width: 640px;
|
||||
max-height: 640px;
|
||||
}
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
result {
|
||||
width=600px;
|
||||
height=448px;
|
||||
display=none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<input type="file" id="file">
|
||||
<input type="submit" value="Store image" id="okbutton" style="display: none">
|
||||
<div>
|
||||
<img id="image" class="image" src="" alt="Picture">
|
||||
</div>
|
||||
<div id="result" class="result" src="" alt="Result">
|
||||
</div>
|
||||
<script src="cropperjs/dist/cropper.js"></script>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var file = document.querySelector("#file");
|
||||
var okbutton = document.querySelector("#okbutton");
|
||||
var image = document.querySelector("#image");
|
||||
var cropper;
|
||||
file.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
okbutton.style.display="block";
|
||||
file.style.display="none";
|
||||
image.onload = () => {
|
||||
image.width="640";
|
||||
image.height="640";
|
||||
URL.revokeObjectURL(image.src); // no longer needed, free memory
|
||||
cropper = new Cropper(image, {
|
||||
dragMode: 'move',
|
||||
aspectRatio: 600/448,
|
||||
autoCropArea: 0.8,
|
||||
restore: false,
|
||||
guides: false,
|
||||
center: false,
|
||||
highlight: false,
|
||||
cropBoxMovable: false,
|
||||
cropBoxResizable: false,
|
||||
toggleDragModeOnDblclick: false,
|
||||
rotatable: true,
|
||||
});
|
||||
}
|
||||
image.src = URL.createObjectURL(this.files[0]); // set src to blob url
|
||||
}
|
||||
});
|
||||
okbutton.addEventListener("click", function() {
|
||||
var canvas = cropper.getCroppedCanvas({
|
||||
width: 600,
|
||||
height: 448,
|
||||
});
|
||||
canvas.toBlob(function (blob) {
|
||||
var formData = new FormData();
|
||||
formData.append('image', blob, 'image.jpg');
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.upload.onprogress = function (e) {
|
||||
var percent = '0';
|
||||
var percentage = '0%';
|
||||
if (e.lengthComputable) {
|
||||
percent = Math.round((e.loaded / e.total) * 100);
|
||||
okbutton.value=percent + '%';
|
||||
}
|
||||
}
|
||||
xhr.onload = function () {
|
||||
var res=document.querySelector("#result");
|
||||
//eeuw
|
||||
res.innerHTML='<img src="data:image/gif;base64,' + this.responseText + '">';
|
||||
res.style.display="block";
|
||||
image.style.display="none";
|
||||
cropper.destroy();
|
||||
okbutton.value="done";
|
||||
}
|
||||
xhr.onerror = function () {
|
||||
okbutton.value="error";
|
||||
}
|
||||
xhr.open("POST", "upload.php");
|
||||
xhr.send(formData);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
41
www/upload.php
Normal file
41
www/upload.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
require("config.php");
|
||||
|
||||
//(id, timestamp, orig_name, epd_bin)
|
||||
|
||||
if (!isset($_FILES["image"])) {
|
||||
header("Location: /index.html");
|
||||
}
|
||||
|
||||
//system("/bin/cp \"".$_FILES["image"]["tmp_name"]."\" /tmp/img.png");
|
||||
$pngfile=tempnam("/tmp","epd");
|
||||
$convproc=popen(__DIR__."/conv/conv -p \"".$pngfile."\" \"".$_FILES["image"]["tmp_name"]."\"", "r");
|
||||
|
||||
$mysqli = mysqli_connect("localhost",$username, $pass, $db);
|
||||
|
||||
$stmt = $mysqli->prepare("INSERT INTO images (orig_name,epd_bin) VALUES (?,?)");
|
||||
$orig_name="";
|
||||
$null=0;
|
||||
$stmt->bind_param("sb", $orig_name, $null);
|
||||
while(!feof($convproc)) {
|
||||
$stmt->send_long_data(1, fread($convproc, 1024));
|
||||
}
|
||||
$stmt->send_long_data(1, $bin);
|
||||
|
||||
$ret=pclose($convproc);
|
||||
|
||||
if ($ret!=0) {
|
||||
unlink($pngfile);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$stmt->execute() || die($stmt->error);
|
||||
|
||||
//header("Content-Type: image/png");
|
||||
//readfile($pngfile);
|
||||
echo base64_encode(file_get_contents($pngfile));
|
||||
|
||||
unlink($pngfile);
|
||||
|
||||
?>
|
Loading…
Add table
Reference in a new issue