Skip to content

Commit ac62fe5

Browse files
committed
Add email support
1 parent 85b9375 commit ac62fe5

10 files changed

Lines changed: 903 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 679 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ serde_json = "1.0.134"
1010
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
1111
log = "0.4.22"
1212
regex = "1.11.1"
13-
toml = "0.8.19"
13+
toml = "0.8.19"
14+
lettre = { version = "0.11.11", features = ["rustls-tls"] }

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ Should work in theory on all the supported printers of flashforge-finder-api
1414
The `docs` folder includes documentation for use in [Bruno](https://www.usebruno.com/), replace the ip with your server's
1515

1616
In general, for now:
17-
* `http://localhost:8080/<printer ip>/info` - Get printer info
18-
* `http://localhost:8080/<printer ip>/status` - Get printer status
19-
* `http://localhost:8080/<printer ip>/temperature` - Get sensor temperatures, B for bed, T0 for main sensor
20-
* `http://localhost:8080/<printer ip>/head-position` - Get the printer's head position
21-
* `http://localhost:8080/<printer ip>/progress` - Get print progress
17+
* `http://localhost:8080/apis/printers/:printerId/info` - Get printer info
18+
* `http://localhost:8080/apis/printers/:printerId/status` - Get printer status
19+
* `http://localhost:8080/apis/printers/:printerId/temperatures` - Get sensor temperatures, B for bed, T0 for main sensor
20+
* `http://localhost:8080/apis/printers/:printerId/head-position` - Get the printer's head position
21+
* `http://localhost:8080/apis/printers/:printerId/progress` - Get print progress
2222

2323
## Future Work
2424

2525
* [ ] Built in mjpeg proxy
2626
* So multiple clients can view at once
2727
* [ ] Notifications (email, push?, webhooks?) on completion
2828
* [ ] Simple UI that replaces need of polar3d
29-
* [ ] Use config file for printer ips, instead of manually putting IP
29+
* [x] Use config file for printer ips, instead of manually putting IP

config.example.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# SMTP Server to send emails with
33
host = "smtp.domain.com"
44
port = 587
5-
auth = "tls" # or "SSL"
5+
encryption = "starttls" # or "tls" or "none"
66
user = ""
77
password = ""
88

99
[notifications]
10+
# Where to send to
11+
emails = ["your@email.com"]
12+
1013
# What channels to send notifications to, [] or exclude for none
1114
on_done = ["smtp"]
1215

src/config.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,27 @@ impl Config {
2121
}
2222
}
2323

24+
#[derive(Serialize, Deserialize, Debug)]
25+
#[serde(rename_all = "lowercase")]
26+
pub enum EmailEncryption {
27+
None,
28+
StartTLS,
29+
TLS
30+
}
31+
2432
#[derive(Debug, Serialize, Deserialize)]
2533
pub struct EmailConfig {
26-
host: String,
27-
port: u16,
28-
auth: String,
29-
user: String,
30-
password: String
34+
pub(crate) host: String,
35+
pub(crate) port: u16,
36+
pub(crate) encryption: EmailEncryption,
37+
pub(crate) user: String,
38+
pub(crate) password: String
3139
}
3240
#[derive(Debug, Serialize, Deserialize)]
3341
pub struct NotificationConfig {
34-
on_done: Option<Vec<String>>
42+
pub(crate) emails: Option<Vec<String>>,
43+
44+
pub(crate) on_done: Option<Vec<String>>
3545
}
3646

3747
#[derive(Debug, Serialize, Deserialize)]
@@ -42,4 +52,5 @@ pub struct GeneralConfig {
4252
#[derive(Debug, Serialize, Deserialize)]
4353
pub struct PrinterConfig {
4454
pub(crate) ip: IpAddr
45-
}
55+
}
56+

src/main.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ mod printers;
77
mod routes;
88

