Initial commit

This commit is contained in:
Sasa Karanovic
2026-05-18 19:51:42 -04:00
commit f3efb34f8e
79 changed files with 61865 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
### PlatformIO ###
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json
.pio
# Various Altium files
*.PrjPCBStructure
*.SchDocPreview
*.PcbDocPreview
__Previews
*.PrjPcbStructure
*.Dat
*.REP
*.TLT
*.LOG
*.log
*.htm
*.$$$
#useless dirs
Project\ Outputs
Project\ Logs
Project Outputs for*
#History
History
#Exports, but not manual ones
exports/*
!exports/manual
#Python stuff
scripts/__pycache__/
*.bak
*.dwl
*.dwl2
Firmware/data/
Firmware/src/version.h
Firmware/src/binary_data.ino
+44
View File
@@ -0,0 +1,44 @@
# How to build firmware
For our firmware development, we are going to use Arduino. Mainly because it's user friendly, easy to use and
most hobbyists are familiar with it or have already used it. While we could build the same or even more efficient
firmware using C and ESP-IDF, we are going to stick with Arduino and hope that this allows the project to be
more friendly and easier to use/modify by wider DIY/hacker community.
## How can I build/compile firmware?
Arduino has many third party libraries that you can use and also build firmware for many different boards.
However, depending for which board you are building firmware and also which libraries your project (sketch) uses,
you might need to manually install them or download them trough board/library manager.
While above mentioned solution works fine, it can lead to issues like installing the "wrong/different" version
of the library, different board files, "wrong/different" build/upload tools and so on. On top of that, since board files
and libraries are shared, we could install a library or a board file while working on another project that breaks our current project.
Hopefully you can see why this is not so great for open-source open-hardware project where we want everyone to be able to easily
checkout a repository and hit the ground running without spending hours setting up and debugging what library or board files are missing.
Luckily we can automate/avoid this by using a build system like PlatformIO. So below we will have two ways of building
the firmware, one will be "easy" by leveraging PlatformiIO build system, and the other one will be a manual one by using
Arduino IDE and manually searching and downloading all the board/library files.
## - The super easy way - Compile and upload using PlatformIO build system -
### 1. Install PlatformIO
Installing PlatformIO CLI is pretty straight-forward and also well documented for Windows, Linux and MacOS.
You will need to follow few steps and get PlatformIO CLI installed, detailed tutorial can be found at https://platformio.org/install/cli
Make sure to install [PlatformIO Core](https://docs.platformio.org/en/latest//core/installation.html#installation-methods 'https://docs.platformio.org/en/latest//core/installation.html#installation-methods') and also that it is available trough [shell](https://docs.platformio.org/en/latest//core/installation.html#piocore-install-shell-commands 'PlatformIO Core - Install Shell Commands¶').
### 2. Build firmware
Open shell/command-prompt and navigate to 'DIY-Filament-Dryer_v2/Firmware' folder.
1. Compile the firmware by typing `pio run`, PlatformIO will download all the required board files and libraries and finally compile the firmware.
2. Upload the firmware with `pio run --target upload --upload-port <COM-PORT>`. Make sure to replace `<COM-PORT>` with your ESP32's COM port (ie COM1 or /dev/ttyACM0)
Every time you make a firmware change or change the web page/UI, you need to re-compile and re-upload the firmware.
[<- Go back to repository root](../README.md)
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400}
+273
View File
@@ -0,0 +1,273 @@
body {
padding-top: 120px;
background-color: #212529;
}
pre {
background: #f7f7f9;
}
iframe {
overflow: hidden;
border: none;
}
@media (min-width: 768px) {
body > .navbar-transparent {
box-shadow: none;
}
body > .navbar-transparent .navbar-nav > .open > a {
box-shadow: none;
}
}
.dark-bg {
background-color: #212529;
}
.light-bg {
background-color: #212529;
}
#home,
#help {
font-size: 0.9rem;
}
#home .navbar,
#help .navbar {
background: #349aed;
background: linear-gradient(145deg, #349aed 50%, #34d8ed 100%);
transition: box-shadow 200ms ease-in;
}
#home .navbar-transparent,
#help .navbar-transparent {
background: none !important;
box-shadow: none;
}
#home .navbar-brand .nav-link,
#help .navbar-brand .nav-link {
display: inline-block;
margin-right: -30px;
}
#home .nav-link,
#help .nav-link {
text-transform: uppercase;
font-weight: 500;
color: #fff;
}
#home {
padding-top: 0;
}
#home .btn {
padding: 0.6rem 0.55rem 0.5rem;
box-shadow: none;
font-size: 0.7rem;
font-weight: 500;
}
.bs-docs-section {
margin-top: 4em;
}
.bs-docs-section .page-header h1 {
padding: 2rem 0;
font-size: 3rem;
}
.dropdown-menu.show[aria-labelledby="themes"] {
display: -ms-flexbox;
display: flex;
width: 420px;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item {
width: 33.333%;
}
.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item:first-child {
width: 100%;
}
.bs-component {
position: relative;
}
.bs-component + .bs-component {
margin-top: 1rem;
}
.bs-component .card {
margin-bottom: 1rem;
}
.bs-component .modal {
position: relative;
top: auto;
right: auto;
left: auto;
bottom: auto;
z-index: 1;
display: block;
}
.bs-component .modal-dialog {
width: 90%;
}
.bs-component .popover {
position: relative;
display: inline-block;
width: 220px;
margin: 20px;
}
.source-button {
display: none;
position: absolute;
top: 0;
right: 0;
z-index: 100;
font-weight: 700;
}
.source-button:hover {
cursor: pointer;
}
.bs-component:hover .source-button {
display: block;
}
#source-modal pre {
max-height: calc(100vh - 11rem);
background-color: rgba(0, 0, 0, 0.7);
color: rgba(255, 255, 255, 0.7);
}
.nav-tabs {
margin-bottom: 15px;
}
.progress {
margin-bottom: 10px;
}
#footer {
margin: 5em 0;
}
#footer li {
float: left;
margin-right: 1.5em;
margin-bottom: 1.5em;
}
#footer p {
clear: left;
margin-bottom: 0;
}
.splash {
padding: 12em 0 6em;
background: #349aed;
background: linear-gradient(145deg, #349aed 50%, #34d8ed 100%);
color: #fff;
text-align: center;
}
.splash .logo {
width: 160px;
}
.splash h1 {
font-size: 3em;
color: #fff;
}
.splash #social {
margin: 2em 0 3em;
}
.splash .alert {
margin: 2em 0;
border: none;
}
.splash .sponsor a {
color: #fff;
}
.section-tout {
padding: 6em 0 1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: #eaf1f1;
text-align: center;
}
.section-tout .icon {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-align: center;
align-items: center;
width: 80px;
height: 80px;
margin: 0 auto 1rem;
background: #349aed;
background: linear-gradient(145deg, #3b9cea 50%, #3db8eb 100%);
border-radius: 50%;
font-size: 2rem;
color: rgba(0, 0, 0, 0.5);
}
.section-tout p {
margin-bottom: 5em;
}
.section-preview {
padding: 4em 0;
}
.section-preview .preview {
margin-bottom: 4em;
background-color: #eaf1f1;
}
.section-preview .preview .image {
position: relative;
}
.section-preview .preview .image::before {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
pointer-events: none;
}
.section-preview .preview .options {
padding: 2em;
border: 1px solid rgba(0, 0, 0, 0.05);
border-top: none;
text-align: center;
}
.section-preview .preview .options p {
margin-bottom: 2em;
}
.section-preview .dropdown-menu {
text-align: left;
}
.section-preview .lead {
margin-bottom: 2em;
}
.sponsor #carbonads {
max-width: 240px;
margin: 0 auto;
}
.sponsor .carbon-text {
display: block;
margin-top: 1em;
font-size: 12px;
}
.sponsor .carbon-poweredby {
float: right;
margin-top: 1em;
font-size: 10px;
}
@media (max-width: 767px) {
.splash {
padding-top: 8em;
}
.splash .logo {
width: 100px;
}
.splash h1 {
font-size: 2em;
}
#banner {
margin-bottom: 2em;
text-align: center;
}
}
.btn-group {
font-size: 13px;
}
.tooltip {
display: none;
position: absolute;
padding: 10px;
color: #777;
background-color: #fff;
border: 1px solid #777;
box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.5);
border-radius: 3px;
}
File diff suppressed because one or more lines are too long
+54
View File
@@ -0,0 +1,54 @@
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../webfonts/lato-1.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../webfonts/lato-2.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../webfonts/lato-3.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../webfonts/lato-4.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../webfonts/lato-5.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../webfonts/lato-6.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+5
View File
@@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+209
View File
@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Filament Dry Box v2 - Sasa Karanovic</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/custom.min.css" />
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/brands.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet">
<link rel="shortcut icon" type="image/jpg" href="img/favicon.png" />
</head>
<body class="d-flex flex-column min-vh-100">
<div class="navbar navbar-expand-lg fixed-top navbar-dark dark-bg">
<div class="container">
<a href="index.html" class="navbar-brand">Filament Dry Box v2</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="settings.html">Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://sasakaranovic.com/projects/diy-filament-dry-box-v2/">Project Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/SasaKaranovic/DIY-Filament-Dryer-v2" target="_blank">Source Code</a>
</li>
</ul>
</div>
</div>
</div>
<!-- Container -->
<div class="container">
<!-- Content - First row -->
<div class="row">
<!-- Left column -->
<div class="col-lg-5 col-md-5 col-sm-5">
<!-- DryBox status title -->
<div class="page-header">
<h2>DryBox Status</h2>
</div>
<!-- DryBox status title end -->
<!-- DryBox status -->
<div class="row">
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-sm table-borderless">
<tr>
<td colspan="4"><span id="status-box" class="badge badge-pill badge-warning">Dry box is $STATUS$</span></td>
</tr>
<tr>
<th scope="col"></th>
<th scope="col">Inside</th>
<th scope="col">Outside</th>
<th scope="col"></th>
</tr>
<tr>
<td>Temperature</td>
<td><span id="status-temp-inside">$TEMP_IN$</span>°C</td>
<td><span id="status-temp-outside">$TEMP_OUT$</span>°C</td>
<td></td>
</tr>
<tr>
<td>Humidity</td>
<td><span id="status-humidity-inside">$HUMIDITY_IN$</span>%</td>
<td><span id="status-humidity-outside">$HUMIDITY_OUT$</span>%</td>
<td></td>
</tr>
<tr>
<td colspan="4"></td>
</tr>
<tr>
<td colspan="4"></td>
</tr>
<tr>
<td colspan="4"></td>
</tr>
<tr>
<th scope="col"></th>
<th scope="col">Current</th>
<th scope="col">Limit</th>
<th scope="col"></th>
</tr>
<tr>
<td>Heater Temperature</td>
<td><span id="status-temp-heater">$HEATER_TEMPERATURE$</span>°C</td>
<td><span id="status-temp-heater-max">$TARGET_HEATER_TEMPERATURE$</span>°C</td>
<td></td>
</tr>
<tr>
<td>Temperature Inside</td>
<td><span id="status-temp-inside2">$TEMP_IN$</span>°C</td>
<td><span id="status-temp-inside-target">$TARGET_INSIDE_TEMPERATURE$</span>°C</td>
<td></td>
</tr>
<tr>
<td colspan="1">Heater Power</td>
<td colspan="3"><span id="status-heater-power">$HEATER_POWER$</span>%</td>
</tr>
<tr>
<td colspan="1">Fan speed</td>
<td colspan="3"><span id="status-fan-speed">$FAN_POWER$</span>%</td>
</tr>
</table>
</div>
</div>
</div>
<!-- DryBox status END -->
</div>
<!-- Left column END -->
<!-- Right column -->
<div class="col-lg-5 col-md-5 col-sm-5 offset-lg-1">
<div class="row">
<!-- DryBox control -->
<div class="col-lg-12">
<h2>Control</h2>
</div>
<!-- DryBox control END -->
<div class="col-lg-12">
<legend>Temperature</legend>
<fieldset class="form-group">
<label for="labelSetTemperature">Target temperature: $TARGET_INSIDE_TEMPERATURE$°C</label>
<div class="float-right">
<a href="#" id="pop-temp"><img src="img/help.png" /></a>
</div>
<input type="range" class="custom-range" id="sliderTemperature" min="15" max="80" step="1" value="$TARGET_INSIDE_TEMPERATURE$" />
</fieldset>
<legend>Fan speed</legend>
<fieldset class="form-group">
<label for="labelSetFanSpeed">Fan speed: $FAN_POWER$%</label>
<div class="float-right">
<a href="#" id="pop-fan"><img src="img/help.png" /></a>
</div>
<input type="range" class="custom-range" id="sliderFanSpeed" min="0" max="100" step="5" value="$FAN_POWER$" />
</fieldset>
<legend>Max Heater Temperature</legend>
<fieldset class="form-group">
<label for="labelSetHeaterTemperature">Max heater temperature: $TARGET_HEATER_TEMPERATURE$°C</label>
<div class="float-right">
<a href="#" id="pop-heater"><img src="img/help.png" /></a>
</div>
<input type="range" class="custom-range" id="sliderHeaterTemperature" min="30" max="80" step="1" value="$TARGET_HEATER_TEMPERATURE$" />
</fieldset>
</div>
<div class="col-lg-12">
<div class="btn-group btn-group-md mr-2" role="group">
<button type="button" id="btnSet" class="btn btn-primary mr-2"><i class="fas fa-fire"></i> Turn ON</button>
<button type="button" id="btnTurnOff" class="btn btn-primary"><i class="fas fa-power-off"></i> Turn OFF</button>
</div>
</div>
</div>
</div>
<!-- Right column END -->
</div>
<!-- Content - First row END -->
<!-- Content - Second row -->
<div class="clearfix"></div>
<div style="padding-top: 3em;">
<div id="chart" style="margin-top: 3em;">
<canvas id="myChart" width="600" height="200"></canvas>
</div>
</div>
<!-- Content - Second row END -->
</div>
<!-- Container END -->
<!-- FOOTER -->
<footer class="w-100 py-2 flex-shrink-0 mt-auto dark-bg">
<div class="container py-2">
<div class="row gy-4 gx-5">
<div class="col-lg-6 col-md-6">
<p class="small text-muted">
Dry box controller by <a href="https://SasaKaranovic.com/" target="_blank">Saša Karanović</a><br>
Released as open-source and open-hardware for you to hack and enjoy! :)<br>
Firmware version $VERSION_MAJOR$$VERSION_MINOR$$VERSION_PATCH$
</p>
</div>
<div class="col-lg-6 col-md-6">
<a class="m-2" href="https://sasakaranovic.com/" target="_blank"><i class="fas fa-home"></i> Blog</a>
<a class="m-2" href="https://youtube.com/c/sasakaranovic" target="_blank"><i class="fab fa-youtube"></i> YouTube</a>
<a class="m-2" href="https://twitter.com/_sasakaranovic_" target="_blank"><i class="fab fa-twitter"></i> Twitter</a>
<a class="m-2" href="https://github.com/sasakaranovic" target="_blank"><i class="fab fa-github"></i> GitHub</a>
<a class="m-2" href="https://github.com/sponsors/SasaKaranovic" target="_blank"><i class="fas fa-heart"></i> Sponsor on GitHub</a>
</div>
</div>
</div>
</footer>
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src='js/chart.js'></script>
<script src="js/drybox.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+214
View File
@@ -0,0 +1,214 @@
let measurements_chart;
// Shorthand for $( document ).ready()
$(function() {
$('#sliderTemperature').on('input change', function(){
$("label[for='labelSetTemperature']").text("Target temperature: " + parseInt(this.value) +"°C");
});
$('#sliderHeaterTemperature').on('input change', function(){
$("label[for='labelSetHeaterTemperature']").text("Max heater temperature: " + parseInt(this.value) +"°C");
});
$('#sliderFanSpeed').on('input change', function(){
$("label[for='labelSetFanSpeed']").text("Fan speed: " + parseInt(this.value) +"%");
});
$('#btnSet').on('click', function(){
var temperature = $("#sliderTemperature").val();
var heater = $("#sliderHeaterTemperature").val();
var fanspeed = $("#sliderFanSpeed").val();
// Send
$.ajax({
url: "/api/v0/turn_on",
type: "get", //send it through get method
data: {
temperature: temperature,
heater: heater,
fanspeed: fanspeed,
},
timeout: 2000,
success: function(response) {
//Do Something
console.log(response);
update_status(false);
},
error: function(xhr) {
//Do Something to handle error
}
});
});
$('#btnTurnOff').on('click', function(){
// Send
$.ajax({
url: "/api/v0/turn_off",
type: "get", //send it through get method
timeout: 2000,
success: function(response) {
console.log(response);
update_status(false);
},
error: function(xhr) {
//Do Something to handle error
}
});
});
$('a#pop-temp').popover({
trigger : 'click',
placement : 'top',
html: true,
title : 'Target Temperature',
content : 'This is the maximum/desired temperature we want inside the box.<br>\
The system will try to reach and then maintain this temperature.',
});
$('a#pop-fan').popover({
trigger : 'click',
placement : 'top',
html: true,
title : 'Fan speed',
content : 'Set the speed of the internal fan used to circulate the air inside the dry box.<br><br>\
0% = Off<br>\
50% = 50% of fan\'s maximum RPM<br>\
100% = Full speed',
});
$('a#pop-heater').popover({
trigger : 'click',
placement : 'top',
html: true,
title : 'Max heater temperature',
content : 'Maximum allowed temerature heater can reach.<br>This limit should be set to avoid damage to your filament or the box/heater itself.<br>\
For example; Let\'s say that your heater could reach max temperature of 150°C. However this could start to melt your filament, melt the dry box or start a fire.<br>\
In order to avoid this, we set a heater temperature limit to make sure heater never passes this set temperature during normal operation.<br>\
This limit will also affect how long it takes to reach target temperature.',
});
const ctx = document.getElementById('myChart');
measurements_chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [
{
label: 'Heater',
data: []
},
{
label: 'Enclosure',
data: []
}
]
},
options: {
animation: false, // disables all animations
scales: {
y: {
beginAtZero: false,
suggestedMin: 25,
// suggestedMax: 2.25,
ticks: {
// forces step size to be 1.0 units
stepSize: 1
}
},
x: {
beginAtZero: false,
reverse: true
}
},
plugins: {
legend: {
display: true,
}
}
}
});
// Finally update status
update_status(true);
});
function update_status(rearm)
{
var ajaxTime= new Date().getTime();
$.ajax({
url: "/api/v0/status",
type: "get", //send it through get method
timeout: 2000,
success: function(response) {
console.log(response);
var res = jQuery.parseJSON(response);
$('#status-temp-target').text(res.target_temp_in);
$('#status-fan-speed').text(res.fan_speed);
$('#status-temp-inside').text(res.temp_in.toFixed(1));
$('#status-temp-inside2').text(res.temp_in.toFixed(1));
$('#status-temp-inside-target').text(res.target_temp_in.toFixed(1));
$('#status-temp-heater').text(res.temp_heater.toFixed(1));
$('#status-temp-heater-max').text(res.max_temp_heater.toFixed(0));
$('#status-humidity-inside').text(res.humid_in.toFixed(0));
$('#status-temp-outside').text(res.temp_out.toFixed(0));
$('#status-humidity-outside').text(res.humid_out.toFixed(0));
$('#status-heater-power').text(res.heater_power.toFixed(0));
chart_add_point(0, {x: get_timestamp(), y: res.temp_heater.toFixed(1)});
chart_add_point(1, {x: get_timestamp(), y: res.temp_in.toFixed(1)});
// Homed status
if(res.status == 1) {
$('#status-box').text('Dry box is on').addClass('badge-success').removeClass('badge-warning').removeClass('badge-danger');
}
else {
$('#status-box').text('Dry box is off').addClass('badge-warning').removeClass('badge-success').removeClass('badge-danger');
}
},
error: function(xhr) {
$('#status-box').text('Dry box is unreachable!').addClass('badge-danger').removeClass('badge-success').removeClass('badge-warning');
}
}).done(function () {
var totalTime = new Date().getTime()-ajaxTime;
// Here I want to get the how long it took to load some.php and use it further
});;
if(rearm) {
rearm_status();
}
}
function rearm_status()
{
setTimeout(function () {
update_status(true);
}, 2000);
}
function chart_add_point(chartIndex, newData) {
measurements_chart.data.datasets[chartIndex].data.unshift(newData);
if (measurements_chart.data.datasets[chartIndex].data.length >= 900)
{
measurements_chart.data.datasets[chartIndex].data.shift();
}
measurements_chart.update('none');
}
function get_timestamp()
{
let date = new Date();
return twoDigitPad(date.getHours()) +':'+ twoDigitPad(date.getMinutes()) +':'+ twoDigitPad(date.getSeconds());
}
function twoDigitPad(num) {
return num < 10 ? "0" + num : num;
}
File diff suppressed because one or more lines are too long
+74
View File
@@ -0,0 +1,74 @@
// Shorthand for $( document ).ready()
$(function() {
$('#btn_SavePIDSettings').on('click', function(){
var kp = $("#input_kp").val();
var ki = $("#input_ki").val();
var kd = $("#input_kd").val();
// Send
$.ajax({
url: "/api/v0/pid/set",
type: "get", //send it through get method
data: {
p: kp,
i: ki,
d: kd,
},
timeout: 2000,
success: function(response) {
console.log(response);
alert("PID values updated and applied!");
},
error: function(xhr) {
console.log(xhr);
alert("Error updating values. Please check console.");
}
});
});
$('a#pop-pid-calc').popover({
trigger : 'click',
placement : 'top',
html: true,
title : 'PID Calculation',
content : 'We output PWM signal to the heater that can have duty between 0 and PWM_MAX_VALUE.<br>\
`Target temperature` - Either desired inside temperature, or maximum allowed heater temperature (whichever is lower). <br>\
T_current - Current measured temperature. <br>\
T_previous - Previously measured temperature.<br>\
T_delta - T_current - T_previous.<br>\
T_target - Current target temperature. <br>\
T_avg - Average temperature, sampled over last $ADC_HEATER_SAMPLES$ cycles.<br>\<br>\
PWM value is calculated as follows:<br>\
PWM_P = (T_target - T_current) * KP<br>\
PWM_I = (T_target - T_avg) * KI<br>\
PWM_D = (T_target - T_delta) * KD<br>\
PWM = PWM_P + PWM_I + PWM_D\
'
});
$('#btn_SaveTemperatureSettings').on('click', function(){
var compensateIn = $("#input_in_temperature").val();
var compensateOut = $("#input_out_temperature").val();
// Send
$.ajax({
url: "/api/v0/compensation/temperature/set",
type: "get", //send it through get method
data: {
temperatureIn: compensateIn,
temperatureOut: compensateOut,
},
timeout: 2000,
success: function(response) {
console.log(response);
alert("Temperature compensation values updated and applied!");
},
error: function(xhr) {
console.log(xhr);
alert("Error updating values. Please check console.");
}
});
});
});
+368
View File
@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Filament Dry Box v2 - Sasa Karanovic</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/custom.min.css" />
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/brands.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet">
<link rel="shortcut icon" type="image/jpg" href="img/favicon.png" />
</head>
<body class="d-flex flex-column min-vh-100">
<div class="navbar navbar-expand-lg fixed-top navbar-dark dark-bg">
<div class="container">
<a href="index.html" class="navbar-brand">Filament Dry Box v2</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="settings.html">Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://sasakaranovic.com/projects/diy-filament-dry-box-v2/">Project Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/SasaKaranovic/DIY-Filament-Dryer-v2" target="_blank">Source Code</a>
</li>
</ul>
</div>
</div>
</div>
<!-- Page header - Camera slider -->
<div class="container">
<!-- Main content -->
<div class="row">
<!-- Left column -->
<div class="col-lg-5 col-md-5 col-sm-5">
<div class="row mt-4">
<div class="col-lg-12 mr-1">
<div class="row">
<div class="col-lg-12 mr-1">
<legend>PID Values</legend>
<div class="alert alert-danger" role="alert">
<p><i class="fas fa-exclamation"></i>&nbsp;WARNING&nbsp;<i class="fas fa-exclamation"></i></p>
<p>Do _NOT_ modify below values unless you are absolutely sure that you know what you are doing!</p>
</div>
</div>
<!-- Left column -->
<div class="col-lg-7 mr-2">
<div class="float-right">
<a href="#" id="pop-pid-calc"><img src="img/help.png" /></a>
</div>
<div class="form-group">
<label class="control-label">KP coefficient</label>
<div class="input-group mb-1">
<div class="input-group-prepend">
<span class="input-group-text">P</span>
</div>
<input type="text" id="input_kp" class="form-control" size="5" maxlength="3" value="$PID_KP$">
</div>
</div>
<div class="form-group">
<label class="control-label">KI coefficient</label>
<div class="input-group mb-1">
<div class="input-group-prepend">
<span class="input-group-text">I</span>
</div>
<input type="text" id="input_ki" class="form-control" size="5" maxlength="3" value="$PID_KI$">
</div>
</div>
<div class="form-group">
<label class="control-label">KD coefficient</label>
<div class="input-group mb-1">
<div class="input-group-prepend">
<span class="input-group-text">D</span>
</div>
<input type="text" id="input_kd" class="form-control" size="5" maxlength="3" value="$PID_KD$">
</div>
</div>
<div class="row mr-4">
<div class="col-lg-12 mr-2">
&nbsp;
</div>
</div>
<div>
<button type="button" id="btn_SavePIDSettings" class="btn btn-primary">Save settings</button>
</div>
<div class="row mr-4">
<div class="col-lg-12 mr-2">
&nbsp;
</div>
</div>
<div>
<button type="button" id="btn_ResetSettings" class="btn btn-primary">Reset to default settings</button>
</div>
</div> <!-- End left speed column -->
</div >
<div class="clearfix"></div>
<div style="padding-top: 3em;"></div>
<div class="row">
<div class="col-lg-12 mr-1">
<legend>Temperature compensation</legend>
</div>
<!-- Left column -->
<div class="col-lg-7 mr-2">
<div class="float-right">
<a href="#" id="pop-pid-calc"><img src="img/help.png" /></a>
</div>
<div class="form-group">
<label class="control-label">Inside temperature</label>
<div class="input-group mb-1">
<input type="text" id="input_in_temperature" class="form-control" size="5" maxlength="6" value="$COMPENSATE_TEMP_IN$">
</div>
</div>
<div class="form-group">
<label class="control-label">Outside temperature</label>
<div class="input-group mb-1">
<input type="text" id="input_out_temperature" class="form-control" size="5" maxlength="6" value="$COMPENSATE_TEMP_OUT$">
</div>
</div>
<div class="row mr-4">
<div class="col-lg-12 mr-2">
&nbsp;
</div>
</div>
<div>
<button type="button" id="btn_SaveTemperatureSettings" class="btn btn-primary">Save settings</button>
</div>
<div class="row mr-4">
<div class="col-lg-12 mr-2">
&nbsp;
</div>
</div>
</div> <!-- End left speed column -->
</div >
</div>
</div>
</div> <!-- End left column -->
<!-- Right column -->
<div class="col-lg-5 col-md-5 col-sm-5 offset-2">
<div class="row">
</div>
</div> <!-- End right column -->
<div class="container">
<div class="p-5 text-center bg-body-tertiary">
<h1 class="text-body-emphasis">Firmware information</h1>
<table class="table table-sm table-striped text-left">
<thead>
<tr class="d-flex">
<th class="col-4 text-break">Parameter</th>
<th class="col-2">Value</th>
<th class="col-6">Note</th>
</tr>
</thead>
<tbody>
<tr class="d-flex">
<td class="col-4 text-break">PWM_HEATER_INVERT_VALUES</td>
<td class="col-2 text-break">$PWM_HEATER_INVERT_VALUES$</td>
<td class="col-6">Maximum number of calibration points supported by firmware.</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_FREQ_HEATER</td>
<td class="col-2 text-break">$PWM_FREQ_HEATER$</td>
<td class="col-6">PWM frequency used for heater signal</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_FREQ_FAN</td>
<td class="col-2 text-break">$PWM_FREQ_FAN$</td>
<td class="col-6">PWM frequency used for fan PWM signal</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_MAX_VALUE</td>
<td class="col-2 text-break">$PWM_MAX_VALUE$</td>
<td class="col-6">Maximum value of the PWM channel</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_HEATER_INVERT_VALUES</td>
<td class="col-2 text-break">$PWM_HEATER_INVERT_VALUES$</td>
<td class="col-6">Heater driver requires inverted (1) or non-inverted (0) signal</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_CH_HEATER</td>
<td class="col-2 text-break">GPIO$PWM_CH_HEATER$</td>
<td class="col-6">GPIO controlling the heater</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PWM_CH_FAN</td>
<td class="col-2 text-break">GPIO$PWM_CH_FAN$</td>
<td class="col-6">GPIO controlling the fan</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">LIMIT_TEMP_IN_MAX</td>
<td class="col-2 text-break">$LIMIT_TEMP_IN_MAX$</td>
<td class="col-6">Maximum INSIDE temperature</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">LIMIT_TEMP_HEATER_MAX</td>
<td class="col-2 text-break">$LIMIT_TEMP_HEATER_MAX$</td>
<td class="col-6">Maximum HEATER temperature</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">NTC_BETA</td>
<td class="col-2 text-break">$NTC_BETA$</td>
<td class="col-6">Beta value of NTC thermistor</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">NTC_RESISTANCE</td>
<td class="col-2 text-break">$NTC_RESISTANCE$</td>
<td class="col-6">NTC resistance</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">NTC_R1_VALUE</td>
<td class="col-2 text-break">$NTC_R1_VALUE$</td>
<td class="col-6">Value of R1 in the NTC resistor divider</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">ADC_HEATER_SAMPLES</td>
<td class="col-2 text-break">$ADC_HEATER_SAMPLES$</td>
<td class="col-6">Number of ADC samples for NTC conversion</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">ADC_MAX_VAUE</td>
<td class="col-2 text-break">$ADC_MAX_VAUE$</td>
<td class="col-6">Maximum ADC value</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">COMPENSATE_TEMP_IN</td>
<td class="col-2 text-break">$COMPENSATE_TEMP_IN$</td>
<td class="col-6">Inside temperature compensation</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">COMPENSATE_TEMP_OUT</td>
<td class="col-2 text-break">$COMPENSATE_TEMP_OUT$</td>
<td class="col-6">Outside temperature compensation</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">WIFI_CHECK_CONNECTION_MS</td>
<td class="col-2 text-break">$WIFI_CHECK_CONNECTION_MS$</td>
<td class="col-6">How often (in miliseconds) to check for WiFi connectivity</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">BUILD_TIME</td>
<td class="col-2 text-break">$BUILD_TIME$</td>
<td class="col-6">Time when firmware was compiled</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">BUILD_DATE</td>
<td class="col-2 text-break">$BUILD_DATE$</td>
<td class="col-6">Date when firmware was compiled</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">BUILD_HASH</td>
<td class="col-2 text-break">$BUILD_HASH$</td>
<td class="col-6">Build Git hash (short)</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">ADC_CHIP</td>
<td class="col-2 text-break">$ADC_CHIP$</td>
<td class="col-6">ADC chip populated on the board</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PID_MIN_VALUE</td>
<td class="col-2 text-break">$PID_MIN_VALUE$</td>
<td class="col-6">Minimum accepted P/I/D coefficient</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">PID_MAX_VALUE</td>
<td class="col-2 text-break">$PID_MAX_VALUE$</td>
<td class="col-6">Maximum accepted P/I/D coefficient</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">DEF_DEBUG_PID</td>
<td class="col-2 text-break">$DEF_DEBUG_PID$</td>
<td class="col-6">PID debug prinout over serial (1=Enabled)</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">I2C_DEVICE_LIST</td>
<td class="col-2 text-break">$I2C_DEVICE_LIST$</td>
<td class="col-6">List of I2C devices detected</td>
</tr>
<tr class="d-flex">
<td class="col-4 text-break">FW_INT_TEMP_LIB_STYPE</td>
<td class="col-2 text-break">$FW_INT_TEMP_LIB_STYPE$</td>
<td class="col-6">FW SHT library sensor type</td>
</tr>
</tbody>
</table>
<div class="">
<p class="col-lg-8 mx-auto fs-5 text-muted mb-4">
Values in this table are provided for development/debugging purposes (and just for fun).<br>
You should not have to worry about them during devices normal operation.
</p>
</div>
</div>
</div>
</div>
<!-- FOOTER -->
<footer class="w-100 py-2 flex-shrink-0 mt-auto dark-bg">
<div class="container py-2">
<div class="row gy-4 gx-5">
<div class="col-lg-6 col-md-6">
<p class="small text-muted">
Dry box controller by <a href="https://SasaKaranovic.com/" target="_blank">Saša Karanović</a><br>
Released as open-source and open-hardware for you to hack and enjoy! :)<br>
Firmware version $VERSION_MAJOR$$VERSION_MINOR$$VERSION_PATCH$
</p>
</div>
<div class="col-lg-6 col-md-6">
<a class="m-2" href="https://sasakaranovic.com/" target="_blank"><i class="fas fa-home"></i> Blog</a>
<a class="m-2" href="https://youtube.com/c/sasakaranovic" target="_blank"><i class="fab fa-youtube"></i> YouTube</a>
<a class="m-2" href="https://twitter.com/_sasakaranovic_" target="_blank"><i class="fab fa-twitter"></i> Twitter</a>
<a class="m-2" href="https://github.com/sasakaranovic" target="_blank"><i class="fab fa-github"></i> GitHub</a>
<a class="m-2" href="https://github.com/sponsors/SasaKaranovic" target="_blank"><i class="fas fa-heart"></i> Sponsor on GitHub</a>
</div>
</div>
</div>
</footer>
</div>
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/settings.js"></script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+236
View File
@@ -0,0 +1,236 @@
import os
import io
import shutil
import gzip
from datetime import datetime
from itertools import islice
# Todo: Add option to chose between generating h+c files or .ino file
# Todo: Add option to configure custom server handlers
class espembed:
def __init__(self, data_directory, output_directory):
self.data_directory = data_directory
self.output_directory = output_directory
self.files = {}
self.max_line_length = 60
self.supported_extensions = {
".htm": { "gzip": False, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "text/html", "ctype": "uint8_t"},
".html":{ "gzip": False, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "text/html", "ctype": "uint8_t"},
".js": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "text/javascript", "ctype": "uint8_t"},
".css": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "text/css", "ctype": "uint8_t"},
".ico": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_image, "encoding": "image/x-icon", "ctype": "uint8_t"},
".png": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_image, "encoding": "image/png", "ctype": "uint8_t"},
".jpg": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_image, "encoding": "image/jpeg", "ctype": "uint8_t"},
".jpeg":{ "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_image, "encoding": "image/jpeg", "ctype": "uint8_t"},
".gif": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_image, "encoding": "image/gif", "ctype": "uint8_t"},
".woff": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "font/woff", "ctype": "uint8_t"},
".woff2": { "gzip": True, "template_processor": None, "handler_generator": self.gen_handler_default, "encoding": "font/woff2", "ctype": "uint8_t"},
}
self.web_handler_links = []
self.web_data_header = ""
self.web_data_content = ""
# self.output_path = output_directory
self.output_filename = "binary_data.ino"
def add_file(self, filepath, handler, encoding=None, template_processor=None, handler_generator=None):
extension = os.path.splitext(filepath)[1]
if extension not in self.supported_extensions.keys():
print(f"File `{filepath}` is not in the supported extensions list. Skipping.")
return None
# Option to override default encoding
if encoding is None:
encoding = self.supported_extensions[extension]['encoding']
# Option to override default template_processor
if template_processor is None:
template_processor = self.supported_extensions[extension]['template_processor']
# Option to override default handler_generator
if handler_generator is None:
if self.supported_extensions[extension]['handler_generator'] is None:
handler_generator = self.gen_handler_default
else:
handler_generator = self.supported_extensions[extension]['handler_generator']
# Read file contents
with open(filepath, mode="rb") as f:
content = f.read()
# If gzip is enabled
if self.supported_extensions[extension]['gzip']:
print(len(content))
content = gzip.compress(content)
print(len(content))
gzipped = True
else:
gzipped = False
item = {
'binary_data' : content,
"gzipped": gzipped,
'size': len(content),
'handler': handler ,
'encoding': encoding,
'ctype': self.supported_extensions[extension]['ctype'],
'template_processor': template_processor,
'handler_generator': handler_generator
}
print(f"\t- File content length {len(content)} (gzipped: {gzipped}))")
self.files[handler] = item
def add_directory(self, directory, base_start):
for root, dirs, files in os.walk(directory):
for file in files:
extension = os.path.splitext(file)[1]
if extension in self.supported_extensions.keys():
handler_base = root.replace(base_start, '').replace('\\', '/')
handler = f"{handler_base}/{file}"
filepath = os.path.join(root, file)
print(f"-- Adding handler for `{handler}` located at `{filepath}`")
self.add_file(filepath, handler)
def binary_data_to_hex_string(self, data):
# Providing compatibility for pre 3.12
def _batched(iterable, n):
if n < 1:
raise ValueError('n must be at least one')
it = iter(iterable)
while batch := tuple(islice(it, n)):
yield batch
ret = ','.join('0x{:02X}'.format(x) for x in data)
ret = ",\n".join(", ".join(l) for l in _batched(ret.split(","), 16))
return ret
def generate_c_const_name(self, handler):
ret = f"web_data_{handler}"
ret = ret.replace('/', '_')
ret = ret.replace('\\', '_')
ret = ret.replace('-', '_')
ret = ret.replace('.', '_')
ret = ret.replace('__', '_')
return ret
def write_file_data(self, handler, data):
dec_name = self.generate_c_const_name(handler)
data_arr_str = self.binary_data_to_hex_string(data)
header = f""
content = f""
item = { 'handler': handler, 'header':header, 'content':content }
self.web_handler_links.append(item)
def gen_handler_default(self, data_const_name, item):
gzip = ""
if item['gzipped']:
gzip = 'response->addHeader("Content-Encoding", "gzip");\n\t\t'
if item['template_processor'] is None:
template_processor = ""
else:
template_processor = f", {item['template_processor']}"
# request->send_P(200, "{encoding}", {variable_name});
ret = f'''server.on("{item['handler']}", HTTP_GET, [](AsyncWebServerRequest *request){{
AsyncWebServerResponse *response = request->beginResponse_P(200, "{item['encoding']}", {data_const_name}, {item['size']}{template_processor});
{gzip}request->send(response);
}});'''
return ret
def gen_handler_image(self, data_const_name, item):
gzip = ""
if item['gzipped']:
gzip = 'response->addHeader("Content-Encoding", "gzip");\n\t\t'
ret = f'''server.on("{item['handler']}", HTTP_GET, [](AsyncWebServerRequest *request){{
AsyncWebServerResponse *response = request->beginResponse_P(200, "{item['encoding']}", {data_const_name}, {item['size']});
{gzip}request->send(response);
}});'''
return ret
def set_template_processor(self, extension, function_name):
try:
extension = extension.lower()
self.supported_extensions[extension]['template_processor'] = function_name
except KeyError as e:
print(f"Extensions `{extension}` is not supported.")
return
def generate_output_file(self, filepath):
forward_declarations = io.StringIO()
data_section = io.StringIO()
request_handlers = io.StringIO()
data_section.write("\n\n//---- BEGIN DATA SECTION ----\n\n")
request_handlers.write("\n\n//---- Request handler ----\n")
request_handlers.write("void server_init_handlers(void)\n{\n")
# Iterate over all items and generate content for output file
for handler, item in self.files.items():
'''
# Write forward declarations
forward_declarations.write('\n\n//---- Forward delcarations ----\n\n')
for _, item in self.files.items():
forward_declarations.write(f"// Data for handler `{item['handler']}`\n")
forward_declarations.write(f"extern const char {item['c_data_const_name']}[{item['size']}];\n")
'''
# --- Write item to data section
# todo: Optionally we could gzip/compress data here before embedding
c_data_const_name = self.generate_c_const_name(handler)
c_hex_str_data = self.binary_data_to_hex_string(item['binary_data'])
c_server_handler = item['handler_generator'](c_data_const_name, self.files[handler])
data_section.write(f"// Content of `{handler}`\n")
data_section.write(f"const {item['ctype']} {c_data_const_name}[{item['size']}] PROGMEM = {{\n{c_hex_str_data}\n}};\n")
# f.write(f"const uint8_t {item['c_data_const_name']}[] PROGMEM = {{\n{item['c_hex_str_data']}\n}};\n")
# --- Create request handler for item
# Write server request handlers
request_handlers.write(f"\t{c_server_handler}\n")
# -- Finalize data and handler sections
data_section.write("\n\n//---- END DATA SECTION ----\n\n")
request_handlers.write("}\n")
# Merge content
content = data_section.getvalue() + '\n\n' + request_handlers.getvalue()
self.write_ino_file(content);
data_section.close()
request_handlers.close()
forward_declarations.close()
def write_ino_file(self, content):
filename = os.path.join(self.output_directory, self.output_filename)
with open(filename, mode='w', encoding='utf-8') as f:
f.write("// This file was auto generated using ESP32 Data Embed script\n")
f.write("// Please do not manually modify this file\n")
f.write("// since any changes to this file will be overwritten on next build.\n")
f.write(f"// Generated on {datetime.today().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write(content)
f.write("\n// ---- End of auto generated file\n")
def run(self):
for handler, file_data in self.files.items():
self.write_file_data(handler, file_data['binary_data'])
self.generate_output_file(self.output_directory)
def main(data_dir, src_dir):
embed = espembed(data_dir, src_dir)
embed.set_template_processor('.htm', 'template_const_processor')
embed.set_template_processor('.html', 'template_const_processor')
embed.add_directory('data', 'data')
embed.run()
Import("env")
print("\n--- Embedding binary data...")
path = os.getcwd()
data_dir = os.path.join(path, 'data')
src_dir = os.path.join(path, 'src')
main(data_dir, src_dir)
print("--- Embedding binary data: complete")
+46
View File
@@ -0,0 +1,46 @@
# Sasa Karanovic's Simple script to make `version.h` file from current date-time and git commit
import datetime
import subprocess
def get_git_revision_hash() -> str:
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
def get_git_revision_short_hash() -> str:
return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
def write_version_to_file(filepath, contents):
with open(filepath, "w", encoding="utf-8") as f:
f.write(contents)
def make_version_string():
now = datetime.datetime.now()
year = now.strftime('%Y')
month = now.strftime('%m')
day = now.strftime('%d')
git_long_hash = get_git_revision_hash()
git_short_hash = get_git_revision_short_hash()
version_header = \
f"""#ifndef __DIY_DRYBOX_V2_FW_VERSION_H_INC__
#define __DIY_DRYBOX_V2_FW_VERSION_H_INC__
#define VERSION_MAJOR "{year}"
#define VERSION_MINOR "{month}"
#define VERSION_PATCH "{day}"
#define GITH_LONG_HASH "{git_long_hash}"
#define GITH_SHORT_HASH "{git_short_hash}"
#endif
"""
print(f"New version {year}-{month}-{day}")
return version_header
def make_version():
print("--- Cutting version ---")
version = make_version_string()
write_version_to_file('src/version.h', version)
Import("env")
make_version()
+72
View File
@@ -0,0 +1,72 @@
# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664
Import("env") # noqa
import os
import shutil
import subprocess
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
try:
import esptool
except ImportError:
env.Execute("$PYTHONEXE -m pip install esptool")
else:
import subprocess
from SCons.Script import ARGUMENTS
# Copy over the default sdkconfig.
from os import path
if path.exists("./sdkconfig.defaults"):
os.makedirs(".temp", exist_ok=True)
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf")
def esp32_create_combined_bin(source, target, env):
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
print("-------------------------------------------------------")
# if verbose:
if True:
print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/merged-firmware.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
flash_size = env.BoardConfig().get("upload.flash_size")
cmd = [
"--chip",
chip,
"merge-bin",
"-o",
new_file_name,
"--flash-size",
flash_size,
]
# if verbose:
print(f"Sections: {sections}")
if True:
print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
if verbose:
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
cmd += [hex(app_offset), firmware_name]
# if verbose:
if True:
print(f" - {hex(app_offset)} | {firmware_name}")
print()
print(f"Using esptool arguments: {' '.join(cmd)}")
print()
# if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
# esptool.main(cmd)
# else:
subprocess.run(["esptool", *cmd])
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa
+116
View File
@@ -0,0 +1,116 @@
# Sasa Karanovic's Simple minify script to minify html, css and js data
import os
import shutil
import minify_html
from rjsmin import jsmin
class Minify:
def __init__(self, work_dir, raw_directory, min_directory):
self.work_dir = work_dir
self.raw_directory = raw_directory
self.min_directory = min_directory
self._bytes_saved = { 'html': 0, 'js': 0, 'css': 0 }
print(f"Working directory: {self.work_dir}")
print(f"Source directory: {self.raw_directory}")
print(f"Data directory: {self.min_directory}")
def run(self):
print("--- Running minifier ---")
self._copy_source()
minify_files = self._find_minify_candidates(self.min_directory)
for file in minify_files:
self._minify(file)
print("---")
def show_stats(self, key=None):
if key is not None:
print(f"Saved {self._bytes_saved[key]} bytes by minifying `.{key}` files")
else:
total = 0
for key in self._bytes_saved:
print(f"Saved {self._bytes_saved[key]} bytes by minifying `.{key}` files")
total = total + self._bytes_saved[key]
print(f"Total {total} bytes saved")
def _find_minify_candidates(self, folder):
minify_candidates = []
for root, dirs, files in os.walk(folder):
for file in files:
if file.lower().endswith(('.htm', '.html', '.js', '.css')):
minify_candidates.append(os.path.join(root, file))
return minify_candidates
def _update_saved_count(self, category, amount):
self._bytes_saved[category] = self._bytes_saved[category] + amount
def _minify(self, file):
if file.lower().endswith(('.html', '.htm')):
content = self._read_file(file)
content_minified = self._min_html(content)
print(f" -> Minified {file} [saved {len(content)-len(content_minified)} bytes]")
self._write_file(file, content_minified)
elif file.lower().endswith('.js'):
content = self._read_file(file)
content_minified = self._min_js(content)
self._write_file(file, content_minified)
print(f" -> Minified {file} [saved {len(content)-len(content_minified)} bytes]")
else:
print(f" -> No minifier available for file {file}")
def _min_html(self, content):
size_pre = len(content)
content = minify_html.minify(content, minify_js=True, remove_processing_instructions=True)
size_after = len(content)
self._update_saved_count('html', size_pre-size_after)
return content
def _min_js(self, content):
size_pre = len(content)
content = jsmin(content)
size_after = len(content)
self._update_saved_count('js', size_pre-size_after)
return content
def _copy_source(self):
shutil.copytree(self.raw_directory, self.min_directory, dirs_exist_ok=True)
def _read_file(self, file):
with open(file, mode="r", encoding="utf-8") as f:
content = f.read()
return content
def _write_file(self, file, content):
with open(file, mode="w", encoding="utf-8") as f:
f.write(content)
def _list_dir(self, filetype):
filtered = []
files = os.listdir(self.min_directory)
for file in files:
if file.endswith(f'.{filetype}'):
filepath = os.path.join(self.min_directory, file)
filtered.append(filepath)
return filtered
def minify(source, target, env):
# work_dir = os.path.dirname(os.path.abspath(__file__))
work_dir = os.getcwd()
raw_directory = os.path.join(work_dir, 'data_pre')
min_directory = os.path.join(work_dir, 'data')
print("-- Removing previous data directory")
shutil.rmtree(min_directory, ignore_errors=True)
print("-- Running data minify script")
mc = Minify(work_dir, raw_directory, min_directory)
mc.run()
mc.show_stats()
Import("env")
minify(None, None, None)
+52
View File
@@ -0,0 +1,52 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = esp32c3
[env:esp32_ota]
extends = env:esp32c3
upload_protocol = espota
[env:esp32c3]
board_build.f_cpu = 160000000L
board_build.f_flash = 80000000L
board_build.flash_mode = qio
board_build.partitions = huge_app.csv
framework = arduino
platform = https://github.com/platformio/platform-espressif32.git
board = esp32-c3-devkitm-1
upload_protocol = esptool
Monitor_speed = 115200
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DUSE_ESP32
-DUSE_ESP32_FRAMEWORK_ARDUINO
-DAUDIO_NO_SD_FS ; i2s_audio
-DTEMPLATE_PLACEHOLDER=36 ; '$'
extra_scripts =
pre:make_version.py
pre:minify_data.py
pre:embed_data.py
post:merge_firmware.py
lib_deps=
SPI
WiFi
Wire
https://github.com/esphome/ESPAsyncWebServer @ ^3.2.2
https://github.com/esphome/AsyncTCP @ ^2.1.3
ESPmDNS @ 2.0.0
https://github.com/tzapu/WiFiManager.git
https://github.com/Sensirion/arduino-sht @ ^1.2.5
https://github.com/RobTillaart/CRC @ ^1.0.2
+2
View File
@@ -0,0 +1,2 @@
minify-html>=0.15.0
rjsmin>=1.2.3
+130
View File
@@ -0,0 +1,130 @@
#include <Wire.h>
uint32_t adc_samples[ADC_MAX_SAMPLES] = {0};
uint8_t rxBuff[2] = {0};
uint8_t rxPos = 0;
uint32_t nADCTimeoutTick = 0;
void adc_init(void)
{
}
float readHeaterTemperature(void)
{
uint32_t heater_adc = adc_sample_data(ADC_HEATER_SAMPLES);
float temperature;
float v_out;
float resistance;
// Output voltage
v_out = ( (ADC_VDD/(ADC_MAX_VAUE-1) ) * heater_adc);
// Resistance
resistance = (NTC_R1_VALUE*(ADC_VDD-v_out))/v_out;
temperature = resistance / NTC_RESISTANCE; // (R/Ro)
temperature = log(temperature); // ln(R/Ro)
temperature /= NTC_BETA; // 1/B * ln(R/Ro)
temperature += 1.0 / (25 + 273.15); // + (1/To)
temperature = 1.0 / temperature; // Invert
temperature -= 273.15;
return temperature;
}
// Helper function to indicate (in the UI) which
// algo is used for calculating final ADC value
String adc_get_algo(void)
{
#ifdef ADC_FINAL_ADC_VALUE_USING_MEAN
return String("MEAN");
#else
return String("AVERAGE");
#endif
}
String adc_get_chip(void)
{
#ifdef ADC_CHIP_IS_MCP3221
return String("MCP3221");
#else
return String("MCP3021");
#endif
}
// This function will take `N` samples and store them into an array
// After that we should use one of the available functions to calculate value
// ie. calculate average, mean, trimmed_mean etc.
uint32_t adc_sample_data(uint8_t nSamples)
{
if(nSamples > ADC_MAX_SAMPLES)
{
Serial.print("Limiting number of samples to ");
Serial.println(ADC_MAX_SAMPLES);
nSamples = ADC_MAX_SAMPLES;
}
else if (nSamples <= 0)
{
Serial.println("Requested invalid number of samples. Aborting.");
return 0;
}
uint32_t accumulator = 0;
uint32_t sample = 0;
rxPos = 0;
while(rxPos++ < nSamples)
{
// Request two bytes
Wire.requestFrom(I2C_ADDR_ADC, 2);
// // Wait for data to become available
nADCTimeoutTick = millis() + 10;
while( (Wire.available() < 2) && (millis()<=nADCTimeoutTick) );
// Read two bytes
rxBuff[0] = Wire.read();
rxBuff[1] = Wire.read();
// Accumulate value
#ifdef ADC_CHIP_IS_MCP3221
// Using MCP3221
sample = ( (rxBuff[0] << 8 | (rxBuff[1])));
#else
// Using MCP3021
sample = ( (rxBuff[0] << 6 | (rxBuff[1]) >> 2));
#endif
adc_samples[rxPos] = sample;
accumulator += sample;
}
// Divide value with number of samples
return (accumulator/nSamples);
}
int qsort_compare( const void* a, const void* b)
{
uint32_t int_a = * ( (uint32_t*) a );
uint32_t int_b = * ( (uint32_t*) b );
// an easy expression for comparing
return (int_a > int_b) - (int_a < int_b);
}
uint32_t adc_get_mean(void)
{
// We want to have 3 or more samples
if (rxPos <= 2)
{
Serial.println("Insufficient number of samples. Returing the first value");
return adc_samples[0];
}
// Sort samples
qsort(adc_samples, rxPos, sizeof(uint32_t), qsort_compare);
// Get the "middle" element
// (we will round up/down on even number of samples, but we can live with that for now)
return adc_samples[rxPos/2];
}
+108
View File
@@ -0,0 +1,108 @@
#ifndef _DIY_DRYBOX_CFG_H_
#define _DIY_DRYBOX_CFG_H_
// clang-format off
#define MDNS_NAME "sk-drybox"
#define PIN_HEATER_CTL 4
#define PIN_FAN_PWM 5
#define PIN_FAN_TACHO
#define PIN_LED_RED 10
#define PIN_LED_GREEN 7
#define PIN_LED_BLUE 6
#define PIN_I2C_SDA 0
#define PIN_I2C_SCL 3
#define PIN_ENCL_I2C_CLK 0
#define PIN_ENCL_I2C_SDA 1
#define I2C_ADDR_SENSOR_EXTERNAL 0x70
#define I2C_ADDR_SENSOR_INTERNAL 0x40
#define I2C_ADDR_ADC 0x4D
#define I2C_READ_MAX_TRIES 2
#define ADC_HEATER_SAMPLES 10
#define ADC_MAX_SAMPLES 64
#define ADC_VDD 3.3
#define NTC_RESISTANCE 10000
#define NTC_BETA 4150
#define NTC_ROOM_TEMPERATURE 25
#define NTC_R1_VALUE 100000
#define SHT_SAMPLING_PERIOD 3000
#ifdef ADC_CHIP_IS_MCP3221
#define ADC_MAX_VAUE 2048
#else
#define ADC_MAX_VAUE 1024
#endif
#define LED_ON(x) do{digitalWrite(x, LOW);}while(0)
#define LED_OFF(x) do{digitalWrite(x, HIGH);}while(0)
#define LED_ALL_OFF() do{digitalWrite(PIN_LED_RED, HIGH);\
digitalWrite(PIN_LED_GREEN, HIGH);\
digitalWrite(PIN_LED_BLUE, HIGH); }while(0)
#define LED_ALL_ON() do{digitalWrite(PIN_LED_RED, LOW);\
digitalWrite(PIN_LED_GREEN, LOW);\
digitalWrite(PIN_LED_BLUE, LOW); }while(0)
#define LED_STATUS_WIFI PIN_LED_GREEN
#define LED_STATUS_HEATER PIN_LED_RED
#define PID_SAMPLES 100
#define PID_TEMP_PROXIMITY 5
#define PID_DEFAULT_KP 92
#define PID_DEFAULT_KI 81
#define PID_DEFAULT_KD 50
#define PID_MIN_VALUE 0
#define PID_MAX_VALUE 1000
#define PWM_HEATER_INVERT_VALUES 1
#define PWM_FREQ_HEATER 500
#define PWM_FREQ_FAN 25000
#define PWM_FREQ_LED 25000
#define PWM_RESOLUTION 10
#define PWM_MAX_VALUE 1024L
#define HEATER_PWM_OFF 0
#define PWM_CH_HEATER 0
#define PWM_CH_FAN 2
#define PWM_CH_LED_RED 3
#define PWM_CH_LED_GREEN 4
#define PWM_CH_LED_BLUE 5
#define TEMPERATURE_PRECISION 10
#define LIMIT_TEMP_IN_MIN 20
#define LIMIT_TEMP_IN_MAX 70
#define LIMIT_TEMP_HEATER_MIN 20
#define LIMIT_TEMP_HEATER_MAX 75
#define LIMIT_FAN_SPEED_MIN 0
#define LIMIT_FAN_SPEED_MAX 100
#define WDT_TIMEOUT 5
#define WIFI_CHECK_CONNECTION_MS 10000
#define DEF_DEBUG_CALIBRATE_ADC 0
#define DEF_DEBUG_PID 1
#define DEF_DEBUG_HETER_SAMPLES 0
#define DEF_DEBUG_SENSOR_SAMPLES 0
#define DEF_DEBUG_PWM_VALUES 0
typedef struct eeprom_data
{
uint32_t pid_kp;
uint32_t pid_ki;
uint32_t pid_kd;
float out_temp_comp;
float in_temp_comp;
uint32_t checksum;
} eeprom_data_t;
// --- Safety features ---
// Do not modify unless you understand what impact your changes will have
#define SAFETY_HEATER_REACH_MAX_TEMP_PERIOD 1800000 // 30 minutes
#define SAFETY_HEATER_REACH_MAX_TEMP_TOLERANCE 5
#define SAFETY_HEATER_MIN_TEMP_INCREASE_PERIOD 60000 // 1 minute
#define SAFETY_HEATER_MIN_TEMP_INCREASE 2
#define SAFETY_HEATER_OVERSHOOT_LIMIT 10
#define SAFETY_INSIDE_TEMPERATURE_REACHED_TOLERANCE 5
#define SAFETY_HEATER_HIGH_PWM_THRESHOLD 922 // ~90% of 1024 (PWM_MAX_VALUE)
#define SAFETY_HEATER_HIGH_PWM_PERIOD 90000 // 1 minute
#define SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE 3
#define SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE_THRESHOLD 35
#define SAFETY_HEATER_MIN_START_TEMPERATURE 5
#define SAFETY_HEATER_DISCONNECTED_TEMPERATURE -5
// clang-format on
#endif
+461
View File
@@ -0,0 +1,461 @@
#include <Arduino.h>
#include <esp_task_wdt.h>
#include "config.h"
#include "drybox_pid.hpp"
SKDryBox_PID::SKDryBox_PID(int HeaterCH, int HeaterLED)
{
_PWM_CH_Heater = HeaterCH;
_HeaterLED = HeaterLED;
PID_VAR_KP = PID_DEFAULT_KP;
PID_VAR_KI = PID_DEFAULT_KI;
PID_VAR_KD = PID_DEFAULT_KD;
}
void SKDryBox_PID::_safety_tick(void)
{
/*
This function will perform various 'safety checks' in firmware.
Here we will only check if we have a reason to believe something is 'wrong' and
we should disable the drybox.
To be on the safe side we will act even on the occasional potential 'false positive' event
if it means we don't have a 'false negative'.
Mainly we are focused on when to turn the heater off and disable dry-box to:
- Avoid overheating/fire
- Avoid damage
- Flag misconfiguration or faulty equipment
## 1: DryBox has been ON for X minute(s) but the heater has not yet reached max temperature.
Inside target temperature has _never_ been reached AND
SAFETY_HEATER_REACH_MAX_TEMP_PERIOD milliseconds has passed since drybox has been turned on
and the heater has not yet reached the heater (max temperature - SAFETY_HEATER_REACH_MAX_TEMP_TOLERANCE).
- We assume temperature probe might be faulty, disconnected or not attached to the heater
## 2: Heater temperature has not increased by a minimum expected amount after x minutes has passed.
Inside target temperature has _never_ been reached AND
SAFETY_HEATER_MIN_TEMP_INCREASE_PERIOD milliseconds has passed since drybox has been turned on
and the heater temperature has not increased by SAFETY_HEATER_MIN_TEMP_INCREASE
## 3: Heater temperature has over-shot the target maximum/limit temperature by x degrees.
If heater temperature has over-shot by SAFETY_HEATER_OVERSHOOT_LIMIT or more
## 4: Heater has been running with high PWM or more for X minute(s) but the heater temperature has not increased.
If the heater PWM value has been _continuously_ over SAFETY_HEATER_HIGH_PWM_THRESHOLD for more than
a SAFETY_HEATER_HIGH_PWM_PERIOD milliseconds, and the heater temperature is
below SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE_THRESHOLD and the heater temperature has not increased by
at least SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE degrees.
*/
// ##0: General one-off housekeeping
// If ever heater temperature is less than 0 degrees, we can assume something is wrong
// This is only if dry box heater is turned on
if(temperature_heater < SAFETY_HEATER_DISCONNECTED_TEMPERATURE)
{
Serial.println("SAFETY PANIC: #0 Heater temperature is abnormal!");
this->_panic_disable_drybox();
}
// Track if we ever reached target IN temperature
if ( (!sf_TargetInTemperatureWasReached) &&
(temperature_in >= (target_temperature_in-SAFETY_INSIDE_TEMPERATURE_REACHED_TOLERANCE)))
{
sf_TargetInTemperatureWasReached = true;
Serial.println("NOTE: Target IN temperature has been approached.");
}
// Track if we ever reached max heater temperature limit
if ( (!sf_HeaterMaxTemperatureReached) &&
(temperature_heater >= (max_temperature_heater-SAFETY_HEATER_REACH_MAX_TEMP_TOLERANCE)))
{
sf_HeaterMaxTemperatureReached = true;
Serial.println("NOTE: Heater max temperature has been approached.");
}
// Heater temperature has increased since start
if( (!sf_HeaterHasWarmedUp) &&
(temperature_heater >= (sf_HeaterStartTemperature+SAFETY_HEATER_MIN_TEMP_INCREASE)))
{
sf_HeaterHasWarmedUp = true;
Serial.println("NOTE: Heater temperature has increased since start.");
}
// ##1: DryBox has been ON for X minute(s) but the heater has not yet reached max temperature.
// (Except for a case when target IN temperature has already been reached because)
if(!sf_TargetInTemperatureWasReached && !sf_HeaterMaxTemperatureReached)
{
if (millis() >= (sf_StartTime + SAFETY_HEATER_REACH_MAX_TEMP_PERIOD))
{
Serial.println("SAFETY PANIC: #1 Heater failed to reach max temperature!");
this->_panic_disable_drybox();
}
}
// ## 2: Heater temperature has not increased by a minimum amount after x minutes has passed.
if(!sf_HeaterHasWarmedUp)
{
if (millis() >= (sf_StartTime+SAFETY_HEATER_MIN_TEMP_INCREASE_PERIOD))
{
Serial.println("SAFETY PANIC: #2 Heater failed to increase temperature!");
this->_panic_disable_drybox();
}
}
// ## 3: Heater temperature has over-shot the target maximum/limit temperature by x degrees.
if (temperature_heater >= (max_temperature_heater + SAFETY_HEATER_OVERSHOOT_LIMIT))
{
Serial.println("SAFETY PANIC: #3 Temperature over-shot!");
this->_panic_disable_drybox();
}
// ## 4: Heater has been running with high PWM for more for X minute(s) but the heater temperature has not increased.
if ((sf_HighPWMIsActive) && (millis() >= (sf_HighPWMStartTime+SAFETY_HEATER_HIGH_PWM_PERIOD)))
{
// Only expect heater temperature to rise quickly at 100% PWM if we are below SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE_THRESHOLD temperature
if(temperature_heater < SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE_THRESHOLD)
{
if(temperature_heater < (sf_HighPWMHeaterStartTemp + SAFETY_HEATER_HIGH_PWM_TEMP_INCREASE))
{
Serial.println("SAFETY PANIC: #4 Heater running at high PWM but temperature not increasing!");
this->_panic_disable_drybox();
}
}
}
}
void SKDryBox_PID::TurnOn()
{
// We don't start if heater temperature is below SAFETY_HEATER_MIN_START_TEMPERATURE
if(temperature_heater <= SAFETY_HEATER_MIN_START_TEMPERATURE)
{
Serial.println("SAFETY: Heater temperature is abnormal!!!");
Serial.println("SAFETY: DryBox will NOT turn on!");
return;
}
// Update safety values if we just started
if(!_drybox_is_on)
{
sf_StartTime = millis();
sf_HeaterStartTemperature = temperature_heater;
_drybox_is_on = true;
LED_ON(_HeaterLED);
Serial.println("DryBox heater on");
}
else
{
Serial.println("DryBox updated");
}
};
void SKDryBox_PID::TurnOff()
{
this->_set_heater_pwm(HEATER_PWM_OFF);
this->SetTargetInTemperature(0);
this->SetMaxHeaterTemperature(0);
this->_reset_safety_variables();
_drybox_is_on = false;
LED_OFF(_HeaterLED);
Serial.println("DryBox heater off");
};
void SKDryBox_PID::_reset_safety_variables(void)
{
uint32_t sf_StartTime = 0;
bool sf_HeaterMaxTemperatureReached = false;
bool sf_TargetInTemperatureWasReached = false;
bool sf_HeaterHasWarmedUp = false;
float sf_HeaterStartTemperature = 0;
bool sf_HighPWMIsActive = false;
uint32_t sf_HighPWMStartTime = 0;
float sf_HighPWMHeaterStartTemp = 0;
}
void SKDryBox_PID::_panic_disable_drybox(void)
{
this->_set_heater_pwm(HEATER_PWM_OFF);
this->SetTargetInTemperature(0);
this->SetMaxHeaterTemperature(0);
_drybox_is_on = false;
LED_ALL_OFF();
while(1)
{
esp_task_wdt_reset();
digitalWrite(_HeaterLED, LOW);
delay(100);
digitalWrite(_HeaterLED, HIGH);
delay(100);
}
}
bool SKDryBox_PID::RecordTemperatures(float in, float heater)
{
// Debug
#if DEF_DEBUG_HETER_SAMPLES
Serial.println((String)in + "\t" + heater + "\t" +
target_temperature_in);
#endif
if (_ready_for_recalc)
{
Serial.println("Record temp was called while recalc was pending! Ignoring record temp request.");
return false;
}
// Update 'current' values
temperature_in = in;
temperature_heater = heater;
// Log 'historic' values
temperature_samples_in[ts_pos] = in;
temperature_samples_heater[ts_pos] = heater;
if (ts_pos == 0)
{
// Record milliseconds of our first sample
pid_first_millis = millis();
}
ts_pos++;
if (ts_pos >= PID_SAMPLES)
{
pid_last_millis = millis();
ts_pos = 0;
_ready_for_recalc = true;
return true;
}
return false;
}
bool SKDryBox_PID::SetTargetInTemperature(float val)
{
target_temperature_in = val;
return true;
}
bool SKDryBox_PID::SetMaxHeaterTemperature(float val)
{
max_temperature_heater = val;
return true;
}
bool SKDryBox_PID::SetPID(uint32_t kp, uint32_t ki, uint32_t kd)
{
if( (kp<PID_MIN_VALUE) || (kp>PID_MAX_VALUE) ||
(ki<PID_MIN_VALUE) || (ki>PID_MAX_VALUE) ||
(kd<PID_MIN_VALUE) || (kd>PID_MAX_VALUE) )
{
Serial.println("Invalid new PID values. Aborting.");
return false;
}
PID_VAR_KP = kp;
PID_VAR_KI = ki;
PID_VAR_KD = kd;
#if DEF_DEBUG_PID
Serial.println((String) "PID P:" + PID_VAR_KP);
Serial.println((String) "PID I:" + PID_VAR_KI);
Serial.println((String) "PID D:" + PID_VAR_KD);
#endif
return true;
}
void SKDryBox_PID::PeriodicTick(void)
{
// If DryBox is off.. just abort, we don't have anything to do.
if (!_drybox_is_on)
{
_ready_for_recalc = false;
return;
}
// First run safety check
this->_safety_tick();
// If we have enough samples for PWM recalc
if(_ready_for_recalc)
{
this->_heater_recalc_pwm();
_ready_for_recalc = false;
}
}
// --- Private functions
void SKDryBox_PID::_set_heater_pwm(uint32_t pwm)
{
if (pwm > PWM_MAX_VALUE)
{
pwm = PWM_MAX_VALUE;
}
// Mark HighPWM event
if (pwm >= SAFETY_HEATER_HIGH_PWM_THRESHOLD)
{
// Don't re-activate if already active
if (!sf_HighPWMIsActive)
{
sf_HighPWMIsActive = true;
sf_HighPWMStartTime = millis();
sf_HighPWMHeaterStartTemp = temperature_heater;
// Serial.println("HighPWM ON");
}
}
// Otherwise clear HighPWM event
else
{
// Serial.println("HighPWM OFF");
sf_HighPWMIsActive = false;
}
#if PWM_HEATER_INVERT_VALUES
pwm = PWM_MAX_VALUE - pwm; // Driver signal is inverted
#endif
heater_power = 100 - ((float)pwm / (float)PWM_MAX_VALUE) * 100.0;
#if DEF_DEBUG_PWM_VALUES
Serial.println((String) "set_heater_pwm: " + pwm + "/" + PWM_MAX_VALUE);
#endif
ledcWrite(_PWM_CH_Heater, pwm);
}
void SKDryBox_PID::_heater_recalc_pwm(void)
{
// We are using a simple PID-like control loop to calculate PWM value for a
// heater Heater is heating up our box to a target temperature and keeping
// it steady after/if that temperature is reached. At the same time we have
// to ensure heater does not go beyond our "safe" temperature and start
// damaging itself/fillament/cables/enclosure etc.
//
// As a simple solution, we will use the following flow
// 1. Check if heater temperature is higher or equal to the maximum set
// heater temperature
// - if True, use heater max temperature as the target for our PID
// controller
// - if False, use enclosure maximum temperature as the target for
// our PID controller
float average = 0;
float pid_del_p;
float pid_del_i;
float pid_del_d;
int32_t pid_val_p;
int32_t pid_val_i;
int32_t pid_val_d;
int32_t calc_pwm;
int32_t pwm_val;
float pid_target_temperature = 0.0;
float pid_temperature = 0.0;
float pid_temperature_previous = 0.0;
unsigned long pid_time_diff;
// Make sure values are valid
if (isnan(temperature_heater))
{
Serial.println("Invalid temperature values");
Serial.println((String) "temp_heater:" + temperature_heater);
Serial.println((String) "target_temperature_in:" +
target_temperature_in);
this->_set_heater_pwm(HEATER_PWM_OFF);
return;
}
// Inside temperature has NOT reached the target temperature
// However, heater temperature is approaching it's maximum allowed
// temperature. Then, regulate heater max temperature
if ((temperature_heater >= (max_temperature_heater - PID_TEMP_PROXIMITY)) &&
(temperature_in < target_temperature_in))
{
// Serial.println("Using HEATER temperature as the limit.");
for (uint8_t i = 0; i < PID_SAMPLES; i++)
{
average += temperature_samples_heater[i];
}
average = average / PID_SAMPLES;
pid_target_temperature = max_temperature_heater;
pid_temperature = temperature_heater;
// pid_temperature_previous = temperature_samples_heater[PID_SAMPLES - 1]; // This should be index 0??
pid_temperature_previous = temperature_samples_heater[0];
}
// Heater is not close to it's maximum allowed temperature
// Or, inside temperature has already reached the target value
else
{
// Serial.println("Using IN temperature as the limit.");
for (uint8_t i = 0; i < PID_SAMPLES; i++)
{
average += temperature_samples_in[i];
}
average = average / PID_SAMPLES;
pid_target_temperature = target_temperature_in;
pid_temperature = temperature_in;
// pid_temperature_previous = temperature_samples_in[PID_SAMPLES - 1]; // This should be index 0??
pid_temperature_previous = temperature_samples_in[0];
}
// P
pid_del_p = pid_target_temperature - pid_temperature;
pid_val_p = (int32_t)((float)(pid_del_p)*PID_VAR_KP);
// I
pid_del_i = pid_target_temperature - average;
pid_val_i = (int32_t)((float)(pid_del_i)*PID_VAR_KI);
// D
pid_del_d = pid_temperature_previous - pid_temperature;
pid_time_diff = pid_last_millis - pid_first_millis;
if (pid_time_diff > 0)
{
float time_elapsed = pid_time_diff / 1000.f;
float fval = 0.0;
fval = (pid_del_d / time_elapsed) * (float)PID_VAR_KD;
pid_val_d = (int32_t)fval;
}
else
{
pid_val_d = 0;
}
// P + I + D
pwm_val = pid_val_p + pid_val_i + pid_val_d;
calc_pwm = pwm_val;
if (pwm_val > PWM_MAX_VALUE)
{
pwm_val = PWM_MAX_VALUE;
}
else if (pwm_val < 0)
{
pwm_val = 0;
}
// Serial debug
#if DEF_DEBUG_PID
if (millis() >= pid_debug_next)
{
Serial.printf("Delta P: %4.4f I: %4.4f D: %4.4f T:%dms\r\n",
pid_del_p, pid_del_i, pid_del_d, pid_last_millis - pid_first_millis);
Serial.printf("PWM Val P: %4ld I: %4ld D:%4ld\r\n",
pid_val_p, pid_val_i, pid_val_d);
Serial.printf("PWM : %4d\r\n\r\n",
pwm_val);
pid_debug_next = millis() + 500;
}
#endif
// Finally update the heater
this->_set_heater_pwm(pwm_val);
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef __SK_DRYBOX_INC_HPP__
#define __SK_DRYBOX_INC_HPP__
/*
Sasa Karanovic's DryBox Class
Simple abstraction for DryBox functions.
This class allows us to keep main PID logic and rest of the code separated.
Hopefully this makes editing and reviewing the 'main' portion of the code easier.
Before running the class we need to (unless we are OK with default values):
-> Set PID values
-> Set target inside temperature
-> Set maximum heater temperature
This class expects to periodically:
-> Get called to receive heater and 'inside' temperature
-> RecordInTemperature and RecordHeaterTemperature
-> Run the PID and update the heater PWM output
-> PeriodicTick
*/
class SKDryBox_PID {
public:
SKDryBox_PID(int HeaterCH, int HeaterLED);
bool RecordTemperatures(float in, float heater);
void TurnOn(void);
void TurnOff(void);
bool SetTargetInTemperature(float val);
bool SetMaxHeaterTemperature(float val);
bool SetPID(uint32_t kp, uint32_t ki, uint32_t kd);
void PeriodicTick(void);
bool isDryBoxOn(void) { return _drybox_is_on; }
uint8_t GetHeaterPower(void) { return heater_power; };
float GetInTemperature(void) { return temperature_in; };
float GetTargetTemperatureIn(void) { return target_temperature_in; };
float GetHeaterTemperature(void) { return temperature_heater; };
float GetHeaterMaxTemperature(void) { return max_temperature_heater; };
uint32_t GetPID_KP(void) { return PID_VAR_KP; }
uint32_t GetPID_KI(void) { return PID_VAR_KI; }
uint32_t GetPID_KD(void) { return PID_VAR_KD; }
private:
int _PWM_CH_Heater;
int _HeaterLED;
bool _drybox_is_on = false;
bool _ready_for_recalc = false;
float temperature_samples_in[PID_SAMPLES] = {0};
float temperature_samples_heater[PID_SAMPLES] = {0};
uint8_t ts_pos = 0;
float target_temperature_in = 25.0;
float max_temperature_heater = 55.0;
float temperature_in = 0;
float temperature_heater = 0;
uint8_t heater_power = 0;
unsigned long pid_first_millis = 0;
unsigned long pid_last_millis = 0;
unsigned long pid_debug_next = 0;
uint32_t PID_VAR_KP = PID_DEFAULT_KP;
uint32_t PID_VAR_KI = PID_DEFAULT_KI;
uint32_t PID_VAR_KD = PID_DEFAULT_KD;
uint32_t sf_StartTime = 0;
bool sf_HeaterMaxTemperatureReached = false;
bool sf_TargetInTemperatureWasReached = false;
bool sf_HeaterHasWarmedUp = false;
float sf_HeaterStartTemperature = 0;
bool sf_HighPWMIsActive = false;
uint32_t sf_HighPWMStartTime = 0;
float sf_HighPWMHeaterStartTemp = 0;
void _reset_safety_variables(void);
void _panic_disable_drybox(void);
void _set_heater_pwm(uint32_t pwm);
void _safety_tick(void);
void _heater_recalc_pwm(void);
};
#endif
+53
View File
@@ -0,0 +1,53 @@
void eeprom_init(void)
{
EEPROM.begin(sizeof(eeprom_data_t));
}
bool eeprom_load(void)
{
bool ret;
uint32_t calc_crc;
// Read EEPROM data
EEPROM.readBytes(0, (void *)&eepromData, sizeof(eeprom_data_t));
// Calculate CRC
calc_crc = calcCRC32((uint8_t*)&eepromData, (sizeof(eeprom_data_t)-4) );
if (eepromData.checksum == calc_crc)
{
Serial.println("EEPROM CRC is valid");
ret = true;
}
else
{
Serial.println("Calibration in EEPROM is invalid. Resetting!");
eeprom_reset();
ret = false;
}
return ret;
}
void eeprom_reset(void)
{
eepromData.pid_kp = PID_DEFAULT_KP;
eepromData.pid_ki = PID_DEFAULT_KI;
eepromData.pid_kd = PID_DEFAULT_KD;
eepromData.in_temp_comp = 0;
eepromData.out_temp_comp = 0;
eeprom_save();
}
void eeprom_save(void)
{
uint32_t calc_crc;
calc_crc = calcCRC32((uint8_t*)&eepromData, (sizeof(eeprom_data_t)-4) );
eepromData.checksum = calc_crc;
EEPROM.writeBytes(0, (void *)&eepromData, sizeof(eeprom_data_t));
EEPROM.commit();
}
+304
View File
@@ -0,0 +1,304 @@
#include <Arduino.h>
#include <esp_task_wdt.h>
#include <ESPmDNS.h>
#include <EEPROM.h>
#include <WiFiManager.h>
#include <AsyncTCP.h>
#include "ESPAsyncWebServer.h"
#include "WiFi.h"
#include <Wire.h>
#include "SHTSensor.h"
#include "config.h"
#include "CRC.h"
#include "version.h"
#include "drybox_pid.hpp"
WiFiManager wifiManager;
AsyncWebServer server(80);
int WiFi_status = WL_IDLE_STATUS;
// SHTSensor sensor_in(SHTSensor::SHT3X_ALT); // SK
SHTSensor sensor_in(SHTSensor::SHT3X); // DryBox
// SHTSensor sensor_in(SHTSensor::SHT2X); // Generic
SHTSensor sensor_out(SHTSensor::SHTC3);
uint32_t sht_next_sample = 0;
float temperature_in = 0;
float temperature_out = 0;
float temperature_heater = 0;
uint8_t box_status = 0;
unsigned long wifi_tick_previous = 0;
eeprom_data_t eepromData = {0};
uint8_t i2c_device_list[10] = {0};
float humidity_in = 0;
uint8_t humidity_out = 0;
uint8_t fan_duty = 0;
SKDryBox_PID SKDryBox(PWM_CH_HEATER, LED_STATUS_HEATER);
void setup()
{
// Immediately setup and turn off heater
pinMode(PIN_HEATER_CTL, OUTPUT);
pinMode(PIN_FAN_PWM, OUTPUT);
// Heater PWM -- Make sure heater is initially OFF
ledcSetup(PWM_CH_HEATER, PWM_FREQ_HEATER, PWM_RESOLUTION);
ledcAttachPin(PIN_HEATER_CTL, PWM_CH_HEATER);
// ledcWrite(PWM_CH_HEATER, HEATER_PWM_OFF);
SKDryBox.TurnOff();
// Setup LEDs
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_LED_BLUE, OUTPUT);
LED_OFF(PIN_LED_RED);
LED_OFF(PIN_LED_GREEN);
LED_ON(PIN_LED_BLUE);
// Configure Serial communication
Serial.begin(115200);
delay(2000);
Serial.setDebugOutput(true);
Serial.println("Sasa Karanovic's DIY DryBox v2");
Serial.println("https://sasakaranovic.com");
Serial.println("Version: " + String(VERSION_MAJOR) + "-" + String(VERSION_MINOR) + "-" + String(VERSION_PATCH) );
WiFi.mode(WIFI_STA);
WiFi.setHostname(MDNS_NAME);
WiFi.setTxPower(WIFI_POWER_8_5dBm);
int txPower = WiFi.getTxPower();
Serial.print("TX power: ");
Serial.println(txPower);
delay(1000);
// FAN PW -- Make sure FAN PWM is set to maximum (fan OFF)
ledcSetup(PWM_CH_FAN, PWM_FREQ_FAN, PWM_RESOLUTION);
ledcAttachPin(PIN_FAN_PWM, PWM_CH_FAN);
ledcWrite(PWM_CH_FAN, PWM_MAX_VALUE);
eeprom_init();
if (eeprom_load())
{
Serial.println("PID values loaded from EEPROM.");
SKDryBox.SetPID(eepromData.pid_kp, eepromData.pid_ki, eepromData.pid_kd);
}
else
{
Serial.println("FAILED to load PID values from EEPROM! Using defaults.");
}
// Setup WiFi
bool res;
res = wifiManager.autoConnect("SK-DryBox");
wifiManager.setHostname(MDNS_NAME);
wifiManager.setTitle("DryBox v2 WiFi Setup");
wifiManager.setConnectRetries(4);
if(!res) {
Serial.println("Failed to connect or hit timeout");
ESP.restart();
}
else {
//if you get here you have connected to the WiFi
Serial.println("Connected.");
}
// Setup WDT
esp_task_wdt_init(WDT_TIMEOUT, true); //enable panic so ESP32 restarts
esp_task_wdt_add(NULL); //add current thread to WDT watch
// Setup temperature and humidity oneWireSensors
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
i2c_scan();
adc_init();
sensor_in.init();
sensor_out.init();
sensor_in.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM);
sensor_out.setAccuracy(SHTSensor::SHT_ACCURACY_MEDIUM);
Serial.println("");
if (!MDNS.begin(MDNS_NAME))
{
Serial.println("Error starting mDNS");
}
else
{
Serial.println((String) "mDNS http://" + MDNS_NAME + ".local");
MDNS.addService("http", "tcp", 80);
}
Serial.print("WiFi IP: ");
Serial.println(WiFi.localIP());
setupWebServer();
server.begin();
Serial.println("Ready to go.");
// LED_OFF(PIN_LED_BLUE);
}
//
// ** Main loop **
//
void loop()
{
esp_task_wdt_reset();
// Sample temperature and humidity from all available sensors
temperature_heater = readHeaterTemperature();
// Sampling SHT temperature takes long time so we will do it every SHT_SAMPLING_PERIOD miliseconds
// Also SHT measures IN and OUT ambient temperature which should not be able to make huge jumps
if(millis() >= sht_next_sample)
{
sample_sens_in_and_out();
sht_next_sample = millis() + SHT_SAMPLING_PERIOD;
}
// Run DryBox
SKDryBox.RecordTemperatures(temperature_in, temperature_heater);
SKDryBox.PeriodicTick();
// Check if we have WiFi connection, if not try to reconnect
check_wifi_connection();
}
void sample_sens_in_and_out(void)
{
uint8_t tries = I2C_READ_MAX_TRIES;
// Read internal sensor
while(tries--)
{
if(sensor_in.readSample())
{
temperature_in = sensor_in.getTemperature() + eepromData.in_temp_comp;
humidity_in = sensor_in.getHumidity();
break;
}
}
if (tries == 0)
{
Serial.println("Error reading internal sensor!");
}
// Read external sensor
tries=I2C_READ_MAX_TRIES;
while(tries--)
{
if(sensor_out.readSample())
{
temperature_out = sensor_out.getTemperature() + eepromData.out_temp_comp;
humidity_out = sensor_out.getHumidity();
break;
}
}
if (tries == 0)
{
Serial.println("Error reading external sensor!");
}
}
void set_fan_duty(uint8_t duty)
{
if (duty > 100)
{
duty = 100;
}
fan_duty = duty;
uint32_t pwm_raw_fan = ((PWM_MAX_VALUE * (100 - duty)) / 100.0);
#if DEF_DEBUG_PWM_VALUES
Serial.println((String) "Setting fan to " + pwm_raw_fan);
#endif
ledcWrite(PWM_CH_FAN, pwm_raw_fan);
}
void check_wifi_connection(void)
{
unsigned long currentMillis = millis();
// Check WiFi status every WIFI_CHECK_CONNECTION_MS miliseconds
if ((currentMillis - wifi_tick_previous) >= WIFI_CHECK_CONNECTION_MS)
{
if (WiFi.status() == WL_CONNECTED)
{
LED_ON(LED_STATUS_WIFI);
LED_OFF(PIN_LED_BLUE);
}
else
{
LED_OFF(LED_STATUS_WIFI);
LED_ON(PIN_LED_BLUE);
Serial.println("Reconnecting to WiFi...");
WiFi.reconnect();
}
wifi_tick_previous = currentMillis;
}
}
void i2c_scan(void)
{
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ )
{
// The i2c_scanner uses the return value of
// the Write.endTransmisstion to see if
// a device did acknowledge to the address.
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address<16)
{
Serial.print("0");
}
Serial.print(address,HEX);
Serial.println(" !");
i2c_device_list[nDevices++] = address;
}
else if (error==4)
{
Serial.print("Unknown error at address 0x");
if (address<16)
{
Serial.print("0");
}
Serial.println(address,HEX);
}
}
if (nDevices == 0)
{
Serial.println("No I2C devices found\n");
}
else
{
Serial.println("done\n");
}
}
bool update_pid_coefficients(uint32_t p, uint32_t i, uint32_t d)
{
if(SKDryBox.SetPID(p, i, d))
{
eepromData.pid_kp = p;
eepromData.pid_ki = i;
eepromData.pid_kd = d;
eeprom_save();
return true;
}
return false;
}
+452
View File
@@ -0,0 +1,452 @@
// Helper function that allows us to replace template variable in .html file
// with a value from our code.
const String template_const_processor(const String& var)
{
if (var == "BUILD_DATE")
{
return String(__DATE__);
}
else if (var == "BUILD_TIME")
{
return String(__TIME__);
}
else if (var == "DEVICE_NAME")
{
return String(MDNS_NAME);
}
else if (var == "STATUS")
{
return String(box_status);
}
else if (var == "TEMP_IN")
{
return String((int)SKDryBox.GetInTemperature());
}
else if (var == "TEMP_OUT")
{
return String((int)temperature_out);
}
else if (var == "HUMIDITY_IN")
{
return String((int)humidity_in);
}
else if (var == "HUMIDITY_OUT")
{
return String((int)humidity_out);
}
else if (var == "HEATER_TEMPERATURE")
{
return String((int)SKDryBox.GetHeaterTemperature());
}
else if (var == "TARGET_HEATER_TEMPERATURE")
{
return String((int)SKDryBox.GetHeaterMaxTemperature());
}
else if (var == "TARGET_INSIDE_TEMPERATURE")
{
return String((int)SKDryBox.GetTargetTemperatureIn());
}
else if (var == "FAN_POWER")
{
return String(fan_duty);
}
else if (var == "HEATER_POWER")
{
return String(SKDryBox.GetHeaterPower());
}
// PID values
else if (var == "PID_KP")
{
return String(SKDryBox.GetPID_KP());
}
else if (var == "PID_KI")
{
return String(SKDryBox.GetPID_KI());
}
else if (var == "PID_KD")
{
return String(SKDryBox.GetPID_KD());
}
else if (var == "PWM_HEATER_INVERT_VALUES")
{
return String(PWM_HEATER_INVERT_VALUES);
}
else if (var == "PWM_FREQ_HEATER")
{
return String(PWM_FREQ_HEATER);
}
else if (var == "PWM_FREQ_FAN")
{
return String(PWM_FREQ_FAN);
}
else if (var == "PWM_MAX_VALUE")
{
return String(PWM_MAX_VALUE);
}
else if (var == "ADC_HEATER_SAMPLES")
{
return String(ADC_HEATER_SAMPLES);
}
else if (var == "PWM_CH_HEATER")
{
return String(PWM_CH_HEATER);
}
else if (var == "PWM_CH_FAN")
{
return String(PWM_CH_FAN);
}
else if (var == "LIMIT_TEMP_IN_MAX")
{
return String(LIMIT_TEMP_IN_MAX);
}
else if (var == "LIMIT_TEMP_HEATER_MAX")
{
return String(LIMIT_TEMP_HEATER_MAX);
}
else if (var == "ADC_MAX_VAUE")
{
return String(ADC_MAX_VAUE);
}
else if (var == "WIFI_CHECK_CONNECTION_MS")
{
return String(WIFI_CHECK_CONNECTION_MS);
}
else if (var == "ADC_CHIP")
{
return adc_get_chip();
}
else if (var == "NTC_RESISTANCE")
{
return String(NTC_RESISTANCE);
}
else if (var == "NTC_BETA")
{
return String(NTC_BETA);
}
else if (var == "NTC_R1_VALUE")
{
return String(NTC_R1_VALUE);
}
else if (var == "ADC_MAX_SAMPLES")
{
return String(ADC_MAX_SAMPLES);
}
else if (var == "PID_MIN_VALUE")
{
return String(PID_MIN_VALUE);
}
else if (var == "PID_MAX_VALUE")
{
return String(PID_MAX_VALUE);
}
else if (var == "DEF_DEBUG_PID")
{
return String(DEF_DEBUG_PID);
}
else if (var == "BUILD_TIME")
{
return String(__TIME__);
}
else if (var == "BUILD_DATE")
{
return String(__DATE__);
}
else if (var == "VERSION_MAJOR")
{
return String(VERSION_MAJOR);
}
else if (var == "VERSION_MINOR")
{
return String(VERSION_MINOR);
}
else if (var == "VERSION_PATCH")
{
return String(VERSION_PATCH);
}
else if (var == "COMPENSATE_TEMP_IN")
{
return String(eepromData.in_temp_comp);
}
else if (var == "COMPENSATE_TEMP_OUT")
{
return String(eepromData.out_temp_comp);
}
else if(var == "I2C_DEVICE_LIST")
{
char buff[50] = {0};
int len;
len = snprintf(buff, 400,
"0x%02X 0x%02X 0x%02X 0x%02X 0x%02X",
i2c_device_list[0],
i2c_device_list[1],
i2c_device_list[2],
i2c_device_list[3],
i2c_device_list[4]);
return String(buff);
}
else if(var == "FW_INT_TEMP_LIB_STYPE")
{
uint8_t sht_type = sensor_in.getSensorType();
switch(sht_type)
{
case 0:
return String("AUTO_DETECT");
break;
case 1:
return String("SHT3X");
break;
case 2:
return String("SHT85");
break;
case 3:
return String("SHT3X_ALT");
break;
case 4:
return String("SHTC1");
break;
case 5:
return String("SHTC3");
break;
case 6:
return String("SHTW1");
break;
case 7:
return String("SHTW2");
break;
case 8:
return String("SHT4X");
break;
case 9:
return String("SHT2X");
break;
}
}
else if(var == "BUILD_HASH")
{
return String(GITH_SHORT_HASH);
}
Serial.print("Unknown template variable: ");
Serial.println(var);
return String("__MISSING_VAR__");
}
void setupWebServer(void)
{
server.onNotFound([](AsyncWebServerRequest *request) {
Serial.print("404: ");
Serial.println(request->url());
request->send(404);
});
server_init_handlers();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->redirect("/index.html");
});
// Get dry box status
server.on("/api/v0/status", HTTP_GET, [](AsyncWebServerRequest *request) {
char buff[400] = {0};
int len;
len =
snprintf(buff, 400,
"{\"status\":%d,\"target_temp_in\":%4.2f,"
"\"max_temp_heater\":%4.2f,"
"\"temp_in\":%4.2f,\"temp_heater\":%4.2f,\"humid_in\":%4.2f,"
"\"fan_speed\":%d,\"temp_out\":%4.2f,\"humid_out\":%d,\"heater_power\":%d}",
SKDryBox.isDryBoxOn(),
SKDryBox.GetTargetTemperatureIn(), SKDryBox.GetHeaterMaxTemperature(),
SKDryBox.GetInTemperature(), SKDryBox.GetHeaterTemperature(), humidity_in, fan_duty,
temperature_out, humidity_out, SKDryBox.GetHeaterPower());
if (len)
{
request->send(200, "text/plain", buff);
}
else
{
request->send(500, "text/plain",
"{\"status\": \"Internal server error\"}");
}
});
// Get dry box status
server.on("/api/v0/wifi_info", HTTP_GET, [](AsyncWebServerRequest *request) {
char buff[400] = {0};
int len;
len = snprintf(buff, 400, "{\"status\":\"ok\", \"SSID\":\"%s\", \"RSSI\":\"%4d\", \"CH\":\"%2d\" }", WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.channel());
});
// Turn OFF dry box
server.on("/api/v0/turn_off", HTTP_GET, [](AsyncWebServerRequest *request) {
box_status = 0;
SKDryBox.TurnOff();
set_fan_duty(0);
request->send(200, "text/plain", "{\"status\": \"OK\"}");
});
// Turn ON dry box and set target temperature, max heater temperature and fan speed
server.on("/api/v0/turn_on", HTTP_GET, [](AsyncWebServerRequest *request) {
// Check if temperature, heater temperature and fan speed arguments are present
if (request->hasParam("temperature") && request->hasParam("heater") &&
request->hasParam("fanspeed"))
{
String str_temperature;
String str_heater;
String str_fanspeed;
int32_t temperature = 0;
int32_t heater = 0;
int32_t fanspeed = 0;
str_temperature = request->getParam("temperature")->value();
temperature = str_temperature.toInt();
str_heater = request->getParam("heater")->value();
heater = str_heater.toInt();
str_fanspeed = request->getParam("fanspeed")->value();
fanspeed = str_fanspeed.toInt();
#if DEF_DEUG_WEB_API
Serial.println((String) "Target temp: " + temperature +
"C Heater: " + heater + "C Fan Speed: " + fanspeed);
#endif
// Check drybox temperature and heater temperature limit
if ( (temperature <= LIMIT_TEMP_IN_MAX) && (heater <= LIMIT_TEMP_HEATER_MAX) )
{
SKDryBox.SetTargetInTemperature(temperature);
SKDryBox.SetMaxHeaterTemperature(heater);
SKDryBox.TurnOn();
set_fan_duty(fanspeed);
box_status = 1;
request->send(200, "text/plain", "{\"status\": \"OK\"}");
return;
}
// If either of them are out of range, return bad request status
else
{
request->send(400, "text/plain", "{\"status\": \"Bad request. Limit error!\"}");
return;
}
}
// Not all arguments are present in the request
else
{
request->send(400, "text/plain", "{\"status\": \"Bad request\"}");
return;
}
});
// Set PID parameters
server.on("/api/v0/pid/set", HTTP_GET, [](AsyncWebServerRequest *request) {
// Check if temperature, heater temperature and fan speed arguments are present
if (request->hasParam("p") && request->hasParam("i") &&
request->hasParam("d"))
{
String str_kp;
String str_ki;
String str_kd;
int32_t kp = 0;
int32_t ki = 0;
int32_t kd = 0;
str_kp = request->getParam("p")->value();
kp = str_kp.toInt();
str_ki = request->getParam("i")->value();
ki = str_ki.toInt();
str_kd = request->getParam("d")->value();
kd = str_kd.toInt();
// Check drybox temperature and heater temperature limit
if ( int_in_range(kp, PID_MIN_VALUE, PID_MAX_VALUE) &&
int_in_range(ki, PID_MIN_VALUE, PID_MAX_VALUE) &&
int_in_range(kd, PID_MIN_VALUE, PID_MAX_VALUE)
)
{
SKDryBox.SetPID(kp, ki, kd);
eepromData.pid_kp = kp;
eepromData.pid_ki = ki;
eepromData.pid_kd = kd;
eeprom_save();
request->send(200, "text/plain", "{\"status\": \"OK\"}");
return;
}
// If either of them are out of range, return bad request status
else
{
request->send(400, "text/plain", "{\"status\": \"Bad request. Limit error!\"}");
return;
}
}
// Not all arguments are present in the request
else
{
request->send(400, "text/plain", "{\"status\": \"Bad request\"}");
return;
}
});
// Set Temperature compensation values
server.on("/api/v0/compensation/temperature/set", HTTP_GET, [](AsyncWebServerRequest *request) {
// Check if temperature, heater temperature and fan speed arguments are present
if (request->hasParam("temperatureIn") && request->hasParam("temperatureOut"))
{
String str_temp_in;
String str_temp_out;
float comp_in = 0;
float comp_out = 0;
str_temp_in = request->getParam("temperatureIn")->value();
comp_in = str_temp_in.toFloat();
str_temp_out = request->getParam("temperatureOut")->value();
comp_out = str_temp_out.toFloat();
// Check drybox temperature and heater temperature limit
eepromData.in_temp_comp = comp_in;
eepromData.out_temp_comp = comp_out;
eeprom_save();
Serial.println(str_temp_in);
Serial.println(str_temp_out);
request->send(200, "text/plain", "{\"status\": \"OK\"}");
return;
}
// Not all arguments are present in the request
else
{
request->send(400, "text/plain", "{\"status\": \"Bad request\"}");
return;
}
});
}
bool int_in_range (int value, int min, int max)
{
return ( (min<=value) && (value<max) );
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,8 @@
Board Thickness: 1.6mm
Material: FR4 (or similar)
Copper: 1oz
Layers: 2
Soldermask: Black
Silkscreen: White
Surface finish: HASL (Lead Free)
Tented Vias: Yes
@@ -0,0 +1,130 @@
M48
;Layer_Color=9474304
;FILE_FORMAT=4:4
METRIC,LZ
;TYPE=PLATED
T01F00S00C0.4000
T03F00S00C0.9000
T04F00S00C1.1000
T05F00S00C1.3000
T06F00S00C1.4000
T07F00S00C1.6000
T08F00S00C1.7000
T09F00S00C3.1000
;TYPE=NON_PLATED
T10F00S00C0.6500
T11F00S00C3.1000
%
T01
X00184Y00049
X0021Y00026
X00219Y00085
X00164Y00145
X-0015Y00066
X-0016
Y00055
X-0015
Y00044
X-0016
X-00159Y00226
X-001447Y00245
X-0013175Y002129
X-0008367Y0019667
X-00035Y00168
X-00008Y00203
X00002
X00012
X00022
X00032
Y00214
X00022
X00012
X00002
X-00008
Y00225
X00002
X00012
X00022
X00032
X-00003Y00269
X-00017Y00274
X-00033Y00269
X-00047Y00265
X-0005Y00305
X-00061193Y00295039
X-00072Y00317
X-00094Y0036525
Y00394
X-00076Y00385
X-00057Y00403
X-00047Y00414
X-00037Y00418
X-00017Y00412
X-00009Y00425
X-0001Y00442
X-00012Y00459
X-0001Y00477
X-000246Y004699
X-00052Y00465
X000159Y004699
Y00457
Y004426
X0001Y00421
Y00404
X-00006Y003885
Y003775
Y003665
X-00017
X-00028
Y003775
Y003885
X-00017
X0006779Y00379454
X000509Y0042
X0000869Y00315536
X-00108Y00279
X-00136Y00289
X-00169Y00267
X-00149Y00392
X-00137Y00416
X-00128Y004595
X-00098Y00433
X00151Y00203
T03
X-0021Y002046
Y0023
Y002554
T04
X-00205Y00314401
Y00339801
Y00365201
Y00390601
T05
X-001834Y00339801
T06
X00203Y001083
Y001337
Y001822
Y002076
Y00233
Y002584
T07
X-0009335Y00099
Y00133
X0004135
Y00099
T08
X-000904Y00055
X-000396
X000402
X00091
T09
X00215Y-0001703
X-00215
T10
X001782Y003311
Y003889
T11
X-00215Y0054797
X00215
M30
@@ -0,0 +1,29 @@
M48
;Layer_Color=9474304
;FILE_FORMAT=4:4
METRIC,LZ
;TYPE=PLATED
T02F00S00C0.6000
;TYPE=NON_PLATED
%
G90
G05
T02
G00X001787Y003168
M15
G01X001677
M16
G00X00219Y003168
M15
G01X00211
M16
G00X00219Y004032
M15
G01X00211
M16
G00X001787Y004032
M15
G01X001677
M16
M17
M30
@@ -0,0 +1,54 @@
G04:AMPARAMS|DCode=20|XSize=0.75mm|YSize=0.35mm|CornerRadius=0.0438mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=90.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD20*
21,1,0.7500,0.2625,0,0,90.0*
21,1,0.6625,0.3500,0,0,90.0*
1,1,0.0875,0.1313,0.3313*
1,1,0.0875,0.1313,-0.3313*
1,1,0.0875,-0.1313,-0.3313*
1,1,0.0875,-0.1313,0.3313*
%
G04:AMPARAMS|DCode=24|XSize=0.25mm|YSize=0.75mm|CornerRadius=0.0313mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD24*
21,1,0.2500,0.6875,0,0,0.0*
21,1,0.1875,0.7500,0,0,0.0*
1,1,0.0625,0.0938,-0.3438*
1,1,0.0625,-0.0938,-0.3438*
1,1,0.0625,-0.0938,0.3438*
1,1,0.0625,0.0938,0.3438*
%
G04:AMPARAMS|DCode=25|XSize=0.25mm|YSize=0.75mm|CornerRadius=0.0313mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD25*
21,1,0.2500,0.6875,0,0,270.0*
21,1,0.1875,0.7500,0,0,270.0*
1,1,0.0625,-0.3438,-0.0938*
1,1,0.0625,-0.3438,0.0938*
1,1,0.0625,0.3438,0.0938*
1,1,0.0625,0.3438,-0.0938*
%
G04:AMPARAMS|DCode=47|XSize=0.9532mm|YSize=0.5532mm|CornerRadius=0.1454mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=90.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD47*
21,1,0.9532,0.2625,0,0,90.0*
21,1,0.6625,0.5532,0,0,90.0*
1,1,0.2907,0.1313,0.3313*
1,1,0.2907,0.1313,-0.3313*
1,1,0.2907,-0.1313,-0.3313*
1,1,0.2907,-0.1313,0.3313*
%
G04:AMPARAMS|DCode=51|XSize=0.41mm|YSize=0.91mm|CornerRadius=0.1113mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD51*
21,1,0.4100,0.6875,0,0,0.0*
21,1,0.1875,0.9100,0,0,0.0*
1,1,0.2225,0.0938,-0.3438*
1,1,0.2225,-0.0938,-0.3438*
1,1,0.2225,-0.0938,0.3438*
1,1,0.2225,0.0938,0.3438*
%
G04:AMPARAMS|DCode=52|XSize=0.41mm|YSize=0.91mm|CornerRadius=0.1113mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD52*
21,1,0.4100,0.6875,0,0,270.0*
21,1,0.1875,0.9100,0,0,270.0*
1,1,0.2225,-0.3438,-0.0938*
1,1,0.2225,-0.3438,0.0938*
1,1,0.2225,0.3438,0.0938*
1,1,0.2225,0.3438,-0.0938*
%
@@ -0,0 +1,25 @@
----------------------------------------------------------------------------------------------------------------------------------
NCDrill File Report For: DryBox.PcbDoc 2024-10-09 22:39:16
----------------------------------------------------------------------------------------------------------------------------------
Layer Pair : Top Layer to Bottom Layer
ASCII RoundHoles File : DryBox-RoundHoles.TXT
ASCII SlotHoles File : DryBox-SlotHoles.TXT
Tool Hole Size Hole Tolerance Hole Type Hole Count Plated Tool Travel
----------------------------------------------------------------------------------------------------------------------------------
T1 0.4mm (16mil) Round 74 PTH 279.80mm (11.02inch)
T2 0.6mm (24mil) Slot 4 PTH 38.14mm (1.50inch)
T3 0.9mm (35mil) Round 3 PTH 5.08mm (0.20inch)
T4 1.1mm (43mil) Round 4 PTH 7.62mm (0.30inch)
T5 1.3mm (51mil) Round 1 PTH 0.00mm (0.00inch)
T6 1.4mm (55mil) Round 6 PTH 15.01mm (0.59inch)
T7 1.6mm (63mil) Round 4 PTH 20.27mm (0.80inch)
T8 1.7mm (67mil) Round 4 PTH 18.14mm (0.71inch)
T9 3.1mm (122mil) Round 2 PTH 43.00mm (1.69inch)
T10 0.65mm (26mil) Round 2 NPTH 5.78mm (0.23inch)
T11 3.1mm (122mil) Round 2 NPTH 43.00mm (1.69inch)
----------------------------------------------------------------------------------------------------------------------------------
Totals 106
Total Processing Time (hh:mm:ss) : 00:00:00
@@ -0,0 +1,21 @@
------------------------------------------------------------------------------------------
Gerber File Extension Report For: DryBox.GBR 2024-10-09 22:39:14
------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------
Layer Extension Layer Description
------------------------------------------------------------------------------------------
.GTO Top Overlay
.GTP Top Paste
.GTS Top Solder
.GTL Top Layer
.GBL Bottom Layer
.GBS Bottom Solder
.GBP Bottom Paste
.GBO Bottom Overlay
.GM8 Route Tool Path
.GKO Keep-Out Layer
.GD1 Drill Drawing
.GG1 Drill Guide
------------------------------------------------------------------------------------------
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,24.8.2 (39)*
G04*
G04 Layer_Color=128*
%FSLAX44Y44*%
%MOMM*%
G71*
G04*
G04 #@! TF.SameCoordinates,74074FDB-1FAA-448C-9F72-8EE957A70AFD*
G04*
G04*
G04 #@! TF.FilePolarity,Positive*
G04*
G01*
G75*
M02*
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,442 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,24.8.2 (39)*
G04*
G04 Layer_Color=128*
%FSLAX44Y44*%
%MOMM*%
G71*
G04*
G04 #@! TF.SameCoordinates,74074FDB-1FAA-448C-9F72-8EE957A70AFD*
G04*
G04*
G04 #@! TF.FilePolarity,Positive*
G04*
G01*
G75*
%ADD104C,0.0508*%
D104*
X33350Y99000D02*
X49350D01*
X41350Y91000D02*
Y107000D01*
X33350Y133000D02*
X49350D01*
X41350Y125000D02*
Y141000D01*
X-101350Y99000D02*
X-85350D01*
X-93350Y91000D02*
Y107000D01*
X-101350Y133000D02*
X-85350D01*
X-93350Y125000D02*
Y141000D01*
X196000Y233000D02*
X210000D01*
X203000Y226000D02*
Y240000D01*
X196000Y207600D02*
X210000D01*
X203000Y200600D02*
Y214600D01*
X196000Y182200D02*
X210000D01*
X203000Y175200D02*
Y189200D01*
X196000Y258400D02*
X210000D01*
X203000Y251400D02*
Y265400D01*
X196000Y133700D02*
X210000D01*
X203000Y126700D02*
Y140700D01*
X196000Y108300D02*
X210000D01*
X203000Y101300D02*
Y115300D01*
X-214500Y204600D02*
X-205500D01*
X-210000Y200100D02*
Y209100D01*
X-214500Y230000D02*
X-205500D01*
X-210000Y225500D02*
Y234500D01*
X-214500Y255400D02*
X-205500D01*
X-210000Y250900D02*
Y259900D01*
X-48100Y55000D02*
X-31100D01*
X-39600Y46500D02*
Y63500D01*
X-98900Y55000D02*
X-81900D01*
X-90400Y46500D02*
Y63500D01*
X82500Y55000D02*
X99500D01*
X91000Y46500D02*
Y63500D01*
X31700Y55000D02*
X48700D01*
X40200Y46500D02*
Y63500D01*
X174950Y331100D02*
X181450D01*
X178200Y327850D02*
Y334350D01*
X174950Y388900D02*
X181450D01*
X178200Y385650D02*
Y392150D01*
X170200Y403200D02*
X176200D01*
X173200Y400200D02*
Y406200D01*
X170200Y316800D02*
X176200D01*
X173200Y313800D02*
Y319800D01*
X212000Y403200D02*
X218000D01*
X215000Y400200D02*
Y406200D01*
X212000Y316800D02*
X218000D01*
X215000Y313800D02*
Y319800D01*
X199500Y-17030D02*
X230500D01*
X215000Y-32530D02*
Y-1530D01*
X-230500Y-17030D02*
X-199500D01*
X-215000Y-32530D02*
Y-1530D01*
X-230500Y547970D02*
X-199500D01*
X-215000Y532470D02*
Y563470D01*
X199500Y547970D02*
X230500D01*
X215000Y532470D02*
Y563470D01*
X-189900Y339801D02*
X-176900D01*
X-183400Y333301D02*
Y346301D01*
X-210500Y314401D02*
X-199500D01*
X-205000Y308901D02*
Y319901D01*
X-210500Y339801D02*
X-199500D01*
X-205000Y334301D02*
Y345301D01*
X-210500Y365201D02*
X-199500D01*
X-205000Y359701D02*
Y370701D01*
X-210500Y390601D02*
X-199500D01*
X-205000Y385101D02*
Y396101D01*
X65790Y379454D02*
X69790D01*
X67790Y377454D02*
Y381454D01*
X-138000Y289000D02*
X-134000D01*
X-136000Y287000D02*
Y291000D01*
X-110000Y279000D02*
X-106000D01*
X-108000Y277000D02*
Y281000D01*
X-12000Y477000D02*
X-8000D01*
X-10000Y475000D02*
Y479000D01*
X-139000Y416000D02*
X-135000D01*
X-137000Y414000D02*
Y418000D01*
X6690Y315536D02*
X10690D01*
X8690Y313536D02*
Y317536D01*
X-8000Y388500D02*
X-4000D01*
X-6000Y386500D02*
Y390500D01*
X-8000Y377500D02*
X-4000D01*
X-6000Y375500D02*
Y379500D01*
X-8000Y366500D02*
X-4000D01*
X-6000Y364500D02*
Y368500D01*
X-19000Y388500D02*
X-15000D01*
X-17000Y386500D02*
Y390500D01*
X-19000Y366500D02*
X-15000D01*
X-17000Y364500D02*
Y368500D01*
X-30000Y388500D02*
X-26000D01*
X-28000Y386500D02*
Y390500D01*
X-30000Y377500D02*
X-26000D01*
X-28000Y375500D02*
Y379500D01*
X-30000Y366500D02*
X-26000D01*
X-28000Y364500D02*
Y368500D01*
X-49000Y414000D02*
X-45000D01*
X-47000Y412000D02*
Y416000D01*
X-54000Y465000D02*
X-50000D01*
X-52000Y463000D02*
Y467000D01*
X-74000Y317000D02*
X-70000D01*
X-72000Y315000D02*
Y319000D01*
X-26600Y469900D02*
X-22600D01*
X-24600Y467900D02*
Y471900D01*
X-59000Y403000D02*
X-55000D01*
X-57000Y401000D02*
Y405000D01*
X-5000Y269000D02*
X-1000D01*
X-3000Y267000D02*
Y271000D01*
X-19000Y274000D02*
X-15000D01*
X-17000Y272000D02*
Y276000D01*
X-35000Y269000D02*
X-31000D01*
X-33000Y267000D02*
Y271000D01*
X-52000Y305000D02*
X-48000D01*
X-50000Y303000D02*
Y307000D01*
X-49000Y265000D02*
X-45000D01*
X-47000Y263000D02*
Y267000D01*
X-63193Y295039D02*
X-59193D01*
X-61193Y293039D02*
Y297039D01*
X-130000Y459500D02*
X-126000D01*
X-128000Y457500D02*
Y461500D01*
X-151000Y392000D02*
X-147000D01*
X-149000Y390000D02*
Y394000D01*
X-146700Y245000D02*
X-142700D01*
X-144700Y243000D02*
Y247000D01*
X-161000Y226000D02*
X-157000D01*
X-159000Y224000D02*
Y228000D01*
X-171000Y267000D02*
X-167000D01*
X-169000Y265000D02*
Y269000D01*
X-96000Y394000D02*
X-92000D01*
X-94000Y392000D02*
Y396000D01*
X-100000Y433000D02*
X-96000D01*
X-98000Y431000D02*
Y435000D01*
X162000Y145000D02*
X166000D01*
X164000Y143000D02*
Y147000D01*
X-39000Y418000D02*
X-35000D01*
X-37000Y416000D02*
Y420000D01*
X-19000Y412000D02*
X-15000D01*
X-17000Y410000D02*
Y414000D01*
X-96000Y365250D02*
X-92000D01*
X-94000Y363250D02*
Y367250D01*
X149000Y203000D02*
X153000D01*
X151000Y201000D02*
Y205000D01*
X208000Y26000D02*
X212000D01*
X210000Y24000D02*
Y28000D01*
X182000Y49000D02*
X186000D01*
X184000Y47000D02*
Y51000D01*
X217000Y85000D02*
X221000D01*
X219000Y83000D02*
Y87000D01*
X-37000Y168000D02*
X-33000D01*
X-35000Y166000D02*
Y170000D01*
X-162000Y66000D02*
X-158000D01*
X-160000Y64000D02*
Y68000D01*
X-162000Y44000D02*
X-158000D01*
X-160000Y42000D02*
Y46000D01*
X-162000Y55000D02*
X-158000D01*
X-160000Y53000D02*
Y57000D01*
X-152000Y55000D02*
X-148000D01*
X-150000Y53000D02*
Y57000D01*
X-152000Y44000D02*
X-148000D01*
X-150000Y42000D02*
Y46000D01*
X-152000Y66000D02*
X-148000D01*
X-150000Y64000D02*
Y68000D01*
X0Y225000D02*
X4000D01*
X2000Y223000D02*
Y227000D01*
X0Y203000D02*
X4000D01*
X2000Y201000D02*
Y205000D01*
X0Y214000D02*
X4000D01*
X2000Y212000D02*
Y216000D01*
X-10000Y214000D02*
X-6000D01*
X-8000Y212000D02*
Y216000D01*
X-10000Y203000D02*
X-6000D01*
X-8000Y201000D02*
Y205000D01*
X-10000Y225000D02*
X-6000D01*
X-8000Y223000D02*
Y227000D01*
X10000Y225000D02*
X14000D01*
X12000Y223000D02*
Y227000D01*
X10000Y203000D02*
X14000D01*
X12000Y201000D02*
Y205000D01*
X10000Y214000D02*
X14000D01*
X12000Y212000D02*
Y216000D01*
X20000Y214000D02*
X24000D01*
X22000Y212000D02*
Y216000D01*
X20000Y203000D02*
X24000D01*
X22000Y201000D02*
Y205000D01*
X20000Y225000D02*
X24000D01*
X22000Y223000D02*
Y227000D01*
X13900Y457000D02*
X17900D01*
X15900Y455000D02*
Y459000D01*
X8000Y421000D02*
X12000D01*
X10000Y419000D02*
Y423000D01*
X8000Y404000D02*
X12000D01*
X10000Y402000D02*
Y406000D01*
X-12000Y442000D02*
X-8000D01*
X-10000Y440000D02*
Y444000D01*
X-14000Y459000D02*
X-10000D01*
X-12000Y457000D02*
Y461000D01*
X-11000Y425000D02*
X-7000D01*
X-9000Y423000D02*
Y427000D01*
X30000Y225000D02*
X34000D01*
X32000Y223000D02*
Y227000D01*
X30000Y203000D02*
X34000D01*
X32000Y201000D02*
Y205000D01*
X30000Y214000D02*
X34000D01*
X32000Y212000D02*
Y216000D01*
X-85670Y196670D02*
X-81670D01*
X-83670Y194670D02*
Y198670D01*
X-133750Y212900D02*
X-129750D01*
X-131750Y210900D02*
Y214900D01*
X48900Y420000D02*
X52900D01*
X50900Y418000D02*
Y422000D01*
X-78000Y385000D02*
X-74000D01*
X-76000Y383000D02*
Y387000D01*
X13900Y469900D02*
X17900D01*
X15900Y467900D02*
Y471900D01*
X13900Y442600D02*
X17900D01*
X15900Y440600D02*
Y444600D01*
M02*
@@ -0,0 +1,86 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,24.8.2 (39)*
G04*
G04 Layer_Color=16711935*
%FSLAX44Y44*%
%MOMM*%
G71*
G04*
G04 #@! TF.SameCoordinates,74074FDB-1FAA-448C-9F72-8EE957A70AFD*
G04*
G04*
G04 #@! TF.FilePolarity,Positive*
G04*
G01*
G75*
%ADD15C,0.1000*%
D15*
X-222000Y582000D02*
G03*
X-242000Y562000I0J-20000D01*
G01*
Y490215D02*
G03*
X-231785Y480000I10215J0D01*
G01*
X-159285Y419895D02*
G03*
X-149071Y430109I0J10215D01*
G01*
X-149285Y469785D02*
G03*
X-159500Y480000I-10215J0D01*
G01*
X-231785D02*
G03*
X-242000Y469785I0J-10215D01*
G01*
X-231785Y420105D02*
G03*
X-242000Y409891I0J-10215D01*
G01*
Y430320D02*
G03*
X-231785Y420105I10215J0D01*
G01*
X-242000Y-25000D02*
G03*
X-222000Y-45000I20000J0D01*
G01*
X222000Y-45000D02*
G03*
X242000Y-25000I0J20000D01*
G01*
Y562000D02*
G03*
X222000Y582000I-20000J0D01*
G01*
X-231785Y480000D02*
X-159500D01*
X-242000Y435210D02*
Y464895D01*
Y410000D02*
X-242000Y-25000D01*
X-242000Y490215D02*
Y562000D01*
X-149071Y430109D02*
Y440000D01*
X-149285Y460000D02*
Y469785D01*
X-231571Y419895D02*
X-159285D01*
X-242000Y464895D02*
Y469785D01*
Y430320D02*
Y435210D01*
X-222000Y-45000D02*
X222000Y-45000D01*
X-222000Y-45000D02*
X222000Y-45000D01*
X-197000D02*
X197000Y-45000D01*
X-222000Y582000D02*
X222000D01*
X242000Y562000D02*
X242000Y-25000D01*
M02*
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,515 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,24.8.2 (39)*
G04*
G04 Layer_Color=8421504*
%FSLAX44Y44*%
%MOMM*%
G71*
G04*
G04 #@! TF.SameCoordinates,74074FDB-1FAA-448C-9F72-8EE957A70AFD*
G04*
G04*
G04 #@! TF.FilePolarity,Positive*
G04*
G01*
G75*
G04:AMPARAMS|DCode=20|XSize=0.75mm|YSize=0.35mm|CornerRadius=0.0438mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=90.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD20*
21,1,0.7500,0.2625,0,0,90.0*
21,1,0.6625,0.3500,0,0,90.0*
1,1,0.0875,0.1313,0.3313*
1,1,0.0875,0.1313,-0.3313*
1,1,0.0875,-0.1313,-0.3313*
1,1,0.0875,-0.1313,0.3313*
%
%ADD20ROUNDEDRECTD20*%
%ADD21R,1.6000X0.7000*%
%ADD22R,1.1000X0.6000*%
%ADD23R,0.6000X1.1000*%
G04:AMPARAMS|DCode=24|XSize=0.25mm|YSize=0.75mm|CornerRadius=0.0313mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD24*
21,1,0.2500,0.6875,0,0,0.0*
21,1,0.1875,0.7500,0,0,0.0*
1,1,0.0625,0.0938,-0.3438*
1,1,0.0625,-0.0938,-0.3438*
1,1,0.0625,-0.0938,0.3438*
1,1,0.0625,0.0938,0.3438*
%
%ADD24ROUNDEDRECTD24*%
G04:AMPARAMS|DCode=25|XSize=0.25mm|YSize=0.75mm|CornerRadius=0.0313mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD25*
21,1,0.2500,0.6875,0,0,270.0*
21,1,0.1875,0.7500,0,0,270.0*
1,1,0.0625,-0.3438,-0.0938*
1,1,0.0625,-0.3438,0.0938*
1,1,0.0625,0.3438,0.0938*
1,1,0.0625,0.3438,-0.0938*
%
%ADD25ROUNDEDRECTD25*%
%ADD26R,3.5000X3.5000*%
%ADD27R,0.9000X0.7000*%
%ADD28R,0.7000X0.9000*%
%ADD29R,0.9000X0.9650*%
%ADD30R,0.8000X0.9000*%
%ADD31C,1.0000*%
%ADD32R,1.0500X0.6000*%
%ADD33R,0.8500X0.7500*%
%ADD34R,2.2000X4.2000*%
%ADD35R,1.1500X0.3000*%
%ADD36R,1.1500X0.6000*%
%ADD37R,1.3400X1.8000*%
%ADD38R,1.3000X0.6000*%
%ADD39R,4.1000X4.4100*%
%ADD40R,0.9650X0.9000*%
%ADD41R,0.8000X0.5500*%
%ADD42R,1.2300X2.0200*%
%ADD43R,1.0000X1.3000*%
%ADD44R,0.9000X1.6000*%
%ADD45R,1.8000X0.4000*%
%ADD46O,2.3000X1.3000*%
D20*
X-200000Y440300D02*
D03*
X-190000D02*
D03*
Y459700D02*
D03*
X-200000D02*
D03*
D21*
X-195000Y450000D02*
D03*
D22*
X167750Y39500D02*
D03*
Y58500D02*
D03*
X196250D02*
D03*
Y49000D02*
D03*
Y39500D02*
D03*
D23*
X-145500Y278750D02*
D03*
X-136000D02*
D03*
X-126500D02*
D03*
Y307250D02*
D03*
X-145500D02*
D03*
D24*
X500Y402000D02*
D03*
X-4500D02*
D03*
X-9500D02*
D03*
X-14500D02*
D03*
X-19500D02*
D03*
X-24500D02*
D03*
X-29500D02*
D03*
X-34500D02*
D03*
Y353000D02*
D03*
X-29500D02*
D03*
X-24500D02*
D03*
X-19500D02*
D03*
X-14500D02*
D03*
X-9500D02*
D03*
X-4500D02*
D03*
X500D02*
D03*
D25*
X-41500Y395000D02*
D03*
Y390000D02*
D03*
Y385000D02*
D03*
Y380000D02*
D03*
Y375000D02*
D03*
Y370000D02*
D03*
Y365000D02*
D03*
Y360000D02*
D03*
X7500D02*
D03*
Y365000D02*
D03*
Y370000D02*
D03*
Y375000D02*
D03*
Y380000D02*
D03*
Y385000D02*
D03*
Y390000D02*
D03*
Y395000D02*
D03*
D26*
X-17000Y377500D02*
D03*
D27*
X-39000Y331400D02*
D03*
Y315600D02*
D03*
X-9000Y331400D02*
D03*
Y315600D02*
D03*
X-24000Y331400D02*
D03*
Y315600D02*
D03*
X-84000Y279100D02*
D03*
Y294900D02*
D03*
X-99000Y279100D02*
D03*
Y294900D02*
D03*
X-113000Y456400D02*
D03*
Y440600D02*
D03*
X-38000D02*
D03*
Y456400D02*
D03*
X-22000Y440600D02*
D03*
Y456400D02*
D03*
X149000Y252100D02*
D03*
Y267900D02*
D03*
X214000Y41100D02*
D03*
Y56900D02*
D03*
X-222500Y457900D02*
D03*
Y442100D02*
D03*
X500Y465400D02*
D03*
Y449600D02*
D03*
X-68000Y456400D02*
D03*
Y440600D02*
D03*
X-83000Y456400D02*
D03*
Y440600D02*
D03*
X-52000Y456400D02*
D03*
Y440600D02*
D03*
X-98000D02*
D03*
Y456400D02*
D03*
X-139250Y212900D02*
D03*
Y197100D02*
D03*
X134000Y267900D02*
D03*
Y252100D02*
D03*
X68000Y385400D02*
D03*
Y369600D02*
D03*
X-124250Y212900D02*
D03*
Y197100D02*
D03*
X-53000Y279100D02*
D03*
Y294900D02*
D03*
X-69000Y279100D02*
D03*
Y294900D02*
D03*
D28*
X-95400Y232500D02*
D03*
X-79600D02*
D03*
X189900Y22000D02*
D03*
X174100D02*
D03*
X190900Y76000D02*
D03*
X175100D02*
D03*
X218900D02*
D03*
X203100D02*
D03*
X-82400Y385000D02*
D03*
X-66600D02*
D03*
X35100Y397500D02*
D03*
X50900D02*
D03*
X35100Y351000D02*
D03*
X50900D02*
D03*
X50900Y412500D02*
D03*
X35100D02*
D03*
X-82400Y365250D02*
D03*
X-66600D02*
D03*
X35100Y336000D02*
D03*
X50900D02*
D03*
X-82400Y350250D02*
D03*
X-66600D02*
D03*
X15900Y477500D02*
D03*
X100D02*
D03*
X50900Y427500D02*
D03*
X35100D02*
D03*
X15900Y435000D02*
D03*
X100D02*
D03*
X137100Y328000D02*
D03*
X152900D02*
D03*
X137100Y403000D02*
D03*
X152900D02*
D03*
D29*
X151000Y191000D02*
D03*
Y175000D02*
D03*
X-47000Y237000D02*
D03*
Y221000D02*
D03*
X-32000Y237000D02*
D03*
Y221000D02*
D03*
X151000Y161000D02*
D03*
Y145000D02*
D03*
D30*
X-31000Y203000D02*
D03*
X-49000D02*
D03*
X-40000Y183000D02*
D03*
X-186500Y257500D02*
D03*
X-168500D02*
D03*
X-177500Y277500D02*
D03*
D31*
X-224000Y154000D02*
D03*
X214000Y464000D02*
D03*
D32*
X-101939Y215670D02*
D03*
Y206170D02*
D03*
Y196670D02*
D03*
X-73439D02*
D03*
Y206170D02*
D03*
Y215670D02*
D03*
D33*
X36250Y382750D02*
D03*
Y372250D02*
D03*
X49750Y382750D02*
D03*
Y372250D02*
D03*
D34*
X-182000Y55500D02*
D03*
Y128500D02*
D03*
D35*
X167450Y342500D02*
D03*
Y347500D02*
D03*
Y352500D02*
D03*
Y377500D02*
D03*
Y372500D02*
D03*
Y367500D02*
D03*
Y357500D02*
D03*
Y362500D02*
D03*
D36*
Y384000D02*
D03*
Y336000D02*
D03*
Y392000D02*
D03*
Y328000D02*
D03*
D37*
X45700Y166000D02*
D03*
X74300D02*
D03*
X79700Y281000D02*
D03*
X108300D02*
D03*
D38*
X47066Y188098D02*
D03*
Y200798D02*
D03*
Y213498D02*
D03*
Y226198D02*
D03*
D39*
X86944Y207148D02*
D03*
D40*
X-9000Y168000D02*
D03*
X-25000D02*
D03*
X5000Y183000D02*
D03*
X21000D02*
D03*
Y168000D02*
D03*
X5000D02*
D03*
X-116000Y329000D02*
D03*
X-132000D02*
D03*
X-117000Y374000D02*
D03*
X-133000D02*
D03*
X-117000Y389000D02*
D03*
X-133000D02*
D03*
X-205250Y287500D02*
D03*
X-221250D02*
D03*
X-185500Y225000D02*
D03*
X-169500D02*
D03*
X-185500Y240000D02*
D03*
X-169500D02*
D03*
D41*
X-132500Y345500D02*
D03*
Y354500D02*
D03*
X-116500D02*
D03*
Y345500D02*
D03*
D42*
X-101850Y255000D02*
D03*
X-73150D02*
D03*
D43*
X-125300Y235000D02*
D03*
X-144700D02*
D03*
X-125300Y255000D02*
D03*
X-144700D02*
D03*
D44*
X-59500Y322000D02*
D03*
X-100500D02*
D03*
D45*
X0Y535500D02*
D03*
Y504500D02*
D03*
D46*
X173200Y403200D02*
D03*
Y316800D02*
D03*
X215000Y403200D02*
D03*
Y316800D02*
D03*
M02*
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
Layer Pairs Export File for PCB: C:\Users\Sasa\Desktop\git\_shop\DryBox\Hardware\DryBox.PcbDoc
LayersSetName=Top_Bot_Thru_Holes|DrillFile=drybox-roundholes.txt|DrillLayers=gtl,gbl
LayersSetName=Top_Bot_Slot_Holes|DrillFile=drybox-slotholes.txt|DrillLayers=gtl,gbl
@@ -0,0 +1,8 @@
DRC Rules Export File for PCB: C:\Users\Sasa\Desktop\git\_shop\DryBox\Hardware\DryBox.PcbDoc
RuleKind=Clearance|RuleName=Clearance_3|Scope=Board|Minimum=7.87
RuleKind=Clearance|RuleName=Clearance_2|Scope=Board|Minimum=5.91
RuleKind=Clearance|RuleName=Clearance_1|Scope=Board|Minimum=11.81
RuleKind=SolderMaskExpansion|RuleName=SolderMaskExpansion|Scope=Board|Minimum=4.00
RuleKind=Width|RuleName=Width|Scope=Board|Minimum=5.91
RuleKind=Clearance|RuleName=Clearance|Scope=Board|Minimum=5.91
RuleKind=ShortCircuit|RuleName=ShortCircuit|Scope=Board|Allowed=0
@@ -0,0 +1,95 @@
D10 ROUNDED 7.874 7.874 0.000 LINE 0.000
D11 ROUNDED 9.842 9.842 0.000 LINE 0.000
D12 ROUNDED 11.811 11.811 0.000 LINE 0.000
D13 ROUNDED 5.000 5.000 0.000 LINE 0.000
D14 ROUNDED 8.000 8.000 0.000 LINE 0.000
D15 ROUNDED 3.937 3.937 0.000 LINE 0.000
D16 ROUNDED 5.906 5.906 0.000 LINE 0.000
D17 ROUNDED 5.020 5.020 0.000 LINE 0.000
D18 ROUNDED 10.000 10.000 0.000 LINE 0.000
D19 ROUNDED 7.000 7.000 0.000 LINE 0.000
D21 RECTANGULAR 27.559 62.992 0.000 FLASH 90.000
D22 RECTANGULAR 23.622 43.307 0.000 FLASH 270.000
D23 RECTANGULAR 23.622 43.307 0.000 FLASH 180.000
D26 RECTANGULAR 137.795 137.795 0.000 FLASH 270.000
D27 RECTANGULAR 27.559 35.433 0.000 FLASH 90.000
D28 RECTANGULAR 27.559 35.433 0.000 FLASH 180.000
D29 RECTANGULAR 37.992 35.433 0.000 FLASH 90.000
D30 RECTANGULAR 31.496 35.433 0.000 FLASH 0.000
D31 ROUNDED 39.370 39.370 0.000 FLASH 0.000
D32 RECTANGULAR 23.622 41.339 0.000 FLASH 90.000
D33 RECTANGULAR 33.465 29.528 0.000 FLASH 180.000
D34 RECTANGULAR 165.354 86.614 0.000 FLASH 270.000
D35 RECTANGULAR 11.811 45.276 0.000 FLASH 90.000
D36 RECTANGULAR 23.622 45.276 0.000 FLASH 90.000
D37 RECTANGULAR 52.756 70.866 0.000 FLASH 180.000
D38 RECTANGULAR 51.181 23.622 0.000 FLASH 0.000
D39 RECTANGULAR 161.417 173.622 0.000 FLASH 0.000
D40 RECTANGULAR 37.992 35.433 0.000 FLASH 180.000
D41 RECTANGULAR 21.654 31.496 0.000 FLASH 270.000
D42 RECTANGULAR 79.528 48.425 0.000 FLASH 270.000
D43 RECTANGULAR 39.370 51.181 0.000 FLASH 0.000
D44 RECTANGULAR 35.433 62.992 0.000 FLASH 180.000
D45 RECTANGULAR 70.866 15.748 0.000 FLASH 0.000
D46 ROUNDED 90.551 51.181 0.000 FLASH 180.000
D48 RECTANGULAR 35.559 70.992 0.000 FLASH 90.000
D49 RECTANGULAR 31.622 51.307 0.000 FLASH 270.000
D50 RECTANGULAR 31.622 51.307 0.000 FLASH 180.000
D53 RECTANGULAR 145.795 145.795 0.000 FLASH 270.000
D54 RECTANGULAR 35.559 43.433 0.000 FLASH 90.000
D55 RECTANGULAR 35.559 43.433 0.000 FLASH 180.000
D56 RECTANGULAR 45.992 43.433 0.000 FLASH 90.000
D57 RECTANGULAR 39.496 43.433 0.000 FLASH 0.000
D58 ROUNDED 74.803 74.803 0.000 FLASH 0.000
D59 RECTANGULAR 31.622 49.339 0.000 FLASH 90.000
D60 RECTANGULAR 41.465 37.528 0.000 FLASH 180.000
D61 RECTANGULAR 173.354 94.614 0.000 FLASH 270.000
D62 RECTANGULAR 19.811 53.276 0.000 FLASH 90.000
D63 RECTANGULAR 31.622 53.276 0.000 FLASH 90.000
D64 RECTANGULAR 60.756 78.866 0.000 FLASH 180.000
D65 RECTANGULAR 59.181 31.622 0.000 FLASH 0.000
D66 RECTANGULAR 169.417 181.622 0.000 FLASH 0.000
D67 RECTANGULAR 45.992 43.433 0.000 FLASH 180.000
D68 RECTANGULAR 29.654 39.496 0.000 FLASH 270.000
D69 RECTANGULAR 87.528 56.425 0.000 FLASH 270.000
D70 RECTANGULAR 47.370 59.181 0.000 FLASH 0.000
D71 RECTANGULAR 43.433 70.992 0.000 FLASH 180.000
D72 RECTANGULAR 78.866 23.748 0.000 FLASH 0.000
D73 ROUNDED 145.795 118.236 0.000 FLASH 0.000
D74 ROUNDED 86.740 126.110 0.000 FLASH 90.000
D75 ROUNDED 67.055 67.055 0.000 FLASH 0.000
D76 RECTANGULAR 67.055 67.055 0.000 FLASH 270.000
D77 ROUNDED 165.480 165.480 0.000 FLASH 0.000
D78 ROUNDED 33.591 33.591 0.000 FLASH 0.000
D79 ROUNDED 98.551 59.181 0.000 FLASH 180.000
D80 ROUNDED 145.795 145.795 0.000 FLASH 0.000
D81 ROUNDED 8.000 8.000 0.000 FLASH 0.000
D82 ROUNDED 63.118 63.118 0.000 FLASH 0.000
D83 ROUNDED 86.740 86.740 0.000 FLASH 0.000
D84 ROUNDED 39.496 39.496 0.000 FLASH 0.000
D85 ROUNDED 39.370 39.370 0.000 LINE 0.000
D86 ROUNDED 19.685 19.685 0.000 LINE 0.000
D87 ROUNDED 15.748 15.748 0.000 LINE 0.000
D88 ROUNDED 35.433 35.433 0.000 LINE 0.000
D89 ROUNDED 27.559 27.559 0.000 LINE 0.000
D90 ROUNDED 23.622 23.622 0.000 LINE 0.000
D91 ROUNDED 31.496 31.496 0.000 LINE 0.000
D92 ROUNDED 78.740 78.740 0.000 LINE 0.000
D93 ROUNDED 52.756 52.756 0.000 LINE 0.000
D94 ROUNDED 137.795 110.236 0.000 FLASH 0.000
D95 ROUNDED 78.740 118.110 0.000 FLASH 90.000
D96 ROUNDED 59.055 59.055 0.000 FLASH 0.000
D97 RECTANGULAR 59.055 59.055 0.000 FLASH 270.000
D98 ROUNDED 157.480 157.480 0.000 FLASH 0.000
D99 ROUNDED 25.591 25.591 0.000 FLASH 0.000
D100 ROUNDED 137.795 137.795 0.000 FLASH 0.000
D101 ROUNDED 55.118 55.118 0.000 FLASH 0.000
D102 ROUNDED 78.740 78.740 0.000 FLASH 0.000
D103 ROUNDED 31.496 31.496 0.000 FLASH 0.000
D104 ROUNDED 2.000 2.000 0.000 LINE 0.000
D105 ROUNDED 7.874 7.874 0.000 FLASH 0.000
D106 ROUNDED 15.000 15.000 0.000 LINE 0.000
D107 ROUNDED 1.000 1.000 0.000 LINE 0.000
D108 ROUNDED 16.000 16.000 0.000 LINE 0.000
D109 ROUNDED 14.000 14.000 0.000 LINE 0.000
D110 ROUNDED 6.000 6.000 0.000 LINE 0.000
@@ -0,0 +1,96 @@
Altium Designer Pick and Place Locations
C:\Users\Sasa\Desktop\git\_shop\DryBox\Hardware\Project Outputs for DryBox\Pick Place\Pick Place for DryBox.txt
========================================================================================================================
File Design Information:
Date: 09-10-24
Time: 22:39
Revision: 1c9de3defc70aa6f167d8c94982d31005a57ed81
Variant: No variations
Units used: mm
Designator Comment Layer Center-X(mm) Center-Y(mm) Rotation Description
U3 NCP161ASN330T1G TopLayer -13.6000mm 29.3000mm 90 "Linear Voltage Regulator IC Positive Fixed 1 Output 450mA SOT-23-5"
C26 Cap TopLayer -8.4000mm 28.7000mm 90 Capacitor
C25 Cap TopLayer -9.9000mm 28.7000mm 90 Capacitor
L2 Inductor TopLayer -3.8000mm 44.8500mm 90 "1.1A 2.2nH 100mO 0603 Inductors (SMD) ROHS"
U1 SHTC3-TR-10KS TopLayer -19.5000mm 45.0000mm 90 "DFN-4-EP(2x2) Temperature and Humidity Sensor ROHS"
R1 Res TopLayer 18.3000mm 7.6000mm 180 "100mW Thick Film Resistors 75V ±100ppm/? ±1% 100kO 0603 Chip Resistor - Surface Mount ROHS"
U5 MCP3021A5T-E/OT TopLayer 18.2000mm 4.9000mm 180 "10 Bit Analog to Digital Converter 1 Input 1 SAR SOT-23-5"
C2 Cap TopLayer 21.4000mm 4.9000mm 90 Capacitor
C1 Cap TopLayer 18.2000mm 2.2000mm 180 Capacitor
P1 DB2ERC-2.54-3P-GN TopLayer -21.0000mm 23.0000mm 270 "150V 5A 2 Bend -40?~+105? Copper Alloy 1 2.54mm 1x3P Green Board Edge/Receptacle-Close Tin Push-Pull,P=2.54mm Pluggable System Terminal Block ROHS"
P7 DB2EVC-2.54-4P-GN TopLayer 20.3000mm 22.0300mm 90 "150V 5A 3 Straight pin -40?~+105? Copper Alloy 1 2.54mm 1x3P Green Board Edge/Receptacle-Close Tin Plugin,P=2.54mm Pluggable System Terminal Block ROHS"
P5 DB2EVC-2.54-2P-GN TopLayer 20.3000mm 12.1000mm 90 "2 Position Terminal Block Header, Male Pins, Shrouded (4 Side) 0.098" (2.50mm) Vertical Through Hole"
MH4 "Mounting Hole" TopLayer 21.5000mm -1.7030mm 0 "Board Mounting Hole"
MH3 "Mounting Hole" TopLayer 21.5000mm 54.7970mm 0 "Board Mounting Hole"
MH2 "Mounting Hole" TopLayer -21.5000mm -1.7030mm 0 "Board Mounting Hole"
MH1 "Mounting Hole" TopLayer -21.5000mm 54.7970mm 0 "Board Mounting Hole"
FD2 "Fiducial 1mm" TopLayer -22.4000mm 15.4000mm 0 ""
FD1 "Fiducial 1mm" TopLayer 21.4000mm 46.4000mm 0 ""
D4 "RGB LED" TopLayer -12.4500mm 35.0000mm 180 "Red, Green, Blue (RGB) - LED Indication - Discrete 1.9V Red, 2.8V Green, 2.8V Blue 0606 (1616 Metric)"
L1 Inductor TopLayer -8.7500mm 25.5000mm 180 Inductor
X1 40MHz TopLayer 4.3000mm 37.7500mm 180 "40MHz 9pF ±10ppm SMD2016-4P Crystals ROHS"
U4 AP63205WU-7 TopLayer -8.7690mm 20.6170mm 0 "Step-down type Fixed 2A 5V 3.8V~32V TSOT-23-6 DC-DC Converters ROHS"
SW3 Switch TopLayer -8.0000mm 32.2000mm 0 "50mA Surface Mount SPST 100MO 100,000 Times 12V 160gf@±50gf SMD Tactile Switches ROHS"
R30 Res TopLayer 14.9000mm 26.0000mm 270 Resistor
R29 Res TopLayer 4.3000mm 35.1000mm 0 Resistor
R28 Res TopLayer 14.5000mm 40.3000mm 0 Resistor
R27 Res TopLayer 14.5000mm 32.8000mm 0 Resistor
R26 Res TopLayer -12.4000mm 32.9000mm 180 Resistor
R25 Res TopLayer -12.5000mm 37.4000mm 180 Resistor
R24 Res TopLayer -12.5000mm 38.9000mm 180 Resistor
R21 Res TopLayer -7.4500mm 36.5250mm 0 Resistor
R16 Res TopLayer -9.8000mm 44.8500mm 90 Resistor
R15 Res TopLayer 13.4000mm 26.0000mm 270 Resistor
R14 Res TopLayer 0.0500mm 45.7500mm 270 Resistor
P9 "USB-C Recepticle" TopLayer 19.1225mm 36.0000mm 90 "3A 1 Surface Mount 16P Female -25?~+85? Type-C SMD USB Connectors ROHS"
C35 Cap TopLayer 6.8000mm 37.7500mm 270 Capacitor
C34 Cap TopLayer -5.2000mm 44.8500mm 270 Capacitor
C33 Cap TopLayer -2.2000mm 44.8500mm 270 Capacitor
C32 Cap TopLayer -3.9000mm 32.3500mm 90 Capacitor
C31 Cap TopLayer -7.4500mm 38.5000mm 0 Capacitor
C30 Cap TopLayer -0.9000mm 32.3500mm 90 Capacitor
C29 Cap TopLayer 15.1000mm 18.3000mm 90 Capacitor
C28 Cap TopLayer -22.2500mm 45.0000mm 270 Capacitor
C23 Cap TopLayer -8.7500mm 23.2500mm 180 Capacitor
C19 Cap TopLayer 4.3000mm 39.7500mm 0 Capacitor
C18 Cap TopLayer 4.3000mm 42.7500mm 180 Capacitor
C17 Cap TopLayer 4.3000mm 41.2500mm 180 Capacitor
C16 Cap TopLayer -6.8000mm 44.8500mm 270 Capacitor
C6 Cap TopLayer -13.5000mm 23.5000mm 0 Capacitor
C5 Cap TopLayer -13.5000mm 25.5000mm 0 Capacitor
C4 Cap TopLayer -13.9250mm 20.5000mm 270 Capacitor
A1 ANT3216LL00R2400A TopLayer 0.0000mm 52.0000mm 90 "RF ANT 2.45GHZ CHIP SOLDER SMD"
C27 Cap TopLayer 15.1000mm 15.3000mm 270 Capacitor
C3 Cap TopLayer -12.4250mm 20.5000mm 270 Capacitor
R9 Res TopLayer 21.1000mm 7.6000mm 180 "100mW Thick Film Resistors 75V ±100ppm/? ±1% 100kO 0603 Chip Resistor - Surface Mount ROHS"
R23 Res TopLayer -1.7000mm 16.8000mm 180 Resistor
R19 Res TopLayer 1.3000mm 16.8000mm 180 Resistor
R17 Res TopLayer 1.3000mm 18.3000mm 0 Resistor
R13 Res TopLayer -17.7500mm 24.0000mm 0 Resistor
R5 Res TopLayer -17.7500mm 22.5000mm 0 Resistor
R12 Res TopLayer -21.3250mm 28.7500mm 180 Resistor
R8 Res TopLayer -7.4500mm 35.0250mm 0 Resistor
Q1 DMN62D0U-13 TopLayer -17.7500mm 26.7500mm 90 "MOSFET N-CH 60V 0.38A"
D2 MBR1020VL TopLayer 9.4000mm 28.1000mm 180 "Diode Schottky 20V 1A Surface Mount SOD-123F"
U2 ESP32-C3-FH4 TopLayer -1.7000mm 37.7500mm 270 "Ultra-Low-Power SoC with RISC-V Single-Core CPU 2.4 GHz Wi-Fi (802.11 b/g/n) and Bluetooth® 5 (LE)"
R20 Res TopLayer -8.3000mm 44.8500mm 270 Resistor
R4 Res TopLayer 4.3000mm 33.6000mm 0 Resistor
C13 Cap TopLayer -5.3000mm 28.7000mm 270 Capacitor
C12 Cap TopLayer -6.9000mm 28.7000mm 270 Capacitor
C11 Cap TopLayer -2.4000mm 32.3500mm 90 Capacitor
C10 "Cap - 220uF 35V" TopLayer -18.2000mm 9.2000mm 270 "220 µF 35V Aluminum - Polymer Capacitors Radial, Can - SMD 29mOhm 2000 Hrs @ 125°C"
C9 Cap TopLayer 0.8000mm 43.5000mm 180 Capacitor
C8 Cap TopLayer 0.8000mm 47.7500mm 180 Capacitor
C7 Cap TopLayer -11.3000mm 44.8500mm 90 Capacitor
Q3 DMN62D0U-13 TopLayer -4.0000mm 19.3000mm 270 "MOSFET N-CH 60V 0.38A"
P3 DB2ERC-2.54-2P-GN TopLayer -6.5000mm 5.5000mm 0 "150V 5A 2 Bend -40?~+105? Copper Alloy 1 2.54mm 1x2P Green Board Edge/Receptacle-Close Tin Push-Pull,P=2.54mm Pluggable System Terminal Block ROHS"
P11 WM4330-ND TopLayer -19.4200mm 35.2501mm 90 "4 Positions Header Connector 0.100" (2.54mm) Through Hole Tin"
F1 3557-2 TopLayer -2.6000mm 11.6000mm 0 "Fuse Block 30 A 500V 1 Circuit Blade Through Hole"
R18 Res TopLayer -4.7000mm 22.9000mm 270 Resistor
R22 Res TopLayer -3.2000mm 22.9000mm 270 Resistor
Q2 PSMN9R1-30YL TopLayer 6.7005mm 20.7148mm 0 "MOSFET N-CH 30V 37A LFPAK"
P8 DB2ERC-2.54-2P-GN TopLayer 6.5600mm 5.5000mm 0 "150V 5A 2 Bend -40?~+105? Copper Alloy 1 2.54mm 1x2P Green Board Edge/Receptacle-Close Tin Push-Pull,P=2.54mm Pluggable System Terminal Block ROHS"
D3 MBR230S1F-7 TopLayer 6.0000mm 16.6000mm 180 "Diode Schottky 20V 1A Surface Mount SOD-123F"
+44
View File
@@ -0,0 +1,44 @@
# DIY WiFi Enabled 3D Printing Filament Dry Box v2
## What is this project about?
[![DIY WiFi Enabled Filament Dry Box v2](http://img.youtube.com/vi/1QqDBpliQO0/0.jpg)](http://www.youtube.com/watch?v=1QqDBpliQO0 "DIY WiFi Enabled Filament Dry Box v2")
Above video shows the finished DryBox v2, how it works and motivation behind some of the design decisions.
## Can I build my own?
Yes! All hardware and firmware source files are available in their respective folders.
The above video explains the idea and how hardware and firmware works. It's recommended that you watch that first.
There is [documentation page](https://docs.sasakaranovic.com/drybox/) that can help you with all the parts and tools you might need.
Also the original [DIY Filament Dryer v1 video](https://www.youtube.com/watch?v=1QqDBpliQO0) can fill in some gaps.
## Can I buy a kit?
Yes! If you don't want to make your own electronics, you can buy the kit from the [web shop](https://shop.sasakaranovic.com/products/drybox-v2-diy-dry-box). You will still have to source some of the off-the-shelf parts yourself.
---
Blog page: https://sasakaranovic.com/projects/diy-filament-dry-box-v2
YouTube video: http://www.youtube.com/watch?v=1QqDBpliQO0
---
<br/><br/>
#### Sasa Karanovic
<a href="https://sasakaranovic.com/" target="_blank" title="Sasa Karanovic Home Page"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_home.png" width="16"> Home Page</a> &nbsp;&middot;&nbsp;
<a href="https://shop.sasakaranovic.com/" target="_blank" title="Sasa Karanovic Web Shop"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_home.png" width="16"> Web Shop</a> &nbsp;&middot;&nbsp;
<a href="https://youtube.com/c/sasakaranovic" target="_blank" title="Sasa Karanovic on YouTube"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_youtube.png" width="16"> YouTube</a> &nbsp;&middot;&nbsp;
<a href="https://github.com/sasakaranovic" target="_blank" title="Sasa Karanovic on GitHub"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_github.png" width="16"> GitHub</a> &nbsp;&middot;&nbsp;
<a href="https://twitter.com/_sasakaranovic_" target="_blank" title="Sasa Karanovic on Twitter"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_twitter.png" width="16"> Twitter</a> &nbsp;&middot;&nbsp;
<a href="https://instagram.com/_sasakaranovic_" target="_blank" title="Sasa Karanovic on Instagram"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_instagram.png" width="16"> Instagram</a> &nbsp;&middot;&nbsp;
<a href="https://github.com/sponsors/SasaKaranovic" target="_blank" title="Sponsor on GitHub"><img src="https://raw.githubusercontent.com/SasaKaranovic/common/master/assets/img_github.png" width="16"> Sponsor on GitHub</a>