diff --git a/ReadMe.md b/ReadMe.md index d3bf7f56d..946afe3bf 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -95,6 +95,7 @@ Also check changelog in releases for latest updates! - i2c Tools [(By NaejEL)](https://github.com/NaejEL/flipperzero-i2ctools) - C0 -> SCL / C1 -> SDA / GND -> GND | 3v3 logic levels only! - Temperature Sensor Plugin - HTU21D / SI7021 [(By Mywk)](https://github.com/Mywk/FlipperTemperatureSensor) - [How to Connect](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/applications/plugins/temperature_sensor/Readme.md) - HC-SR04 Distance sensor - Ported and modified by @xMasterX [(original by Sanqui)](https://github.com/Sanqui/flipperzero-firmware/tree/hc_sr04) - How to connect -> (5V -> VCC) / (GND -> GND) / (13|TX -> Trig) / (14|RX -> Echo) +- Morse Code [(by wh00hw)](https://github.com/wh00hw/MorseCodeFAP) Games: - DOOM (fixed) [(By p4nic4ttack)](https://github.com/p4nic4ttack/doom-flipper-zero/) diff --git a/applications/plugins/dht_temp_sensor/DHT.c b/applications/plugins/dht_temp_sensor/DHT.c new file mode 100644 index 000000000..63a189ce1 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/DHT.c @@ -0,0 +1,169 @@ +#include "DHT.h" + +#define lineDown() furi_hal_gpio_write(sensor->GPIO, false) +#define lineUp() furi_hal_gpio_write(sensor->GPIO, true) +#define getLine() furi_hal_gpio_read(sensor->GPIO) +#define Delay(d) furi_delay_ms(d) + +DHT_data DHT_getData(DHT_sensor* sensor) { + DHT_data data = {-128.0f, -128.0f}; + +#if DHT_POLLING_CONTROL == 1 + /* Ограничение по частоте опроса датчика */ + //Определение интервала опроса в зависимости от датчика + uint16_t pollingInterval; + if(sensor->type == DHT11) { + pollingInterval = DHT_POLLING_INTERVAL_DHT11; + } else { + pollingInterval = DHT_POLLING_INTERVAL_DHT22; + } + + //Если интервал маленький, то возврат последнего удачного значения + if((furi_get_tick() - sensor->lastPollingTime < pollingInterval) && + sensor->lastPollingTime != 0) { + data.hum = sensor->lastHum; + data.temp = sensor->lastTemp; + return data; + } + sensor->lastPollingTime = furi_get_tick() + 1; +#endif + + //Опускание линии данных на 18 мс + lineDown(); +#ifdef DHT_IRQ_CONTROL + //Выключение прерываний, чтобы ничто не мешало обработке данных + __disable_irq(); +#endif + Delay(18); + + //Подъём линии + lineUp(); + + /* Ожидание ответа от датчика */ + uint16_t timeout = 0; + while(!getLine()) { + timeout++; + if(timeout > DHT_TIMEOUT) { +#ifdef DHT_IRQ_CONTROL + __enable_irq(); +#endif + //Если датчик не отозвался, значит его точно нет + //Обнуление последнего удачного значения, чтобы + //не получать фантомные значения + sensor->lastHum = -128.0f; + sensor->lastTemp = -128.0f; + + return data; + } + } + //Ожидание спада + while(getLine()) { + timeout++; + if(timeout > DHT_TIMEOUT) { +#ifdef DHT_IRQ_CONTROL + __enable_irq(); +#endif + //Если датчик не отозвался, значит его точно нет + //Обнуление последнего удачного значения, чтобы + //не получать фантомные значения + sensor->lastHum = -128.0f; + sensor->lastTemp = -128.0f; + + return data; + } + } + timeout = 0; + //Ожидание подъёма + while(!getLine()) { + timeout++; + if(timeout > DHT_TIMEOUT) { + if(timeout > DHT_TIMEOUT) { +#ifdef DHT_IRQ_CONTROL + __enable_irq(); +#endif + //Если датчик не отозвался, значит его точно нет + //Обнуление последнего удачного значения, чтобы + //не получать фантомные значения + sensor->lastHum = -128.0f; + sensor->lastTemp = -128.0f; + + return data; + } + } + } + timeout = 0; + //Ожидание спада + while(getLine()) { + timeout++; + if(timeout > DHT_TIMEOUT) { +#ifdef DHT_IRQ_CONTROL + __enable_irq(); +#endif + //Если датчик не отозвался, значит его точно нет + //Обнуление последнего удачного значения, чтобы + //не получать фантомные значения + sensor->lastHum = -128.0f; + sensor->lastTemp = -128.0f; + return data; + } + } + + /* Чтение ответа от датчика */ + uint8_t rawData[5] = {0, 0, 0, 0, 0}; + for(uint8_t a = 0; a < 5; a++) { + for(uint8_t b = 7; b != 255; b--) { + uint16_t hT = 0, lT = 0; + //Пока линия в низком уровне, инкремент переменной lT + while(!getLine() && lT != 65535) lT++; + //Пока линия в высоком уровне, инкремент переменной hT + timeout = 0; + while(getLine() && hT != 65535) hT++; + //Если hT больше lT, то пришла единица + if(hT > lT) rawData[a] |= (1 << b); + } + } +#ifdef DHT_IRQ_CONTROL + //Включение прерываний после приёма данных + __enable_irq(); +#endif + /* Проверка целостности данных */ + if((uint8_t)(rawData[0] + rawData[1] + rawData[2] + rawData[3]) == rawData[4]) { + //Если контрольная сумма совпадает, то конвертация и возврат полученных значений + if(sensor->type == DHT22) { + data.hum = (float)(((uint16_t)rawData[0] << 8) | rawData[1]) * 0.1f; + //Проверка на отрицательность температуры + if(!(rawData[2] & (1 << 7))) { + data.temp = (float)(((uint16_t)rawData[2] << 8) | rawData[3]) * 0.1f; + } else { + rawData[2] &= ~(1 << 7); + data.temp = (float)(((uint16_t)rawData[2] << 8) | rawData[3]) * -0.1f; + } + } + if(sensor->type == DHT11) { + data.hum = (float)rawData[0]; + data.temp = (float)rawData[2]; + //DHT11 производства ASAIR имеют дробную часть в температуре + //А ещё температуру измеряет от -20 до +60 *С + //Вот прикол, да? + if(rawData[3] != 0) { + //Проверка знака + if(!(rawData[3] & (1 << 7))) { + //Добавление положительной дробной части + data.temp += rawData[3] * 0.1f; + } else { + //А тут делаем отрицательное значение + rawData[3] &= ~(1 << 7); + data.temp += rawData[3] * 0.1f; + data.temp *= -1; + } + } + } + } + +#if DHT_POLLING_CONTROL == 1 + sensor->lastHum = data.hum; + sensor->lastTemp = data.temp; +#endif + + return data; +} \ No newline at end of file diff --git a/applications/plugins/dht_temp_sensor/DHT.h b/applications/plugins/dht_temp_sensor/DHT.h new file mode 100644 index 000000000..5b460e5cf --- /dev/null +++ b/applications/plugins/dht_temp_sensor/DHT.h @@ -0,0 +1,39 @@ +#ifndef DHT_H_ +#define DHT_H_ + +#include + +/* Настройки */ +#define DHT_TIMEOUT 65534 //Количество итераций, после которых функция вернёт пустые значения +#define DHT_POLLING_CONTROL 1 //Включение проверки частоты опроса датчика +#define DHT_POLLING_INTERVAL_DHT11 \ + 2000 //Интервал опроса DHT11 (0.5 Гц по даташиту). Можно поставить 1500, будет работать +#define DHT_POLLING_INTERVAL_DHT22 1000 //Интервал опроса DHT22 (1 Гц по даташиту) +#define DHT_IRQ_CONTROL //Выключать прерывания во время обмена данных с датчиком +/* Структура возвращаемых датчиком данных */ +typedef struct { + float hum; + float temp; +} DHT_data; + +/* Тип используемого датчика */ +typedef enum { DHT11, DHT22 } DHT_type; + +/* Структура объекта датчика */ +typedef struct { + char name[11]; + const GpioPin* GPIO; //Пин датчика + DHT_type type; //Тип датчика (DHT11 или DHT22) + +//Контроль частоты опроса датчика. Значения не заполнять! +#if DHT_POLLING_CONTROL == 1 + uint32_t lastPollingTime; //Время последнего опроса датчика + float lastTemp; //Последнее значение температуры + float lastHum; //Последнее значение влажности +#endif +} DHT_sensor; + +/* Прототипы функций */ +DHT_data DHT_getData(DHT_sensor* sensor); //Получить данные с датчика + +#endif diff --git a/applications/plugins/dht_temp_sensor/application.fam b/applications/plugins/dht_temp_sensor/application.fam new file mode 100644 index 000000000..ad74919a4 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/application.fam @@ -0,0 +1,13 @@ +App( + appid="quenon_dht_mon", + name="[DHT] Temp. Monitor", + apptype=FlipperAppType.EXTERNAL, + entry_point="quenon_dht_mon_app", + cdefines=["QUENON_DHT_MON"], + requires=[ + "gui", + ], + fap_category="GPIO", + fap_icon="icon.png", + stack_size=2 * 1024, +) \ No newline at end of file diff --git a/applications/plugins/dht_temp_sensor/icon.png b/applications/plugins/dht_temp_sensor/icon.png new file mode 100644 index 000000000..1730432e7 Binary files /dev/null and b/applications/plugins/dht_temp_sensor/icon.png differ diff --git a/applications/plugins/dht_temp_sensor/quenon_dht_mon.c b/applications/plugins/dht_temp_sensor/quenon_dht_mon.c new file mode 100644 index 000000000..bbb91ec3d --- /dev/null +++ b/applications/plugins/dht_temp_sensor/quenon_dht_mon.c @@ -0,0 +1,460 @@ +#include "quenon_dht_mon.h" + +//Порты ввода/вывода, которые не были обозначены в общем списке +const GpioPin SWC_10 = {.pin = LL_GPIO_PIN_14, .port = GPIOA}; +const GpioPin SIO_12 = {.pin = LL_GPIO_PIN_13, .port = GPIOA}; +const GpioPin TX_13 = {.pin = LL_GPIO_PIN_6, .port = GPIOB}; +const GpioPin RX_14 = {.pin = LL_GPIO_PIN_7, .port = GPIOB}; + +//Количество доступных портов ввода/вывода +#define GPIO_ITEMS (sizeof(gpio_item) / sizeof(GpioItem)) + +//Перечень достуных портов ввода/вывода +static const GpioItem gpio_item[] = { + {2, "2", &gpio_ext_pa7}, + {3, "3", &gpio_ext_pa6}, + {4, "4", &gpio_ext_pa4}, + {5, "5", &gpio_ext_pb3}, + {6, "6", &gpio_ext_pb2}, + {7, "7", &gpio_ext_pc3}, + {10, "10 (SWC)", &SWC_10}, + {12, "12 (SIO)", &SIO_12}, + {13, "13 (TX)", &TX_13}, + {14, "14 (RX)", &RX_14}, + {15, "15", &gpio_ext_pc1}, + {16, "16", &gpio_ext_pc0}, + {17, "17 (1W)", &ibutton_gpio}}; + +//Данные плагина +static PluginData* app; + +uint8_t DHTMon_GPIO_to_int(const GpioPin* gpio) { + if(gpio == NULL) return 255; + for(uint8_t i = 0; i < GPIO_ITEMS; i++) { + if(gpio_item[i].pin->pin == gpio->pin && gpio_item[i].pin->port == gpio->port) { + return gpio_item[i].num; + } + } + return 255; +} + +const GpioPin* DHTMon_GPIO_form_int(uint8_t name) { + for(uint8_t i = 0; i < GPIO_ITEMS; i++) { + if(gpio_item[i].num == name) { + return gpio_item[i].pin; + } + } + return NULL; +} + +const GpioPin* DHTMon_GPIO_from_index(uint8_t index) { + if(index > GPIO_ITEMS) return NULL; + return gpio_item[index].pin; +} + +uint8_t DHTMon_GPIO_to_index(const GpioPin* gpio) { + if(gpio == NULL) return 255; + for(uint8_t i = 0; i < GPIO_ITEMS; i++) { + if(gpio_item[i].pin->pin == gpio->pin && gpio_item[i].pin->port == gpio->port) { + return i; + } + } + return 255; +} + +const char* DHTMon_GPIO_getName(const GpioPin* gpio) { + if(gpio == NULL) return NULL; + for(uint8_t i = 0; i < GPIO_ITEMS; i++) { + if(gpio_item[i].pin->pin == gpio->pin && gpio_item[i].pin->port == gpio->port) { + return gpio_item[i].name; + } + } + return NULL; +} + +void DHTMon_sensors_init(void) { + //Включение 5V если на порту 1 FZ его нет + if(furi_hal_power_is_otg_enabled() != true) { + furi_hal_power_enable_otg(); + } + + //Настройка GPIO загруженных датчиков + for(uint8_t i = 0; i < app->sensors_count; i++) { + //Высокий уровень по умолчанию + furi_hal_gpio_write(app->sensors[i].GPIO, true); + //Режим работы - OpenDrain, подтяжка включается на всякий случай + furi_hal_gpio_init( + app->sensors[i].GPIO, //Порт FZ + GpioModeOutputOpenDrain, //Режим работы - открытый сток + GpioPullUp, //Принудительная подтяжка линии данных к питанию + GpioSpeedVeryHigh); //Скорость работы - максимальная + } +} + +void DHTMon_sensors_deinit(void) { + //Возврат исходного состояния 5V + if(app->last_OTG_State != true) { + furi_hal_power_disable_otg(); + } + + //Перевод портов GPIO в состояние по умолчанию + for(uint8_t i = 0; i < app->sensors_count; i++) { + furi_hal_gpio_init( + app->sensors[i].GPIO, //Порт FZ + GpioModeAnalog, //Режим работы - аналог + GpioPullNo, //Отключение подтяжки + GpioSpeedLow); //Скорость работы - низкая + //Установка низкого уровня + furi_hal_gpio_write(app->sensors[i].GPIO, false); + } +} + +bool DHTMon_sensor_check(DHT_sensor* sensor) { + /* Проверка имени */ + //1) Строка должна быть длиной от 1 до 10 символов + //2) Первый символ строки должен быть только 0-9, A-Z, a-z и _ + if(strlen(sensor->name) == 0 || strlen(sensor->name) > 10 || + (!(sensor->name[0] >= '0' && sensor->name[0] <= '9') && + !(sensor->name[0] >= 'A' && sensor->name[0] <= 'Z') && + !(sensor->name[0] >= 'a' && sensor->name[0] <= 'z') && !(sensor->name[0] == '_'))) { + FURI_LOG_D(APP_NAME, "Sensor [%s] name check failed\r\n", sensor->name); + return false; + } + //Проверка GPIO + if(DHTMon_GPIO_to_int(sensor->GPIO) == 255) { + FURI_LOG_D( + APP_NAME, + "Sensor [%s] GPIO check failed: %d\r\n", + sensor->name, + DHTMon_GPIO_to_int(sensor->GPIO)); + return false; + } + //Проверка типа датчика + if(sensor->type != DHT11 && sensor->type != DHT22) { + FURI_LOG_D(APP_NAME, "Sensor [%s] type check failed: %d\r\n", sensor->name, sensor->type); + return false; + } + + //Возврат истины если всё ок + FURI_LOG_D(APP_NAME, "Sensor [%s] all checks passed\r\n", sensor->name); + return true; +} + +void DHTMon_sensor_delete(DHT_sensor* sensor) { + if(sensor == NULL) return; + //Делаем параметры датчика неверными + sensor->name[0] = '\0'; + sensor->type = 255; + //Теперь сохраняем текущие датчики. Сохранятор не сохранит неисправный датчик + DHTMon_sensors_save(); + //Перезагружаемся с SD-карты + DHTMon_sensors_reload(); +} + +uint8_t DHTMon_sensors_save(void) { + //Выделение памяти для потока + app->file_stream = file_stream_alloc(app->storage); + uint8_t savedSensorsCount = 0; + //Переменная пути к файлу + char filepath[sizeof(APP_PATH_FOLDER) + sizeof(APP_FILENAME)] = {0}; + //Составление пути к файлу + strcpy(filepath, APP_PATH_FOLDER); + strcat(filepath, "/"); + strcat(filepath, APP_FILENAME); + + //Открытие потока. Если поток открылся, то выполнение сохранения датчиков + if(file_stream_open(app->file_stream, filepath, FSAM_READ_WRITE, FSOM_CREATE_ALWAYS)) { + const char template[] = + "#DHT monitor sensors file\n#Name - name of sensor. Up to 10 sumbols\n#Type - type of sensor. DHT11 - 0, DHT22 - 1\n#GPIO - connection port. May being 2-7, 10, 12-17\n#Name Type GPIO\n"; + stream_write(app->file_stream, (uint8_t*)template, strlen(template)); + //Сохранение датчиков + for(uint8_t i = 0; i < app->sensors_count; i++) { + //Если параметры датчика верны, то сохраняемся + if(DHTMon_sensor_check(&app->sensors[i])) { + stream_write_format( + app->file_stream, + "%s %d %d\n", + app->sensors[i].name, + app->sensors[i].type, + DHTMon_GPIO_to_int(app->sensors[i].GPIO)); + savedSensorsCount++; + } + } + } else { + //TODO: печать ошибки на экран + FURI_LOG_E(APP_NAME, "cannot create sensors file\r\n"); + } + stream_free(app->file_stream); + + return savedSensorsCount; +} + +bool DHTMon_sensors_load(void) { + //Обнуление количества датчиков + app->sensors_count = -1; + //Очистка предыдущих датчиков + memset(app->sensors, 0, sizeof(app->sensors)); + + //Открытие файла на SD-карте + //Выделение памяти для потока + app->file_stream = file_stream_alloc(app->storage); + //Переменная пути к файлу + char filepath[sizeof(APP_PATH_FOLDER) + sizeof(APP_FILENAME)] = {0}; + //Составление пути к файлу + strcpy(filepath, APP_PATH_FOLDER); + strcat(filepath, "/"); + strcat(filepath, APP_FILENAME); + //Открытие потока к файлу + if(!file_stream_open(app->file_stream, filepath, FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) { + //Если файл отсутствует, то создание болванки + FURI_LOG_W(APP_NAME, "Missing sensors file. Creating new file\r\n"); + app->sensors_count = 0; + stream_free(app->file_stream); + DHTMon_sensors_save(); + return false; + } + //Вычисление размера файла + size_t file_size = stream_size(app->file_stream); + if(file_size == (size_t)0) { + //Выход если файл пустой + FURI_LOG_W(APP_NAME, "Sensors file is empty\r\n"); + app->sensors_count = 0; + stream_free(app->file_stream); + return false; + } + + //Выделение памяти под загрузку файла + uint8_t* file_buf = malloc(file_size); + //Опустошение буфера файла + memset(file_buf, 0, file_size); + //Загрузка файла + if(stream_read(app->file_stream, file_buf, file_size) != file_size) { + //Выход при ошибке чтения + FURI_LOG_E(APP_NAME, "Error reading sensor file\r\n"); + app->sensors_count = 0; + stream_free(app->file_stream); + return false; + } + //Построчное чтение файла + char* line = strtok((char*)file_buf, "\n"); + while(line != NULL && app->sensors_count < MAX_SENSORS) { + if(line[0] != '#') { + DHT_sensor s = {0}; + int type, port; + char name[11]; + sscanf(line, "%s %d %d", name, &type, &port); + s.type = type; + s.GPIO = DHTMon_GPIO_form_int(port); + + name[10] = '\0'; + strcpy(s.name, name); + //Если данные корректны, то + if(DHTMon_sensor_check(&s) == true) { + //Установка нуля при первом датчике + if(app->sensors_count == -1) app->sensors_count = 0; + //Добавление датчика в общий список + app->sensors[app->sensors_count] = s; + //Увеличение количества загруженных датчиков + app->sensors_count++; + } + } + line = strtok((char*)NULL, "\n"); + } + stream_free(app->file_stream); + free(file_buf); + + //Обнуление количества датчиков если ни один из них не был загружен + if(app->sensors_count == -1) app->sensors_count = 0; + + //Инициализация портов датчиков если таковые есть + if(app->sensors_count > 0) { + DHTMon_sensors_init(); + return true; + } else { + return false; + } + return false; +} + +bool DHTMon_sensors_reload(void) { + DHTMon_sensors_deinit(); + return DHTMon_sensors_load(); +} + +/** + * @brief Обработчик отрисовки экрана + * + * @param canvas Указатель на холст + * @param ctx Данные плагина + */ +static void render_callback(Canvas* const canvas, void* ctx) { + PluginData* app = acquire_mutex((ValueMutex*)ctx, 25); + if(app == NULL) { + return; + } + //Вызов отрисовки главного экрана + scene_main(canvas, app); + + release_mutex((ValueMutex*)ctx, app); +} + +/** + * @brief Обработчик нажатия кнопок главного экрана + * + * @param input_event Указатель на событие + * @param event_queue Указатель на очередь событий + */ +static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { + furi_assert(event_queue); + + PluginEvent event = {.type = EventTypeKey, .input = *input_event}; + furi_message_queue_put(event_queue, &event, FuriWaitForever); +} + +/** + * @brief Выделение места под переменные плагина + * + * @return true Если всё прошло успешно + * @return false Если в процессе загрузки произошла ошибка + */ +static bool DHTMon_alloc(void) { + //Выделение места под данные плагина + app = malloc(sizeof(PluginData)); + //Выделение места под очередь событий + app->event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); + + //Обнуление количества датчиков + app->sensors_count = -1; + + //Инициализация мутекса + if(!init_mutex(&app->state_mutex, app, sizeof(PluginData))) { + FURI_LOG_E(APP_NAME, "cannot create mutex\r\n"); + return false; + } + + // Set system callbacks + app->view_port = view_port_alloc(); + view_port_draw_callback_set(app->view_port, render_callback, &app->state_mutex); + view_port_input_callback_set(app->view_port, input_callback, app->event_queue); + + // Open GUI and register view_port + app->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + + app->view_dispatcher = view_dispatcher_alloc(); + + sensorActions_sceneCreate(app); + sensorEdit_sceneCreate(app); + + app->widget = widget_alloc(); + view_dispatcher_add_view(app->view_dispatcher, WIDGET_VIEW, widget_get_view(app->widget)); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, TEXTINPUT_VIEW, text_input_get_view(app->text_input)); + + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + //Уведомления + app->notifications = furi_record_open(RECORD_NOTIFICATION); + + //Подготовка хранилища + app->storage = furi_record_open(RECORD_STORAGE); + storage_common_mkdir(app->storage, APP_PATH_FOLDER); + app->file_stream = file_stream_alloc(app->storage); + + return true; +} + +/** + * @brief Освыбождение памяти после работы приложения + */ +static void DHTMon_free(void) { + //Автоматическое управление подсветкой + notification_message(app->notifications, &sequence_display_backlight_enforce_auto); + + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_NOTIFICATION); + + text_input_free(app->text_input); + widget_free(app->widget); + sensorEdit_sceneRemove(); + sensorActions_screneRemove(); + view_dispatcher_free(app->view_dispatcher); + + furi_record_close(RECORD_GUI); + + 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); + delete_mutex(&app->state_mutex); + + free(app); +} + +/** + * @brief Точка входа в приложение + * + * @return Код ошибки + */ +int32_t quenon_dht_mon_app() { + if(!DHTMon_alloc()) { + DHTMon_free(); + return 255; + } + //Постоянное свечение подсветки + notification_message(app->notifications, &sequence_display_backlight_enforce_on); + //Сохранение состояния наличия 5V на порту 1 FZ + app->last_OTG_State = furi_hal_power_is_otg_enabled(); + + //Загрузка датчиков с SD-карты + DHTMon_sensors_load(); + + app->currentSensorEdit = &app->sensors[0]; + + PluginEvent event; + for(bool processing = true; processing;) { + FuriStatus event_status = furi_message_queue_get(app->event_queue, &event, 100); + + acquire_mutex_block(&app->state_mutex); + + if(event_status == FuriStatusOk) { + // press events + if(event.type == EventTypeKey) { + if(event.input.type == InputTypePress) { + switch(event.input.key) { + case InputKeyUp: + break; + case InputKeyDown: + break; + case InputKeyRight: + break; + case InputKeyLeft: + break; + case InputKeyOk: + view_port_update(app->view_port); + release_mutex(&app->state_mutex, app); + mainMenu_scene(app); + break; + case InputKeyBack: + processing = false; + break; + } + } + } + } + + view_port_update(app->view_port); + release_mutex(&app->state_mutex, app); + } + //Освобождение памяти и деинициализация + DHTMon_sensors_deinit(); + DHTMon_free(); + + return 0; +} +//TODO: Обработка ошибок +//TODO: Пропуск использованных портов в меню добавления датчиков \ No newline at end of file diff --git a/applications/plugins/dht_temp_sensor/quenon_dht_mon.h b/applications/plugins/dht_temp_sensor/quenon_dht_mon.h new file mode 100644 index 000000000..4e888f6c1 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/quenon_dht_mon.h @@ -0,0 +1,176 @@ +#ifndef QUENON_DHT_MON +#define QUENON_DHT_MON + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "DHT.h" + +#define APP_NAME "DHT_monitor" +#define APP_PATH_FOLDER "/ext/dht_monitor" +#define APP_FILENAME "sensors.txt" +#define MAX_SENSORS 5 + +// //Виды менюшек +typedef enum { + MAIN_MENU_VIEW, + ADDSENSOR_MENU_VIEW, + TEXTINPUT_VIEW, + SENSOR_ACTIONS_VIEW, + WIDGET_VIEW, +} MENU_VIEWS; + +typedef enum { + EventTypeTick, + EventTypeKey, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} PluginEvent; + +typedef struct { + const uint8_t num; + const char* name; + const GpioPin* pin; +} GpioItem; + +//Структура с данными плагина +typedef struct { + //Очередь сообщений + FuriMessageQueue* event_queue; + //Мутекс + ValueMutex state_mutex; + //Вьюпорт + ViewPort* view_port; + //GUI + Gui* gui; + NotificationApp* notifications; + ViewDispatcher* view_dispatcher; + View* view; + TextInput* text_input; + VariableItem* item; + Widget* widget; + + char txtbuff[30]; //Буффер для печати строк на экране + bool last_OTG_State; //Состояние OTG до запуска приложения + Storage* storage; //Хранилище датчиков + Stream* file_stream; //Поток файла с датчиками + int8_t sensors_count; // Количество загруженных датчиков + DHT_sensor sensors[MAX_SENSORS]; //Сохранённые датчики + DHT_data data; //Инфа из датчика + DHT_sensor* currentSensorEdit; //Указатель на редактируемый датчик + +} PluginData; + +/* ================== Работа с GPIO ================== */ +/** + * @brief Конвертация GPIO в его номер на корпусе FZ + * + * @param gpio Указатель на преобразовываемый GPIO + * @return Номер порта на корпусе FZ + */ +uint8_t DHTMon_GPIO_to_int(const GpioPin* gpio); +/** + * @brief Конвертация номера порта на корпусе FZ в GPIO + * + * @param name Номер порта на корпусе FZ + * @return Указатель на GPIO при успехе, NULL при ошибке + */ +const GpioPin* DHTMon_GPIO_form_int(uint8_t name); +/** + * @brief Преобразование порядкового номера порта в GPIO + * + * @param index Индекс порта от 0 до GPIO_ITEMS-1 + * @return Указатель на GPIO при успехе, NULL при ошибке + */ +const GpioPin* DHTMon_GPIO_from_index(uint8_t index); +/** + * @brief Преобразование GPIO в порядковый номер порта + * + * @param gpio Указатель на GPIO + * @return index при успехе, 255 при ошибке + */ +uint8_t DHTMon_GPIO_to_index(const GpioPin* gpio); + +/** + * @brief Получить имя GPIO в виде строки + * + * @param gpio Искомый порт + * @return char* Указатель на строку с именем порта + */ +const char* DHTMon_GPIO_getName(const GpioPin* gpio); + +/* ================== Работа с датчиками ================== */ +/** + * @brief Инициализация портов ввода/вывода датчиков + */ +void DHTMon_sensors_init(void); +/** + * @brief Функция деинициализации портов ввода/вывода датчиков + */ +void DHTMon_sensors_deinit(void); +/** + * @brief Проверка корректности параметров датчика + * + * @param sensor Указатель на проверяемый датчик + * @return true Параметры датчика корректные + * @return false Параметры датчика некорректные + */ +bool DHTMon_sensor_check(DHT_sensor* sensor); +/** + * @brief Удаление датчика из списка и перезагрузка + * + * @param sensor Указатель на удаляемый датчик + */ +void DHTMon_sensor_delete(DHT_sensor* sensor); +/** + * @brief Сохранение датчиков на SD-карту + * + * @return Количество сохранённых датчиков + */ +uint8_t DHTMon_sensors_save(void); +/** + * @brief Загрузка датчиков с SD-карты + * + * @return true Был загружен хотя бы 1 датчик + * @return false Датчики отсутствуют + */ +bool DHTMon_sensors_load(void); +/** + * @brief Перезагрузка датчиков с SD-карты + * + * @return true Когда был загружен хотя бы 1 датчик + * @return false Ни один из датчиков не был загружен + */ +bool DHTMon_sensors_reload(void); + +void scene_main(Canvas* const canvas, PluginData* app); +void mainMenu_scene(PluginData* app); + +void sensorEdit_sceneCreate(PluginData* app); +void sensorEdit_scene(PluginData* app); +void sensorEdit_sceneRemove(void); + +void sensorActions_sceneCreate(PluginData* app); +void sensorActions_scene(PluginData* app); +void sensorActions_screneRemove(void); +#endif \ No newline at end of file diff --git a/applications/plugins/dht_temp_sensor/scenes/DHTMon_mainMenu_scene.c b/applications/plugins/dht_temp_sensor/scenes/DHTMon_mainMenu_scene.c new file mode 100644 index 000000000..26ac9ca89 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/scenes/DHTMon_mainMenu_scene.c @@ -0,0 +1,157 @@ +#include "../quenon_dht_mon.h" +//Текущий вид +static View* view; +//Список +static VariableItemList* variable_item_list; + +/** + * @brief Функция обработки нажатия кнопки "Назад" + * + * @param context Указатель на данные приложения + * @return ID вида в который нужно переключиться + */ +static uint32_t actions_exitCallback(void* context) { + PluginData* app = context; + UNUSED(app); + //Возвращаем ID вида, в который нужно вернуться + return VIEW_NONE; +} +/** + * @brief Функция обработки нажатия средней кнопки + * + * @param context Указатель на данные приложения + * @param index На каком элементе списка была нажата кнопка + */ +static void enterCallback(void* context, uint32_t index) { + PluginData* app = context; + if((uint8_t)index < (uint8_t)app->sensors_count) { + app->currentSensorEdit = &app->sensors[index]; + sensorActions_scene(app); + } + if((uint8_t)index == (uint8_t)app->sensors_count) { + app->currentSensorEdit = &app->sensors[app->sensors_count++]; + strcpy(app->currentSensorEdit->name, "NewSensor"); + app->currentSensorEdit->GPIO = DHTMon_GPIO_from_index(0); + app->currentSensorEdit->type = DHT11; + sensorEdit_scene(app); + } +} + +/** + * @brief Создание списка действий с указанным датчиком + * + * @param app Указатель на данные плагина + */ +void mainMenu_scene(PluginData* app) { + variable_item_list = variable_item_list_alloc(); + //Сброс всех элементов меню + variable_item_list_reset(variable_item_list); + //Добавление названий датчиков в качестве элементов списка + for(uint8_t i = 0; i < app->sensors_count; i++) { + variable_item_list_add(variable_item_list, app->sensors[i].name, 1, NULL, NULL); + } + if(app->sensors_count < (uint8_t)MAX_SENSORS) { + variable_item_list_add(variable_item_list, " + Add new sensor +", 1, NULL, NULL); + } + + //Добавление колбека на нажатие средней кнопки + variable_item_list_set_enter_callback(variable_item_list, enterCallback, app); + + //Создание вида из списка + view = variable_item_list_get_view(variable_item_list); + //Добавление колбека на нажатие кнопки "Назад" + view_set_previous_callback(view, actions_exitCallback); + //Добавление вида в диспетчер + view_dispatcher_add_view(app->view_dispatcher, MAIN_MENU_VIEW, view); + + view_dispatcher_enable_queue(app->view_dispatcher); + + //Переключение на наш вид + view_dispatcher_switch_to_view(app->view_dispatcher, MAIN_MENU_VIEW); + + //Запуск диспетчера + view_dispatcher_run(app->view_dispatcher); + + //Очистка списка элементов + variable_item_list_free(variable_item_list); + //Удаление вида после обработки + view_dispatcher_remove_view(app->view_dispatcher, MAIN_MENU_VIEW); +} + +/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +*/ + +// static VariableItemList* variable_item_list; +// /* ============== Главное меню ============== */ +// static uint32_t mainMenu_exitCallback(void* context) { +// UNUSED(context); +// variable_item_list_free(variable_item_list); +// DHT_sensors_reload(); +// return VIEW_NONE; +// } +// static void mainMenu_enterCallback(void* context, uint32_t index) { +// PluginData* app = context; +// if((uint8_t)index == (uint8_t)app->sensors_count) { +// addSensor_scene(app); +// view_dispatcher_run(app->view_dispatcher); +// } +// } +// void mainMenu_scene(PluginData* app) { +// variable_item_list = variable_item_list_alloc(); +// variable_item_list_reset(variable_item_list); +// for(uint8_t i = 0; i < app->sensors_count; i++) { +// variable_item_list_add(variable_item_list, app->sensors[i].name, 1, NULL, NULL); +// } +// variable_item_list_add(variable_item_list, "+ Add new sensor +", 1, NULL, NULL); + +// app->view = variable_item_list_get_view(variable_item_list); +// app->view_dispatcher = view_dispatcher_alloc(); + +// view_dispatcher_enable_queue(app->view_dispatcher); +// view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); +// view_dispatcher_add_view(app->view_dispatcher, MAIN_MENU_VIEW, app->view); +// view_dispatcher_switch_to_view(app->view_dispatcher, MAIN_MENU_VIEW); + +// variable_item_list_set_enter_callback(variable_item_list, mainMenu_enterCallback, app); +// view_set_previous_callback(app->view, mainMenu_exitCallback); +// } \ No newline at end of file diff --git a/applications/plugins/dht_temp_sensor/scenes/DHTMon_main_scene.c b/applications/plugins/dht_temp_sensor/scenes/DHTMon_main_scene.c new file mode 100644 index 000000000..aab343752 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/scenes/DHTMon_main_scene.c @@ -0,0 +1,40 @@ +#include "../quenon_dht_mon.h" + +/* ============== Главный экран ============== */ +void scene_main(Canvas* const canvas, PluginData* app) { + //Рисование бара + canvas_draw_box(canvas, 0, 0, 128, 14); + canvas_set_color(canvas, ColorWhite); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 32, 11, "DHT Monitor"); + + canvas_set_color(canvas, ColorBlack); + if(app->sensors_count > 0) { + if(!furi_hal_power_is_otg_enabled()) { + furi_hal_power_enable_otg(); + } + for(uint8_t i = 0; i < app->sensors_count; i++) { + app->data = DHT_getData(&app->sensors[i]); + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, 24 + 10 * i, app->sensors[i].name); + + canvas_set_font(canvas, FontSecondary); + if(app->data.hum == -128.0f && app->data.temp == -128.0f) { + canvas_draw_str(canvas, 96, 24 + 10 * i, "timeout"); + } else { + snprintf( + app->txtbuff, + sizeof(app->txtbuff), + "%2.1f*C/%d%%", + (double)app->data.temp, + (int8_t)app->data.hum); + canvas_draw_str(canvas, 64, 24 + 10 * i, app->txtbuff); + } + } + } else { + canvas_set_font(canvas, FontSecondary); + if(app->sensors_count == 0) canvas_draw_str(canvas, 0, 24, "Sensors not found"); + if(app->sensors_count == -1) canvas_draw_str(canvas, 0, 24, "Loading..."); + } +} diff --git a/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorActions_scene.c b/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorActions_scene.c new file mode 100644 index 000000000..ae7674f70 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorActions_scene.c @@ -0,0 +1,194 @@ +#include "../quenon_dht_mon.h" + +//Текущий вид +static View* view; +//Список +static VariableItemList* variable_item_list; + +/* ================== Информация о датчике ================== */ +/** + * @brief Функция обработки нажатия кнопки "Назад" + * + * @param context Указатель на данные приложения + * @return ID вида в который нужно переключиться + */ +static uint32_t infoWidget_exitCallback(void* context) { + PluginData* app = context; + UNUSED(app); + //Возвращаем ID вида, в который нужно вернуться + return SENSOR_ACTIONS_VIEW; +} +/** + * @brief Обработчик нажатий на кнопку в виджете + * + * @param result Какая из кнопок была нажата + * @param type Тип нажатия + * @param context Указатель на данные плагина + */ +static void infoWidget_callback(GuiButtonType result, InputType type, void* context) { + PluginData* app = context; + //Коротко нажата левая кнопка (Back) + if(result == GuiButtonTypeLeft && type == InputTypeShort) { + view_dispatcher_switch_to_view(app->view_dispatcher, SENSOR_ACTIONS_VIEW); + } +} +/** + * @brief Создание виджета информации о датчике + * + * @param app Указатель на данные плагина + */ +static void sensorInfo_widget(PluginData* app) { + //Очистка виджета + widget_reset(app->widget); + //Добавление кнопок + widget_add_button_element(app->widget, GuiButtonTypeLeft, "Back", infoWidget_callback, app); + + char str[32]; + snprintf(str, sizeof(str), "\e#%s\e#", app->currentSensorEdit->name); + widget_add_text_box_element(app->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, str, false); + snprintf(str, sizeof(str), "\e#Type:\e# %s", app->currentSensorEdit->type ? "DHT22" : "DHT11"); + widget_add_text_box_element(app->widget, 0, 0, 128, 47, AlignLeft, AlignCenter, str, false); + snprintf( + str, sizeof(str), "\e#GPIO:\e# %s", DHTMon_GPIO_getName(app->currentSensorEdit->GPIO)); + widget_add_text_box_element(app->widget, 0, 0, 128, 72, AlignLeft, AlignCenter, str, false); + view_set_previous_callback(widget_get_view(app->widget), infoWidget_exitCallback); + view_dispatcher_switch_to_view(app->view_dispatcher, WIDGET_VIEW); +} + +/* ================== Подтверждение удаления ================== */ +/** + * @brief Функция обработки нажатия кнопки "Назад" + * + * @param context Указатель на данные приложения + * @return ID вида в который нужно переключиться + */ +static uint32_t deleteWidget_exitCallback(void* context) { + PluginData* app = context; + UNUSED(app); + //Возвращаем ID вида, в который нужно вернуться + return SENSOR_ACTIONS_VIEW; +} +/** + * @brief Обработчик нажатий на кнопку в виджете + * + * @param result Какая из кнопок была нажата + * @param type Тип нажатия + * @param context Указатель на данные плагина + */ +static void deleteWidget_callback(GuiButtonType result, InputType type, void* context) { + PluginData* app = context; + //Коротко нажата левая кнопка (Cancel) + if(result == GuiButtonTypeLeft && type == InputTypeShort) { + view_dispatcher_switch_to_view(app->view_dispatcher, SENSOR_ACTIONS_VIEW); + } + //Коротко нажата правая кнопка (Delete) + if(result == GuiButtonTypeRight && type == InputTypeShort) { + //Удаление датчика + DHTMon_sensor_delete(app->currentSensorEdit); + //Выход из меню + view_dispatcher_switch_to_view(app->view_dispatcher, VIEW_NONE); + } +} +/** + * @brief Создание виджета удаления датчика + * + * @param app Указатель на данные плагина + */ +static void sensorDelete_widget(PluginData* app) { + //Очистка виджета + widget_reset(app->widget); + //Добавление кнопок + widget_add_button_element( + app->widget, GuiButtonTypeLeft, "Cancel", deleteWidget_callback, app); + widget_add_button_element( + app->widget, GuiButtonTypeRight, "Delete", deleteWidget_callback, app); + + char delete_str[32]; + snprintf(delete_str, sizeof(delete_str), "\e#Delete %s?\e#", app->currentSensorEdit->name); + widget_add_text_box_element( + app->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, delete_str, false); + snprintf( + delete_str, + sizeof(delete_str), + "\e#Type:\e# %s", + app->currentSensorEdit->type ? "DHT22" : "DHT11"); + widget_add_text_box_element( + app->widget, 0, 0, 128, 47, AlignLeft, AlignCenter, delete_str, false); + snprintf( + delete_str, + sizeof(delete_str), + "\e#GPIO:\e# %s", + DHTMon_GPIO_getName(app->currentSensorEdit->GPIO)); + widget_add_text_box_element( + app->widget, 0, 0, 128, 72, AlignLeft, AlignCenter, delete_str, false); + view_set_previous_callback(widget_get_view(app->widget), deleteWidget_exitCallback); + view_dispatcher_switch_to_view(app->view_dispatcher, WIDGET_VIEW); +} + +/* ================== Меню действий ================== */ +/** + * @brief Функция обработки нажатия средней кнопки + * + * @param context Указатель на данные приложения + * @param index На каком элементе списка была нажата кнопка + */ +static void enterCallback(void* context, uint32_t index) { + PluginData* app = context; + if(index == 0) { + sensorInfo_widget(app); + } + if(index == 1) { + sensorEdit_scene(app); + } + if(index == 2) { + sensorDelete_widget(app); + } +} + +/** + * @brief Функция обработки нажатия кнопки "Назад" + * + * @param context Указатель на данные приложения + * @return ID вида в который нужно переключиться + */ +static uint32_t actions_exitCallback(void* context) { + PluginData* app = context; + UNUSED(app); + //Возвращаем ID вида, в который нужно вернуться + return MAIN_MENU_VIEW; +} + +/** + * @brief Создание списка действий с указанным датчиком + * + * @param app Указатель на данные плагина + */ +void sensorActions_sceneCreate(PluginData* app) { + variable_item_list = variable_item_list_alloc(); + //Сброс всех элементов меню + variable_item_list_reset(variable_item_list); + //Добавление элементов в список + variable_item_list_add(variable_item_list, "Info", 0, NULL, NULL); + variable_item_list_add(variable_item_list, "Edit", 0, NULL, NULL); + variable_item_list_add(variable_item_list, "Delete", 0, NULL, NULL); + + //Добавление колбека на нажатие средней кнопки + variable_item_list_set_enter_callback(variable_item_list, enterCallback, app); + + //Создание вида из списка + view = variable_item_list_get_view(variable_item_list); + //Добавление колбека на нажатие кнопки "Назад" + view_set_previous_callback(view, actions_exitCallback); + //Добавление вида в диспетчер + view_dispatcher_add_view(app->view_dispatcher, SENSOR_ACTIONS_VIEW, view); +} +void sensorActions_scene(PluginData* app) { + //Сброс выбранного пункта в ноль + variable_item_list_set_selected_item(variable_item_list, 0); + //Переключение на наш вид + view_dispatcher_switch_to_view(app->view_dispatcher, SENSOR_ACTIONS_VIEW); +} + +void sensorActions_screneRemove(void) { + variable_item_list_free(variable_item_list); +} diff --git a/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorEdit_scene.c b/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorEdit_scene.c new file mode 100644 index 000000000..b9c5a1134 --- /dev/null +++ b/applications/plugins/dht_temp_sensor/scenes/DHTMon_sensorEdit_scene.c @@ -0,0 +1,100 @@ +#include "../quenon_dht_mon.h" + +static VariableItem* nameItem; +static VariableItemList* variable_item_list; + +static const char* const sensorsTypes[2] = { + "DHT11", + "DHT22", +}; + +// /* ============== Добавление датчика ============== */ +static uint32_t addSensor_exitCallback(void* context) { + UNUSED(context); + DHTMon_sensors_reload(); + return VIEW_NONE; +} + +static void addSensor_sensorTypeChanged(VariableItem* item) { + uint8_t index = variable_item_get_current_value_index(item); + PluginData* app = variable_item_get_context(item); + variable_item_set_current_value_text(item, sensorsTypes[index]); + app->currentSensorEdit->type = index; +} + +static void addSensor_GPIOChanged(VariableItem* item) { + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, DHTMon_GPIO_getName(DHTMon_GPIO_from_index(index))); + PluginData* app = variable_item_get_context(item); + app->currentSensorEdit->GPIO = DHTMon_GPIO_from_index(index); +} + +static void addSensor_sensorNameChanged(void* context) { + PluginData* app = context; + variable_item_set_current_value_text(nameItem, app->currentSensorEdit->name); + view_dispatcher_switch_to_view(app->view_dispatcher, ADDSENSOR_MENU_VIEW); +} +static void addSensor_sensorNameChange(PluginData* app) { + text_input_set_header_text(app->text_input, "Sensor name"); + //По неясной мне причине в длину строки входит терминатор. Поэтому при длине 10 приходится указывать 11 + text_input_set_result_callback( + app->text_input, addSensor_sensorNameChanged, app, app->currentSensorEdit->name, 11, true); + view_dispatcher_switch_to_view(app->view_dispatcher, TEXTINPUT_VIEW); +} + +static void addSensor_enterCallback(void* context, uint32_t index) { + PluginData* app = context; + if(index == 0) { + addSensor_sensorNameChange(app); + } + if(index == 3) { + //Сохранение датчика + DHTMon_sensors_save(); + DHTMon_sensors_reload(); + view_dispatcher_switch_to_view(app->view_dispatcher, VIEW_NONE); + } +} + +void sensorEdit_sceneCreate(PluginData* app) { + variable_item_list = variable_item_list_alloc(); + + variable_item_list_reset(variable_item_list); + + variable_item_list_set_enter_callback(variable_item_list, addSensor_enterCallback, app); + + app->view = variable_item_list_get_view(variable_item_list); + + view_set_previous_callback(app->view, addSensor_exitCallback); + + view_dispatcher_add_view(app->view_dispatcher, ADDSENSOR_MENU_VIEW, app->view); +} +void sensorEdit_scene(PluginData* app) { + //Очистка списка + variable_item_list_reset(variable_item_list); + + //Имя редактируемого датчика + nameItem = variable_item_list_add(variable_item_list, "Name: ", 1, NULL, NULL); + variable_item_set_current_value_index(nameItem, 0); + variable_item_set_current_value_text(nameItem, app->currentSensorEdit->name); + + //Тип датчика + app->item = + variable_item_list_add(variable_item_list, "Type:", 2, addSensor_sensorTypeChanged, app); + + variable_item_set_current_value_index(nameItem, app->currentSensorEdit->type); + variable_item_set_current_value_text(app->item, sensorsTypes[app->currentSensorEdit->type]); + + //GPIO + app->item = + variable_item_list_add(variable_item_list, "GPIO:", 13, addSensor_GPIOChanged, app); + variable_item_set_current_value_index( + app->item, DHTMon_GPIO_to_index(app->currentSensorEdit->GPIO)); + variable_item_set_current_value_text( + app->item, DHTMon_GPIO_getName(app->currentSensorEdit->GPIO)); + variable_item_list_add(variable_item_list, "Save", 1, NULL, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ADDSENSOR_MENU_VIEW); +} +void sensorEdit_sceneRemove(void) { + variable_item_list_free(variable_item_list); +} \ No newline at end of file diff --git a/applications/plugins/morse_code/application.fam b/applications/plugins/morse_code/application.fam new file mode 100644 index 000000000..47a986233 --- /dev/null +++ b/applications/plugins/morse_code/application.fam @@ -0,0 +1,15 @@ +App( + appid="morse_code", + name="Morse Code", + apptype=FlipperAppType.EXTERNAL, + entry_point="morse_code_app", + cdefines=["APP_MORSE_CODE"], + requires=[ + "gui", + ], + stack_size=1 * 1024, + order=20, + fap_icon="morse_code_10px.png", + fap_category="Music" + +) \ No newline at end of file diff --git a/applications/plugins/morse_code/morse_code.c b/applications/plugins/morse_code/morse_code.c new file mode 100644 index 000000000..0b4790721 --- /dev/null +++ b/applications/plugins/morse_code/morse_code.c @@ -0,0 +1,161 @@ +#include "morse_code_worker.h" +#include +#include +#include +#include +#include +#include +#include + +static const float MORSE_CODE_VOLUMES[] = {0, .25, .5, .75, 1}; + +typedef struct { + FuriString* words; + uint8_t volume; + uint32_t dit_delta; +} MorseCodeModel; + +typedef struct { + MorseCodeModel* model; + FuriMutex** model_mutex; + + FuriMessageQueue* input_queue; + + ViewPort* view_port; + Gui* gui; + + MorseCodeWorker* worker; +} MorseCode; + + +static void render_callback(Canvas* const canvas, void* ctx) { + MorseCode* morse_code = ctx; + furi_check(furi_mutex_acquire(morse_code->model_mutex, FuriWaitForever) == FuriStatusOk); + // border around the edge of the screen + canvas_set_font(canvas, FontPrimary); + + //write words + elements_multiline_text_aligned(canvas, 64, 30, AlignCenter, AlignCenter, furi_string_get_cstr(morse_code->model->words)); + + // volume view_port + uint8_t vol_bar_x_pos = 124; + uint8_t vol_bar_y_pos = 0; + const uint8_t volume_h = + (64 / (COUNT_OF(MORSE_CODE_VOLUMES) - 1)) * morse_code->model->volume; + canvas_draw_frame(canvas, vol_bar_x_pos, vol_bar_y_pos, 4, 64); + canvas_draw_box(canvas, vol_bar_x_pos, vol_bar_y_pos + (64 - volume_h), 4, volume_h); + + //dit bpm + canvas_draw_str_aligned( + canvas, 0, 10, AlignLeft, AlignCenter, furi_string_get_cstr(furi_string_alloc_printf("Dit: %ld ms", morse_code->model->dit_delta))); + + //button info + elements_button_center(canvas, "Press/Hold"); + furi_mutex_release(morse_code->model_mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + MorseCode* morse_code = ctx; + furi_message_queue_put(morse_code->input_queue, input_event, FuriWaitForever); +} + +static void morse_code_worker_callback( + FuriString* words, + void* context) { + MorseCode* morse_code = context; + furi_check(furi_mutex_acquire(morse_code->model_mutex, FuriWaitForever) == FuriStatusOk); + morse_code->model->words = words; + furi_mutex_release(morse_code->model_mutex); + view_port_update(morse_code->view_port); +} + +MorseCode* morse_code_alloc() { + MorseCode* instance = malloc(sizeof(MorseCode)); + + instance->model = malloc(sizeof(MorseCodeModel)); + instance->model->words = furi_string_alloc_set_str(""); + instance->model->volume = 3; + instance->model->dit_delta = 150; + instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + instance->worker = morse_code_worker_alloc(); + + morse_code_worker_set_callback(instance->worker, morse_code_worker_callback, instance); + + instance->view_port = view_port_alloc(); + view_port_draw_callback_set(instance->view_port, render_callback, instance); + view_port_input_callback_set(instance->view_port, input_callback, instance); + + // Open GUI and register view_port + instance->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + return instance; +} + +void morse_code_free(MorseCode* instance) { + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close(RECORD_GUI); + view_port_free(instance->view_port); + + morse_code_worker_free(instance->worker); + + furi_message_queue_free(instance->input_queue); + + furi_mutex_free(instance->model_mutex); + + free(instance->model); + free(instance); +} + +int32_t morse_code_app() { + MorseCode* morse_code = morse_code_alloc(); + InputEvent input; + morse_code_worker_start(morse_code->worker); + morse_code_worker_set_volume( + morse_code->worker, MORSE_CODE_VOLUMES[morse_code->model->volume]); + morse_code_worker_set_dit_delta(morse_code->worker, morse_code->model->dit_delta); + while(furi_message_queue_get(morse_code->input_queue, &input, FuriWaitForever) == FuriStatusOk){ + furi_check(furi_mutex_acquire(morse_code->model_mutex, FuriWaitForever) == FuriStatusOk); + if(input.key == InputKeyBack) { + furi_mutex_release(morse_code->model_mutex); + break; + }else if(input.key == InputKeyOk){ + if(input.type == InputTypePress) + morse_code_worker_play(morse_code->worker, true); + else if(input.type == InputTypeRelease) + morse_code_worker_play(morse_code->worker, false); + }else if(input.key == InputKeyUp && input.type == InputTypePress){ + if(morse_code->model->volume < COUNT_OF(MORSE_CODE_VOLUMES) - 1) + morse_code->model->volume++; + morse_code_worker_set_volume( + morse_code->worker, MORSE_CODE_VOLUMES[morse_code->model->volume]); + }else if(input.key == InputKeyDown && input.type == InputTypePress){ + if(morse_code->model->volume > 0) + morse_code->model->volume--; + morse_code_worker_set_volume( + morse_code->worker, MORSE_CODE_VOLUMES[morse_code->model->volume]); + }else if(input.key == InputKeyLeft && input.type == InputTypePress){ + if(morse_code->model->dit_delta > 10) + morse_code->model->dit_delta-=10; + morse_code_worker_set_dit_delta( + morse_code->worker, morse_code->model->dit_delta); + } + else if(input.key == InputKeyRight && input.type == InputTypePress){ + if(morse_code->model->dit_delta >= 10) + morse_code->model->dit_delta+=10; + morse_code_worker_set_dit_delta( + morse_code->worker, morse_code->model->dit_delta); + } + + FURI_LOG_D("Input", "%s %s %ld", input_get_key_name(input.key), input_get_type_name(input.type), input.sequence); + + furi_mutex_release(morse_code->model_mutex); + view_port_update(morse_code->view_port); + } + morse_code_worker_stop(morse_code->worker); + morse_code_free(morse_code); + return 0; +} \ No newline at end of file diff --git a/applications/plugins/morse_code/morse_code_10px.png b/applications/plugins/morse_code/morse_code_10px.png new file mode 100644 index 000000000..087c5b239 Binary files /dev/null and b/applications/plugins/morse_code/morse_code_10px.png differ diff --git a/applications/plugins/morse_code/morse_code_worker.c b/applications/plugins/morse_code/morse_code_worker.c new file mode 100644 index 000000000..54ee747c5 --- /dev/null +++ b/applications/plugins/morse_code/morse_code_worker.c @@ -0,0 +1,164 @@ +#include "morse_code_worker.h" +#include +#include + + +#define TAG "MorseCodeWorker" + +#define MORSE_CODE_VERSION 0 + +//A-Z0-1 +const char morse_array[36][6] ={ + ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", + "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", ".----", "..---", "...--", "....-", ".....", + "-....", "--...", "---..", "----.", "-----" + }; +const char symbol_array[36] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; + +struct MorseCodeWorker { + FuriThread* thread; + MorseCodeWorkerCallback callback; + void* callback_context; + bool is_running; + bool play; + float volume; + uint32_t dit_delta; + FuriString* buffer; + FuriString* words; +}; + +void morse_code_worker_fill_buffer(MorseCodeWorker* instance, uint32_t duration){ + FURI_LOG_D("MorseCode: Duration", "%ld", duration); + if( duration <= instance->dit_delta) + furi_string_push_back(instance->buffer, *DOT); + else if(duration <= (instance->dit_delta * 3)) + furi_string_push_back(instance->buffer, *LINE); + if(furi_string_size(instance->buffer) > 5) + furi_string_reset(instance->buffer); + FURI_LOG_D("MorseCode: Buffer", "%s", furi_string_get_cstr(instance->buffer)); +} + +void morse_code_worker_fill_letter(MorseCodeWorker* instance){ + if(furi_string_size(instance->words) > 63) + furi_string_reset(instance->words); + for (size_t i = 0; i < sizeof(morse_array); i++){ + if(furi_string_cmp_str(instance->buffer, morse_array[i]) == 0){ + furi_string_push_back(instance->words, symbol_array[i]); + furi_string_reset(instance->buffer); + break; + } + } + FURI_LOG_D("MorseCode: Words", "%s", furi_string_get_cstr(instance->words)); +} + + +static int32_t morse_code_worker_thread_callback(void* context) { + furi_assert(context); + MorseCodeWorker* instance = context; + bool was_playing = false; + uint32_t start_tick = 0; + uint32_t end_tick = 0; + bool pushed = true; + bool spaced = true; + while(instance->is_running){ + furi_delay_ms(SLEEP); + if(instance->play){ + if(!was_playing){ + start_tick = furi_get_tick(); + furi_hal_speaker_start(FREQUENCY, instance->volume); + was_playing = true; + } + }else{ + if(was_playing){ + pushed = false; + spaced = false; + furi_hal_speaker_stop(); + end_tick = furi_get_tick(); + was_playing = false; + morse_code_worker_fill_buffer(instance, end_tick - start_tick); + start_tick = 0; + } + } + if(!pushed){ + if(end_tick + (instance->dit_delta * 3) < furi_get_tick()){ + //NEW LETTER + morse_code_worker_fill_letter(instance); + if(instance->callback) + instance->callback(instance->words, instance->callback_context); + pushed = true; + } + } + if(!spaced){ + if(end_tick + (instance->dit_delta * 7) < furi_get_tick()){ + //NEW WORD + furi_string_push_back(instance->words, *SPACE); + if(instance->callback) + instance->callback(instance->words, instance->callback_context); + spaced = true; + } + } + } + return 0; +} + +MorseCodeWorker* morse_code_worker_alloc() { + MorseCodeWorker* instance = malloc(sizeof(MorseCodeWorker)); + instance->thread = furi_thread_alloc(); + furi_thread_set_name(instance->thread, "MorseCodeWorker"); + furi_thread_set_stack_size(instance->thread, 1024); + furi_thread_set_context(instance->thread, instance); + furi_thread_set_callback(instance->thread, morse_code_worker_thread_callback); + instance->play = false; + instance->volume = 1.0f; + instance->dit_delta = 150; + instance->buffer = furi_string_alloc_set_str(""); + instance->words = furi_string_alloc_set_str(""); + return instance; +} + +void morse_code_worker_free(MorseCodeWorker* instance) { + furi_assert(instance); + furi_thread_free(instance->thread); + free(instance); +} + +void morse_code_worker_set_callback( + MorseCodeWorker* instance, + MorseCodeWorkerCallback callback, + void* context) { + furi_assert(instance); + instance->callback = callback; + instance->callback_context = context; +} + +void morse_code_worker_play(MorseCodeWorker* instance, bool play){ + furi_assert(instance); + instance->play = play; +} + +void morse_code_worker_set_volume(MorseCodeWorker* instance, float level){ + furi_assert(instance); + instance->volume = level; +} + +void morse_code_worker_set_dit_delta(MorseCodeWorker* instance, uint32_t delta){ + furi_assert(instance); + instance->dit_delta = delta; +} + +void morse_code_worker_start(MorseCodeWorker* instance) { + furi_assert(instance); + furi_assert(instance->is_running == false); + instance->is_running = true; + furi_thread_start(instance->thread); + FURI_LOG_D("MorseCode: Start", "is Running"); +} + +void morse_code_worker_stop(MorseCodeWorker* instance) { + furi_assert(instance); + furi_assert(instance->is_running == true); + instance->is_running = false; + furi_thread_join(instance->thread); + FURI_LOG_D("MorseCode: Stop", "Stop"); +} diff --git a/applications/plugins/morse_code/morse_code_worker.h b/applications/plugins/morse_code/morse_code_worker.h new file mode 100644 index 000000000..cc84a2674 --- /dev/null +++ b/applications/plugins/morse_code/morse_code_worker.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#define FREQUENCY 261.63f +#define SLEEP 10 +#define DOT "." +#define LINE "-" +#define SPACE " " + +typedef void (*MorseCodeWorkerCallback)( + FuriString* buffer, + void* context); + +typedef struct MorseCodeWorker MorseCodeWorker; + +MorseCodeWorker* morse_code_worker_alloc(); + +void morse_code_worker_free(MorseCodeWorker* instance); + +void morse_code_worker_set_callback( + MorseCodeWorker* instance, + MorseCodeWorkerCallback callback, + void* context); + +void morse_code_worker_start(MorseCodeWorker* instance); + +void morse_code_worker_stop(MorseCodeWorker* instance); + +void morse_code_worker_play(MorseCodeWorker* instance, bool play); + +void morse_code_worker_set_volume(MorseCodeWorker* instance, float level); + +void morse_code_worker_set_dit_delta(MorseCodeWorker* instance, uint32_t delta); + + + + + + diff --git a/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c b/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c index 71438e1c9..fe95ab6a9 100644 --- a/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c +++ b/applications/plugins/usbkeyboard/views/usb_hid_keyboard.c @@ -43,7 +43,7 @@ typedef struct { #define KEY_WIDTH 9 #define KEY_HEIGHT 12 #define KEY_PADDING 1 -#define ROW_COUNT 6 +#define ROW_COUNT 7 #define COLUMN_COUNT 12 // 0 width items are not drawn, but there value is used @@ -136,6 +136,21 @@ const UsbHidKeyboardKey usb_hid_keyboard_keyset[ROW_COUNT][COLUMN_COUNT] = { {.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB}, {.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB}, }, + { + {.width = 1, .icon = NULL, .key = "1", .shift_key = "1", .value = HID_KEYBOARD_F1}, + {.width = 1, .icon = NULL, .key = "2", .shift_key = "2", .value = HID_KEYBOARD_F2}, + {.width = 1, .icon = NULL, .key = "3", .shift_key = "3", .value = HID_KEYBOARD_F3}, + {.width = 1, .icon = NULL, .key = "4", .shift_key = "4", .value = HID_KEYBOARD_F4}, + {.width = 1, .icon = NULL, .key = "5", .shift_key = "5", .value = HID_KEYBOARD_F5}, + {.width = 1, .icon = NULL, .key = "6", .shift_key = "6", .value = HID_KEYBOARD_F6}, + {.width = 1, .icon = NULL, .key = "7", .shift_key = "7", .value = HID_KEYBOARD_F7}, + {.width = 1, .icon = NULL, .key = "8", .shift_key = "8", .value = HID_KEYBOARD_F8}, + {.width = 1, .icon = NULL, .key = "9", .shift_key = "9", .value = HID_KEYBOARD_F9}, + {.width = 1, .icon = NULL, .key = "0", .shift_key = "0", .value = HID_KEYBOARD_F10}, + {.width = 1, .icon = NULL, .key = "1", .shift_key = "1", .value = HID_KEYBOARD_F11}, + {.width = 1, .icon = NULL, .key = "2", .shift_key = "2", .value = HID_KEYBOARD_F12}, + } + }; static void usb_hid_keyboard_to_upper(char* str) { @@ -217,6 +232,7 @@ static void usb_hid_keyboard_draw_callback(Canvas* canvas, void* context) { // Select if back is clicked and its the backspace key // Deselect when the button clicked or not hovered bool keySelected = (x <= model->x && model->x < (x + key.width)) && y == model->y; + keySelected = y == ROW_COUNT - 1 ? !keySelected : keySelected; bool backSelected = model->back_pressed && key.value == HID_KEYBOARD_DELETE; usb_hid_keyboard_draw_key( canvas, @@ -369,4 +385,4 @@ void usb_hid_keyboard_free(UsbHidKeyboard* usb_hid_keyboard) { View* usb_hid_keyboard_get_view(UsbHidKeyboard* usb_hid_keyboard) { furi_assert(usb_hid_keyboard); return usb_hid_keyboard->view; -} \ No newline at end of file +}