99
use std::net::{AddrParseError, SocketAddr, ToSocketAddrs};
10-
use log::{debug,info};
10+
use std::sync::{Arc, Mutex};
11+
use log::{debug, info};
1112
use rocket::{catch, catchers, get, launch, routes, serde::json::Json, Request};
1213
use tracing_subscriber::layer::SubscriberExt;
1314
use tracing_subscriber::util::SubscriberInitExt;
@@ -36,11 +37,13 @@ fn rocket() -> _ {
3637
.with(tracing_subscriber::fmt::layer())
3738
.init();
3839

39-
let mut printers = Printers::new();
40-
let config = Config::load();
40+
let config = Arc::new(Config::load());
41+
let mut printers = Printers::new(config.clone());
4142
for (id, printer_config) in &config.printers {
4243
printers.add_printer(id.to_string(), printer_config.ip)
4344
}
45+
let printers = Arc::new(Mutex::new(printers));
46+
Printers::start_watch_thread(printers.clone());
4447

4548
let mut rk_config = rocket::Config::default();
4649
rk_config.port = 8080;

src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub struct PrinterHeadPosition {
5050
#[derive(Serialize, Clone)]
5151
pub struct PrinterTemperature(pub HashMap<String, TemperatureMeasurement>);
5252

53-
#[derive(Serialize, Clone)]
53+
#[derive(Serialize, Clone, Debug)]
5454
pub struct PrinterProgress {
5555
pub layer: (u32, u32),
5656
pub byte: (u32, u32)

src/printer.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
1+
use std::fmt::Display;
12
use std::io::{Read, Write};
23
use std::net::{IpAddr, SocketAddr, TcpStream};
34
use std::time::Duration;
4-
use log::{debug, trace};
5+
use log::{debug, trace, warn};
56
use crate::models::{PrinterHeadPosition, PrinterInfo, PrinterProgress, PrinterStatus, PrinterTemperature};
67
use crate::socket::{PrinterRequest, PrinterResponse};
78

89
pub struct Printer {
910
socket_addr: SocketAddr,
10-
info: Option<PrinterInfo>
11+
info: Option<PrinterInfo>,
12+
name: String,
1113
}
1214

13-
15+
// The port the TCP API is on
1416
const PRINTER_API_PORT: u16 = 8899;
1517

18+
impl Display for Printer {
19+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20+
write!(f, "{}", self.name)
21+
}
22+
}
1623
impl Printer {
17-
pub fn new(ip_addr: IpAddr) -> Self {
24+
pub fn new(name: String, ip_addr: IpAddr) -> Self {
1825
Printer {
1926
socket_addr: SocketAddr::new(ip_addr, PRINTER_API_PORT),
20-
info: None
27+
info: None,
28+
name
2129
}
2230
}
2331

32+
pub fn name(&self) -> &str {
33+
&self.name
34+
}
35+
36+
pub fn ip(&self) -> IpAddr {
37+
self.socket_addr.ip()
38+
}
39+
2440
pub fn get_meta(&mut self) -> Option<PrinterInfo> {
2541
if self.info.is_none() {
26-
let res = self.send_request(PrinterRequest::GetInfo).ok();
27-
// Should always be PrinterInfo
28-
if let Some(PrinterResponse::PrinterInfo(info)) = res {
29-
self.info = Some(info);
42+
match self.get_info() {
43+
Ok(info) => self.info = Some(info),
44+
Err(e) => {
45+
warn!("printer/{} get_meta error: {}", self.name, e);
46+
}
3047
}
3148
}
3249
self.info.clone()
@@ -87,15 +104,15 @@ impl Printer {
87104
}
88105

89106
pub fn get_progress(&self) -> Result<PrinterProgress, String> {
90-
match self.send_request(PrinterRequest::GetTemperature) {
107+
match self.send_request(PrinterRequest::GetProgress) {
91108
Ok(PrinterResponse::PrinterProgress(t)) => Ok(t),
92109
Ok(_) => panic!("got wrong response from request"),
93110
Err(e) => Err(e)
94111
}
95112
}
96113

97114
pub fn get_head_position(&self) -> Result<PrinterHeadPosition, String> {
98-
match self.send_request(PrinterRequest::GetTemperature) {
115+
match self.send_request(PrinterRequest::GetHeadPosition) {
99116
Ok(PrinterResponse::PrinterHeadPosition(t)) => Ok(t),
100117
Ok(_) => panic!("got wrong response from request"),
101118
Err(e) => Err(e)

src/printers.rs

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,155 @@
11
use std::collections::HashMap;
22
use std::net::IpAddr;
3-
use log::{debug, warn};
3+
use std::ops::Not;
4+
use std::sync::{Arc, Mutex, RwLock};
5+
use std::time::Duration;
6+
use lettre::{Address, Message, SmtpTransport, Transport};
7+
use lettre::message::header::ContentType;
8+
use lettre::message::Mailbox;
9+
use lettre::transport::smtp::authentication::Credentials;
10+
use lettre::transport::smtp::client::{Tls, TlsParameters, TlsParametersBuilder};
11+
use log::{debug, error, trace, warn};
12+
use rocket::yansi::Paint;
13+
use crate::config::{Config, EmailEncryption};
414
use crate::printer::Printer;
15+
use std::fmt::Write;
16+
17+
18+
19+
static PROGRESS_CHECK_INTERVAL: Duration = Duration::from_secs(60);
20+
21+
pub type PrinterManager = Arc<Mutex<Printers>>;
22+
23+
#[derive(Debug)]
24+
enum NotificationType {
25+
PrintComplete
26+
}
27+
28+
impl NotificationType {
29+
pub fn get_subject(&self, printer: &Printer) -> String {
30+
match self {
31+
NotificationType::PrintComplete => format!("Print complete on {}", printer.name()),
32+
_ => printer.name().to_string()
33+
}
34+
}
35+
36+
pub fn get_message(&self, printer: &Printer) -> String {
37+
match self {
38+
NotificationType::PrintComplete => {
39+
let status = printer.get_status().unwrap();
40+
let mut str = String::new();
41+
write!(str, "File: {}\n", status.current_file.unwrap_or("unknown".to_string())).unwrap();
42+
write!(str, "IP: {}\n", printer.ip()).unwrap();
43+
str
44+
// TODO: send an image?
45+
}
46+
_ => "".to_string()
47+
}
48+
}
49+
}
550

651
pub struct Printers {
7-
printers: HashMap<String, Printer>
52+
printers: HashMap<String, Printer>,
53+
config: Arc<Config>,
54+
mailer: Option<SmtpTransport>,
55+
56+
notification_sent: HashMap<String, String> // If printer (key) has value, then a print done notification has been submitted for file (value)
857
}
958

1059
impl Printers {
11-
pub fn new() -> Printers {
12-
Self {
13-
printers: HashMap::new()
60+
pub fn new(config: Arc<Config>) -> Printers {
61+
let mut s = Self {
62+
printers: HashMap::new(),
63+
config,
64+
mailer: None,
65+
notification_sent: HashMap::new()
66+
};
67+
if let Some(smtp) = &s.config.smtp {
68+
if smtp.port <= 0 {
69+
error!("SMTP: Smtp port is invalid, smtp support not enabled");
70+
} else if smtp.user == "" {
71+
error!("SMTP: Smtp user is empty, smtp support not enabled");
72+
} else if smtp.host == "" {
73+
error!("SMTP: Smtp host is empty, smtp support not enabled");
74+
} else {
75+
let builder = match smtp.encryption {
76+
EmailEncryption::None => SmtpTransport::builder_dangerous(&smtp.host),
77+
EmailEncryption::StartTLS => SmtpTransport::starttls_relay(&smtp.host).unwrap(),
78+
EmailEncryption::TLS => SmtpTransport::relay(&smtp.host).unwrap()
79+
};
80+
s.mailer = Some(builder
81+
.port(smtp.port)
82+
.credentials(Credentials::new(smtp.user.to_string(), smtp.password.to_string()))
83+
.build()
84+
)
85+
}
86+
}
87+
s
88+
}
89+
90+
pub fn start_watch_thread(manager: PrinterManager) {
91+
debug!("Starting watch thread at interval {:?}", PROGRESS_CHECK_INTERVAL);
92+
std::thread::spawn(move || {
93+
std::thread::sleep(PROGRESS_CHECK_INTERVAL);
94+
loop {
95+
trace!("Checking printers");
96+
let mut lock = manager.lock().unwrap();
97+
let mut has_sent = lock.notification_sent.clone();
98+
for (id, printer) in &lock.printers {
99+
if let Ok(prog) = printer.get_progress() {
100+
// Check if progress is 100%
101+
if prog.layer.0 >= prog.layer.1 {
102+
// Get current file from status
103+
let status = printer.get_status().unwrap();
104+
if status.current_file.is_none() {
105+
continue;
106+
}
107+
// Check if we have already sent a notification
108+
let current_file = status.current_file.unwrap();
109+
let has_notified = lock.has_notified(id, &current_file);
110+
111+
if !has_notified {
112+
lock.send_notification(printer, NotificationType::PrintComplete);
113+
has_sent.insert(id.clone(), current_file);
114+
}
115+
}
116+
}
117+
}
118+
lock.notification_sent = has_sent;
119+
drop(lock);
120+
std::thread::sleep(PROGRESS_CHECK_INTERVAL);
121+
}
122+
});
123+
}
124+
125+
fn has_notified(&self, printer_id: &str, file_name: &str) -> bool {
126+
!self.notification_sent.contains_key(printer_id) || self.notification_sent.get(printer_id).unwrap() != file_name
127+
}
128+
129+
fn send_notification(&self, printer: &Printer, notification_type: NotificationType) {
130+
debug!("Sending notification: {:?}", notification_type);
131+
let Some(notifications) = &self.config.notifications else { return; };
132+
if let Some(mailer) = &self.mailer {
133+
let user = &self.config.smtp.as_ref().unwrap().user;
134+
trace!("smtp configured, sending from {}", user);
135+
match user.parse() {
136+
Ok(from_addr) => {
137+
let mut builder = Message::builder()
138+
.from(Mailbox::new(None, from_addr))
139+
.subject(notification_type.get_subject(printer))
140+
.header(ContentType::TEXT_PLAIN);
141+
for email in notifications.emails.iter().flatten() {
142+
builder = builder.bcc(email.parse().unwrap())
143+
}
144+
let email = builder.body(notification_type.get_message(printer)).unwrap();
145+
146+
mailer.send(&email).unwrap();
147+
trace!("Sent notification {:?} for printer {}", notification_type, printer);
148+
},
149+
Err(e) => {
150+
error!("Could not parse from address \"{}\": {}", user, e);
151+
}
152+
}
14153
}
15154
}
16155

@@ -24,10 +163,9 @@ impl Printers {
24163

25164
pub fn add_printer(&mut self, id: String, ip: IpAddr) {
26165
debug!("adding printer {} with ip {}", id, ip);
27-
let mut printer = Printer::new(ip);
28-
if printer.get_meta().is_none() {
29-
warn!("printer {} failed to get meta:", id);
30-
}
166+
let mut printer = Printer::new(id.clone(), ip);
167+
printer.get_meta();
31168
self.printers.insert(id, printer);
32169
}
33-
}
170+
}
171+

0 commit comments

Comments
 (0)