Compare commits

...
1 Commits
Author SHA1 Message Date
aZholtikov 1c79271056 wip: 2026-10-12 19:29:39 +03:00
4 changed files with 325 additions and 0 deletions

No files matched your search

+262
View File
@@ -1,4 +1,6 @@
#include "ats0002.h"
#include <stdarg.h>
#include <time.h>
/**
* @brief Normalize the decimal separator in a string from comma to dot.
@@ -60,6 +62,31 @@ static void _get_double_in_range(double *result, double min, double max, const c
* @param airflow Pointer to the current airflow value
* @param data Pointer to air passage data containing anchor points (must not be NULL)
*/
/**
* @brief Get the current date as a formatted string (YYYY-MM-DD).
*
* @param buffer Destination buffer (at least 11 bytes)
*/
static void _get_date_string(char *buffer);
/**
* @brief Save the test report to a minimal PDF file.
*
* @see save_report_to_pdf (public API)
*/
static void _save_report_to_pdf_impl(const char *heat_exchanger_name,
const char *part_number,
const char *side_name,
double airflow,
double airflow_pressure,
const char *unit,
const double *barometric_pressure,
const double *airflow_temperature,
const double *corrected_airflow_pressure,
double pressure_drop_limit,
const char *test_result,
bool recalculated_required);
static void _get_log_interpolated_airflow_pressure_drop_limit(double *limit, const double *airflow, const air_passage_data_t *data);
/**
@@ -331,3 +358,238 @@ static void _calculate_density_corrected_pressure(double *corrected_airflow_pres
double value_7 = (0.28443 * value_5) / value_6;
*corrected_airflow_pressure = *airflow_pressure * value_7;
}
void get_yes_no(bool *answer, const char *prompt)
{
printf("%s (y/n):\n", prompt);
char buf[64];
for (;;)
{
printf("> ");
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL)
{
*answer = false;
return;
}
buf[strcspn(buf, "\n")] = '\0';
if (strcmp(buf, "q") == 0 || strcmp(buf, "Q") == 0)
{
*answer = false;
return;
}
if (strcmp(buf, "y") == 0 || strcmp(buf, "Y") == 0 ||
strcmp(buf, "yes") == 0 || strcmp(buf, "Yes") == 0 || strcmp(buf, "YES") == 0)
{
*answer = true;
return;
}
if (strcmp(buf, "n") == 0 || strcmp(buf, "N") == 0 ||
strcmp(buf, "no") == 0 || strcmp(buf, "No") == 0 || strcmp(buf, "NO") == 0)
{
*answer = false;
return;
}
printf("Please enter 'y' or 'n':\n");
}
}
/* ======================== PDF Report Generation ======================== */
/**
* @brief Escape special characters in PDF string literals.
*
* PDF strings require backslash-escaping for: \ ( )
*
* @param[out] dst Destination buffer
* @param[in] dst_size Size of destination buffer
* @param[in] src Source string
*/
static void _pdf_escape_string(char *dst, size_t dst_size, const char *src)
{
size_t di = 0;
for (size_t si = 0; src[si] != '\0' && di + 2 < dst_size; si++)
{
char c = src[si];
if (c == 0x5C || c == 0x28 || c == 0x29)
{
dst[di++] = 0x5C;
}
dst[di++] = c;
}
dst[di] = '\0';
}
/**
* @brief Get the current date as a formatted string (YYYY-MM-DD).
*
* @param buffer Destination buffer (at least 11 bytes)
*/
static void _get_date_string(char *buffer)
{
time_t now = time(NULL);
struct tm *t = localtime(&now);
snprintf(buffer, 11, "%04d-%02d-%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday);
}
/**
* @brief Build a PDF content stream line for a text label.
*
* Appends a BT/ET text block to the stream buffer and advances the y position.
*
* @param stream_buf Content stream buffer
* @param stream_size Size of the content stream buffer
* @param slen Current stream length (updated in-place)
* @param y Current y position (updated in-place: decremented by 18)
* @param font_size Font size in points
* @param fmt printf-style format string for the text
*/
static void _pdf_add_text_line(char *stream_buf, size_t stream_size, int *slen, double *y,
int font_size, const char *fmt, ...)
{
char raw_line[512];
va_list args;
va_start(args, fmt);
vsnprintf(raw_line, sizeof(raw_line), fmt, args);
va_end(args);
char escaped[1024];
_pdf_escape_string(escaped, sizeof(escaped), raw_line);
*slen += snprintf(stream_buf + *slen, stream_size - (size_t)*slen,
"BT\n/F1 %d Tf\n50 %.0f Td\n(%s) Tj\nET\n",
font_size, *y, escaped);
*y -= 18;
}
/**
* Minimal PDF generator. Creates a single-page PDF with text content.
* Uses PDF 1.4 format with Type1 font (Helvetica). No external dependencies.
*/
static void _save_report_to_pdf_impl(const char *heat_exchanger_name,
const char *part_number,
const char *side_name,
double airflow,
double airflow_pressure,
const char *unit,
const double *barometric_pressure,
const double *airflow_temperature,
const double *corrected_airflow_pressure,
double pressure_drop_limit,
const char *test_result,
bool recalculated_required)
{
char date_str[11];
_get_date_string(date_str);
/* Build filename: <P/N>_<YYYY-MM-DD>.pdf in project root */
char filename[256];
snprintf(filename, sizeof(filename), "%s_%s.pdf", part_number, date_str);
FILE *f = fopen(filename, "wb");
if (!f)
{
printf("Error: Cannot create PDF file '%s'\n", filename);
return;
}
/* --- Build content stream in memory --- */
char stream_buf[4096];
int slen = 0;
double y = 750;
/* Title */
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 14, "%s", heat_exchanger_name);
/* Separator */
slen += snprintf(stream_buf + slen, sizeof(stream_buf) - (size_t)slen,
"50 %.0f m 550 %.0f l 0.5 0.5 0.5 RG 1 w S\n", y + 8, y + 8);
y -= 8;
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "P/N: %s", part_number);
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Side: %s", side_name);
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Airflow: %0.2f LB/MIN", airflow);
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Static inlet pressure: %0.2f %s", airflow_pressure, unit);
if (recalculated_required)
{
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Barometric pressure: %0.2f MBAR", *barometric_pressure);
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Airflow temperature: %0.2f C", *airflow_temperature);
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Corrected static pressure: %0.2f %s", *corrected_airflow_pressure, unit);
}
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Max allowed pressure drop: %0.2f %s", pressure_drop_limit, unit);
y -= 6;
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 11, "Test result: %s", test_result);
/* Footer */
y -= 6;
_pdf_add_text_line(stream_buf, sizeof(stream_buf), &slen, &y, 8, "Report date: %s", date_str);
unsigned int stream_len = (unsigned int)slen;
/* --- Write PDF file --- */
/* Object layout:
* 1 0 obj — Catalog
* 2 0 obj — Pages
* 3 0 obj — Page
* 4 0 obj — Content stream
* 5 0 obj — Font
*/
fprintf(f, "%%PDF-1.4\n");
unsigned int offsets[6] = {0};
offsets[1] = (unsigned int)ftell(f);
fprintf(f, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
offsets[2] = (unsigned int)ftell(f);
fprintf(f, "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
offsets[3] = (unsigned int)ftell(f);
fprintf(f, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
"/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n");
offsets[4] = (unsigned int)ftell(f);
fprintf(f, "4 0 obj\n<< /Length %u >>\nstream\n", stream_len);
fwrite(stream_buf, 1, stream_len, f);
fprintf(f, "\nendstream\nendobj\n");
offsets[5] = (unsigned int)ftell(f);
fprintf(f, "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n");
/* Cross-reference table */
unsigned int xref_offset = (unsigned int)ftell(f);
fprintf(f, "xref\n");
fprintf(f, "0 6\n");
fprintf(f, "0000000000 65535 f \n");
for (int i = 1; i <= 5; i++)
{
fprintf(f, "%010u 00000 n \n", offsets[i]);
}
fprintf(f, "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n");
fprintf(f, "%u\n%%%%EOF\n", xref_offset);
fclose(f);
printf("Report saved to: %s\n", filename);
}
void save_report_to_pdf(const char *heat_exchanger_name,
const char *part_number,
const char *side_name,
double airflow,
double airflow_pressure,
const char *unit,
const double *barometric_pressure,
const double *airflow_temperature,
const double *corrected_airflow_pressure,
double pressure_drop_limit,
const char *test_result,
bool recalculated_required)
{
_save_report_to_pdf_impl(heat_exchanger_name, part_number, side_name,
airflow, airflow_pressure, unit,
barometric_pressure, airflow_temperature,
corrected_airflow_pressure, pressure_drop_limit,
test_result, recalculated_required);
}
+43
View File
@@ -205,3 +205,46 @@ void get_airflow_temperature(double *airflow_temperature, const air_passage_data
* @param[in] data Pointer to air passage data containing operational limits (must not be NULL)
*/
void get_pressure_drop(double *pressure_drop_limit, const double *airflow, const double *airflow_pressure, double *corrected_airflow_pressure, const double *barometric_pressure, const double *airflow_temperature, const air_passage_data_t *data);
/**
* @brief Save the test report to a PDF file in the project root.
*
* Generates a minimal PDF file containing the test summary with P/N and date.
* The file is saved as "<P/N>_YYYY-MM-DD.pdf" in the project root directory.
*
* @param heat_exchanger_name Full heat exchanger description string
* @param part_number Part number string (e.g. "182820-1")
* @param side_name Air passage side description string
* @param airflow Airflow value in LB/MIN
* @param airflow_pressure Static inlet pressure value
* @param unit Pressure unit string ("KPA" or "PSIG")
* @param barometric_pressure Barometric pressure value (may be NULL if not applicable)
* @param airflow_temperature Airflow temperature value (may be NULL if not applicable)
* @param corrected_airflow_pressure Corrected static inlet pressure (may be NULL if not applicable)
* @param pressure_drop_limit Max allowed pressure drop limit
* @param test_result "PASSED" or "FAILED"
* @param recalculated_required Flag indicating if recalculation was performed
*/
void save_report_to_pdf(const char *heat_exchanger_name,
const char *part_number,
const char *side_name,
double airflow,
double airflow_pressure,
const char *unit,
const double *barometric_pressure,
const double *airflow_temperature,
const double *corrected_airflow_pressure,
double pressure_drop_limit,
const char *test_result,
bool recalculated_required);
/**
* @brief Ask the user a yes/no question.
*
* Prompts the user with a yes/no question. Returns true for "yes", false for "no".
* Handles EXIT command.
*
* @param[out] answer Pointer to store the answer (true=yes, false=no, EXIT=quit)
* @param[in] prompt Question to display
*/
void get_yes_no(bool *answer, const char *prompt);
Binary file not shown.
+20
View File
@@ -95,6 +95,26 @@ int main(void)
printf("Test result: %s\n", test_pressure <= pressure_drop_limit ? "PASSED" : "FAILED");
printf("***************************************************\n");
printf("\n");
/* Offer to save report as PDF */
bool save_pdf = false;
get_yes_no(&save_pdf, "Save report to PDF?");
if (save_pdf)
{
save_report_to_pdf(
he->heat_exchanger_description,
he->heat_exchanger_type_data[part_number].part_number_description,
hetd->air_passages_data[air_passage_number].air_passage_description,
airflow,
airflow_pressure,
unit,
apd->recalculated_required ? &barometric_pressure : NULL,
apd->recalculated_required ? &airflow_temperature : NULL,
apd->recalculated_required ? &corrected_airflow_pressure : NULL,
pressure_drop_limit,
test_pressure <= pressure_drop_limit ? "PASSED" : "FAILED",
apd->recalculated_required);
}
printf("\n");
uint16_t command = 0;
get_user_command(&command);
EXIT_CHECK(command);