#include "mh_z19_icons.h" #include #include #include #include #include #include #include typedef enum { GreenStatus, YellowStatus, RedStatus, } StatusPPM; typedef enum { RANGE_2000 = 2000, RANGE_5000 = 5000 } SensorRange; struct MHZ19App { Gui *gui; ViewPort *view_port; FuriMessageQueue *event_queue; FuriMutex *mutex; NotificationApp *notifications; bool have_5v; int32_t current_page; StatusPPM status_ppm; SensorRange sensor_range; const GpioPin *input_pin; int32_t co2_ppm; }; typedef struct MHZ19App MHZ19App; const NotificationSequence green_led_sequence = { &message_green_255, &message_do_not_reset, NULL, }; const NotificationSequence yellow_led_sequence = { &message_green_255, &message_red_255, &message_do_not_reset, &message_vibro_on, &message_note_c5, &message_delay_50, &message_vibro_off, &message_sound_off, NULL, }; const NotificationSequence red_led_sequence = { &message_red_255, &message_do_not_reset, &message_vibro_on, &message_note_c5, &message_delay_50, &message_vibro_off, &message_sound_off, NULL, }; void mh_z19app_draw_callback(Canvas *canvas, void *ctx) { furi_assert(ctx); MHZ19App *app = ctx; canvas_clear(canvas); if (!app->have_5v) { canvas_set_font(canvas, FontPrimary); elements_multiline_text_aligned( canvas, 4, 28, AlignLeft, AlignTop, "5V on GPIO must be\nenabled, or USB must\nbe connected."); return; } else if (app->current_page == 0) { canvas_set_font(canvas, FontPrimary); elements_multiline_text_aligned( canvas, 4, 1, AlignLeft, AlignTop, "Connect sensor MH-Z19 to pins:\n5V -> 1\nGND -> 8\nPWM -> 3\nPress center button..."); return; } else if (app->current_page == 1) { canvas_set_font(canvas, FontPrimary); elements_multiline_text_aligned( canvas, 4, 1, AlignLeft, AlignTop, "Select sensor measuring range by arrows.\nAvailable:\n\t- 0-2000ppm\n\t- 0-5000ppm\nPress center button..."); return; } FuriString *strbuf = furi_string_alloc(); const Icon *icon; FuriString *status_text = furi_string_alloc(); switch (app->status_ppm) { case GreenStatus: icon = &I_passport_okay1_46x49; furi_string_set_str(status_text, "it's OK!"); break; case YellowStatus: icon = &I_passport_bad1_46x49; furi_string_set_str(status_text, "Not good!"); break; case RedStatus: icon = &I_passport_bad3_46x49; furi_string_set_str(status_text, "Very bad!"); break; default: icon = &I_passport_okay1_46x49; furi_string_set_str(status_text, "It's OK!"); break; } const Icon *co2_icon = &I_co2; canvas_draw_icon(canvas, 9, 7, icon); canvas_draw_icon(canvas, 59, 8, co2_icon); furi_string_printf(strbuf, "%ld", app->co2_ppm); canvas_set_font(canvas, FontBigNumbers); canvas_draw_str(canvas, 60, 40, furi_string_get_cstr(strbuf)); canvas_set_font(canvas, FontPrimary); canvas_draw_str(canvas, 60, 55, furi_string_get_cstr(status_text)); canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 110, 40, "ppm"); furi_string_printf(strbuf, "%d", app->sensor_range); canvas_set_font(canvas, FontKeyboard); canvas_draw_str(canvas, 104, 8, furi_string_get_cstr(strbuf)); furi_string_free(strbuf); furi_string_free(status_text); } void mh_z19app_input_callback(InputEvent *input_event, void *ctx) { furi_assert(ctx); FuriMessageQueue *event_queue = ctx; furi_message_queue_put(event_queue, input_event, FuriWaitForever); } MHZ19App *mh_z19app_alloc() { MHZ19App *app = (MHZ19App *)malloc(sizeof(MHZ19App)); app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); if (!app->mutex) { FURI_LOG_E("MH-Z19", "cannot create mutex\r\n"); free(app); return NULL; } furi_mutex_acquire(app->mutex, FuriWaitForever); app->view_port = view_port_alloc(); app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); view_port_draw_callback_set(app->view_port, mh_z19app_draw_callback, app); view_port_input_callback_set(app->view_port, mh_z19app_input_callback, app->event_queue); app->gui = furi_record_open(RECORD_GUI); gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); // Enable 5v power, multiple attempts to avoid issues with power chip protection false triggering uint8_t attempts = 0; while (!furi_hal_power_is_otg_enabled() && attempts++ < 5) { furi_hal_power_enable_otg(); furi_delay_ms(10); } if (furi_hal_power_is_otg_enabled() || furi_hal_power_is_charging()) { app->have_5v = true; } else { app->have_5v = false; } app->input_pin = &gpio_ext_pa6; furi_hal_gpio_init(app->input_pin, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh); app->notifications = furi_record_open(RECORD_NOTIFICATION); furi_mutex_release(app->mutex); return app; } void mh_z19app_free(MHZ19App *app) { furi_assert(app); if (furi_hal_power_is_otg_enabled()) { furi_hal_power_disable_otg(); } view_port_enabled_set(app->view_port, false); gui_remove_view_port(app->gui, app->view_port); view_port_free(app->view_port); furi_message_queue_free(app->event_queue); furi_hal_light_set(LightRed | LightGreen | LightBlue, 0x00); furi_record_close(RECORD_NOTIFICATION); furi_record_close(RECORD_GUI); furi_mutex_free(app->mutex); free(app); } void mh_z19app_init(MHZ19App *app) { furi_mutex_acquire(app->mutex, FuriWaitForever); app->co2_ppm = 0; app->status_ppm = GreenStatus; app->current_page = 0; app->sensor_range = RANGE_2000; app->have_5v = true; notification_message(app->notifications, &green_led_sequence); furi_mutex_release(app->mutex); } int32_t calculate_ppm( int32_t *prevVal, int32_t val, int32_t *th, int32_t *tl, int32_t *h, int32_t *l, SensorRange range) { int32_t tt = furi_get_tick(); if (val == 1) { if (val != *prevVal) { *h = tt; *tl = *h - *l; *prevVal = val; } } else { if (val != *prevVal) { *l = tt; *th = *l - *h; *prevVal = val; return range * (*th - 2) / (*th + *tl - 4); } } return -1; } void change_sensor_range(MHZ19App *app) { if (app->sensor_range == RANGE_2000) { app->sensor_range = RANGE_5000; } else { app->sensor_range = RANGE_2000; } } void send_notification_if_needed(MHZ19App *app, int32_t ppm) { if (ppm > 0) { if (ppm > 1000) { if (app->status_ppm != RedStatus) { notification_message(app->notifications, &red_led_sequence); app->status_ppm = RedStatus; } } else if (ppm > 800) { if (app->status_ppm != YellowStatus) { notification_message(app->notifications, &yellow_led_sequence); app->status_ppm = YellowStatus; } } else { if (app->status_ppm != GreenStatus) { notification_message(app->notifications, &green_led_sequence); app->status_ppm = GreenStatus; } } } } int32_t mh_z19_app(void *p) { UNUSED(p); MHZ19App *app = mh_z19app_alloc(); if (!app) { FURI_LOG_E("MH-Z19", "cannot create app\r\n"); return -1; } mh_z19app_init(app); InputEvent event; int32_t prevVal = 0; int32_t th, tl, h = 0; int32_t l = 0; int32_t ppm = 0; for (bool processing = true; processing;) { furi_mutex_acquire(app->mutex, FuriWaitForever); ppm = calculate_ppm( &prevVal, furi_hal_gpio_read(app->input_pin), &th, &tl, &h, &l, app->sensor_range); if (ppm > 0) { app->co2_ppm = ppm; } send_notification_if_needed(app, app->co2_ppm); if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) { if (event.type == InputTypeShort) { switch (event.key) { case InputKeyBack: processing = false; break; case InputKeyOk: app->current_page++; break; case InputKeyLeft: case InputKeyRight: change_sensor_range(app); break; default: break; } } } furi_mutex_release(app->mutex); view_port_update(app->view_port); } mh_z19app_free(app); return 0; }