v1.0.8 dualside work, have bugs/need work

This commit is contained in:
2026-09-19 23:10:41 +03:00
parent 6475ffa7e0
commit 27b3f4cab4
5 changed files with 78 additions and 37 deletions

0
install.sh Normal file → Executable file
View File

View File

@ -2,7 +2,7 @@ use axum::{response::IntoResponse, Json};
pub async fn get_version() -> impl IntoResponse { pub async fn get_version() -> impl IntoResponse {
Json(serde_json::json!({ Json(serde_json::json!({
"version": "1.0.7", "version": "1.0.8",
"timestamp": chrono::Utc::now().timestamp() "timestamp": chrono::Utc::now().timestamp()
})) }))
} }

View File

@ -11,33 +11,57 @@ pub async fn job_advance(
let job = m.get_mut(&job_id).ok_or_else(|| (StatusCode::NOT_FOUND, "Задание не найдено".into()))?; let job = m.get_mut(&job_id).ok_or_else(|| (StatusCode::NOT_FOUND, "Задание не найдено".into()))?;
let si = match job.phase { let si = match job.phase {
JobPhase::WaitingStart => { // Старт печати текущего файла (первого или после очистки)
let next_idx = 0; JobPhase::WaitingStart | JobPhase::AwaitingClearOutput => {
job.current = Some(next_idx); let idx = job.current.unwrap_or(0);
Some(start_printing_locked(job, next_idx)) Some(start_printing_locked(job, idx))
} }
JobPhase::AwaitingPickup => { JobPhase::AwaitingPickup => {
let c = job.current.unwrap_or(0); let c = job.current.unwrap_or(0);
if job.duplex_first_pass_done { if job.duplex_first_pass_done {
// Первый проход двусторонней печати завершен -> просим переложить
job.duplex_first_pass_done = false; job.duplex_first_pass_done = false;
job.files[c].status = FileStatus::AwaitingClearOutput;
job.phase = JobPhase::AwaitingClearOutput; None
} else {
job.files[c].status = FileStatus::Done;
begin_next_locked(job, c)
}
}
JobPhase::AwaitingClearOutput => {
let c = job.current.unwrap_or(0);
job.files[c].status = FileStatus::AwaitingFlip; job.files[c].status = FileStatus::AwaitingFlip;
job.phase = JobPhase::AwaitingFlip; None job.phase = JobPhase::AwaitingFlip;
None
} else {
// Файл полностью напечатан
job.files[c].status = FileStatus::Done;
// Ищем следующий файл
if let Some(next_si) = begin_next_locked(job, c) {
let next_idx = job.current.unwrap();
// Проверяем, является ли СЛЕДУЮЩИЙ файл двусторонним
let is_next_duplex = job.files[next_idx].settings.as_ref()
.map(|s| s.sides.as_deref() == Some("Двусторонняя"))
.unwrap_or(false);
if is_next_duplex {
// Следующий двусторонний -> просим очистить лоток ПЕРЕД его стартом
// ВАЖНО: Мы НЕ спавним задачу печати здесь.
// Мы меняем статус и ждем вызова advance (кнопки или автостарта)
job.files[next_idx].status = FileStatus::AwaitingClearOutput;
job.phase = JobPhase::AwaitingClearOutput;
None
} else {
// Следующий односторонний -> печатаем сразу
Some(next_si)
} }
} else {
None // Задание завершено
}
}
}
JobPhase::AwaitingFlip => { JobPhase::AwaitingFlip => {
let c = job.current.unwrap(); let c = job.current.unwrap();
job.files[c].status = FileStatus::Printing; job.files[c].status = FileStatus::Printing;
job.phase = JobPhase::Printing; job.phase = JobPhase::Printing;
Some(SpawnInfo { Some(SpawnInfo {
job_id: job.id.clone(), idx: c, job_id: job.id.clone(),
idx: c,
path: job.files[c].path.to_path_buf(), path: job.files[c].path.to_path_buf(),
settings: job.files[c].settings.clone(), settings: job.files[c].settings.clone(),
is_second_pass: true, is_second_pass: true,
@ -48,6 +72,8 @@ pub async fn job_advance(
(job.view(), si) (job.view(), si)
}; };
if let Some(si) = spawn_info { spawn_print_task(store.clone(), si); } if let Some(si) = spawn_info {
spawn_print_task(store.clone(), si);
}
Ok(Json(view)) Ok(Json(view))
} }

View File

@ -10,6 +10,7 @@ pub async fn create_print(
mut multipart: Multipart, mut multipart: Multipart,
) -> Result<Json<JobView>, (StatusCode, String)> { ) -> Result<Json<JobView>, (StatusCode, String)> {
info!("📥 Получен запрос на печать"); info!("📥 Получен запрос на печать");
let mut settings_list: Vec<Option<PrintSettings>> = Vec::new(); let mut settings_list: Vec<Option<PrintSettings>> = Vec::new();
let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new(); let mut files_data: Vec<(String, bytes::Bytes)> = Vec::new();
@ -23,11 +24,15 @@ pub async fn create_print(
settings_list.push(parsed); settings_list.push(parsed);
} else if field_name == "files" { } else if field_name == "files" {
let file_name = field.file_name().unwrap_or("unknown").to_string(); let file_name = field.file_name().unwrap_or("unknown").to_string();
if let Ok(data) = field.bytes().await { files_data.push((file_name, data)); } if let Ok(data) = field.bytes().await {
files_data.push((file_name, data));
}
} }
} }
if files_data.is_empty() { return Err((StatusCode::BAD_REQUEST, "Нет файлов".into())); } if files_data.is_empty() {
return Err((StatusCode::BAD_REQUEST, "Нет файлов".into()));
}
let mut job_files: Vec<JobFile> = Vec::new(); let mut job_files: Vec<JobFile> = Vec::new();
for (i, (file_name, data)) in files_data.into_iter().enumerate() { for (i, (file_name, data)) in files_data.into_iter().enumerate() {
@ -39,19 +44,35 @@ pub async fn create_print(
if fs::write(&*path, &data).await.is_ok() { if fs::write(&*path, &data).await.is_ok() {
let settings = settings_list.get(i).cloned().flatten(); let settings = settings_list.get(i).cloned().flatten();
job_files.push(JobFile { job_files.push(JobFile {
name: file_name, path, settings, name: file_name,
status: FileStatus::Queued, error: None, path,
settings,
status: FileStatus::Queued,
error: None,
}); });
} }
} }
} }
if job_files.is_empty() { return Err((StatusCode::BAD_REQUEST, "Не удалось сохранить файлы".into())); } if job_files.is_empty() {
return Err((StatusCode::BAD_REQUEST, "Не удалось сохранить файлы".into()));
}
// ВСЕГДА начинаем с WaitingStart.
// 1. Если первый файл односторонний - он просто поедет печататься.
// 2. Если первый файл двусторонний - лоток и так пуст, очистка не нужна.
// Очистка (AwaitingClearOutput) будет запрошена ТОЛЬКО при переходе
// от предыдущего файла к следующему двустороннему в advance.rs
let initial_phase = JobPhase::WaitingStart;
let job_id = format!("job-{}", JOB_COUNTER.fetch_add(1, Ordering::SeqCst)); let job_id = format!("job-{}", JOB_COUNTER.fetch_add(1, Ordering::SeqCst));
let job = Job { let job = Job {
id: job_id.clone(), files: job_files, current: None, id: job_id.clone(),
phase: JobPhase::WaitingStart, cups_id: None, duplex_first_pass_done: false, files: job_files,
current: Some(0),
phase: initial_phase,
cups_id: None,
duplex_first_pass_done: false,
}; };
let view = job.view(); let view = job.view();

View File

@ -59,7 +59,6 @@ pub fn spawn_print_task(store: JobStore, info: SpawnInfo) {
if job.files.get(info.idx).map(|f| f.status.clone()) != Some(FileStatus::Printing) { return; } if job.files.get(info.idx).map(|f| f.status.clone()) != Some(FileStatus::Printing) { return; }
if !cups_id.is_empty() { job.cups_id = Some(cups_id); } if !cups_id.is_empty() { job.cups_id = Some(cups_id); }
job.files[info.idx].status = FileStatus::AwaitingPickup; job.files[info.idx].status = FileStatus::AwaitingPickup;
job.phase = JobPhase::AwaitingPickup; job.phase = JobPhase::AwaitingPickup;
@ -77,7 +76,6 @@ pub async fn mark_error_and_continue(store: JobStore, info: SpawnInfo, msg: Stri
let mut m = store.lock().unwrap(); let mut m = store.lock().unwrap();
let job = match m.get_mut(&info.job_id) { Some(j) => j, None => return }; let job = match m.get_mut(&info.job_id) { Some(j) => j, None => return };
if job.phase == JobPhase::Cancelled { return; } if job.phase == JobPhase::Cancelled { return; }
if let Some(f) = job.files.get_mut(info.idx) { if let Some(f) = job.files.get_mut(info.idx) {
if f.status != FileStatus::Printing { return; } if f.status != FileStatus::Printing { return; }
f.status = FileStatus::Error; f.status = FileStatus::Error;
@ -118,10 +116,8 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st
cmd.arg("-n").arg(s.copies.to_string()); cmd.arg("-n").arg(s.copies.to_string());
if s.pages != "all" && !s.pages.is_empty() { cmd.arg("-P").arg(&s.pages); } if s.pages != "all" && !s.pages.is_empty() { cmd.arg("-P").arg(&s.pages); }
if s.color_mode == "bw" { cmd.arg("-o").arg("ColorModel=Gray"); } if s.color_mode == "bw" { cmd.arg("-o").arg("ColorModel=Gray"); }
let fmt = normalize_format(&s.format); let fmt = normalize_format(&s.format);
if !fmt.is_empty() { cmd.arg("-o").arg(format!("PageSize={}", fmt)); } if !fmt.is_empty() { cmd.arg("-o").arg(format!("PageSize={}", fmt)); }
if s.sides.as_deref() == Some("Двусторонняя") && duplex_pass.is_none() { if s.sides.as_deref() == Some("Двусторонняя") && duplex_pass.is_none() {
cmd.arg("-o").arg("sides=two-sided-long-edge"); cmd.arg("-o").arg("sides=two-sided-long-edge");
} }
@ -148,10 +144,8 @@ fn run_lp(path: &Path, settings: Option<&PrintSettings>, duplex_pass: Option<&st
cmd.arg(path); cmd.arg(path);
let out = cmd.env("LC_ALL", "C").output().map_err(|e| format!("lp: {}", e))?; let out = cmd.env("LC_ALL", "C").output().map_err(|e| format!("lp: {}", e))?;
if !out.status.success() { if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string()); return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
} }
Ok(String::from_utf8_lossy(&out.stdout).split_whitespace().nth(3).unwrap_or("").to_string()) Ok(String::from_utf8_lossy(&out.stdout).split_whitespace().nth(3).unwrap_or("").to_string())
} }