diff --git a/ats0002/ats0002.c b/ats0002/ats0002.c index b2281b4..98d2423 100644 --- a/ats0002/ats0002.c +++ b/ats0002/ats0002.c @@ -1,4 +1,6 @@ #include "ats0002.h" +#include +#include /** * @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); /** @@ -330,4 +357,239 @@ static void _calculate_density_corrected_pressure(double *corrected_airflow_pres double value_6 = *airflow_temperature + 273.2; 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:

_.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); } \ No newline at end of file diff --git a/ats0002/ats0002.h b/ats0002/ats0002.h index cde4e90..1d14556 100644 --- a/ats0002/ats0002.h +++ b/ats0002/ats0002.h @@ -204,4 +204,47 @@ void get_airflow_temperature(double *airflow_temperature, const air_passage_data * @param[in] airflow_temperature Pointer to store the current airflow temperature value * @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); \ No newline at end of file +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 "

_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); \ No newline at end of file diff --git a/heat_exchander_airflow_pressure_drop b/heat_exchander_airflow_pressure_drop index 05f1171..754ad1c 100755 Binary files a/heat_exchander_airflow_pressure_drop and b/heat_exchander_airflow_pressure_drop differ diff --git a/heat_exchander_airflow_pressure_drop.c b/heat_exchander_airflow_pressure_drop.c index bfe7a43..c17a6fb 100644 --- a/heat_exchander_airflow_pressure_drop.c +++ b/heat_exchander_airflow_pressure_drop.c @@ -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);