From 904c982b92ca4b6aa4ba7d58548ddf22a4713cf3 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 17:58:55 -0600 Subject: [PATCH 01/43] Begin work on WiFi. Set up auto assets retrieval for WiFi development via batch. Update README. --- README.md | 10 ++- arduino-cli.yaml | 6 +- firmware-flash.bat | 55 ++++++++++----- firmware/stream_to_wifi.cpp | 61 +++++++++------- packages.bat | 136 ++++++++++++++++++++++++++++++++++++ 5 files changed, 219 insertions(+), 49 deletions(-) create mode 100644 packages.bat diff --git a/README.md b/README.md index 7ded4575970..4eff08e969a 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ That's it, let me know if you have any issues! https://dl.espressif.com/dl/package_esp32_index.json ``` + or (a backup) + + ```markdown + https://espressif.github.io/arduino-esp32/package_esp32_index.json + ``` + 6. In the Arduino IDE, go to `Tools > Board > Boards Manager`. 7. Search for `esp32` and install `esp32` by `Espressif Systems`. 8. Plug in your Flipper Zero via USB. Make sure qFlipper or something else isn't connected to it already after doing so. @@ -243,14 +249,14 @@ To contribute to this project, please follow the steps below: 4. Request PR [here][pull-request-link], introduce work via your branch. 5. Wait for review and merge. -When developing the firmware, be sure to download the dependencies by running the `firmware-flash.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies to the directories defined in the `arduino-cli.yaml` file post-run. Add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code, there's a guide on how to get started with that workflow here: +When developing the firmware, be sure to download the dependencies by running the `packages.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies to your Windows TEMP folder. You can run the same script to remove the temporary files when you're done using them. Add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code; there's a guide on how to get started with that workflow here: https://github.com/CodyTolene/Flipper-Zero-Development-Toolkit Example include path: ```markdown -C:/Users//AppData/Local/Temp/arduino-cli/** +C:/Users//AppData/Local/Temp/camera-suite-assets/** ``` Thank you for any and all contributions to this project, I'm looking forward to seeing what you come up with! If you have any questions, please let me know by opening an issue [here][issues-link]. diff --git a/arduino-cli.yaml b/arduino-cli.yaml index 15f76f7331d..5bcdb161312 100644 --- a/arduino-cli.yaml +++ b/arduino-cli.yaml @@ -7,9 +7,9 @@ build_cache: daemon: port: "50051" directories: - data: C:\temp\arduino-cli\data - downloads: C:\temp\arduino-cli\staging - user: C:\temp\arduino-cli\user + data: C:\temp\camera-suite-assets\data + downloads: C:\temp\camera-suite-assets\staging + user: C:\temp\camera-suite-assets\user library: enable_unsafe_install: false logging: diff --git a/firmware-flash.bat b/firmware-flash.bat index e1be4307666..e09370d74e8 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -4,9 +4,9 @@ setlocal EnableDelayedExpansion rem λ set CLI_FOUND_FOLLOW_UP=0 -set CLI_TEMP=%TEMP%\arduino-cli +set CLI_TEMP=%TEMP%\camera-suite-assets set COMPILE_FLAG=firmware\.compile.flag -set CONFIG_FILE=--config-file .\arduino-cli.yaml +set ARDUINO_CLI_CONFIG_FILE=--config-file .\arduino-cli.yaml set DEFAULT_BOARD_FQBN=esp32:esp32:esp32cam set FIRMWARE_SRC=firmware\firmware.ino set SELECTED_BOARD=%DEFAULT_BOARD_FQBN% @@ -28,6 +28,15 @@ echo - USB Channel = 1 (on newer firmware) echo - Baudrate = Host echo - UART Pins = 13,14 echo - RTS/DTR Pins = None +echo. +echo Notes: +echo - You must have Git installed to use this script. If you do not have Git +echo installed, please install it from the following link: +echo https://git-scm.com/downloads +echo - Temporary installation files will be installed to the following directory: +echo %CLI_TEMP% +echo - Temp files will take up approximately 6GB of storage space. +echo - You will have to option to delete the temp files after flashing. echo ------------------------------------------------------------------------------ echo. pause @@ -44,20 +53,20 @@ if not exist "arduino-cli.exe" ( echo When the file is ready, press any key to check again. set /a CLI_FOUND_FOLLOW_UP+=1 if %CLI_FOUND_FOLLOW_UP% geq 2 ( - echo If you're still having issues, feel free to open a ticket at the following link: + echo If you are still having issues, feel free to open a ticket at the following link: echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues ) pause goto :checkCLI ) if %CLI_FOUND_FOLLOW_UP% geq 1 ( - echo File "arduino-cli.exe" found. Continuing... + echo File "arduino-cli.exe" found successfully. Continuing... ) echo Checking configs... -arduino-cli %CONFIG_FILE% config set directories.data %CLI_TEMP%\data -arduino-cli %CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads -arduino-cli %CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* echo Fetching assets... set DATA_FLAG=0 @@ -68,8 +77,18 @@ if not exist "%CLI_TEMP%\downloads" ( set /a "DATA_FLAG+=1" ) if %DATA_FLAG% gtr 0 ( - arduino-cli %CONFIG_FILE% core update-index - arduino-cli %CONFIG_FILE% core install esp32:esp32 + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 + echo Cloning ESPAsyncWebServer repository... + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" ) else ( echo Assets already installed. Skipping... ) @@ -113,7 +132,7 @@ set RETRY_COUNT=1 :uploadLoop echo. echo Preparing firmware upload... Attempt number !RETRY_COUNT!... -arduino-cli %CONFIG_FILE% upload -p %PORT_NUMBER% --fqbn !SELECTED_BOARD! %FIRMWARE_SRC% +arduino-cli %ARDUINO_CLI_CONFIG_FILE% upload -p %PORT_NUMBER% --fqbn !SELECTED_BOARD! %FIRMWARE_SRC% if !ERRORLEVEL! EQU 0 ( goto :uploadSuccess ) else ( @@ -128,7 +147,7 @@ if !ERRORLEVEL! EQU 0 ( goto :uploadFirmware ) else ( echo. - echo If you're still having issues, feel free to open a ticket at the following link: + echo If you are still having issues, feel free to open a ticket at the following link: echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues echo. set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " @@ -147,9 +166,9 @@ echo. echo Firmware upload was successful. echo Cleaning up... echo Restoring default configs... -arduino-cli %CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data -arduino-cli %CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging -arduino-cli %CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% @@ -174,7 +193,7 @@ if /i "%USE_DEFAULT_BOARD%"=="N" ( echo. echo Compiling firmware, this will take a moment... echo. -arduino-cli %CONFIG_FILE% compile --fqbn !SELECTED_BOARD! %FIRMWARE_SRC% +arduino-cli %ARDUINO_CLI_CONFIG_FILE% compile --fqbn !SELECTED_BOARD! %FIRMWARE_SRC% if %ERRORLEVEL% EQU 0 ( echo. echo Firmware compiled successfully. @@ -188,9 +207,9 @@ if %ERRORLEVEL% EQU 0 ( ) echo Cleaning up... echo Restoring default configs... - arduino-cli %CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data - arduino-cli %CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging - arduino-cli %CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp index 12681c1c6ed..5f7c3e589b5 100644 --- a/firmware/stream_to_wifi.cpp +++ b/firmware/stream_to_wifi.cpp @@ -1,11 +1,21 @@ #include "stream_to_wifi.h" +#include +#include + const char *ssid = "ESP"; const char *password = "test123"; bool is_wifi_streaming = false; +AsyncWebServer server(80); + +#define MAX_HTML_SIZE 20000 + +char index_html[MAX_HTML_SIZE] = "TEST"; + void stream_to_wifi() { if (!is_wifi_streaming) { + // Connect to WiFi AP WiFi.mode(WIFI_AP); WiFi.softAP(ssid, password); WiFi.setSleep(false); @@ -17,6 +27,7 @@ void stream_to_wifi() { Serial.println("WiFi connected"); + // Start the web server start_server(); Serial.print("Camera Ready! Use 'http://"); @@ -24,37 +35,35 @@ void stream_to_wifi() { Serial.println("' to connect"); Serial.flush(); - + is_wifi_streaming = true; } } -// Todo void start_server() { - // server.on("/", HTTP_GET, []() { - // if (!camera_model.isStreamToWiFiEnabled) { - // start_wifi_stream(); - // } - // server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - // server.sendHeader("Pragma", "no-cache"); - // server.sendHeader("Expires", "-1"); - // server.setContentLength(CONTENT_LENGTH_UNKNOWN); - // server.send(200, "text/html", ""); - // server.sendContent(""); - // server.sendContent(""); - // server.sendContent(""); - // }); - // server.on("/stream", HTTP_GET, []() { - // if (!camera_model.isStreamToWiFiEnabled) { - // start_wifi_stream(); - // } - // String boundary = "ESP32CAM"; - // String header = "--" + boundary + "\r\nContent-Type: image/jpeg\r\nContent-Length: "; - // String jpeg = dither_image(); - // String response = header + jpeg.length() + "\r\n\r\n" + jpeg + "\r\n"; - // server.send(200, "multipart/x-mixed-replace; boundary=" + boundary, response); - // }); - // server.begin(); + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + if (!camera_model.isStreamToWiFiEnabled) { + start_wifi_stream(); + } + request->send_P(200, "text/html", index_html); + }); + + server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){ + if (!camera_model.isStreamToWiFiEnabled) { + start_wifi_stream(); + } + + String boundary = "ESP32CAM"; + String header = "--" + boundary + "\r\nContent-Type: image/jpeg\r\nContent-Length: "; + String jpeg = dither_image(); + String response = header + jpeg.length() + "\r\n\r\n" + jpeg + "\r\n"; + request->send(200, "multipart/x-mixed-replace; boundary=" + boundary, response); + }); + + // Serve additional resources like images, stylesheets, etc. if needed. + // server.serveStatic("/img", SPIFFS, "/img"); + + server.begin(); } void start_wifi_stream() { diff --git a/packages.bat b/packages.bat new file mode 100644 index 00000000000..9cbf2f8abd2 --- /dev/null +++ b/packages.bat @@ -0,0 +1,136 @@ +@echo off +setlocal EnableDelayedExpansion + +rem λ + +set CLI_TEMP=%TEMP%\camera-suite-assets +set ARDUINO_CLI_CONFIG_FILE=--config-file .\arduino-cli.yaml +set CLI_FOUND_FOLLOW_UP=0 + +chcp 65001 > nul +echo ┏┓ ┓ ┏┳┓ ┓ +echo ┃ ┏┓┏┫┓┏ ┃ ┏┓┃┏┓┏┓┏┓ +echo ┗┛┗┛┗┻┗┫ ┻ ┗┛┗┗ ┛┗┗ +echo ┛ https://github.com/CodyTolene +echo. +echo Flipper Zero - ESP32-CAM Development Packages - Windows 10+ +echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite +echo. +echo ------------------------------------------------------------------------------ +echo This will install all assets needed to get you started with ESP32-CAM +echo development. These files will be installed to the following directory: +echo. +echo "%CLI_TEMP%" +echo. +echo Once installed, you can add them to the "Include path" in your IDE of choice. +echo. +echo Notes: +echo - You must have Git installed to use this script. If you do not have Git +echo installed, please install it from the following link: +echo https://git-scm.com/downloads +echo - Temp files will take up approximately 6GB of storage space. +echo - You can reinstall or delete the temp files be rerunning this script. +echo ------------------------------------------------------------------------------ +echo. +pause +echo. +echo Initializing... + +:checkCLI +if not exist "arduino-cli.exe" ( + echo. + echo The "arduino-cli.exe" file cannot be found. Please download it manually from the following link: + echo https://arduino.github.io/arduino-cli/latest/installation/#download + echo Extract the "arduino-cli.exe" file to the same directory as this script, root of the project. + echo. + echo When the file is ready, press any key to check again. + set /a CLI_FOUND_FOLLOW_UP+=1 + if %CLI_FOUND_FOLLOW_UP% geq 2 ( + echo If you are still having issues, feel free to open a ticket at the following link: + echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues + ) + pause + goto :checkCLI +) +if %CLI_FOUND_FOLLOW_UP% geq 1 ( + echo File "arduino-cli.exe" found successfully. Continuing... +) + +echo File "arduino-cli.exe" found successfully. Continuing... + +echo Checking configs... +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* + +echo Fetching assets... + +set DATA_FLAG=0 +if not exist "%CLI_TEMP%\data" ( + set /a "DATA_FLAG+=1" +) +if not exist "%CLI_TEMP%\downloads" ( + set /a "DATA_FLAG+=1" +) +if %DATA_FLAG% gtr 0 ( + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 + echo Cloning ESPAsyncWebServer repository... + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" +) else ( + echo. + set /p DELETE_TEMP="Assets already installed. Reinstall? (Y/N): " + if /i "!DELETE_TEMP!"=="Y" ( + rmdir /s /q %CLI_TEMP% + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index + arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 + echo Cloning ESPAsyncWebServer repository... + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" + goto :wrapUp + ) + echo. + set /p DELETE_TEMP="Would you like to remove the previously installed dependencies? (Y/N): " + if /i "!DELETE_TEMP!"=="Y" ( + rmdir /s /q %CLI_TEMP% + goto :end + ) +) + +:wrapUp +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user + +echo. +echo The ESP32-CAM development dependencies were installed successfully. +echo. +echo ------------------------------------------------------------------------------ +echo. +echo You can now add the following path to your IDEs "Include path" setting: +echo. +echo "%CLI_TEMP%\**" +echo. + +:end +echo ------------------------------------------------------------------------------ +echo. +echo Fin. Happy programming, friend. +echo. +pause +exit /b From f301d58524beb8dac061815e37613471494b22f4 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 20:54:00 -0600 Subject: [PATCH 02/43] Wrap pull auto pull of assets scripts. Stabilize a few things. --- .gitignore | 3 + README.md | 10 +- firmware-flash.bat | 39 ++++--- arduino-cli.yaml => firmware/arduino-cli.yaml | 8 +- firmware/stream_to_wifi.cpp | 101 +++++++++--------- firmware/stream_to_wifi.h | 15 ++- packages.bat => packages-firmware.bat | 42 ++++---- 7 files changed, 110 insertions(+), 108 deletions(-) rename arduino-cli.yaml => firmware/arduino-cli.yaml (60%) rename packages.bat => packages-firmware.bat (88%) diff --git a/.gitignore b/.gitignore index d3f548f97d7..59642760891 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ __pycache__ dist/* fap/dist/* +firmware/.assets/ +firmware/.build +firmware/build diff --git a/README.md b/README.md index 4eff08e969a..62fa3873a43 100644 --- a/README.md +++ b/README.md @@ -123,12 +123,6 @@ That's it, let me know if you have any issues! https://dl.espressif.com/dl/package_esp32_index.json ``` - or (a backup) - - ```markdown - https://espressif.github.io/arduino-esp32/package_esp32_index.json - ``` - 6. In the Arduino IDE, go to `Tools > Board > Boards Manager`. 7. Search for `esp32` and install `esp32` by `Espressif Systems`. 8. Plug in your Flipper Zero via USB. Make sure qFlipper or something else isn't connected to it already after doing so. @@ -249,14 +243,14 @@ To contribute to this project, please follow the steps below: 4. Request PR [here][pull-request-link], introduce work via your branch. 5. Wait for review and merge. -When developing the firmware, be sure to download the dependencies by running the `packages.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies to your Windows TEMP folder. You can run the same script to remove the temporary files when you're done using them. Add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code; there's a guide on how to get started with that workflow here: +When developing the firmware, be sure to download the dependencies by running the `packages-firmware.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies (Arduino Build & Git Assets) to your Windows TEMP folder. You can run the same script over again later to remove the temporary files when you're done using them. Once installed, add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code; there's a guide on how to get started with that workflow here: https://github.com/CodyTolene/Flipper-Zero-Development-Toolkit Example include path: ```markdown -C:/Users//AppData/Local/Temp/camera-suite-assets/** +C:/Users//AppData/Local/Temp/arduino-cli/** ``` Thank you for any and all contributions to this project, I'm looking forward to seeing what you come up with! If you have any questions, please let me know by opening an issue [here][issues-link]. diff --git a/firmware-flash.bat b/firmware-flash.bat index e09370d74e8..b05bb1fe083 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -4,11 +4,11 @@ setlocal EnableDelayedExpansion rem λ set CLI_FOUND_FOLLOW_UP=0 -set CLI_TEMP=%TEMP%\camera-suite-assets -set COMPILE_FLAG=firmware\.compile.flag -set ARDUINO_CLI_CONFIG_FILE=--config-file .\arduino-cli.yaml +set CLI_TEMP=%TEMP%\arduino-cli +set COMPILE_FLAG=%CLI_TEMP%\.compile.flag +set ARDUINO_CLI_CONFIG_FILE=--config-file %CD%\firmware\arduino-cli.yaml set DEFAULT_BOARD_FQBN=esp32:esp32:esp32cam -set FIRMWARE_SRC=firmware\firmware.ino +set FIRMWARE_SRC=%CD%\firmware\firmware.ino set SELECTED_BOARD=%DEFAULT_BOARD_FQBN% chcp 65001 > nul @@ -32,9 +32,13 @@ echo. echo Notes: echo - You must have Git installed to use this script. If you do not have Git echo installed, please install it from the following link: +echo. echo https://git-scm.com/downloads +echo. echo - Temporary installation files will be installed to the following directory: -echo %CLI_TEMP% +echo. +echo "%CLI_TEMP%" +echo. echo - Temp files will take up approximately 6GB of storage space. echo - You will have to option to delete the temp files after flashing. echo ------------------------------------------------------------------------------ @@ -48,7 +52,7 @@ if not exist "arduino-cli.exe" ( echo. echo The "arduino-cli.exe" file cannot be found. Please download it manually from the following link: echo https://arduino.github.io/arduino-cli/latest/installation/#download - echo Extract the "arduino-cli.exe" file to the same directory as this script, root of the project. + echo Extract the "arduino-cli.exe" file to the same directory as this script. echo. echo When the file is ready, press any key to check again. set /a CLI_FOUND_FOLLOW_UP+=1 @@ -69,6 +73,7 @@ arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* echo Fetching assets... + set DATA_FLAG=0 if not exist "%CLI_TEMP%\data" ( set /a "DATA_FLAG+=1" @@ -80,15 +85,15 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" ) else ( echo Assets already installed. Skipping... ) @@ -166,9 +171,9 @@ echo. echo Firmware upload was successful. echo Cleaning up... echo Restoring default configs... -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% @@ -207,9 +212,9 @@ if %ERRORLEVEL% EQU 0 ( ) echo Cleaning up... echo Restoring default configs... - arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data - arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging - arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% diff --git a/arduino-cli.yaml b/firmware/arduino-cli.yaml similarity index 60% rename from arduino-cli.yaml rename to firmware/arduino-cli.yaml index 5bcdb161312..3ed02739ac9 100644 --- a/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -1,15 +1,15 @@ board_manager: additional_urls: - - https://espressif.github.io/arduino-esp32/package_esp32_index.json + - https://dl.espressif.com/dl/package_esp32_index.json build_cache: compilations_before_purge: 10 ttl: 720h0m0s daemon: port: "50051" directories: - data: C:\temp\camera-suite-assets\data - downloads: C:\temp\camera-suite-assets\staging - user: C:\temp\camera-suite-assets\user + data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data + downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads + user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user library: enable_unsafe_install: false logging: diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp index 5f7c3e589b5..5523ca4fe53 100644 --- a/firmware/stream_to_wifi.cpp +++ b/firmware/stream_to_wifi.cpp @@ -1,69 +1,70 @@ #include "stream_to_wifi.h" -#include -#include - -const char *ssid = "ESP"; -const char *password = "test123"; +// const char *ssid = "ESP"; +// const char *password = "test123"; bool is_wifi_streaming = false; -AsyncWebServer server(80); +// AsyncWebServer server(80); -#define MAX_HTML_SIZE 20000 +// #define MAX_HTML_SIZE 20000 -char index_html[MAX_HTML_SIZE] = "TEST"; +// char index_html[MAX_HTML_SIZE] = "TEST"; void stream_to_wifi() { - if (!is_wifi_streaming) { - // Connect to WiFi AP - WiFi.mode(WIFI_AP); - WiFi.softAP(ssid, password); - WiFi.setSleep(false); + Serial.println("Starting"); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } + // if (!is_wifi_streaming) { + // // Connect to WiFi AP + // WiFi.mode(WIFI_AP); + // WiFi.softAP(ssid, password); + // WiFi.setSleep(false); - Serial.println("WiFi connected"); + // while (WiFi.status() != WL_CONNECTED) { + // delay(500); + // Serial.print("."); + // } - // Start the web server - start_server(); + // Serial.println("WiFi connected"); - Serial.print("Camera Ready! Use 'http://"); - Serial.print(WiFi.softAPIP()); - Serial.println("' to connect"); + // // Start the web server + // start_server(); - Serial.flush(); + // Serial.print("Camera Ready! Use 'http://"); + // Serial.print(WiFi.softAPIP()); + // Serial.println("' to connect"); - is_wifi_streaming = true; - } + // Serial.flush(); + + // is_wifi_streaming = true; + // } } void start_server() { - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - if (!camera_model.isStreamToWiFiEnabled) { - start_wifi_stream(); - } - request->send_P(200, "text/html", index_html); - }); - - server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){ - if (!camera_model.isStreamToWiFiEnabled) { - start_wifi_stream(); - } - - String boundary = "ESP32CAM"; - String header = "--" + boundary + "\r\nContent-Type: image/jpeg\r\nContent-Length: "; - String jpeg = dither_image(); - String response = header + jpeg.length() + "\r\n\r\n" + jpeg + "\r\n"; - request->send(200, "multipart/x-mixed-replace; boundary=" + boundary, response); - }); - - // Serve additional resources like images, stylesheets, etc. if needed. - // server.serveStatic("/img", SPIFFS, "/img"); - - server.begin(); + Serial.println("Starting server"); + + // server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + // if (!camera_model.isStreamToWiFiEnabled) { + // start_wifi_stream(); + // } + // request->send_P(200, "text/html", index_html); + // }); + + // server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){ + // if (!camera_model.isStreamToWiFiEnabled) { + // start_wifi_stream(); + // } + + // String boundary = "ESP32CAM"; + // String header = "--" + boundary + "\r\nContent-Type: image/jpeg\r\nContent-Length: "; + // String jpeg = dither_image(); + // String response = header + jpeg.length() + "\r\n\r\n" + jpeg + "\r\n"; + // request->send(200, "multipart/x-mixed-replace; boundary=" + boundary, response); + // }); + + // // Serve additional resources like images, stylesheets, etc. if needed. + // // server.serveStatic("/img", SPIFFS, "/img"); + + // server.begin(); } void start_wifi_stream() { @@ -79,7 +80,7 @@ void start_wifi_stream() { void stop_wifi_stream() { if (is_wifi_streaming) { - WiFi.softAPdisconnect(true); + // WiFi.softAPdisconnect(true); is_wifi_streaming = false; camera_model.isStreamToWiFiEnabled = false; } diff --git a/firmware/stream_to_wifi.h b/firmware/stream_to_wifi.h index aab970eab43..ff71121a02a 100644 --- a/firmware/stream_to_wifi.h +++ b/firmware/stream_to_wifi.h @@ -1,27 +1,26 @@ #ifndef STREAM_TO_WIFI_H #define STREAM_TO_WIFI_H -// #include "ESPAsyncWebServer.h" // #include // #include -#include -#include -#include +// #include +// #include +// #include +// #include #include "camera.h" #include "camera_model.h" -#include "dither_image.h" /** Start the WiFi camera stream. */ void stream_to_wifi(); -/** Start the WiFi camera stream. */ -void start_wifi_stream(); - /** Start the WiFi server. */ void start_server(); +/** Start the WiFi camera stream. */ +void start_wifi_stream(); + /** Stop the WiFi camera stream. */ void stop_wifi_stream(); diff --git a/packages.bat b/packages-firmware.bat similarity index 88% rename from packages.bat rename to packages-firmware.bat index 9cbf2f8abd2..3971d1ae673 100644 --- a/packages.bat +++ b/packages-firmware.bat @@ -3,8 +3,8 @@ setlocal EnableDelayedExpansion rem λ -set CLI_TEMP=%TEMP%\camera-suite-assets -set ARDUINO_CLI_CONFIG_FILE=--config-file .\arduino-cli.yaml +set CLI_TEMP=%TEMP%\arduino-cli +set ARDUINO_CLI_CONFIG_FILE=--config-file %CD%\firmware\arduino-cli.yaml set CLI_FOUND_FOLLOW_UP=0 chcp 65001 > nul @@ -17,7 +17,7 @@ echo Flipper Zero - ESP32-CAM Development Packages - Windows 10+ echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite echo. echo ------------------------------------------------------------------------------ -echo This will install all assets needed to get you started with ESP32-CAM +echo This will install all assets needed to get you started with ESP32-CAM firmware echo development. These files will be installed to the following directory: echo. echo "%CLI_TEMP%" @@ -27,9 +27,11 @@ echo. echo Notes: echo - You must have Git installed to use this script. If you do not have Git echo installed, please install it from the following link: +echo. echo https://git-scm.com/downloads +echo. echo - Temp files will take up approximately 6GB of storage space. -echo - You can reinstall or delete the temp files be rerunning this script. +echo - You can reinstall or delete the temp files be re-running this script. echo ------------------------------------------------------------------------------ echo. pause @@ -41,7 +43,7 @@ if not exist "arduino-cli.exe" ( echo. echo The "arduino-cli.exe" file cannot be found. Please download it manually from the following link: echo https://arduino.github.io/arduino-cli/latest/installation/#download - echo Extract the "arduino-cli.exe" file to the same directory as this script, root of the project. + echo Extract the "arduino-cli.exe" file to the same directory as this script. echo. echo When the file is ready, press any key to check again. set /a CLI_FOUND_FOLLOW_UP+=1 @@ -56,8 +58,6 @@ if %CLI_FOUND_FOLLOW_UP% geq 1 ( echo File "arduino-cli.exe" found successfully. Continuing... ) -echo File "arduino-cli.exe" found successfully. Continuing... - echo Checking configs... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads @@ -76,15 +76,15 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" ) else ( echo. set /p DELETE_TEMP="Assets already installed. Reinstall? (Y/N): " @@ -93,15 +93,15 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\ESPAsyncWebServer" + git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\arduino-esp32" + git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\ESP8266-Arduino" + git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\AsyncTCP" + git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\ESPAsyncTCP" + git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" goto :wrapUp ) echo. @@ -113,9 +113,9 @@ if %DATA_FLAG% gtr 0 ( ) :wrapUp -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\camera-suite-assets\data -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\camera-suite-assets\staging -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\camera-suite-assets\user +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user echo. echo The ESP32-CAM development dependencies were installed successfully. @@ -124,7 +124,7 @@ echo --------------------------------------------------------------------------- echo. echo You can now add the following path to your IDEs "Include path" setting: echo. -echo "%CLI_TEMP%\**" +echo "%CLI_TEMP%\data\**" echo. :end From 3c5b18b5ced9d37881efc40d0eb7ba4a2e7aa7a2 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 21:15:21 -0600 Subject: [PATCH 03/43] Add git hooks. --- .github/hooks/pre-commit | 16 ++++++++++++++++ firmware/arduino-cli.yaml | 6 +++--- packages-firmware.bat | 8 +++++++- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .github/hooks/pre-commit diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit new file mode 100644 index 00000000000..94946aa4beb --- /dev/null +++ b/.github/hooks/pre-commit @@ -0,0 +1,16 @@ +#!/bin/bash + +# Run ufbt lint in the "fap" folder +fap_folder="fap" + +# Change to the root directory of the repository +cd "$(git rev-parse --show-toplevel)" + +# Run ufbt lint in the "fap" folder +if ! (cd "$fap_folder" && ufbt lint); then + echo "Error: ufbt lint failed in the 'fap' folder. Please fix the issues before committing." + exit 1 +fi + +# If everything is okay, exit with a zero status +exit 0 diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index 3ed02739ac9..b32490fd0cc 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -7,9 +7,9 @@ build_cache: daemon: port: "50051" directories: - data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data - downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads - user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user + data: C:\temp\arduino-cli\data + downloads: C:\temp\arduino-cli\staging + user: C:\temp\arduino-cli\user library: enable_unsafe_install: false logging: diff --git a/packages-firmware.bat b/packages-firmware.bat index 3971d1ae673..14952a58d31 100644 --- a/packages-firmware.bat +++ b/packages-firmware.bat @@ -6,6 +6,8 @@ rem λ set CLI_TEMP=%TEMP%\arduino-cli set ARDUINO_CLI_CONFIG_FILE=--config-file %CD%\firmware\arduino-cli.yaml set CLI_FOUND_FOLLOW_UP=0 +set GITHUB_HOOKS_FOLDER=%CD%\.github\hooks +set GIT_HOOKS_FOLDER=%CD%\.git\hooks chcp 65001 > nul echo ┏┓ ┓ ┏┳┓ ┓ @@ -113,10 +115,14 @@ if %DATA_FLAG% gtr 0 ( ) :wrapUp +echo. +echo Configuring Git hooks... +copy /Y "%GITHUB_HOOKS_FOLDER%" "%GIT_HOOKS_FOLDER%" +echo. +echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user - echo. echo The ESP32-CAM development dependencies were installed successfully. echo. From 6715ffbb199f87919f3a8cb2926534287a0a653a Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 21:16:02 -0600 Subject: [PATCH 04/43] Update git hooks. --- .github/hooks/pre-commit | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit index 94946aa4beb..8e51b40a3ff 100644 --- a/.github/hooks/pre-commit +++ b/.github/hooks/pre-commit @@ -10,6 +10,8 @@ cd "$(git rev-parse --show-toplevel)" if ! (cd "$fap_folder" && ufbt lint); then echo "Error: ufbt lint failed in the 'fap' folder. Please fix the issues before committing." exit 1 +else + echo "ufbt lint passed in the 'fap' folder." fi # If everything is okay, exit with a zero status From cd0ab941e22ca8332a95b761afb197aec9cbecfa Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 21:18:10 -0600 Subject: [PATCH 05/43] Update gitignore. --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 59642760891..d3f548f97d7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,3 @@ __pycache__ dist/* fap/dist/* -firmware/.assets/ -firmware/.build -firmware/build From e7c553ddd045f80cc4e9a1eb1bc605f9f1e906ef Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 22:55:03 -0600 Subject: [PATCH 06/43] Auto fetch additional WiFi Git assets via arduino cli. --- README.md | 4 +- packages-firmware.bat => firmware-assets.bat | 73 +++++++----------- firmware-flash.bat | 57 ++++++-------- firmware/arduino-cli.yaml | 8 +- firmware/stream_to_wifi.cpp | 80 +++++++++----------- firmware/stream_to_wifi.h | 10 ++- 6 files changed, 100 insertions(+), 132 deletions(-) rename packages-firmware.bat => firmware-assets.bat (53%) diff --git a/README.md b/README.md index 62fa3873a43..1ba74d641ec 100644 --- a/README.md +++ b/README.md @@ -243,14 +243,14 @@ To contribute to this project, please follow the steps below: 4. Request PR [here][pull-request-link], introduce work via your branch. 5. Wait for review and merge. -When developing the firmware, be sure to download the dependencies by running the `packages-firmware.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies (Arduino Build & Git Assets) to your Windows TEMP folder. You can run the same script over again later to remove the temporary files when you're done using them. Once installed, add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code; there's a guide on how to get started with that workflow here: +When developing the firmware, be sure to download the dependencies by running the `firmware-assets.bat` batch script at the root of this directory. This will download the ESP32-CAM firmware dependencies (Arduino Build & Git Assets) to your Windows TEMP folder. You can run the same script over again later to remove the temporary files when you're done using them. Once installed, add these dependencies and their directories to your "Include path" in your IDE of choice. I prefer Visual Studio Code; there's a guide on how to get started with that workflow here: https://github.com/CodyTolene/Flipper-Zero-Development-Toolkit Example include path: ```markdown -C:/Users//AppData/Local/Temp/arduino-cli/** +C:/Users//AppData/Local/Temp/arduino-cli/**/** ``` Thank you for any and all contributions to this project, I'm looking forward to seeing what you come up with! If you have any questions, please let me know by opening an issue [here][issues-link]. diff --git a/packages-firmware.bat b/firmware-assets.bat similarity index 53% rename from packages-firmware.bat rename to firmware-assets.bat index 14952a58d31..52a00b9b898 100644 --- a/packages-firmware.bat +++ b/firmware-assets.bat @@ -15,7 +15,7 @@ echo ┃ ┏┓┏┫┓┏ ┃ ┏┓┃┏┓┏┓┏┓ echo ┗┛┗┛┗┻┗┫ ┻ ┗┛┗┗ ┛┗┗ echo ┛ https://github.com/CodyTolene echo. -echo Flipper Zero - ESP32-CAM Development Packages - Windows 10+ +echo Flipper Zero - ESP32-CAM Development Assets - Windows 10+ echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite echo. echo ------------------------------------------------------------------------------ @@ -24,16 +24,11 @@ echo development. These files will be installed to the following directory: echo. echo "%CLI_TEMP%" echo. -echo Once installed, you can add them to the "Include path" in your IDE of choice. +echo Once installed you can add them to the "Include path" in your IDE of choice. echo. -echo Notes: -echo - You must have Git installed to use this script. If you do not have Git -echo installed, please install it from the following link: -echo. -echo https://git-scm.com/downloads -echo. -echo - Temp files will take up approximately 6GB of storage space. -echo - You can reinstall or delete the temp files be re-running this script. +echo Notes: +echo - Temporary installation files will take up approx. 3.5GB of storage space. +echo - You can reinstall or delete the temporary files by re-running this script. echo ------------------------------------------------------------------------------ echo. pause @@ -47,10 +42,10 @@ if not exist "arduino-cli.exe" ( echo https://arduino.github.io/arduino-cli/latest/installation/#download echo Extract the "arduino-cli.exe" file to the same directory as this script. echo. - echo When the file is ready, press any key to check again. + echo When the file is ready press any key to check again. set /a CLI_FOUND_FOLLOW_UP+=1 if %CLI_FOUND_FOLLOW_UP% geq 2 ( - echo If you are still having issues, feel free to open a ticket at the following link: + echo If you are still having issues feel free to open a ticket at the following link: echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues ) pause @@ -60,10 +55,13 @@ if %CLI_FOUND_FOLLOW_UP% geq 1 ( echo File "arduino-cli.exe" found successfully. Continuing... ) -echo Checking configs... +echo Checking and setting arduino-cli config... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* +rem Enable for Git installations (ie `arduino-cli lib install --git-url`). +rem @See "https://arduino.github.io/arduino-cli/0.35/configuration/#configuration-keys" +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install true echo Fetching assets... @@ -74,39 +72,24 @@ if not exist "%CLI_TEMP%\data" ( if not exist "%CLI_TEMP%\downloads" ( set /a "DATA_FLAG+=1" ) +if not exist "%CLI_TEMP%\user" ( + set /a "DATA_FLAG+=1" +) if %DATA_FLAG% gtr 0 ( + :installAssets arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 - echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" - echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" - echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" - echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" - echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/espressif/arduino-esp32.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git ) else ( - echo. - set /p DELETE_TEMP="Assets already installed. Reinstall? (Y/N): " - if /i "!DELETE_TEMP!"=="Y" ( + set /p SHOULD_REINSTALL="Assets already installed. Reinstall? (Y/N): " + if /i "!SHOULD_REINSTALL!"=="Y" ( rmdir /s /q %CLI_TEMP% - arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index - arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 - echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" - echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" - echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" - echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" - echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" - goto :wrapUp + goto :installAssets ) - echo. set /p DELETE_TEMP="Would you like to remove the previously installed dependencies? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% @@ -114,15 +97,13 @@ if %DATA_FLAG% gtr 0 ( ) ) -:wrapUp -echo. -echo Configuring Git hooks... +echo Configuring Git pre-commit hook... copy /Y "%GITHUB_HOOKS_FOLDER%" "%GIT_HOOKS_FOLDER%" -echo. echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false echo. echo The ESP32-CAM development dependencies were installed successfully. echo. @@ -130,13 +111,13 @@ echo --------------------------------------------------------------------------- echo. echo You can now add the following path to your IDEs "Include path" setting: echo. -echo "%CLI_TEMP%\data\**" +echo "%CLI_TEMP%\**\**" echo. :end echo ------------------------------------------------------------------------------ echo. -echo Fin. Happy programming, friend. +echo Fin - Happy programming friend. echo. pause exit /b diff --git a/firmware-flash.bat b/firmware-flash.bat index b05bb1fe083..e44ba5ff5e7 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -22,25 +22,16 @@ echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite echo. echo ------------------------------------------------------------------------------ echo Before you begin please make sure your Flipper Zero is plugged into your PC. -echo Then on your Flipper Zero, open the GPIO menu and select USB-UART Bridge. In -echo the USB-UART Bridge config menu, make sure the following configuration is set: +echo Then on your Flipper Zero open the GPIO menu and select USB-UART Bridge. In +echo the USB-UART Bridge config menu make sure the following configuration is set: echo - USB Channel = 1 (on newer firmware) echo - Baudrate = Host -echo - UART Pins = 13,14 +echo - UART Pins = 13 and 14 echo - RTS/DTR Pins = None echo. -echo Notes: -echo - You must have Git installed to use this script. If you do not have Git -echo installed, please install it from the following link: -echo. -echo https://git-scm.com/downloads -echo. -echo - Temporary installation files will be installed to the following directory: -echo. -echo "%CLI_TEMP%" -echo. -echo - Temp files will take up approximately 6GB of storage space. -echo - You will have to option to delete the temp files after flashing. +echo Notes: +echo - Temporary installation files will take up approx. 3.5GB of storage space. +echo - You will have to option to delete the temporary files after flashing. echo ------------------------------------------------------------------------------ echo. pause @@ -54,10 +45,10 @@ if not exist "arduino-cli.exe" ( echo https://arduino.github.io/arduino-cli/latest/installation/#download echo Extract the "arduino-cli.exe" file to the same directory as this script. echo. - echo When the file is ready, press any key to check again. + echo When the file is ready press any key to check again. set /a CLI_FOUND_FOLLOW_UP+=1 if %CLI_FOUND_FOLLOW_UP% geq 2 ( - echo If you are still having issues, feel free to open a ticket at the following link: + echo If you are still having issues feel free to open a ticket at the following link: echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues ) pause @@ -67,10 +58,13 @@ if %CLI_FOUND_FOLLOW_UP% geq 1 ( echo File "arduino-cli.exe" found successfully. Continuing... ) -echo Checking configs... +echo Checking and setting arduino-cli configs... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* +rem Enable for Git installations (ie `arduino-cli lib install --git-url`). +rem @See "https://arduino.github.io/arduino-cli/0.35/configuration/#configuration-keys" +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install true echo Fetching assets... @@ -81,19 +75,17 @@ if not exist "%CLI_TEMP%\data" ( if not exist "%CLI_TEMP%\downloads" ( set /a "DATA_FLAG+=1" ) +if not exist "%CLI_TEMP%\user" ( + set /a "DATA_FLAG+=1" +) if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 - echo Cloning ESPAsyncWebServer repository... - git clone https://github.com/me-no-dev/ESPAsyncWebServer.git "%CLI_TEMP%\data\ESPAsyncWebServer" - echo Cloning espressif Arduino ESP32 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/espressif/arduino-esp32.git "%CLI_TEMP%\data\arduino-esp32" - echo Cloning ESP8266 repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/esp8266/Arduino.git "%CLI_TEMP%\data\ESP8266-Arduino" - echo Cloning AsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/AsyncTCP.git "%CLI_TEMP%\data\AsyncTCP" - echo Cloning ESPAsyncTCP repository, a dependency of ESPAsyncWebServer... - git clone https://github.com/me-no-dev/ESPAsyncTCP.git "%CLI_TEMP%\data\ESPAsyncTCP" + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/espressif/arduino-esp32.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git + arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git ) else ( echo Assets already installed. Skipping... ) @@ -124,7 +116,7 @@ echo Your ESP32-CAM is ready to be flashed. Please follow the instructions below :uploadFirmware echo. echo 1. Remove ESP32-CAM. Ensure IO0 pin on ESP32-CAM is grounded to the proper GND pin. -echo 2. Hold reset, and insert your ESP32-CAM; hold for a few seconds and release. +echo 2. Hold reset and insert your ESP32-CAM; hold for a few seconds and release. echo 3. Try to time your release simultaneously with continuing to the next step. echo 4. ESP32-CAM should now be in flash mode; allow some time for firmware upload. echo 5. Failure is common; verify all connections if errors persist and try again. @@ -152,7 +144,7 @@ if !ERRORLEVEL! EQU 0 ( goto :uploadFirmware ) else ( echo. - echo If you are still having issues, feel free to open a ticket at the following link: + echo If you are still having issues feel free to open a ticket at the following link: echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues echo. set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " @@ -174,6 +166,7 @@ echo Restoring default configs... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user +arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% @@ -196,7 +189,7 @@ if /i "%USE_DEFAULT_BOARD%"=="N" ( set /p SELECTED_BOARD="Please enter your board FQBN. For example '%DEFAULT_BOARD_FQBN%' with no quotes: " ) echo. -echo Compiling firmware, this will take a moment... +echo Compiling firmware - this will take a moment... echo. arduino-cli %ARDUINO_CLI_CONFIG_FILE% compile --fqbn !SELECTED_BOARD! %FIRMWARE_SRC% if %ERRORLEVEL% EQU 0 ( @@ -219,7 +212,7 @@ if %ERRORLEVEL% EQU 0 ( if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% ) - echo Cleanup completed, press any key to exit. + echo Cleanup completed - press any key to exit. echo. pause exit /b diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index b32490fd0cc..8c8e70980c7 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -7,11 +7,11 @@ build_cache: daemon: port: "50051" directories: - data: C:\temp\arduino-cli\data - downloads: C:\temp\arduino-cli\staging - user: C:\temp\arduino-cli\user + data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data + downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads + user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user library: - enable_unsafe_install: false + enable_unsafe_install: true logging: file: "" format: text diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp index 5523ca4fe53..20db16c2714 100644 --- a/firmware/stream_to_wifi.cpp +++ b/firmware/stream_to_wifi.cpp @@ -1,70 +1,62 @@ #include "stream_to_wifi.h" -// const char *ssid = "ESP"; -// const char *password = "test123"; +// Constants +const char *password = "test123"; +const char *ssid = "ESP"; bool is_wifi_streaming = false; - -// AsyncWebServer server(80); - -// #define MAX_HTML_SIZE 20000 - -// char index_html[MAX_HTML_SIZE] = "TEST"; +char index_html[MAX_HTML_SIZE] = "TEST"; +AsyncWebServer server(80); void stream_to_wifi() { Serial.println("Starting"); - // if (!is_wifi_streaming) { - // // Connect to WiFi AP - // WiFi.mode(WIFI_AP); - // WiFi.softAP(ssid, password); - // WiFi.setSleep(false); + if (!is_wifi_streaming) { + // Connect to WiFi AP + WiFi.mode(WIFI_AP); + WiFi.softAP(ssid, password); + WiFi.setSleep(false); - // while (WiFi.status() != WL_CONNECTED) { - // delay(500); - // Serial.print("."); - // } + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } - // Serial.println("WiFi connected"); + Serial.println("WiFi connected"); - // // Start the web server - // start_server(); + // Start the web server + start_server(); - // Serial.print("Camera Ready! Use 'http://"); - // Serial.print(WiFi.softAPIP()); - // Serial.println("' to connect"); + Serial.print("Camera Ready! Use 'http://"); + Serial.print(WiFi.softAPIP()); + Serial.println("' to connect"); - // Serial.flush(); + Serial.flush(); - // is_wifi_streaming = true; - // } + is_wifi_streaming = true; + } } void start_server() { - Serial.println("Starting server"); + start_wifi_stream(); - // server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - // if (!camera_model.isStreamToWiFiEnabled) { - // start_wifi_stream(); - // } - // request->send_P(200, "text/html", index_html); - // }); + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", index_html); + }); // server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){ - // if (!camera_model.isStreamToWiFiEnabled) { - // start_wifi_stream(); + // camera_fb_t *fb = esp_camera_fb_get(); // Capture a frame + // if (fb) { + // request->send_P(200, "image/jpeg", (const char *)fb->buf, fb->len); + // esp_camera_fb_return(fb); // Release the frame buffer + // } else { + // request->send(500); // Internal Server Error // } - - // String boundary = "ESP32CAM"; - // String header = "--" + boundary + "\r\nContent-Type: image/jpeg\r\nContent-Length: "; - // String jpeg = dither_image(); - // String response = header + jpeg.length() + "\r\n\r\n" + jpeg + "\r\n"; - // request->send(200, "multipart/x-mixed-replace; boundary=" + boundary, response); // }); - // // Serve additional resources like images, stylesheets, etc. if needed. - // // server.serveStatic("/img", SPIFFS, "/img"); + // Serve additional resources like images, stylesheets, etc. if needed. + // server.serveStatic("/img", SPIFFS, "/img"); - // server.begin(); + server.begin(); } void start_wifi_stream() { diff --git a/firmware/stream_to_wifi.h b/firmware/stream_to_wifi.h index ff71121a02a..6a66d4b0751 100644 --- a/firmware/stream_to_wifi.h +++ b/firmware/stream_to_wifi.h @@ -4,14 +4,16 @@ // #include // #include -// #include -// #include -// #include -// #include +#include +#include +#include +#include #include "camera.h" #include "camera_model.h" +#define MAX_HTML_SIZE 20000 + /** Start the WiFi camera stream. */ void stream_to_wifi(); From c45cd08f51770d40fc0565a359e1e0417b87fab4 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 22:59:34 -0600 Subject: [PATCH 07/43] Wrap up firmware assets batch. --- firmware-assets.bat | 3 +++ firmware/arduino-cli.yaml | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/firmware-assets.bat b/firmware-assets.bat index 52a00b9b898..a84901ae344 100644 --- a/firmware-assets.bat +++ b/firmware-assets.bat @@ -84,6 +84,7 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git + goto :cleanup ) else ( set /p SHOULD_REINSTALL="Assets already installed. Reinstall? (Y/N): " if /i "!SHOULD_REINSTALL!"=="Y" ( @@ -97,6 +98,7 @@ if %DATA_FLAG% gtr 0 ( ) ) +:cleanup echo Configuring Git pre-commit hook... copy /Y "%GITHUB_HOOKS_FOLDER%" "%GIT_HOOKS_FOLDER%" echo Resetting arduino-cli config back to defaults... @@ -104,6 +106,7 @@ arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduin arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false + echo. echo The ESP32-CAM development dependencies were installed successfully. echo. diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index 8c8e70980c7..b32490fd0cc 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -7,11 +7,11 @@ build_cache: daemon: port: "50051" directories: - data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data - downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads - user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user + data: C:\temp\arduino-cli\data + downloads: C:\temp\arduino-cli\staging + user: C:\temp\arduino-cli\user library: - enable_unsafe_install: true + enable_unsafe_install: false logging: file: "" format: text From 164429dd913f9543f5f2d67714f21cf7fbba405e Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 23:06:22 -0600 Subject: [PATCH 08/43] Fix typos. --- firmware-assets.bat | 8 ++++---- firmware-flash.bat | 2 +- firmware/arduino-cli.yaml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/firmware-assets.bat b/firmware-assets.bat index a84901ae344..84f57f1ba68 100644 --- a/firmware-assets.bat +++ b/firmware-assets.bat @@ -27,8 +27,8 @@ echo. echo Once installed you can add them to the "Include path" in your IDE of choice. echo. echo Notes: -echo - Temporary installation files will take up approx. 3.5GB of storage space. -echo - You can reinstall or delete the temporary files by re-running this script. +echo - Development asset files will take up approx. 3.5GB of storage space. +echo - You can reinstall or delete the asset files by re-running this script. echo ------------------------------------------------------------------------------ echo. pause @@ -84,7 +84,7 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git - goto :cleanup + goto :wrapUp ) else ( set /p SHOULD_REINSTALL="Assets already installed. Reinstall? (Y/N): " if /i "!SHOULD_REINSTALL!"=="Y" ( @@ -98,7 +98,7 @@ if %DATA_FLAG% gtr 0 ( ) ) -:cleanup +:wrapUp echo Configuring Git pre-commit hook... copy /Y "%GITHUB_HOOKS_FOLDER%" "%GIT_HOOKS_FOLDER%" echo Resetting arduino-cli config back to defaults... diff --git a/firmware-flash.bat b/firmware-flash.bat index e44ba5ff5e7..c125450e874 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -31,7 +31,7 @@ echo - RTS/DTR Pins = None echo. echo Notes: echo - Temporary installation files will take up approx. 3.5GB of storage space. -echo - You will have to option to delete the temporary files after flashing. +echo - You will have the option to delete the temporary files on completion. echo ------------------------------------------------------------------------------ echo. pause diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index b32490fd0cc..8c8e70980c7 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -7,11 +7,11 @@ build_cache: daemon: port: "50051" directories: - data: C:\temp\arduino-cli\data - downloads: C:\temp\arduino-cli\staging - user: C:\temp\arduino-cli\user + data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data + downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads + user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user library: - enable_unsafe_install: false + enable_unsafe_install: true logging: file: "" format: text From 5ffe9ca0ef0708c5ce2248e9bd98246573222c03 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 23:08:36 -0600 Subject: [PATCH 09/43] Add missing reset of arduino cli library set in compile firmware fallback. --- firmware-flash.bat | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firmware-flash.bat b/firmware-flash.bat index c125450e874..2942aacf71f 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -162,7 +162,7 @@ if !ERRORLEVEL! EQU 0 ( echo. echo Firmware upload was successful. echo Cleaning up... -echo Restoring default configs... +echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user @@ -204,10 +204,11 @@ if %ERRORLEVEL% EQU 0 ( goto :compileFirmware ) echo Cleaning up... - echo Restoring default configs... + echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user + arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% From 261bf383f13405bd2bb689f4e83888711e09ebc4 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 28 Jan 2024 23:17:31 -0600 Subject: [PATCH 10/43] Update comments. Prep for first firmware test. --- fap/views/camera_suite_view_wifi_camera.c | 2 +- firmware-flash.bat | 2 +- firmware/arduino-cli.yaml | 8 ++++---- firmware/process_serial_input.cpp | 6 ++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.c index ce0da794706..cea9b9cb4ad 100644 --- a/fap/views/camera_suite_view_wifi_camera.c +++ b/fap/views/camera_suite_view_wifi_camera.c @@ -68,7 +68,7 @@ static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context UNUSED(model); // Stop camera WiFi stream. - // furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'w'}, 1); + // furi_hal_uart_tx(furihaluartidusart1, (uint8_t[]){'w'}, 1); // furi_delay_ms(50); // Go back to the main menu. diff --git a/firmware-flash.bat b/firmware-flash.bat index 2942aacf71f..80f79949e35 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -172,7 +172,7 @@ if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% ) echo. -echo Fin. Happy programming friend. +echo Fin - Happy programming friend. echo. pause exit /b diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index 8c8e70980c7..b32490fd0cc 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -7,11 +7,11 @@ build_cache: daemon: port: "50051" directories: - data: C:\Users\NULL\AppData\Local\Temp\arduino-cli\data - downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads - user: C:\Users\NULL\AppData\Local\Temp\arduino-cli\user + data: C:\temp\arduino-cli\data + downloads: C:\temp\arduino-cli\staging + user: C:\temp\arduino-cli\user library: - enable_unsafe_install: true + enable_unsafe_install: false logging: file: "" format: text diff --git a/firmware/process_serial_input.cpp b/firmware/process_serial_input.cpp index 590f82e3d16..057d58e7a88 100644 --- a/firmware/process_serial_input.cpp +++ b/firmware/process_serial_input.cpp @@ -47,12 +47,10 @@ void process_serial_input() { start_serial_stream(); break; case 'w': - // @todo - // stop_wifi_stream(); + stop_wifi_stream(); break; case 'W': - // @todo - // start_wifi_stream(); + start_wifi_stream(); break; case '0': set_dithering_algorithm(FLOYD_STEINBERG); From def2e2b952d21aa43555deecff7efc6886231138 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 00:39:52 -0600 Subject: [PATCH 11/43] WiFi Testing... --- fap/views/camera_suite_view_wifi_camera.c | 104 +++++++++++++++++++++- fap/views/camera_suite_view_wifi_camera.h | 5 ++ firmware-assets.bat | 4 +- firmware-flash.bat | 2 - firmware/stream_to_wifi.cpp | 64 +++++++------ firmware/stream_to_wifi.h | 1 + 6 files changed, 142 insertions(+), 38 deletions(-) diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.c index cea9b9cb4ad..3cfdf80f60f 100644 --- a/fap/views/camera_suite_view_wifi_camera.c +++ b/fap/views/camera_suite_view_wifi_camera.c @@ -29,13 +29,63 @@ static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* model) { canvas_set_font(canvas, FontSecondary); canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT); - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, "Feature coming soon!"); + canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, "Starting WiFi Stream at:"); // Draw log from camera. canvas_draw_str_aligned( canvas, 3, 13, AlignLeft, AlignTop, furi_string_get_cstr(instance->log)); } +static int32_t camera_suite_wifi_camera_worker(void* context) { + furi_assert(context); + + CameraSuiteViewWiFiCamera* instance = context; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WIFI_WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); + + // Check if an error occurred. + furi_check((events & FuriFlagError) == 0); + + // Check if the thread should stop. + if(events & WorkerEventStop) { + break; + } else if(events & WorkerEventRx) { + size_t length = 0; + do { + size_t buffer_size = 320; + uint8_t data[buffer_size]; + length = + furi_stream_buffer_receive(instance->wifi_rx_stream, data, buffer_size, 0); + if(length > 0) { + data[length] = '\0'; + + with_view_model( + instance->view, + CameraSuiteViewWiFiCameraModel * model, + { + furi_string_cat_printf(model->log, "%s", data); + + // Truncate if too long. + model->log_strlen += length; + if(model->log_strlen >= 4096 - 1) { + furi_string_right(model->log, model->log_strlen / 2); + model->log_strlen = furi_string_size(model->log) + length; + } + }, + true); + } + } while(length > 0); + + with_view_model( + instance->view, CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); }, true); + } + } + + return 0; +} + static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context) { furi_assert(context); furi_assert(event); @@ -68,8 +118,8 @@ static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context UNUSED(model); // Stop camera WiFi stream. - // furi_hal_uart_tx(furihaluartidusart1, (uint8_t[]){'w'}, 1); - // furi_delay_ms(50); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'w'}, 1); + furi_delay_ms(50); // Go back to the main menu. instance->callback(CameraSuiteCustomEventSceneCameraBack, instance->context); @@ -108,7 +158,7 @@ static void camera_suite_view_wifi_camera_enter(void* context) { CameraSuiteViewWiFiCamera* instance = (CameraSuiteViewWiFiCamera*)context; // Start wifi camera stream. - // furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'W'}, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'W'}, 1); with_view_model( instance->view, @@ -117,6 +167,22 @@ static void camera_suite_view_wifi_camera_enter(void* context) { true); } +static void wifi_camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) { + furi_assert(uartIrqEvent); + furi_assert(data); + furi_assert(context); + + // Cast `context` to `CameraSuiteViewWiFiCamera*` and store it in `instance`. + CameraSuiteViewWiFiCamera* instance = context; + + // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the + // `wifi_rx_stream` and set the `WorkerEventRx` flag. + if(uartIrqEvent == UartIrqEventRXNE) { + furi_stream_buffer_send(instance->wifi_rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(instance->wifi_worker_thread), WorkerEventRx); + } +} + CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Allocate memory for the instance CameraSuiteViewWiFiCamera* instance = malloc(sizeof(CameraSuiteViewWiFiCamera)); @@ -131,6 +197,24 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Set context for the view (furi_assert crashes in events without this) view_set_context(instance->view, instance); + // Allocate a stream buffer + instance->wifi_rx_stream = furi_stream_buffer_alloc(1024, 1); + + // Allocate a thread for this camera to run on. + FuriThread* thread = furi_thread_alloc_ex( + "Camera_Suite_WiFi_Rx_Thread", 1024, camera_suite_wifi_camera_worker, instance); + instance->wifi_worker_thread = thread; + furi_thread_start(instance->wifi_worker_thread); + + // Disable console. + furi_hal_console_disable(); + + // 115200 is the default baud rate for the ESP32-CAM. + furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + + // Enable UART1 and set the IRQ callback. + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, wifi_camera_on_irq_cb, instance); + // Set draw callback view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_wifi_camera_draw); @@ -155,6 +239,18 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* instance) { furi_assert(instance); + // Remove the IRQ callback. + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); + + // Free the worker thread. + furi_thread_free(instance->wifi_worker_thread); + + // Free the allocated stream buffer. + furi_stream_buffer_free(instance->wifi_rx_stream); + + // Re-enable the console. + furi_hal_console_enable(); + with_view_model( instance->view, CameraSuiteViewWiFiCameraModel * model, diff --git a/fap/views/camera_suite_view_wifi_camera.h b/fap/views/camera_suite_view_wifi_camera.h index 64c6285784a..4cb40d4487d 100644 --- a/fap/views/camera_suite_view_wifi_camera.h +++ b/fap/views/camera_suite_view_wifi_camera.h @@ -18,16 +18,21 @@ #include "../helpers/camera_suite_custom_event.h" +#define WIFI_WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) + typedef void (*CameraSuiteViewWiFiCameraCallback)(CameraSuiteCustomEvent event, void* context); typedef struct CameraSuiteViewWiFiCamera { View* view; CameraSuiteViewCameraCallback callback; void* context; + FuriStreamBuffer* wifi_rx_stream; + FuriThread* wifi_worker_thread; } CameraSuiteViewWiFiCamera; typedef struct { FuriString* log; + size_t log_strlen; } CameraSuiteViewWiFiCameraModel; CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc(); diff --git a/firmware-assets.bat b/firmware-assets.bat index 84f57f1ba68..33b684ffa2b 100644 --- a/firmware-assets.bat +++ b/firmware-assets.bat @@ -80,8 +80,6 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/espressif/arduino-esp32.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git goto :wrapUp @@ -114,7 +112,7 @@ echo --------------------------------------------------------------------------- echo. echo You can now add the following path to your IDEs "Include path" setting: echo. -echo "%CLI_TEMP%\**\**" +echo "%CLI_TEMP%\**" echo. :end diff --git a/firmware-flash.bat b/firmware-flash.bat index 80f79949e35..195ebd9b063 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -82,8 +82,6 @@ if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/espressif/arduino-esp32.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/esp8266/Arduino.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git ) else ( diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp index 20db16c2714..325e35fcb8a 100644 --- a/firmware/stream_to_wifi.cpp +++ b/firmware/stream_to_wifi.cpp @@ -1,28 +1,35 @@ #include "stream_to_wifi.h" -// Constants -const char *password = "test123"; -const char *ssid = "ESP"; +char password[30] = "test123"; +char ssid[30] = "ESP"; + bool is_wifi_streaming = false; char index_html[MAX_HTML_SIZE] = "TEST"; + +DNSServer dnsServer; AsyncWebServer server(80); -void stream_to_wifi() { - Serial.println("Starting"); +class CaptiveRequestHandler : public AsyncWebHandler { +public: + CaptiveRequestHandler() {} + virtual ~CaptiveRequestHandler() {} + + bool canHandle(AsyncWebServerRequest *request) { return true; } + + void handleRequest(AsyncWebServerRequest *request) { + request->send_P(200, "text/html", index_html); + } +}; +void stream_to_wifi() { if (!is_wifi_streaming) { + Serial.println("Starting WiFi stream..."); + // Connect to WiFi AP WiFi.mode(WIFI_AP); WiFi.softAP(ssid, password); WiFi.setSleep(false); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - - Serial.println("WiFi connected"); - // Start the web server start_server(); @@ -33,46 +40,45 @@ void stream_to_wifi() { Serial.flush(); is_wifi_streaming = true; + } else { + dnsServer.processNextRequest(); } } void start_server() { - start_wifi_stream(); - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html); + Serial.println("Client connected."); }); - // server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request){ - // camera_fb_t *fb = esp_camera_fb_get(); // Capture a frame - // if (fb) { - // request->send_P(200, "image/jpeg", (const char *)fb->buf, fb->len); - // esp_camera_fb_return(fb); // Release the frame buffer - // } else { - // request->send(500); // Internal Server Error - // } - // }); - - // Serve additional resources like images, stylesheets, etc. if needed. - // server.serveStatic("/img", SPIFFS, "/img"); - + dnsServer.start(53, "*", WiFi.softAPIP()); + server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); server.begin(); } void start_wifi_stream() { - turn_flash_on(); // Physical test indicator that we're streaming. + // Physical test indicator that we're streaming. + turn_flash_on(); + camera_model.isStreamToSerialEnabled = false; + set_camera_config_defaults(CAMERA_FUNCTION_WIFI); set_camera_model_defaults(CAMERA_FUNCTION_WIFI); set_camera_defaults(CAMERA_FUNCTION_WIFI); + // @todo - Dynamically set ssid and password via prompts. + camera_model.isStreamToWiFiEnabled = true; + turn_flash_off(); } void stop_wifi_stream() { if (is_wifi_streaming) { - // WiFi.softAPdisconnect(true); + WiFi.setSleep(true); + server.end(); + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_OFF); is_wifi_streaming = false; camera_model.isStreamToWiFiEnabled = false; } diff --git a/firmware/stream_to_wifi.h b/firmware/stream_to_wifi.h index 6a66d4b0751..24ed3dcd725 100644 --- a/firmware/stream_to_wifi.h +++ b/firmware/stream_to_wifi.h @@ -4,6 +4,7 @@ // #include // #include +#include #include #include #include From 7e58beb92af0cddee12c96986bd1b1ea19fc63ba Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 00:41:29 -0600 Subject: [PATCH 12/43] Add notes --- fap/views/camera_suite_view_wifi_camera.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.c index 3cfdf80f60f..d4b9c9d6644 100644 --- a/fap/views/camera_suite_view_wifi_camera.c +++ b/fap/views/camera_suite_view_wifi_camera.c @@ -201,6 +201,7 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { instance->wifi_rx_stream = furi_stream_buffer_alloc(1024, 1); // Allocate a thread for this camera to run on. + // @NOTICE: THIS SEEMINGLY BREAKS THE CAMERA VIEW THREAD... FuriThread* thread = furi_thread_alloc_ex( "Camera_Suite_WiFi_Rx_Thread", 1024, camera_suite_wifi_camera_worker, instance); instance->wifi_worker_thread = thread; From 18c81270f453c6a3c22c5df2b2170d738396372f Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 01:09:09 -0600 Subject: [PATCH 13/43] Update server handler and add new response for get --- firmware/stream_to_wifi.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp index 325e35fcb8a..29c6cae1965 100644 --- a/firmware/stream_to_wifi.cpp +++ b/firmware/stream_to_wifi.cpp @@ -1,6 +1,6 @@ #include "stream_to_wifi.h" -char password[30] = "test123"; +// char password[30] = "test123"; char ssid[30] = "ESP"; bool is_wifi_streaming = false; @@ -9,10 +9,10 @@ char index_html[MAX_HTML_SIZE] = "TEST"; DNSServer dnsServer; AsyncWebServer server(80); -class CaptiveRequestHandler : public AsyncWebHandler { +class RequestHandler : public AsyncWebHandler { public: - CaptiveRequestHandler() {} - virtual ~CaptiveRequestHandler() {} + RequestHandler() {} + virtual ~RequestHandler() {} bool canHandle(AsyncWebServerRequest *request) { return true; } @@ -27,7 +27,7 @@ void stream_to_wifi() { // Connect to WiFi AP WiFi.mode(WIFI_AP); - WiFi.softAP(ssid, password); + WiFi.softAP(ssid); WiFi.setSleep(false); // Start the web server @@ -51,8 +51,14 @@ void start_server() { Serial.println("Client connected."); }); + server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send( + 200, "text/html", + ""); + }); + dnsServer.start(53, "*", WiFi.softAPIP()); - server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); + server.addHandler(new RequestHandler()).setFilter(ON_AP_FILTER); server.begin(); } From 13fb3288e6f07b8b294f283f1bf3aaff11fcf6b1 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 17:22:35 -0600 Subject: [PATCH 14/43] Use new UART in RC and convert app to c++ --- fap/{camera_suite.c => camera_suite.cpp} | 13 +- fap/camera_suite.h | 10 +- ...suite_haptic.c => camera_suite_haptic.cpp} | 8 +- fap/helpers/camera_suite_haptic.h | 2 + ...amera_suite_led.c => camera_suite_led.cpp} | 6 +- fap/helpers/camera_suite_led.h | 2 + ...ite_speaker.c => camera_suite_speaker.cpp} | 6 +- fap/helpers/camera_suite_speaker.h | 2 + ...ite_storage.c => camera_suite_storage.cpp} | 6 +- fap/helpers/camera_suite_storage.h | 7 +- ...a_suite_scene.c => camera_suite_scene.cpp} | 0 ....c => camera_suite_scene_app_settings.cpp} | 14 +- ....c => camera_suite_scene_cam_settings.cpp} | 16 +- ...camera.c => camera_suite_scene_camera.cpp} | 8 +- ...e_guide.c => camera_suite_scene_guide.cpp} | 8 +- ...ene_menu.c => camera_suite_scene_menu.cpp} | 8 +- ...e_start.c => camera_suite_scene_start.cpp} | 8 +- ...a.c => camera_suite_scene_wifi_camera.cpp} | 8 +- ..._camera.c => camera_suite_view_camera.cpp} | 234 +++++++++--------- fap/views/camera_suite_view_camera.h | 44 ++-- ...ew_guide.c => camera_suite_view_guide.cpp} | 29 ++- ...ew_start.c => camera_suite_view_start.cpp} | 28 ++- ...ra.c => camera_suite_view_wifi_camera.cpp} | 164 ++++++------ fap/views/camera_suite_view_wifi_camera.h | 32 +-- 24 files changed, 344 insertions(+), 319 deletions(-) rename fap/{camera_suite.c => camera_suite.cpp} (92%) rename fap/helpers/{camera_suite_haptic.c => camera_suite_haptic.cpp} (85%) rename fap/helpers/{camera_suite_led.c => camera_suite_led.cpp} (92%) rename fap/helpers/{camera_suite_speaker.c => camera_suite_speaker.cpp} (82%) rename fap/helpers/{camera_suite_storage.c => camera_suite_storage.cpp} (96%) rename fap/scenes/{camera_suite_scene.c => camera_suite_scene.cpp} (100%) rename fap/scenes/{camera_suite_scene_app_settings.c => camera_suite_scene_app_settings.cpp} (86%) rename fap/scenes/{camera_suite_scene_cam_settings.c => camera_suite_scene_cam_settings.cpp} (88%) rename fap/scenes/{camera_suite_scene_camera.c => camera_suite_scene_camera.cpp} (88%) rename fap/scenes/{camera_suite_scene_guide.c => camera_suite_scene_guide.cpp} (88%) rename fap/scenes/{camera_suite_scene_menu.c => camera_suite_scene_menu.cpp} (93%) rename fap/scenes/{camera_suite_scene_start.c => camera_suite_scene_start.cpp} (88%) rename fap/scenes/{camera_suite_scene_wifi_camera.c => camera_suite_scene_wifi_camera.cpp} (88%) rename fap/views/{camera_suite_view_camera.c => camera_suite_view_camera.cpp} (75%) rename fap/views/{camera_suite_view_guide.c => camera_suite_view_guide.cpp} (84%) rename fap/views/{camera_suite_view_start.c => camera_suite_view_start.cpp} (91%) rename fap/views/{camera_suite_view_wifi_camera.c => camera_suite_view_wifi_camera.cpp} (62%) diff --git a/fap/camera_suite.c b/fap/camera_suite.cpp similarity index 92% rename from fap/camera_suite.c rename to fap/camera_suite.cpp index 5b14eb81292..d7c977c7c21 100644 --- a/fap/camera_suite.c +++ b/fap/camera_suite.cpp @@ -1,29 +1,28 @@ #include "camera_suite.h" -#include bool camera_suite_custom_event_callback(void* context, uint32_t event) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); return scene_manager_handle_custom_event(app->scene_manager, event); } void camera_suite_tick_event_callback(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); scene_manager_handle_tick_event(app->scene_manager); } // Leave app if back button pressed. bool camera_suite_navigation_event_callback(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); return scene_manager_handle_back_event(app->scene_manager); } CameraSuite* camera_suite_app_alloc() { - CameraSuite* app = malloc(sizeof(CameraSuite)); - app->gui = furi_record_open(RECORD_GUI); - app->notification = furi_record_open(RECORD_NOTIFICATION); + CameraSuite* app = static_cast(malloc(sizeof(CameraSuite))); + app->gui = static_cast(furi_record_open(RECORD_GUI)); + app->notification = static_cast(furi_record_open(RECORD_NOTIFICATION)); // Turn backlight on. notification_message(app->notification, &sequence_display_backlight_on); diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 154b08597aa..4d96482c35b 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -19,6 +18,8 @@ #include "views/camera_suite_view_wifi_camera.h" #include "helpers/camera_suite_storage.h" +#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) + #define TAG "Camera Suite" typedef struct { @@ -89,3 +90,10 @@ typedef enum { CameraSuiteLedOff, CameraSuiteLedOn, } CameraSuiteLedState; + +typedef enum { + // Reserved for StreamBuffer internal event + WorkerEventReserved = (1 << 0), + WorkerEventStop = (1 << 1), + WorkerEventRx = (1 << 2), +} WorkerEventFlags; diff --git a/fap/helpers/camera_suite_haptic.c b/fap/helpers/camera_suite_haptic.cpp similarity index 85% rename from fap/helpers/camera_suite_haptic.c rename to fap/helpers/camera_suite_haptic.cpp index 237a9600430..47db852c360 100644 --- a/fap/helpers/camera_suite_haptic.c +++ b/fap/helpers/camera_suite_haptic.cpp @@ -1,8 +1,8 @@ -#include "camera_suite_haptic.h" #include "../camera_suite.h" +#include "camera_suite_haptic.h" void camera_suite_play_happy_bump(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->haptic != 1) { return; } @@ -12,7 +12,7 @@ void camera_suite_play_happy_bump(void* context) { } void camera_suite_play_bad_bump(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->haptic != 1) { return; } @@ -22,7 +22,7 @@ void camera_suite_play_bad_bump(void* context) { } void camera_suite_play_long_bump(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->haptic != 1) { return; } diff --git a/fap/helpers/camera_suite_haptic.h b/fap/helpers/camera_suite_haptic.h index 9b7651f97d5..6b83599a9da 100644 --- a/fap/helpers/camera_suite_haptic.h +++ b/fap/helpers/camera_suite_haptic.h @@ -1,3 +1,5 @@ +#pragma once + #include void camera_suite_play_happy_bump(void* context); diff --git a/fap/helpers/camera_suite_led.c b/fap/helpers/camera_suite_led.cpp similarity index 92% rename from fap/helpers/camera_suite_led.c rename to fap/helpers/camera_suite_led.cpp index c4f1a85d7ac..1a60482417a 100644 --- a/fap/helpers/camera_suite_led.c +++ b/fap/helpers/camera_suite_led.cpp @@ -1,8 +1,8 @@ -#include "camera_suite_led.h" #include "../camera_suite.h" +#include "camera_suite_led.h" void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->led != 1) { return; } @@ -29,7 +29,7 @@ void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { } void camera_suite_led_reset(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); notification_message(app->notification, &sequence_reset_red); notification_message(app->notification, &sequence_reset_green); notification_message(app->notification, &sequence_reset_blue); diff --git a/fap/helpers/camera_suite_led.h b/fap/helpers/camera_suite_led.h index 074947da150..9a551db29f1 100644 --- a/fap/helpers/camera_suite_led.h +++ b/fap/helpers/camera_suite_led.h @@ -1,3 +1,5 @@ +#pragma once + void camera_suite_led_set_rgb(void* context, int red, int green, int blue); void camera_suite_led_reset(void* context); diff --git a/fap/helpers/camera_suite_speaker.c b/fap/helpers/camera_suite_speaker.cpp similarity index 82% rename from fap/helpers/camera_suite_speaker.c rename to fap/helpers/camera_suite_speaker.cpp index c2a5a7dd0e5..9fe2672bea0 100644 --- a/fap/helpers/camera_suite_speaker.c +++ b/fap/helpers/camera_suite_speaker.cpp @@ -1,10 +1,10 @@ -#include "camera_suite_speaker.h" #include "../camera_suite.h" +#include "camera_suite_speaker.h" #define NOTE_INPUT 587.33f void camera_suite_play_input_sound(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->speaker != 1) { return; } @@ -15,7 +15,7 @@ void camera_suite_play_input_sound(void* context) { } void camera_suite_stop_all_sound(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); if(app->speaker != 1) { return; } diff --git a/fap/helpers/camera_suite_speaker.h b/fap/helpers/camera_suite_speaker.h index 2119bbec597..5bc739a3c14 100644 --- a/fap/helpers/camera_suite_speaker.h +++ b/fap/helpers/camera_suite_speaker.h @@ -1,3 +1,5 @@ +#pragma once + #define NOTE_INPUT 587.33f void camera_suite_play_input_sound(void* context); diff --git a/fap/helpers/camera_suite_storage.c b/fap/helpers/camera_suite_storage.cpp similarity index 96% rename from fap/helpers/camera_suite_storage.c rename to fap/helpers/camera_suite_storage.cpp index cbc4cf3c608..654795a3b4f 100644 --- a/fap/helpers/camera_suite_storage.c +++ b/fap/helpers/camera_suite_storage.cpp @@ -1,7 +1,7 @@ #include "camera_suite_storage.h" static Storage* camera_suite_open_storage() { - return furi_record_open(RECORD_STORAGE); + return static_cast(furi_record_open(RECORD_STORAGE)); } static void camera_suite_close_storage() { @@ -15,7 +15,7 @@ static void camera_suite_close_config_file(FlipperFormat* file) { } void camera_suite_save_settings(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); FURI_LOG_D(TAG, "Saving Settings"); Storage* storage = camera_suite_open_storage(); @@ -70,7 +70,7 @@ void camera_suite_save_settings(void* context) { } void camera_suite_read_settings(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); Storage* storage = camera_suite_open_storage(); FlipperFormat* fff_file = flipper_format_file_alloc(storage); diff --git a/fap/helpers/camera_suite_storage.h b/fap/helpers/camera_suite_storage.h index df035385ee3..e925c34eebd 100644 --- a/fap/helpers/camera_suite_storage.h +++ b/fap/helpers/camera_suite_storage.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -5,9 +7,6 @@ #include "../camera_suite.h" -#ifndef CAMERA_SUITE_STORAGE_H -#define CAMERA_SUITE_STORAGE_H - #define BOILERPLATE_SETTINGS_FILE_VERSION 1 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/camera_suite") #define BOILERPLATE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/camera_suite.conf" @@ -25,5 +24,3 @@ void camera_suite_save_settings(void* context); void camera_suite_read_settings(void* context); - -#endif diff --git a/fap/scenes/camera_suite_scene.c b/fap/scenes/camera_suite_scene.cpp similarity index 100% rename from fap/scenes/camera_suite_scene.c rename to fap/scenes/camera_suite_scene.cpp diff --git a/fap/scenes/camera_suite_scene_app_settings.c b/fap/scenes/camera_suite_scene_app_settings.cpp similarity index 86% rename from fap/scenes/camera_suite_scene_app_settings.c rename to fap/scenes/camera_suite_scene_app_settings.cpp index dce74772d71..e1262d6e80e 100644 --- a/fap/scenes/camera_suite_scene_app_settings.c +++ b/fap/scenes/camera_suite_scene_app_settings.cpp @@ -32,7 +32,7 @@ const uint32_t led_value[2] = { }; static void camera_suite_scene_app_settings_set_haptic(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, haptic_text[index]); @@ -40,26 +40,26 @@ static void camera_suite_scene_app_settings_set_haptic(VariableItem* item) { } static void camera_suite_scene_app_settings_set_speaker(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, speaker_text[index]); app->speaker = speaker_value[index]; } static void camera_suite_scene_app_settings_set_led(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, led_text[index]); app->led = led_value[index]; } void camera_suite_scene_app_settings_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_app_settings_on_enter(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); VariableItem* item; uint8_t value_index; @@ -96,7 +96,7 @@ void camera_suite_scene_app_settings_on_enter(void* context) { } bool camera_suite_scene_app_settings_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -105,7 +105,7 @@ bool camera_suite_scene_app_settings_on_event(void* context, SceneManagerEvent e } void camera_suite_scene_app_settings_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); variable_item_list_set_selected_item(app->variable_item_list, 0); variable_item_list_reset(app->variable_item_list); } diff --git a/fap/scenes/camera_suite_scene_cam_settings.c b/fap/scenes/camera_suite_scene_cam_settings.cpp similarity index 88% rename from fap/scenes/camera_suite_scene_cam_settings.c rename to fap/scenes/camera_suite_scene_cam_settings.cpp index 47861eb78cd..8766c9276c0 100644 --- a/fap/scenes/camera_suite_scene_cam_settings.c +++ b/fap/scenes/camera_suite_scene_cam_settings.cpp @@ -50,7 +50,7 @@ const uint32_t jpeg_value[2] = { }; static void camera_suite_scene_cam_settings_set_camera_orientation(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, orientation_text[index]); @@ -58,7 +58,7 @@ static void camera_suite_scene_cam_settings_set_camera_orientation(VariableItem* } static void camera_suite_scene_cam_settings_set_camera_dither(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, dither_text[index]); @@ -66,7 +66,7 @@ static void camera_suite_scene_cam_settings_set_camera_dither(VariableItem* item } static void camera_suite_scene_cam_settings_set_flash(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, flash_text[index]); @@ -74,7 +74,7 @@ static void camera_suite_scene_cam_settings_set_flash(VariableItem* item) { } static void camera_suite_scene_cam_settings_set_jpeg(VariableItem* item) { - CameraSuite* app = variable_item_get_context(item); + CameraSuite* app = static_cast(variable_item_get_context(item)); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, jpeg_text[index]); @@ -82,12 +82,12 @@ static void camera_suite_scene_cam_settings_set_jpeg(VariableItem* item) { } void camera_suite_scene_cam_settings_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_cam_settings_on_enter(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); VariableItem* item; uint8_t value_index; @@ -138,7 +138,7 @@ void camera_suite_scene_cam_settings_on_enter(void* context) { } bool camera_suite_scene_cam_settings_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -147,7 +147,7 @@ bool camera_suite_scene_cam_settings_on_event(void* context, SceneManagerEvent e } void camera_suite_scene_cam_settings_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); variable_item_list_set_selected_item(app->variable_item_list, 0); variable_item_list_reset(app->variable_item_list); } diff --git a/fap/scenes/camera_suite_scene_camera.c b/fap/scenes/camera_suite_scene_camera.cpp similarity index 88% rename from fap/scenes/camera_suite_scene_camera.c rename to fap/scenes/camera_suite_scene_camera.cpp index 809d9a5c11a..9279d9096a7 100644 --- a/fap/scenes/camera_suite_scene_camera.c +++ b/fap/scenes/camera_suite_scene_camera.cpp @@ -4,20 +4,20 @@ void camera_suite_view_camera_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_camera_on_enter(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); camera_suite_view_camera_set_callback( app->camera_suite_view_camera, camera_suite_view_camera_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdCamera); } bool camera_suite_scene_camera_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -47,6 +47,6 @@ bool camera_suite_scene_camera_on_event(void* context, SceneManagerEvent event) } void camera_suite_scene_camera_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); } diff --git a/fap/scenes/camera_suite_scene_guide.c b/fap/scenes/camera_suite_scene_guide.cpp similarity index 88% rename from fap/scenes/camera_suite_scene_guide.c rename to fap/scenes/camera_suite_scene_guide.cpp index 6599058ef68..c204d21e143 100644 --- a/fap/scenes/camera_suite_scene_guide.c +++ b/fap/scenes/camera_suite_scene_guide.cpp @@ -4,20 +4,20 @@ void camera_suite_view_guide_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_guide_on_enter(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); camera_suite_view_guide_set_callback( app->camera_suite_view_guide, camera_suite_view_guide_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdGuide); } bool camera_suite_scene_guide_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -46,6 +46,6 @@ bool camera_suite_scene_guide_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_guide_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_menu.c b/fap/scenes/camera_suite_scene_menu.cpp similarity index 93% rename from fap/scenes/camera_suite_scene_menu.c rename to fap/scenes/camera_suite_scene_menu.cpp index a1ca022922a..aa18f542707 100644 --- a/fap/scenes/camera_suite_scene_menu.c +++ b/fap/scenes/camera_suite_scene_menu.cpp @@ -14,12 +14,12 @@ enum SubmenuIndex { }; void camera_suite_scene_menu_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_menu_on_enter(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); submenu_add_item( app->submenu, @@ -63,7 +63,7 @@ void camera_suite_scene_menu_on_enter(void* context) { } bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); if(event.type == SceneManagerEventTypeBack) { // Exit application. @@ -102,6 +102,6 @@ bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_menu_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); submenu_reset(app->submenu); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_start.c b/fap/scenes/camera_suite_scene_start.cpp similarity index 88% rename from fap/scenes/camera_suite_scene_start.c rename to fap/scenes/camera_suite_scene_start.cpp index 0dda05edef0..37cd0a4eb37 100644 --- a/fap/scenes/camera_suite_scene_start.c +++ b/fap/scenes/camera_suite_scene_start.cpp @@ -4,20 +4,20 @@ void camera_suite_scene_start_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_start_on_enter(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); camera_suite_view_start_set_callback( app->camera_suite_view_start, camera_suite_scene_start_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdStartscreen); } bool camera_suite_scene_start_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -50,6 +50,6 @@ bool camera_suite_scene_start_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_start_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_wifi_camera.c b/fap/scenes/camera_suite_scene_wifi_camera.cpp similarity index 88% rename from fap/scenes/camera_suite_scene_wifi_camera.c rename to fap/scenes/camera_suite_scene_wifi_camera.cpp index 2df3db81bf1..abbc60b9ae2 100644 --- a/fap/scenes/camera_suite_scene_wifi_camera.c +++ b/fap/scenes/camera_suite_scene_wifi_camera.cpp @@ -4,20 +4,20 @@ void camera_suite_view_wifi_camera_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_wifi_camera_on_enter(void* context) { furi_assert(context); - CameraSuite* app = context; + CameraSuite* app = static_cast(context); camera_suite_view_wifi_camera_set_callback( app->camera_suite_view_wifi_camera, camera_suite_view_wifi_camera_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdWiFiCamera); } bool camera_suite_scene_wifi_camera_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -46,6 +46,6 @@ bool camera_suite_scene_wifi_camera_on_event(void* context, SceneManagerEvent ev } void camera_suite_scene_wifi_camera_on_exit(void* context) { - CameraSuite* app = context; + CameraSuite* app = static_cast(context); UNUSED(app); } \ No newline at end of file diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.cpp similarity index 75% rename from fap/views/camera_suite_view_camera.c rename to fap/views/camera_suite_view_camera.cpp index 7d0ebe8a918..0a810a045d8 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.cpp @@ -1,12 +1,5 @@ #include "../camera_suite.h" -#include -#include -#include -#include -#include -#include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_led.h" +#include "camera_suite_view_camera.h" static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint8_t orientation) { furi_assert(canvas); @@ -36,11 +29,11 @@ static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint } } -static void camera_suite_view_camera_draw(Canvas* canvas, void* model) { +static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) { furi_assert(canvas); - furi_assert(model); + furi_assert(uart_dump_model); - UartDumpModel* uartDumpModel = model; + UartDumpModel* model = static_cast(uart_dump_model); // Clear the screen. canvas_set_color(canvas, ColorBlack); @@ -53,14 +46,14 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* model) { uint8_t y = p / ROW_BUFFER_LENGTH; // 0 .. 63 for(uint8_t i = 0; i < 8; ++i) { - if((uartDumpModel->pixels[p] & (1 << (7 - i))) != 0) { - draw_pixel_by_orientation(canvas, (x * 8) + i, y, uartDumpModel->orientation); + if((model->pixels[p] & (1 << (7 - i))) != 0) { + draw_pixel_by_orientation(canvas, (x * 8) + i, y, model->orientation); } } } // Draw the pinout guide if the camera is not initialized. - if(!uartDumpModel->is_initialized) { + if(!model->is_initialized) { // Clear the screen. canvas_clear(canvas); @@ -156,13 +149,13 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* model) { } } -static void save_image_to_flipper_sd_card(void* model) { - furi_assert(model); +static void save_image_to_flipper_sd_card(void* uart_dump_model) { + furi_assert(uart_dump_model); - UartDumpModel* uartDumpModel = model; + UartDumpModel* model = static_cast(uart_dump_model); // This pointer is used to access the storage. - Storage* storage = furi_record_open(RECORD_STORAGE); + Storage* storage = static_cast(furi_record_open(RECORD_STORAGE)); // This pointer is used to access the filesystem. File* file = storage_file_alloc(storage); @@ -179,7 +172,7 @@ static void save_image_to_flipper_sd_card(void* model) { FuriString* file_name = furi_string_alloc(); // Get the current date and time. - FuriHalRtcDateTime datetime = {0}; + FuriHalRtcDateTime datetime = {}; furi_hal_rtc_get_datetime(&datetime); // Create the file name. @@ -201,9 +194,9 @@ static void save_image_to_flipper_sd_card(void* model) { // Free the file name after use. furi_string_free(file_name); - if(!uartDumpModel->is_inverted) { + if(!model->is_inverted) { for(size_t i = 0; i < FRAME_BUFFER_LENGTH; ++i) { - uartDumpModel->pixels[i] = ~uartDumpModel->pixels[i]; + model->pixels[i] = ~model->pixels[i]; } } @@ -222,7 +215,7 @@ static void save_image_to_flipper_sd_card(void* model) { // @todo - Save image based on orientation. for(size_t i = 64; i > 0; --i) { for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) { - row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j]; + row_buffer[j] = model->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j]; } storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH); } @@ -235,33 +228,33 @@ static void save_image_to_flipper_sd_card(void* model) { storage_file_free(file); } -static void - camera_suite_view_camera_model_init(UartDumpModel* const model, CameraSuite* instance_context) { +static void camera_suite_view_camera_model_init(UartDumpModel* model, CameraSuite* app_instance) { furi_assert(model); - furi_assert(instance_context); + furi_assert(app_instance); model->is_dithering_enabled = true; model->is_inverted = false; - uint32_t orientation = instance_context->orientation; + uint32_t orientation = app_instance->orientation; model->orientation = orientation; - for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) { model->pixels[i] = 0; } } -static bool camera_suite_view_camera_input(InputEvent* event, void* context) { - furi_assert(context); - furi_assert(event); +static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera_view_instance) { + furi_assert(camera_view_instance); + furi_assert(input_event); - CameraSuiteViewCamera* instance = context; + CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + uint8_t data[1]; - if(event->type == InputTypeRelease) { - switch(event->key) { - default: // Stop all sounds, reset the LED. - with_view_model( + if(input_event->type == InputTypeRelease) { + if(input_event->key) { + // Stop all sounds, reset the LED. + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { UNUSED(model); camera_suite_play_bad_bump(instance->context); @@ -269,19 +262,20 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 0); }, true); - break; } - } else if(event->type == InputTypePress) { - switch(event->key) { + } else if(input_event->type == InputTypePress) { + switch(input_event->key) { case InputKeyBack: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { UNUSED(model); // Stop camera stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'s'}, 1); + data[0] = 's'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); // Go back to the main menu. @@ -291,9 +285,10 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; } case InputKeyLeft: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { // Play sound. camera_suite_play_happy_bump(instance->context); @@ -302,13 +297,15 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { if(model->is_inverted) { // Camera: Set invert to false on the ESP32-CAM. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); + data[0] = 'i'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); model->is_inverted = false; } else { // Camera: Set invert to true on the ESP32-CAM. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'I'}, 1); + data[0] = 'I'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); model->is_inverted = true; @@ -320,9 +317,10 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; } case InputKeyRight: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { // Play sound. camera_suite_play_happy_bump(instance->context); @@ -331,13 +329,15 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { if(model->is_dithering_enabled) { // Camera: Disable dithering. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'d'}, 1); + data[0] = 'd'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); model->is_dithering_enabled = false; } else { // Camera: Enable dithering. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'D'}, 1); + data[0] = 'D'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); model->is_dithering_enabled = true; @@ -349,9 +349,10 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; } case InputKeyUp: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { UNUSED(model); @@ -361,7 +362,8 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Increase contrast. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'C'}, 1); + data[0] = 'C'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraUp, instance->context); @@ -370,9 +372,10 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; } case InputKeyDown: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { UNUSED(model); @@ -382,7 +385,8 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Reduce contrast. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'c'}, 1); + data[0] = 'c'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraDown, instance->context); @@ -391,9 +395,10 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; } case InputKeyOk: { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { // Play sound. camera_suite_play_long_bump(instance->context); @@ -401,7 +406,8 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // @todo - Save picture directly to ESP32-CAM. - // furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'P'}, 1); + // data[0] = 'P'; + // furi_hal_serial_tx(instance->camera_serial_handle, data, 1); // Save currently displayed image to the Flipper Zero SD card. save_image_to_flipper_sd_card(model); @@ -421,56 +427,60 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { return false; } -static void camera_suite_view_camera_exit(void* context) { - furi_assert(context); +static void camera_suite_view_camera_exit(void* camera_view_instance) { + UNUSED(camera_view_instance); } -static void camera_suite_view_camera_enter(void* context) { - furi_assert(context); +static void camera_suite_view_camera_enter(void* camera_view_instance) { + furi_assert(camera_view_instance); - // Get the camera suite instance context. - CameraSuiteViewCamera* instance = (CameraSuiteViewCamera*)context; + CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + + uint8_t data[1]; // Get the camera suite instance context. - CameraSuite* instance_context = instance->context; + CameraSuite* app_context = static_cast(instance->context); - // Start camera stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'S'}, 1); + // Start serial stream to Flipper Zero. + data[0] = 'S'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); // Get/set dither type. - uint8_t dither_type = instance_context->dither; - furi_hal_uart_tx(FuriHalUartIdUSART1, &dither_type, 1); + uint8_t dither_type = app_context->dither; + furi_hal_serial_tx(instance->camera_serial_handle, &dither_type, 1); furi_delay_ms(50); // Make sure the camera is not inverted. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); + data[0] = 'i'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); // Toggle flash on or off based on the current state. If the user has this // on the flash will stay on the entire time the user is in the camera view. - uint8_t flash_state = instance_context->flash ? 'F' : 'f'; - furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_state, 1); + data[0] = app_context->flash ? 'F' : 'f'; + furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, - { camera_suite_view_camera_model_init(model, instance_context); }, + UartDumpModel*, + model, + { camera_suite_view_camera_model_init(model, app_context); }, true); } -static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) { - furi_assert(uartIrqEvent); - furi_assert(data); - furi_assert(context); +static void camera_on_irq_cb( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* camera_view_instance) { + furi_assert(handle); + furi_assert(camera_view_instance); - // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`. - CameraSuiteViewCamera* instance = context; + CameraSuiteViewCamera* instance = static_cast(camera_view_instance); - // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the - // `camera_rx_stream` and set the `WorkerEventRx` flag. - if(uartIrqEvent == UartIrqEventRXNE) { + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); furi_stream_buffer_send(instance->camera_rx_stream, &data, 1, 0); furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventRx); } @@ -524,15 +534,15 @@ static void process_ringbuffer(UartDumpModel* model, uint8_t const byte) { } } -static int32_t camera_suite_camera_worker(void* context) { - furi_assert(context); +static int32_t camera_suite_camera_worker(void* camera_view_instance) { + furi_assert(camera_view_instance); - CameraSuiteViewCamera* instance = context; + CameraSuiteViewCamera* instance = static_cast(camera_view_instance); while(1) { // Wait for any event on the worker thread. uint32_t events = - furi_thread_flags_wait(CAMERA_WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); + furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); // Check if an error occurred. furi_check((events & FuriFlagError) == 0); @@ -552,9 +562,10 @@ static int32_t camera_suite_camera_worker(void* context) { length = furi_stream_buffer_receive(instance->camera_rx_stream, data, buffer_size, 0); if(length > 0) { - with_view_model( + with_view_model_cpp( instance->view, - UartDumpModel * model, + UartDumpModel*, + model, { // Process the data. for(size_t i = 0; i < length; i++) { @@ -565,8 +576,8 @@ static int32_t camera_suite_camera_worker(void* context) { } } while(length > 0); - with_view_model( - instance->view, UartDumpModel * model, { UNUSED(model); }, true); + with_view_model_cpp( + instance->view, UartDumpModel*, model, { UNUSED(model); }, true); } } @@ -575,7 +586,8 @@ static int32_t camera_suite_camera_worker(void* context) { CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // Allocate memory for the instance - CameraSuiteViewCamera* instance = malloc(sizeof(CameraSuiteViewCamera)); + CameraSuiteViewCamera* instance = + static_cast(malloc(sizeof(CameraSuiteViewCamera))); // Allocate the view object instance->view = view_alloc(); @@ -607,14 +619,14 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { instance->camera_worker_thread = thread; furi_thread_start(instance->camera_worker_thread); - // Disable console. - furi_hal_console_disable(); - - // 115200 is the default baud rate for the ESP32-CAM. - furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + // Set up UART thread. + instance->camera_serial_handle = furi_hal_serial_control_acquire(UART_CH); + furi_check(instance->camera_serial_handle); + furi_hal_serial_init(instance->camera_serial_handle, 230400); // Enable UART1 and set the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance); + furi_hal_serial_async_rx_start( + instance->camera_serial_handle, camera_on_irq_cb, instance, false); return instance; } @@ -622,20 +634,20 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { furi_assert(instance); - // Remove the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); - // Free the worker thread. + furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventStop); + furi_thread_join(instance->camera_worker_thread); furi_thread_free(instance->camera_worker_thread); // Free the allocated stream buffer. furi_stream_buffer_free(instance->camera_rx_stream); - // Re-enable the console. - // furi_hal_console_enable(); + // Deinitialize the UART. + furi_hal_serial_deinit(instance->camera_serial_handle); + furi_hal_serial_control_release(instance->camera_serial_handle); - with_view_model( - instance->view, UartDumpModel * model, { UNUSED(model); }, true); + with_view_model_cpp( + instance->view, UartDumpModel*, model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } @@ -646,11 +658,11 @@ View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* instance) { } void camera_suite_view_camera_set_callback( - CameraSuiteViewCamera* instance, + CameraSuiteViewCamera* camera_view_instance, CameraSuiteViewCameraCallback callback, void* context) { - furi_assert(instance); + furi_assert(camera_view_instance); furi_assert(callback); - instance->callback = callback; - instance->context = context; + camera_view_instance->callback = callback; + camera_view_instance->context = context; } \ No newline at end of file diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index d8143fdf614..b790dae658e 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -1,19 +1,12 @@ #pragma once -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include "../helpers/camera_suite_haptic.h" +#include "../helpers/camera_suite_led.h" +#include "../helpers/camera_suite_speaker.h" #include "../helpers/camera_suite_custom_event.h" #define BITMAP_HEADER_LENGTH 62 @@ -26,26 +19,31 @@ #define RING_BUFFER_LENGTH 19 #define ROW_BUFFER_LENGTH 16 +#ifdef xtreme_settings +/** + * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). + * + * @see https://github.com/Flipper-XFW/Xtreme-Firmware + * @see https://github.com/Flipper-XFW/Xtreme-Apps +*/ +#define UART_CH (xtreme_settings.uart_esp_channel) +#else +#define UART_CH (FuriHalSerialIdUsart) +#endif + static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; -typedef enum { - WorkerEventReserved = (1 << 0), // Reserved for StreamBuffer internal event - WorkerEventStop = (1 << 1), - WorkerEventRx = (1 << 2), -} WorkerEventFlags; - -#define CAMERA_WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) - // Forward declaration typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context); typedef struct CameraSuiteViewCamera { CameraSuiteViewCameraCallback callback; FuriStreamBuffer* camera_rx_stream; + FuriHalSerialHandle* camera_serial_handle; FuriThread* camera_worker_thread; NotificationApp* notification; View* view; @@ -66,9 +64,9 @@ typedef struct UartDumpModel { // Function Prototypes CameraSuiteViewCamera* camera_suite_view_camera_alloc(); -View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_suite_static); -void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static); +View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_view_instance); +void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_view_instance); void camera_suite_view_camera_set_callback( - CameraSuiteViewCamera* camera_suite_view_camera, + CameraSuiteViewCamera* camera_view_instance, CameraSuiteViewCameraCallback callback, void* context); diff --git a/fap/views/camera_suite_view_guide.c b/fap/views/camera_suite_view_guide.cpp similarity index 84% rename from fap/views/camera_suite_view_guide.c rename to fap/views/camera_suite_view_guide.cpp index 4b848846f0e..32a4afd6ea7 100644 --- a/fap/views/camera_suite_view_guide.c +++ b/fap/views/camera_suite_view_guide.cpp @@ -43,15 +43,18 @@ static void camera_suite_view_guide_model_init(CameraSuiteViewGuideModel* const model->some_value = 1; } -bool camera_suite_view_guide_input(InputEvent* event, void* context) { - furi_assert(context); - CameraSuiteViewGuide* instance = context; +bool camera_suite_view_guide_input(InputEvent* event, void* grid_view_instance) { + furi_assert(grid_view_instance); + + CameraSuiteViewGuide* instance = static_cast(grid_view_instance); + if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewGuideModel * model, + CameraSuiteViewGuideModel*, + model, { UNUSED(model); // Go back to the main menu. @@ -79,15 +82,16 @@ void camera_suite_view_guide_exit(void* context) { void camera_suite_view_guide_enter(void* context) { furi_assert(context); CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)context; - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewGuideModel * model, + CameraSuiteViewGuideModel*, + model, { camera_suite_view_guide_model_init(model); }, true); } CameraSuiteViewGuide* camera_suite_view_guide_alloc() { - CameraSuiteViewGuide* instance = malloc(sizeof(CameraSuiteViewGuide)); + CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)malloc(sizeof(CameraSuiteViewGuide)); instance->view = view_alloc(); view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewGuideModel)); view_set_context(instance->view, instance); // furi_assert crashes in events without this @@ -96,9 +100,10 @@ CameraSuiteViewGuide* camera_suite_view_guide_alloc() { view_set_enter_callback(instance->view, camera_suite_view_guide_enter); view_set_exit_callback(instance->view, camera_suite_view_guide_exit); - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewGuideModel * model, + CameraSuiteViewGuideModel*, + model, { camera_suite_view_guide_model_init(model); }, true); @@ -108,8 +113,8 @@ CameraSuiteViewGuide* camera_suite_view_guide_alloc() { void camera_suite_view_guide_free(CameraSuiteViewGuide* instance) { furi_assert(instance); - with_view_model( - instance->view, CameraSuiteViewGuideModel * model, { UNUSED(model); }, true); + with_view_model_cpp( + instance->view, CameraSuiteViewGuideModel*, model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_start.c b/fap/views/camera_suite_view_start.cpp similarity index 91% rename from fap/views/camera_suite_view_start.c rename to fap/views/camera_suite_view_start.cpp index 7376b0e1674..1a4f91b98d4 100644 --- a/fap/views/camera_suite_view_start.c +++ b/fap/views/camera_suite_view_start.cpp @@ -107,14 +107,15 @@ static void camera_suite_view_start_model_init(CameraSuiteViewStartModel* const bool camera_suite_view_start_input(InputEvent* event, void* context) { furi_assert(context); - CameraSuiteViewStart* instance = context; + CameraSuiteViewStart* instance = static_cast(context); if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: // Exit application. - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewStartModel * model, + CameraSuiteViewStartModel*, + model, { UNUSED(model); instance->callback(CameraSuiteCustomEventStartBack, instance->context); @@ -123,9 +124,10 @@ bool camera_suite_view_start_input(InputEvent* event, void* context) { break; case InputKeyOk: // Start the application. - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewStartModel * model, + CameraSuiteViewStartModel*, + model, { UNUSED(model); instance->callback(CameraSuiteCustomEventStartOk, instance->context); @@ -151,16 +153,17 @@ void camera_suite_view_start_exit(void* context) { void camera_suite_view_start_enter(void* context) { furi_assert(context); CameraSuiteViewStart* instance = (CameraSuiteViewStart*)context; - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewStartModel * model, + CameraSuiteViewStartModel*, + model, { camera_suite_view_start_model_init(model); }, true); } CameraSuiteViewStart* camera_suite_view_start_alloc() { // Allocate memory for the instance - CameraSuiteViewStart* instance = malloc(sizeof(CameraSuiteViewStart)); + CameraSuiteViewStart* instance = (CameraSuiteViewStart*)malloc(sizeof(CameraSuiteViewStart)); // Allocate the view object instance->view = view_alloc(); @@ -177,9 +180,10 @@ CameraSuiteViewStart* camera_suite_view_start_alloc() { // Set input callback view_set_input_callback(instance->view, camera_suite_view_start_input); - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewStartModel * model, + CameraSuiteViewStartModel*, + model, { camera_suite_view_start_model_init(model); }, true); @@ -189,8 +193,8 @@ CameraSuiteViewStart* camera_suite_view_start_alloc() { void camera_suite_view_start_free(CameraSuiteViewStart* instance) { furi_assert(instance); - with_view_model( - instance->view, CameraSuiteViewStartModel * model, { UNUSED(model); }, true); + with_view_model_cpp( + instance->view, CameraSuiteViewStartModel*, model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.cpp similarity index 62% rename from fap/views/camera_suite_view_wifi_camera.c rename to fap/views/camera_suite_view_wifi_camera.cpp index d4b9c9d6644..69324c7c2df 100644 --- a/fap/views/camera_suite_view_wifi_camera.c +++ b/fap/views/camera_suite_view_wifi_camera.cpp @@ -1,28 +1,12 @@ #include "../camera_suite.h" -#include -#include -#include -#include -#include -#include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_led.h" +#include "camera_suite_view_wifi_camera.h" -void camera_suite_view_wifi_camera_set_callback( - CameraSuiteViewWiFiCamera* instance, - CameraSuiteViewWiFiCameraCallback callback, - void* context) { - furi_assert(instance); - furi_assert(callback); - instance->callback = callback; - instance->context = context; -} - -static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* model) { +static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* wifi_model) { furi_assert(canvas); - furi_assert(model); + furi_assert(wifi_model); - CameraSuiteViewWiFiCameraModel* instance = model; + CameraSuiteViewWiFiCameraModel* model = + static_cast(wifi_model); canvas_clear(canvas); canvas_set_color(canvas, ColorBlack); @@ -32,18 +16,18 @@ static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* model) { canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, "Starting WiFi Stream at:"); // Draw log from camera. - canvas_draw_str_aligned( - canvas, 3, 13, AlignLeft, AlignTop, furi_string_get_cstr(instance->log)); + canvas_draw_str_aligned(canvas, 3, 13, AlignLeft, AlignTop, furi_string_get_cstr(model->log)); } -static int32_t camera_suite_wifi_camera_worker(void* context) { - furi_assert(context); +static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { + furi_assert(wifi_view_instance); - CameraSuiteViewWiFiCamera* instance = context; + CameraSuiteViewWiFiCamera* instance = + static_cast(wifi_view_instance); while(1) { uint32_t events = - furi_thread_flags_wait(WIFI_WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); + furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); // Check if an error occurred. furi_check((events & FuriFlagError) == 0); @@ -61,9 +45,10 @@ static int32_t camera_suite_wifi_camera_worker(void* context) { if(length > 0) { data[length] = '\0'; - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { furi_string_cat_printf(model->log, "%s", data); @@ -78,26 +63,30 @@ static int32_t camera_suite_wifi_camera_worker(void* context) { } } while(length > 0); - with_view_model( - instance->view, CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); }, true); + with_view_model_cpp( + instance->view, CameraSuiteViewWiFiCameraModel*, model, { UNUSED(model); }, true); } } return 0; } -static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context) { - furi_assert(context); - furi_assert(event); +static bool + camera_suite_view_wifi_camera_input(InputEvent* input_event, void* wifi_view_instance) { + furi_assert(wifi_view_instance); + furi_assert(input_event); - CameraSuiteViewWiFiCamera* instance = context; + CameraSuiteViewWiFiCamera* instance = + static_cast(wifi_view_instance); + uint8_t data[1]; - if(event->type == InputTypeRelease) { - switch(event->key) { + if(input_event->type == InputTypeRelease) { + switch(input_event->key) { default: - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { UNUSED(model); // Stop all sounds, reset the LED. @@ -108,17 +97,19 @@ static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context true); break; } - } else if(event->type == InputTypePress) { - switch(event->key) { + } else if(input_event->type == InputTypePress) { + switch(input_event->key) { case InputKeyBack: { - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { UNUSED(model); // Stop camera WiFi stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'w'}, 1); + data[0] = 'w'; + furi_hal_serial_tx(instance->wifi_serial_handle, data, 1); furi_delay_ms(50); // Go back to the main menu. @@ -142,8 +133,8 @@ static bool camera_suite_view_wifi_camera_input(InputEvent* event, void* context return false; } -static void camera_suite_view_wifi_camera_exit(void* context) { - furi_assert(context); +static void camera_suite_view_wifi_camera_exit(void* wifi_view_instance) { + UNUSED(wifi_view_instance); } static void camera_suite_view_wifi_camera_model_init(CameraSuiteViewWiFiCameraModel* const model) { @@ -151,33 +142,37 @@ static void camera_suite_view_wifi_camera_model_init(CameraSuiteViewWiFiCameraMo furi_string_reserve(model->log, 4096); } -static void camera_suite_view_wifi_camera_enter(void* context) { - furi_assert(context); +static void camera_suite_view_wifi_camera_enter(void* wifi_view_instance) { + furi_assert(wifi_view_instance); - // Get the camera suite instance context. - CameraSuiteViewWiFiCamera* instance = (CameraSuiteViewWiFiCamera*)context; + CameraSuiteViewWiFiCamera* instance = + static_cast(wifi_view_instance); // Start wifi camera stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'W'}, 1); + uint8_t data[1] = {'W'}; + furi_hal_serial_tx(instance->wifi_serial_handle, data, 1); + furi_delay_ms(50); - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { camera_suite_view_wifi_camera_model_init(model); }, true); } -static void wifi_camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) { - furi_assert(uartIrqEvent); - furi_assert(data); - furi_assert(context); +static void wifi_camera_on_irq_cb( + FuriHalSerialHandle* handle, + FuriHalSerialRxEvent event, + void* wifi_view_instance) { + furi_assert(handle); + furi_assert(wifi_view_instance); - // Cast `context` to `CameraSuiteViewWiFiCamera*` and store it in `instance`. - CameraSuiteViewWiFiCamera* instance = context; + CameraSuiteViewWiFiCamera* instance = + static_cast(wifi_view_instance); - // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the - // `wifi_rx_stream` and set the `WorkerEventRx` flag. - if(uartIrqEvent == UartIrqEventRXNE) { + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); furi_stream_buffer_send(instance->wifi_rx_stream, &data, 1, 0); furi_thread_flags_set(furi_thread_get_id(instance->wifi_worker_thread), WorkerEventRx); } @@ -185,7 +180,8 @@ static void wifi_camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Allocate memory for the instance - CameraSuiteViewWiFiCamera* instance = malloc(sizeof(CameraSuiteViewWiFiCamera)); + CameraSuiteViewWiFiCamera* instance = + (CameraSuiteViewWiFiCamera*)malloc(sizeof(CameraSuiteViewWiFiCamera)); // Allocate the view object instance->view = view_alloc(); @@ -207,14 +203,14 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { instance->wifi_worker_thread = thread; furi_thread_start(instance->wifi_worker_thread); - // Disable console. - furi_hal_console_disable(); - - // 115200 is the default baud rate for the ESP32-CAM. - furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + // Set up UART thread. + instance->wifi_serial_handle = furi_hal_serial_control_acquire(UART_CH); + furi_check(instance->wifi_serial_handle); + furi_hal_serial_init(instance->wifi_serial_handle, 230400); // Enable UART1 and set the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, wifi_camera_on_irq_cb, instance); + furi_hal_serial_async_rx_start( + instance->wifi_serial_handle, wifi_camera_on_irq_cb, instance, false); // Set draw callback view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_wifi_camera_draw); @@ -228,9 +224,10 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Set exit callback view_set_exit_callback(instance->view, camera_suite_view_wifi_camera_exit); - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { camera_suite_view_wifi_camera_model_init(model); }, true); @@ -240,21 +237,22 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* instance) { furi_assert(instance); - // Remove the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); - // Free the worker thread. + furi_thread_flags_set(furi_thread_get_id(instance->wifi_worker_thread), WorkerEventStop); + furi_thread_join(instance->wifi_worker_thread); furi_thread_free(instance->wifi_worker_thread); // Free the allocated stream buffer. furi_stream_buffer_free(instance->wifi_rx_stream); - // Re-enable the console. - furi_hal_console_enable(); + // Deinitialize the UART. + furi_hal_serial_deinit(instance->wifi_serial_handle); + furi_hal_serial_control_release(instance->wifi_serial_handle); - with_view_model( + with_view_model_cpp( instance->view, - CameraSuiteViewWiFiCameraModel * model, + CameraSuiteViewWiFiCameraModel*, + model, { furi_string_free(model->log); }, true); view_free(instance->view); @@ -265,3 +263,13 @@ View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* instance furi_assert(instance); return instance->view; } + +void camera_suite_view_wifi_camera_set_callback( + CameraSuiteViewWiFiCamera* instance, + CameraSuiteViewWiFiCameraCallback callback, + void* context) { + furi_assert(instance); + furi_assert(callback); + instance->callback = callback; + instance->context = context; +} diff --git a/fap/views/camera_suite_view_wifi_camera.h b/fap/views/camera_suite_view_wifi_camera.h index 4cb40d4487d..f8eba95b44f 100644 --- a/fap/views/camera_suite_view_wifi_camera.h +++ b/fap/views/camera_suite_view_wifi_camera.h @@ -1,31 +1,21 @@ #pragma once -#include - -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include "../helpers/camera_suite_haptic.h" +#include "../helpers/camera_suite_led.h" +#include "../helpers/camera_suite_speaker.h" #include "../helpers/camera_suite_custom_event.h" -#define WIFI_WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) - typedef void (*CameraSuiteViewWiFiCameraCallback)(CameraSuiteCustomEvent event, void* context); typedef struct CameraSuiteViewWiFiCamera { View* view; CameraSuiteViewCameraCallback callback; void* context; + FuriHalSerialHandle* wifi_serial_handle; FuriStreamBuffer* wifi_rx_stream; FuriThread* wifi_worker_thread; } CameraSuiteViewWiFiCamera; @@ -35,13 +25,11 @@ typedef struct { size_t log_strlen; } CameraSuiteViewWiFiCameraModel; +// Function Prototypes CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc(); - -View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* camera_suite_static); - -void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* camera_suite_static); - +void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* wifi_view_instance); +View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* wifi_view_instance); void camera_suite_view_wifi_camera_set_callback( - CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera, + CameraSuiteViewWiFiCamera* wifi_view_instance, CameraSuiteViewWiFiCameraCallback callback, void* context); From e517e0ee3a7eff02f04751ea4eea73086c06e4ef Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 19:11:46 -0600 Subject: [PATCH 15/43] Update workflows. --- .github/workflows/deploy-main.yml | 26 +++++++------- .github/workflows/lint-pull-request.yml | 25 ------------- .github/workflows/pull-request.yml | 48 +++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/lint-pull-request.yml create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 4c333960bb8..322a47b89f5 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -1,4 +1,4 @@ -name: "Build + upload." +name: "Build & upload." on: push: branches: @@ -15,30 +15,32 @@ jobs: strategy: matrix: include: - # - name: dev - # sdk-channel: dev + - name: "dev" + sdk-channel: dev - - name: release - sdk-channel: release + - name: "rc" + sdk-channel: rc - # - name: rc - # sdk-channel: rc + - name: "release" + sdk-channel: release - name: "ufbt: Build for ${{ matrix.name }}" + name: "Build: ${{ matrix.name }} release." steps: - - name: Checkout + - name: "Checkout." uses: actions/checkout@v3 with: submodules: recursive - - name: Build with ufbt - uses: flipperdevices/flipperzero-ufbt-action@v0.1.3 + # Flipper Zero ufbt action + # https://github.com/flipperdevices/flipperzero-ufbt-action + - name: "Build." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap sdk-channel: ${{ matrix.sdk-channel }} - - name: Upload app artifacts + - name: "Create zip archives (dev/release/rc)" uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-${{ matrix.name }}-${{ steps.build-app.outputs.suffix }}.zip diff --git a/.github/workflows/lint-pull-request.yml b/.github/workflows/lint-pull-request.yml deleted file mode 100644 index 1a3aebade21..00000000000 --- a/.github/workflows/lint-pull-request.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "Build test + lint." -on: pull_request -jobs: - ufbt-build-action: - runs-on: ubuntu-latest - name: "ufbt: Build for Release branch" - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Build with ufbt - # Flipper Zero ufbt action - # https://github.com/flipperdevices/flipperzero-ufbt-action - uses: flipperdevices/flipperzero-ufbt-action@v0.1 - id: build-app - with: - app-dir: ./fap - sdk-channel: release - - - name: Lint sources - uses: flipperdevices/flipperzero-ufbt-action@v0.1 - with: - app-dir: ./fap - skip-setup: true - task: lint diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 00000000000..1b2e9b130c3 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,48 @@ +name: "Lint & build PR." +on: pull_request +jobs: + ufbt-lint-action: + runs-on: ubuntu-latest + name: "Lint PR: ${{ github.event.pull_request.head.repo.name }}." + steps: + - name: "Checkout." + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: "Lint." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + with: + app-dir: ./fap + skip-setup: true + task: lint + + ufbt-build-action: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: "dev" + sdk-channel: dev + + - name: "rc" + sdk-channel: rc + + - name: "release" + sdk-channel: release + + name: "Build: ${{ matrix.name }} test PR release." + steps: + - name: "Checkout." + uses: actions/checkout@v3 + with: + submodules: recursive + + # Flipper Zero ufbt action + # https://github.com/flipperdevices/flipperzero-ufbt-action + - name: "Build." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + id: build-app + with: + app-dir: ./fap + sdk-channel: ${{ matrix.sdk-channel }} From f538c4ea8f9cc4c6482cc93da19303622f1c4564 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 20:17:43 -0600 Subject: [PATCH 16/43] From C++ to C. --- fap/{camera_suite.cpp => camera_suite.c} | 12 ++-- ...suite_haptic.cpp => camera_suite_haptic.c} | 6 +- ...amera_suite_led.cpp => camera_suite_led.c} | 4 +- ...ite_speaker.cpp => camera_suite_speaker.c} | 4 +- ...ite_storage.cpp => camera_suite_storage.c} | 6 +- ...a_suite_scene.cpp => camera_suite_scene.c} | 0 ....cpp => camera_suite_scene_app_settings.c} | 14 ++-- ....cpp => camera_suite_scene_cam_settings.c} | 16 ++--- ...camera.cpp => camera_suite_scene_camera.c} | 8 +-- ...e_guide.cpp => camera_suite_scene_guide.c} | 8 +-- ...ene_menu.cpp => camera_suite_scene_menu.c} | 8 +-- ...e_start.cpp => camera_suite_scene_start.c} | 8 +-- ...a.cpp => camera_suite_scene_wifi_camera.c} | 8 +-- ..._camera.cpp => camera_suite_view_camera.c} | 72 ++++++++----------- ...ew_guide.cpp => camera_suite_view_guide.c} | 25 +++---- ...ew_start.cpp => camera_suite_view_start.c} | 28 ++++---- ...ra.cpp => camera_suite_view_wifi_camera.c} | 52 ++++++-------- 17 files changed, 125 insertions(+), 154 deletions(-) rename fap/{camera_suite.cpp => camera_suite.c} (92%) rename fap/helpers/{camera_suite_haptic.cpp => camera_suite_haptic.c} (85%) rename fap/helpers/{camera_suite_led.cpp => camera_suite_led.c} (92%) rename fap/helpers/{camera_suite_speaker.cpp => camera_suite_speaker.c} (82%) rename fap/helpers/{camera_suite_storage.cpp => camera_suite_storage.c} (96%) rename fap/scenes/{camera_suite_scene.cpp => camera_suite_scene.c} (100%) rename fap/scenes/{camera_suite_scene_app_settings.cpp => camera_suite_scene_app_settings.c} (86%) rename fap/scenes/{camera_suite_scene_cam_settings.cpp => camera_suite_scene_cam_settings.c} (88%) rename fap/scenes/{camera_suite_scene_camera.cpp => camera_suite_scene_camera.c} (88%) rename fap/scenes/{camera_suite_scene_guide.cpp => camera_suite_scene_guide.c} (88%) rename fap/scenes/{camera_suite_scene_menu.cpp => camera_suite_scene_menu.c} (93%) rename fap/scenes/{camera_suite_scene_start.cpp => camera_suite_scene_start.c} (88%) rename fap/scenes/{camera_suite_scene_wifi_camera.cpp => camera_suite_scene_wifi_camera.c} (88%) rename fap/views/{camera_suite_view_camera.cpp => camera_suite_view_camera.c} (92%) rename fap/views/{camera_suite_view_guide.cpp => camera_suite_view_guide.c} (85%) rename fap/views/{camera_suite_view_start.cpp => camera_suite_view_start.c} (91%) rename fap/views/{camera_suite_view_wifi_camera.cpp => camera_suite_view_wifi_camera.c} (85%) diff --git a/fap/camera_suite.cpp b/fap/camera_suite.c similarity index 92% rename from fap/camera_suite.cpp rename to fap/camera_suite.c index d7c977c7c21..107bc64fb83 100644 --- a/fap/camera_suite.cpp +++ b/fap/camera_suite.c @@ -2,27 +2,27 @@ bool camera_suite_custom_event_callback(void* context, uint32_t event) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; return scene_manager_handle_custom_event(app->scene_manager, event); } void camera_suite_tick_event_callback(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; scene_manager_handle_tick_event(app->scene_manager); } // Leave app if back button pressed. bool camera_suite_navigation_event_callback(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; return scene_manager_handle_back_event(app->scene_manager); } CameraSuite* camera_suite_app_alloc() { - CameraSuite* app = static_cast(malloc(sizeof(CameraSuite))); - app->gui = static_cast(furi_record_open(RECORD_GUI)); - app->notification = static_cast(furi_record_open(RECORD_NOTIFICATION)); + CameraSuite* app = malloc(sizeof(CameraSuite)); + app->gui = furi_record_open(RECORD_GUI); + app->notification = furi_record_open(RECORD_NOTIFICATION); // Turn backlight on. notification_message(app->notification, &sequence_display_backlight_on); diff --git a/fap/helpers/camera_suite_haptic.cpp b/fap/helpers/camera_suite_haptic.c similarity index 85% rename from fap/helpers/camera_suite_haptic.cpp rename to fap/helpers/camera_suite_haptic.c index 47db852c360..5b08f563d2b 100644 --- a/fap/helpers/camera_suite_haptic.cpp +++ b/fap/helpers/camera_suite_haptic.c @@ -2,7 +2,7 @@ #include "camera_suite_haptic.h" void camera_suite_play_happy_bump(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->haptic != 1) { return; } @@ -12,7 +12,7 @@ void camera_suite_play_happy_bump(void* context) { } void camera_suite_play_bad_bump(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->haptic != 1) { return; } @@ -22,7 +22,7 @@ void camera_suite_play_bad_bump(void* context) { } void camera_suite_play_long_bump(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->haptic != 1) { return; } diff --git a/fap/helpers/camera_suite_led.cpp b/fap/helpers/camera_suite_led.c similarity index 92% rename from fap/helpers/camera_suite_led.cpp rename to fap/helpers/camera_suite_led.c index 1a60482417a..64723b8b4da 100644 --- a/fap/helpers/camera_suite_led.cpp +++ b/fap/helpers/camera_suite_led.c @@ -2,7 +2,7 @@ #include "camera_suite_led.h" void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->led != 1) { return; } @@ -29,7 +29,7 @@ void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { } void camera_suite_led_reset(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; notification_message(app->notification, &sequence_reset_red); notification_message(app->notification, &sequence_reset_green); notification_message(app->notification, &sequence_reset_blue); diff --git a/fap/helpers/camera_suite_speaker.cpp b/fap/helpers/camera_suite_speaker.c similarity index 82% rename from fap/helpers/camera_suite_speaker.cpp rename to fap/helpers/camera_suite_speaker.c index 9fe2672bea0..78962dd49c2 100644 --- a/fap/helpers/camera_suite_speaker.cpp +++ b/fap/helpers/camera_suite_speaker.c @@ -4,7 +4,7 @@ #define NOTE_INPUT 587.33f void camera_suite_play_input_sound(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->speaker != 1) { return; } @@ -15,7 +15,7 @@ void camera_suite_play_input_sound(void* context) { } void camera_suite_stop_all_sound(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; if(app->speaker != 1) { return; } diff --git a/fap/helpers/camera_suite_storage.cpp b/fap/helpers/camera_suite_storage.c similarity index 96% rename from fap/helpers/camera_suite_storage.cpp rename to fap/helpers/camera_suite_storage.c index 654795a3b4f..cbc4cf3c608 100644 --- a/fap/helpers/camera_suite_storage.cpp +++ b/fap/helpers/camera_suite_storage.c @@ -1,7 +1,7 @@ #include "camera_suite_storage.h" static Storage* camera_suite_open_storage() { - return static_cast(furi_record_open(RECORD_STORAGE)); + return furi_record_open(RECORD_STORAGE); } static void camera_suite_close_storage() { @@ -15,7 +15,7 @@ static void camera_suite_close_config_file(FlipperFormat* file) { } void camera_suite_save_settings(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; FURI_LOG_D(TAG, "Saving Settings"); Storage* storage = camera_suite_open_storage(); @@ -70,7 +70,7 @@ void camera_suite_save_settings(void* context) { } void camera_suite_read_settings(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; Storage* storage = camera_suite_open_storage(); FlipperFormat* fff_file = flipper_format_file_alloc(storage); diff --git a/fap/scenes/camera_suite_scene.cpp b/fap/scenes/camera_suite_scene.c similarity index 100% rename from fap/scenes/camera_suite_scene.cpp rename to fap/scenes/camera_suite_scene.c diff --git a/fap/scenes/camera_suite_scene_app_settings.cpp b/fap/scenes/camera_suite_scene_app_settings.c similarity index 86% rename from fap/scenes/camera_suite_scene_app_settings.cpp rename to fap/scenes/camera_suite_scene_app_settings.c index e1262d6e80e..dce74772d71 100644 --- a/fap/scenes/camera_suite_scene_app_settings.cpp +++ b/fap/scenes/camera_suite_scene_app_settings.c @@ -32,7 +32,7 @@ const uint32_t led_value[2] = { }; static void camera_suite_scene_app_settings_set_haptic(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, haptic_text[index]); @@ -40,26 +40,26 @@ static void camera_suite_scene_app_settings_set_haptic(VariableItem* item) { } static void camera_suite_scene_app_settings_set_speaker(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, speaker_text[index]); app->speaker = speaker_value[index]; } static void camera_suite_scene_app_settings_set_led(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, led_text[index]); app->led = led_value[index]; } void camera_suite_scene_app_settings_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_app_settings_on_enter(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; VariableItem* item; uint8_t value_index; @@ -96,7 +96,7 @@ void camera_suite_scene_app_settings_on_enter(void* context) { } bool camera_suite_scene_app_settings_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -105,7 +105,7 @@ bool camera_suite_scene_app_settings_on_event(void* context, SceneManagerEvent e } void camera_suite_scene_app_settings_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; variable_item_list_set_selected_item(app->variable_item_list, 0); variable_item_list_reset(app->variable_item_list); } diff --git a/fap/scenes/camera_suite_scene_cam_settings.cpp b/fap/scenes/camera_suite_scene_cam_settings.c similarity index 88% rename from fap/scenes/camera_suite_scene_cam_settings.cpp rename to fap/scenes/camera_suite_scene_cam_settings.c index 8766c9276c0..47861eb78cd 100644 --- a/fap/scenes/camera_suite_scene_cam_settings.cpp +++ b/fap/scenes/camera_suite_scene_cam_settings.c @@ -50,7 +50,7 @@ const uint32_t jpeg_value[2] = { }; static void camera_suite_scene_cam_settings_set_camera_orientation(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, orientation_text[index]); @@ -58,7 +58,7 @@ static void camera_suite_scene_cam_settings_set_camera_orientation(VariableItem* } static void camera_suite_scene_cam_settings_set_camera_dither(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, dither_text[index]); @@ -66,7 +66,7 @@ static void camera_suite_scene_cam_settings_set_camera_dither(VariableItem* item } static void camera_suite_scene_cam_settings_set_flash(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, flash_text[index]); @@ -74,7 +74,7 @@ static void camera_suite_scene_cam_settings_set_flash(VariableItem* item) { } static void camera_suite_scene_cam_settings_set_jpeg(VariableItem* item) { - CameraSuite* app = static_cast(variable_item_get_context(item)); + CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, jpeg_text[index]); @@ -82,12 +82,12 @@ static void camera_suite_scene_cam_settings_set_jpeg(VariableItem* item) { } void camera_suite_scene_cam_settings_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_cam_settings_on_enter(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; VariableItem* item; uint8_t value_index; @@ -138,7 +138,7 @@ void camera_suite_scene_cam_settings_on_enter(void* context) { } bool camera_suite_scene_cam_settings_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -147,7 +147,7 @@ bool camera_suite_scene_cam_settings_on_event(void* context, SceneManagerEvent e } void camera_suite_scene_cam_settings_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; variable_item_list_set_selected_item(app->variable_item_list, 0); variable_item_list_reset(app->variable_item_list); } diff --git a/fap/scenes/camera_suite_scene_camera.cpp b/fap/scenes/camera_suite_scene_camera.c similarity index 88% rename from fap/scenes/camera_suite_scene_camera.cpp rename to fap/scenes/camera_suite_scene_camera.c index 9279d9096a7..809d9a5c11a 100644 --- a/fap/scenes/camera_suite_scene_camera.cpp +++ b/fap/scenes/camera_suite_scene_camera.c @@ -4,20 +4,20 @@ void camera_suite_view_camera_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_camera_on_enter(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; camera_suite_view_camera_set_callback( app->camera_suite_view_camera, camera_suite_view_camera_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdCamera); } bool camera_suite_scene_camera_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -47,6 +47,6 @@ bool camera_suite_scene_camera_on_event(void* context, SceneManagerEvent event) } void camera_suite_scene_camera_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); } diff --git a/fap/scenes/camera_suite_scene_guide.cpp b/fap/scenes/camera_suite_scene_guide.c similarity index 88% rename from fap/scenes/camera_suite_scene_guide.cpp rename to fap/scenes/camera_suite_scene_guide.c index c204d21e143..6599058ef68 100644 --- a/fap/scenes/camera_suite_scene_guide.cpp +++ b/fap/scenes/camera_suite_scene_guide.c @@ -4,20 +4,20 @@ void camera_suite_view_guide_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_guide_on_enter(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; camera_suite_view_guide_set_callback( app->camera_suite_view_guide, camera_suite_view_guide_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdGuide); } bool camera_suite_scene_guide_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -46,6 +46,6 @@ bool camera_suite_scene_guide_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_guide_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_menu.cpp b/fap/scenes/camera_suite_scene_menu.c similarity index 93% rename from fap/scenes/camera_suite_scene_menu.cpp rename to fap/scenes/camera_suite_scene_menu.c index aa18f542707..a1ca022922a 100644 --- a/fap/scenes/camera_suite_scene_menu.cpp +++ b/fap/scenes/camera_suite_scene_menu.c @@ -14,12 +14,12 @@ enum SubmenuIndex { }; void camera_suite_scene_menu_submenu_callback(void* context, uint32_t index) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); } void camera_suite_scene_menu_on_enter(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; submenu_add_item( app->submenu, @@ -63,7 +63,7 @@ void camera_suite_scene_menu_on_enter(void* context) { } bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); if(event.type == SceneManagerEventTypeBack) { // Exit application. @@ -102,6 +102,6 @@ bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_menu_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; submenu_reset(app->submenu); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_start.cpp b/fap/scenes/camera_suite_scene_start.c similarity index 88% rename from fap/scenes/camera_suite_scene_start.cpp rename to fap/scenes/camera_suite_scene_start.c index 37cd0a4eb37..0dda05edef0 100644 --- a/fap/scenes/camera_suite_scene_start.cpp +++ b/fap/scenes/camera_suite_scene_start.c @@ -4,20 +4,20 @@ void camera_suite_scene_start_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_start_on_enter(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; camera_suite_view_start_set_callback( app->camera_suite_view_start, camera_suite_scene_start_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdStartscreen); } bool camera_suite_scene_start_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -50,6 +50,6 @@ bool camera_suite_scene_start_on_event(void* context, SceneManagerEvent event) { } void camera_suite_scene_start_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); } \ No newline at end of file diff --git a/fap/scenes/camera_suite_scene_wifi_camera.cpp b/fap/scenes/camera_suite_scene_wifi_camera.c similarity index 88% rename from fap/scenes/camera_suite_scene_wifi_camera.cpp rename to fap/scenes/camera_suite_scene_wifi_camera.c index abbc60b9ae2..2df3db81bf1 100644 --- a/fap/scenes/camera_suite_scene_wifi_camera.cpp +++ b/fap/scenes/camera_suite_scene_wifi_camera.c @@ -4,20 +4,20 @@ void camera_suite_view_wifi_camera_callback(CameraSuiteCustomEvent event, void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, event); } void camera_suite_scene_wifi_camera_on_enter(void* context) { furi_assert(context); - CameraSuite* app = static_cast(context); + CameraSuite* app = context; camera_suite_view_wifi_camera_set_callback( app->camera_suite_view_wifi_camera, camera_suite_view_wifi_camera_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdWiFiCamera); } bool camera_suite_scene_wifi_camera_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { @@ -46,6 +46,6 @@ bool camera_suite_scene_wifi_camera_on_event(void* context, SceneManagerEvent ev } void camera_suite_scene_wifi_camera_on_exit(void* context) { - CameraSuite* app = static_cast(context); + CameraSuite* app = context; UNUSED(app); } \ No newline at end of file diff --git a/fap/views/camera_suite_view_camera.cpp b/fap/views/camera_suite_view_camera.c similarity index 92% rename from fap/views/camera_suite_view_camera.cpp rename to fap/views/camera_suite_view_camera.c index 0a810a045d8..ad4bdcdec6a 100644 --- a/fap/views/camera_suite_view_camera.cpp +++ b/fap/views/camera_suite_view_camera.c @@ -33,7 +33,7 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) furi_assert(canvas); furi_assert(uart_dump_model); - UartDumpModel* model = static_cast(uart_dump_model); + UartDumpModel* model = uart_dump_model; // Clear the screen. canvas_set_color(canvas, ColorBlack); @@ -152,10 +152,10 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) static void save_image_to_flipper_sd_card(void* uart_dump_model) { furi_assert(uart_dump_model); - UartDumpModel* model = static_cast(uart_dump_model); + UartDumpModel* model = uart_dump_model; // This pointer is used to access the storage. - Storage* storage = static_cast(furi_record_open(RECORD_STORAGE)); + Storage* storage = furi_record_open(RECORD_STORAGE); // This pointer is used to access the filesystem. File* file = storage_file_alloc(storage); @@ -245,16 +245,15 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera furi_assert(camera_view_instance); furi_assert(input_event); - CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + CameraSuiteViewCamera* instance = camera_view_instance; uint8_t data[1]; if(input_event->type == InputTypeRelease) { if(input_event->key) { // Stop all sounds, reset the LED. - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { UNUSED(model); camera_suite_play_bad_bump(instance->context); @@ -266,10 +265,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera } else if(input_event->type == InputTypePress) { switch(input_event->key) { case InputKeyBack: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { UNUSED(model); @@ -285,10 +283,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera break; } case InputKeyLeft: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { // Play sound. camera_suite_play_happy_bump(instance->context); @@ -317,10 +314,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera break; } case InputKeyRight: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { // Play sound. camera_suite_play_happy_bump(instance->context); @@ -349,10 +345,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera break; } case InputKeyUp: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { UNUSED(model); @@ -372,10 +367,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera break; } case InputKeyDown: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { UNUSED(model); @@ -395,10 +389,9 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera break; } case InputKeyOk: { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { // Play sound. camera_suite_play_long_bump(instance->context); @@ -434,12 +427,12 @@ static void camera_suite_view_camera_exit(void* camera_view_instance) { static void camera_suite_view_camera_enter(void* camera_view_instance) { furi_assert(camera_view_instance); - CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + CameraSuiteViewCamera* instance = camera_view_instance; uint8_t data[1]; // Get the camera suite instance context. - CameraSuite* app_context = static_cast(instance->context); + CameraSuite* app_context = instance->context; // Start serial stream to Flipper Zero. data[0] = 'S'; @@ -462,10 +455,9 @@ static void camera_suite_view_camera_enter(void* camera_view_instance) { furi_hal_serial_tx(instance->camera_serial_handle, data, 1); furi_delay_ms(50); - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { camera_suite_view_camera_model_init(model, app_context); }, true); } @@ -477,7 +469,7 @@ static void camera_on_irq_cb( furi_assert(handle); furi_assert(camera_view_instance); - CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + CameraSuiteViewCamera* instance = camera_view_instance; if(event == FuriHalSerialRxEventData) { uint8_t data = furi_hal_serial_async_rx(handle); @@ -537,7 +529,7 @@ static void process_ringbuffer(UartDumpModel* model, uint8_t const byte) { static int32_t camera_suite_camera_worker(void* camera_view_instance) { furi_assert(camera_view_instance); - CameraSuiteViewCamera* instance = static_cast(camera_view_instance); + CameraSuiteViewCamera* instance = camera_view_instance; while(1) { // Wait for any event on the worker thread. @@ -562,10 +554,9 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { length = furi_stream_buffer_receive(instance->camera_rx_stream, data, buffer_size, 0); if(length > 0) { - with_view_model_cpp( + with_view_model( instance->view, - UartDumpModel*, - model, + UartDumpModel * model, { // Process the data. for(size_t i = 0; i < length; i++) { @@ -576,8 +567,8 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { } } while(length > 0); - with_view_model_cpp( - instance->view, UartDumpModel*, model, { UNUSED(model); }, true); + with_view_model( + instance->view, UartDumpModel * model, { UNUSED(model); }, true); } } @@ -586,8 +577,7 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // Allocate memory for the instance - CameraSuiteViewCamera* instance = - static_cast(malloc(sizeof(CameraSuiteViewCamera))); + CameraSuiteViewCamera* instance = malloc(sizeof(CameraSuiteViewCamera)); // Allocate the view object instance->view = view_alloc(); @@ -646,8 +636,8 @@ void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { furi_hal_serial_deinit(instance->camera_serial_handle); furi_hal_serial_control_release(instance->camera_serial_handle); - with_view_model_cpp( - instance->view, UartDumpModel*, model, { UNUSED(model); }, true); + with_view_model( + instance->view, UartDumpModel * model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_guide.cpp b/fap/views/camera_suite_view_guide.c similarity index 85% rename from fap/views/camera_suite_view_guide.cpp rename to fap/views/camera_suite_view_guide.c index 32a4afd6ea7..2ff6a069aa7 100644 --- a/fap/views/camera_suite_view_guide.cpp +++ b/fap/views/camera_suite_view_guide.c @@ -46,15 +46,14 @@ static void camera_suite_view_guide_model_init(CameraSuiteViewGuideModel* const bool camera_suite_view_guide_input(InputEvent* event, void* grid_view_instance) { furi_assert(grid_view_instance); - CameraSuiteViewGuide* instance = static_cast(grid_view_instance); + CameraSuiteViewGuide* instance = grid_view_instance; if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewGuideModel*, - model, + CameraSuiteViewGuideModel * model, { UNUSED(model); // Go back to the main menu. @@ -81,17 +80,16 @@ void camera_suite_view_guide_exit(void* context) { void camera_suite_view_guide_enter(void* context) { furi_assert(context); - CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)context; - with_view_model_cpp( + CameraSuiteViewGuide* instance = context; + with_view_model( instance->view, - CameraSuiteViewGuideModel*, - model, + CameraSuiteViewGuideModel * model, { camera_suite_view_guide_model_init(model); }, true); } CameraSuiteViewGuide* camera_suite_view_guide_alloc() { - CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)malloc(sizeof(CameraSuiteViewGuide)); + CameraSuiteViewGuide* instance = malloc(sizeof(CameraSuiteViewGuide)); instance->view = view_alloc(); view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewGuideModel)); view_set_context(instance->view, instance); // furi_assert crashes in events without this @@ -100,10 +98,9 @@ CameraSuiteViewGuide* camera_suite_view_guide_alloc() { view_set_enter_callback(instance->view, camera_suite_view_guide_enter); view_set_exit_callback(instance->view, camera_suite_view_guide_exit); - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewGuideModel*, - model, + CameraSuiteViewGuideModel * model, { camera_suite_view_guide_model_init(model); }, true); @@ -113,8 +110,8 @@ CameraSuiteViewGuide* camera_suite_view_guide_alloc() { void camera_suite_view_guide_free(CameraSuiteViewGuide* instance) { furi_assert(instance); - with_view_model_cpp( - instance->view, CameraSuiteViewGuideModel*, model, { UNUSED(model); }, true); + with_view_model( + instance->view, CameraSuiteViewGuideModel * model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_start.cpp b/fap/views/camera_suite_view_start.c similarity index 91% rename from fap/views/camera_suite_view_start.cpp rename to fap/views/camera_suite_view_start.c index 1a4f91b98d4..7376b0e1674 100644 --- a/fap/views/camera_suite_view_start.cpp +++ b/fap/views/camera_suite_view_start.c @@ -107,15 +107,14 @@ static void camera_suite_view_start_model_init(CameraSuiteViewStartModel* const bool camera_suite_view_start_input(InputEvent* event, void* context) { furi_assert(context); - CameraSuiteViewStart* instance = static_cast(context); + CameraSuiteViewStart* instance = context; if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: // Exit application. - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewStartModel*, - model, + CameraSuiteViewStartModel * model, { UNUSED(model); instance->callback(CameraSuiteCustomEventStartBack, instance->context); @@ -124,10 +123,9 @@ bool camera_suite_view_start_input(InputEvent* event, void* context) { break; case InputKeyOk: // Start the application. - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewStartModel*, - model, + CameraSuiteViewStartModel * model, { UNUSED(model); instance->callback(CameraSuiteCustomEventStartOk, instance->context); @@ -153,17 +151,16 @@ void camera_suite_view_start_exit(void* context) { void camera_suite_view_start_enter(void* context) { furi_assert(context); CameraSuiteViewStart* instance = (CameraSuiteViewStart*)context; - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewStartModel*, - model, + CameraSuiteViewStartModel * model, { camera_suite_view_start_model_init(model); }, true); } CameraSuiteViewStart* camera_suite_view_start_alloc() { // Allocate memory for the instance - CameraSuiteViewStart* instance = (CameraSuiteViewStart*)malloc(sizeof(CameraSuiteViewStart)); + CameraSuiteViewStart* instance = malloc(sizeof(CameraSuiteViewStart)); // Allocate the view object instance->view = view_alloc(); @@ -180,10 +177,9 @@ CameraSuiteViewStart* camera_suite_view_start_alloc() { // Set input callback view_set_input_callback(instance->view, camera_suite_view_start_input); - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewStartModel*, - model, + CameraSuiteViewStartModel * model, { camera_suite_view_start_model_init(model); }, true); @@ -193,8 +189,8 @@ CameraSuiteViewStart* camera_suite_view_start_alloc() { void camera_suite_view_start_free(CameraSuiteViewStart* instance) { furi_assert(instance); - with_view_model_cpp( - instance->view, CameraSuiteViewStartModel*, model, { UNUSED(model); }, true); + with_view_model( + instance->view, CameraSuiteViewStartModel * model, { UNUSED(model); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_wifi_camera.cpp b/fap/views/camera_suite_view_wifi_camera.c similarity index 85% rename from fap/views/camera_suite_view_wifi_camera.cpp rename to fap/views/camera_suite_view_wifi_camera.c index 69324c7c2df..6ee6ab6dd99 100644 --- a/fap/views/camera_suite_view_wifi_camera.cpp +++ b/fap/views/camera_suite_view_wifi_camera.c @@ -5,8 +5,7 @@ static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* wifi_model) furi_assert(canvas); furi_assert(wifi_model); - CameraSuiteViewWiFiCameraModel* model = - static_cast(wifi_model); + CameraSuiteViewWiFiCameraModel* model = wifi_model; canvas_clear(canvas); canvas_set_color(canvas, ColorBlack); @@ -22,8 +21,7 @@ static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* wifi_model) static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { furi_assert(wifi_view_instance); - CameraSuiteViewWiFiCamera* instance = - static_cast(wifi_view_instance); + CameraSuiteViewWiFiCamera* instance = wifi_view_instance; while(1) { uint32_t events = @@ -45,10 +43,9 @@ static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { if(length > 0) { data[length] = '\0'; - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { furi_string_cat_printf(model->log, "%s", data); @@ -63,8 +60,8 @@ static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { } } while(length > 0); - with_view_model_cpp( - instance->view, CameraSuiteViewWiFiCameraModel*, model, { UNUSED(model); }, true); + with_view_model( + instance->view, CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); }, true); } } @@ -76,17 +73,15 @@ static bool furi_assert(wifi_view_instance); furi_assert(input_event); - CameraSuiteViewWiFiCamera* instance = - static_cast(wifi_view_instance); + CameraSuiteViewWiFiCamera* instance = wifi_view_instance; uint8_t data[1]; if(input_event->type == InputTypeRelease) { switch(input_event->key) { default: - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); // Stop all sounds, reset the LED. @@ -100,10 +95,9 @@ static bool } else if(input_event->type == InputTypePress) { switch(input_event->key) { case InputKeyBack: { - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); @@ -145,18 +139,16 @@ static void camera_suite_view_wifi_camera_model_init(CameraSuiteViewWiFiCameraMo static void camera_suite_view_wifi_camera_enter(void* wifi_view_instance) { furi_assert(wifi_view_instance); - CameraSuiteViewWiFiCamera* instance = - static_cast(wifi_view_instance); + CameraSuiteViewWiFiCamera* instance = wifi_view_instance; // Start wifi camera stream. uint8_t data[1] = {'W'}; furi_hal_serial_tx(instance->wifi_serial_handle, data, 1); furi_delay_ms(50); - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { camera_suite_view_wifi_camera_model_init(model); }, true); } @@ -168,8 +160,7 @@ static void wifi_camera_on_irq_cb( furi_assert(handle); furi_assert(wifi_view_instance); - CameraSuiteViewWiFiCamera* instance = - static_cast(wifi_view_instance); + CameraSuiteViewWiFiCamera* instance = wifi_view_instance; if(event == FuriHalSerialRxEventData) { uint8_t data = furi_hal_serial_async_rx(handle); @@ -180,8 +171,7 @@ static void wifi_camera_on_irq_cb( CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Allocate memory for the instance - CameraSuiteViewWiFiCamera* instance = - (CameraSuiteViewWiFiCamera*)malloc(sizeof(CameraSuiteViewWiFiCamera)); + CameraSuiteViewWiFiCamera* instance = malloc(sizeof(CameraSuiteViewWiFiCamera)); // Allocate the view object instance->view = view_alloc(); @@ -224,10 +214,9 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Set exit callback view_set_exit_callback(instance->view, camera_suite_view_wifi_camera_exit); - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { camera_suite_view_wifi_camera_model_init(model); }, true); @@ -249,10 +238,9 @@ void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* instance) { furi_hal_serial_deinit(instance->wifi_serial_handle); furi_hal_serial_control_release(instance->wifi_serial_handle); - with_view_model_cpp( + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel*, - model, + CameraSuiteViewWiFiCameraModel * model, { furi_string_free(model->log); }, true); view_free(instance->view); From 9bdb62fad95d398894b8c498c393cb10475c60f1 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 20:29:55 -0600 Subject: [PATCH 17/43] Update PR linting. --- .github/workflows/pull-request.yml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1b2e9b130c3..3ff3b163891 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,22 +1,6 @@ name: "Lint & build PR." on: pull_request jobs: - ufbt-lint-action: - runs-on: ubuntu-latest - name: "Lint PR: ${{ github.event.pull_request.head.repo.name }}." - steps: - - name: "Checkout." - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: "Lint." - uses: flipperdevices/flipperzero-ufbt-action@v0.1 - with: - app-dir: ./fap - skip-setup: true - task: lint - ufbt-build-action: runs-on: ubuntu-latest strategy: @@ -38,6 +22,13 @@ jobs: with: submodules: recursive + - name: "Lint." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + with: + app-dir: ./fap + skip-setup: true + task: lint + # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - name: "Build." From 85df73f9146568ce05eb4b41f4859e6316417152 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 20:33:55 -0600 Subject: [PATCH 18/43] Update PR linting. --- .github/workflows/pull-request.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3ff3b163891..5330b4ae0b0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,7 +1,7 @@ -name: "Lint & build PR." +name: "PR - Lint & build." on: pull_request jobs: - ufbt-build-action: + ufbt-test-build-action: runs-on: ubuntu-latest strategy: matrix: @@ -15,25 +15,25 @@ jobs: - name: "release" sdk-channel: release - name: "Build: ${{ matrix.name }} test PR release." + name: "PR Build: ${{ matrix.name }}." steps: - - name: "Checkout." + - name: "PR - Checkout." uses: actions/checkout@v3 with: submodules: recursive - - name: "Lint." - uses: flipperdevices/flipperzero-ufbt-action@v0.1 - with: - app-dir: ./fap - skip-setup: true - task: lint - # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "Build." + - name: "PR - Build app." uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap sdk-channel: ${{ matrix.sdk-channel }} + + - name: "PR - Lint." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + with: + app-dir: ./fap + skip-setup: true + task: lint From 92e6d7aa37ff843d264335e1f5adb06bfaa588d3 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 20:54:40 -0600 Subject: [PATCH 19/43] New dev branch workflow. --- .github/workflows/build-dev.yml | 29 ++++++++++++++++++ .../{deploy-main.yml => build-release.yml} | 12 ++++---- .github/workflows/pull-request-dev.yml | 30 +++++++++++++++++++ ...l-request.yml => pull-request-release.yml} | 7 +++-- 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build-dev.yml rename .github/workflows/{deploy-main.yml => build-release.yml} (82%) create mode 100644 .github/workflows/pull-request-dev.yml rename .github/workflows/{pull-request.yml => pull-request-release.yml} (90%) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml new file mode 100644 index 00000000000..8fcc5af6aea --- /dev/null +++ b/.github/workflows/build-dev.yml @@ -0,0 +1,29 @@ +name: "Dev Branch - Build & Upload FAP" +on: + push: + branches: + - dev +jobs: + ufbt-build-action: + runs-on: ubuntu-latest + name: "Build: ${{ matrix.name }}." + steps: + - name: "Checkout." + uses: actions/checkout@v3 + with: + submodules: recursive + + # Flipper Zero ufbt action + # https://github.com/flipperdevices/flipperzero-ufbt-action + - name: "Build app." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + id: build-app + with: + app-dir: ./fap + sdk-channel: dev + + - name: "Create zip archive (dev)" + uses: actions/upload-artifact@v3 + with: + name: ${{ github.event.repository.name }}-dev-${{ steps.build-app.outputs.suffix }}.zip + path: ${{ steps.build-app.outputs.fap-artifacts }} diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/build-release.yml similarity index 82% rename from .github/workflows/deploy-main.yml rename to .github/workflows/build-release.yml index 322a47b89f5..ab6c35f3230 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/build-release.yml @@ -1,14 +1,12 @@ -name: "Build & upload." +name: "Release Branch - Build & Upload FAP" on: push: branches: - # Run on push to main. - main - schedule: - # Run every day at 00:00 UTC (midnight) + # Run every day at 00:00 UTC (midnight) to build the latest release + # for new and upcoming firmware. - cron: "0 0 * * *" - jobs: ufbt-build-action: runs-on: ubuntu-latest @@ -24,7 +22,7 @@ jobs: - name: "release" sdk-channel: release - name: "Build: ${{ matrix.name }} release." + name: "Build: ${{ matrix.name }}." steps: - name: "Checkout." uses: actions/checkout@v3 @@ -33,7 +31,7 @@ jobs: # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "Build." + - name: "Build app." uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: diff --git a/.github/workflows/pull-request-dev.yml b/.github/workflows/pull-request-dev.yml new file mode 100644 index 00000000000..8aa2dd2d092 --- /dev/null +++ b/.github/workflows/pull-request-dev.yml @@ -0,0 +1,30 @@ +name: "Dev Branch PR - Lint & Build Test" +on: + pull_request: + branches: + - dev +jobs: + ufbt-test-build-action: + runs-on: ubuntu-latest + name: "PR Build: ${{ matrix.name }}." + steps: + - name: "PR - Checkout." + uses: actions/checkout@v3 + with: + submodules: recursive + + # Flipper Zero ufbt action + # https://github.com/flipperdevices/flipperzero-ufbt-action + - name: "PR - Build app." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + id: build-app + with: + app-dir: ./fap + sdk-channel: ${{ matrix.sdk-channel }} + + - name: "PR - Lint." + uses: flipperdevices/flipperzero-ufbt-action@v0.1 + with: + app-dir: ./fap + skip-setup: true + task: lint diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request-release.yml similarity index 90% rename from .github/workflows/pull-request.yml rename to .github/workflows/pull-request-release.yml index 5330b4ae0b0..1d2c367dcbf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request-release.yml @@ -1,5 +1,8 @@ -name: "PR - Lint & build." -on: pull_request +name: "Release Branch PR - Lint & Build Test" +on: + pull_request: + branches: + - main jobs: ufbt-test-build-action: runs-on: ubuntu-latest From b3482546a59d6b5543ac6800d2d87f03f93f39d2 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 29 Jan 2024 21:05:24 -0600 Subject: [PATCH 20/43] Fix up dev workflow and various cleanup. --- .github/workflows/build-dev.yml | 10 +++++----- .github/workflows/build-release.yml | 10 +++++----- .github/workflows/pull-request-dev.yml | 12 ++++++------ .github/workflows/pull-request-release.yml | 11 ++++++----- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 8fcc5af6aea..58a15b260a0 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -1,4 +1,4 @@ -name: "Dev Branch - Build & Upload FAP" +name: "Dev - Build & Upload FAP" on: push: branches: @@ -6,23 +6,23 @@ on: jobs: ufbt-build-action: runs-on: ubuntu-latest - name: "Build: ${{ matrix.name }}." + name: "Build: Dev" steps: - - name: "Checkout." + - name: "Checkout" uses: actions/checkout@v3 with: submodules: recursive # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "Build app." + - name: "Build" uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap sdk-channel: dev - - name: "Create zip archive (dev)" + - name: "Create Zip Archive (dev)" uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-dev-${{ steps.build-app.outputs.suffix }}.zip diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index ab6c35f3230..0557cddf109 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,4 +1,4 @@ -name: "Release Branch - Build & Upload FAP" +name: "Release - Build & Upload FAP" on: push: branches: @@ -22,23 +22,23 @@ jobs: - name: "release" sdk-channel: release - name: "Build: ${{ matrix.name }}." + name: "Build: ${{ matrix.name }}" steps: - - name: "Checkout." + - name: "Checkout" uses: actions/checkout@v3 with: submodules: recursive # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "Build app." + - name: "Build" uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap sdk-channel: ${{ matrix.sdk-channel }} - - name: "Create zip archives (dev/release/rc)" + - name: "Create Zip Archives (dev/release/rc)" uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-${{ matrix.name }}-${{ steps.build-app.outputs.suffix }}.zip diff --git a/.github/workflows/pull-request-dev.yml b/.github/workflows/pull-request-dev.yml index 8aa2dd2d092..35f5536f57c 100644 --- a/.github/workflows/pull-request-dev.yml +++ b/.github/workflows/pull-request-dev.yml @@ -1,4 +1,4 @@ -name: "Dev Branch PR - Lint & Build Test" +name: "Dev PR - Lint & Build Test" on: pull_request: branches: @@ -6,23 +6,23 @@ on: jobs: ufbt-test-build-action: runs-on: ubuntu-latest - name: "PR Build: ${{ matrix.name }}." + name: "PR Build: dev" steps: - - name: "PR - Checkout." + - name: "Checkout" uses: actions/checkout@v3 with: submodules: recursive # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "PR - Build app." + - name: "Build" uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap - sdk-channel: ${{ matrix.sdk-channel }} + sdk-channel: dev - - name: "PR - Lint." + - name: "Lint" uses: flipperdevices/flipperzero-ufbt-action@v0.1 with: app-dir: ./fap diff --git a/.github/workflows/pull-request-release.yml b/.github/workflows/pull-request-release.yml index 1d2c367dcbf..0ba84d3aefe 100644 --- a/.github/workflows/pull-request-release.yml +++ b/.github/workflows/pull-request-release.yml @@ -1,4 +1,4 @@ -name: "Release Branch PR - Lint & Build Test" +name: "Release PR - Lint & Build Test" on: pull_request: branches: @@ -6,6 +6,7 @@ on: jobs: ufbt-test-build-action: runs-on: ubuntu-latest + # The following builds must pass before release is accepted: strategy: matrix: include: @@ -18,23 +19,23 @@ jobs: - name: "release" sdk-channel: release - name: "PR Build: ${{ matrix.name }}." + name: "PR Build: ${{ matrix.name }}" steps: - - name: "PR - Checkout." + - name: "Checkout" uses: actions/checkout@v3 with: submodules: recursive # Flipper Zero ufbt action # https://github.com/flipperdevices/flipperzero-ufbt-action - - name: "PR - Build app." + - name: "Build" uses: flipperdevices/flipperzero-ufbt-action@v0.1 id: build-app with: app-dir: ./fap sdk-channel: ${{ matrix.sdk-channel }} - - name: "PR - Lint." + - name: "Lint" uses: flipperdevices/flipperzero-ufbt-action@v0.1 with: app-dir: ./fap From 2be5479aa03d9be4787cafa9b5f0a193a5729551 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sat, 17 Feb 2024 23:15:55 -0600 Subject: [PATCH 21/43] Updates --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build-release.yml | 2 +- fap/camera_suite.h | 27 +++++- fap/helpers/camera_suite_haptic.c | 2 +- fap/helpers/camera_suite_haptic.h | 4 - fap/helpers/camera_suite_led.c | 2 +- fap/helpers/camera_suite_led.h | 3 - fap/helpers/camera_suite_speaker.c | 4 +- fap/helpers/camera_suite_speaker.h | 2 - fap/helpers/camera_suite_storage.h | 7 +- fap/helpers/camera_suite_uart.c | 36 +++++++ fap/helpers/camera_suite_uart.h | 4 + fap/views/camera_suite_view_camera.c | 110 +++++++++++----------- fap/views/camera_suite_view_camera.h | 50 ++-------- fap/views/camera_suite_view_guide.c | 8 +- fap/views/camera_suite_view_guide.h | 1 - fap/views/camera_suite_view_start.c | 5 +- fap/views/camera_suite_view_start.h | 1 - fap/views/camera_suite_view_wifi_camera.c | 92 ++++++++---------- fap/views/camera_suite_view_wifi_camera.h | 28 ++---- 20 files changed, 189 insertions(+), 201 deletions(-) create mode 100644 fap/helpers/camera_suite_uart.c create mode 100644 fap/helpers/camera_suite_uart.h diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 58a15b260a0..1f66c1c2883 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -22,7 +22,7 @@ jobs: app-dir: ./fap sdk-channel: dev - - name: "Create Zip Archive (dev)" + - name: "Create FAP Download (dev)" uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-dev-${{ steps.build-app.outputs.suffix }}.zip diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 0557cddf109..70375382483 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -38,7 +38,7 @@ jobs: app-dir: ./fap sdk-channel: ${{ matrix.sdk-channel }} - - name: "Create Zip Archives (dev/release/rc)" + - name: "Create FAP Downloads (dev/release/rc)" uses: actions/upload-artifact@v3 with: name: ${{ github.event.repository.name }}-${{ matrix.name }}-${{ steps.build-app.outputs.suffix }}.zip diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 4d96482c35b..0919f694025 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -11,16 +14,31 @@ #include #include +#include "helpers/camera_suite_storage.h" #include "scenes/camera_suite_scene.h" +#include "views/camera_suite_view_camera.h" #include "views/camera_suite_view_guide.h" #include "views/camera_suite_view_start.h" -#include "views/camera_suite_view_camera.h" #include "views/camera_suite_view_wifi_camera.h" -#include "helpers/camera_suite_storage.h" + +#define TAG "Camera Suite" +#include +#define FLIPPER_SCREEN_HEIGHT 64 +#define FLIPPER_SCREEN_WIDTH 128 #define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) -#define TAG "Camera Suite" +#ifdef xtreme_settings +/** + * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). + * + * @see https://github.com/Flipper-XFW/Xtreme-Firmware + * @see https://github.com/Flipper-XFW/Xtreme-Apps +*/ +#define UART_CH (xtreme_settings.uart_esp_channel) +#else +#define UART_CH (FuriHalSerialIdUsart) +#endif typedef struct { Gui* gui; @@ -41,6 +59,9 @@ typedef struct { uint32_t speaker; uint32_t led; ButtonMenu* button_menu; + FuriHalSerialHandle* serial_handle; + FuriStreamBuffer* rx_stream; + FuriThread* worker_thread; } CameraSuite; typedef enum { diff --git a/fap/helpers/camera_suite_haptic.c b/fap/helpers/camera_suite_haptic.c index 5b08f563d2b..237a9600430 100644 --- a/fap/helpers/camera_suite_haptic.c +++ b/fap/helpers/camera_suite_haptic.c @@ -1,5 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_haptic.h" +#include "../camera_suite.h" void camera_suite_play_happy_bump(void* context) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_haptic.h b/fap/helpers/camera_suite_haptic.h index 6b83599a9da..f25b0f3a4b7 100644 --- a/fap/helpers/camera_suite_haptic.h +++ b/fap/helpers/camera_suite_haptic.h @@ -1,9 +1,5 @@ -#pragma once - #include void camera_suite_play_happy_bump(void* context); - void camera_suite_play_bad_bump(void* context); - void camera_suite_play_long_bump(void* context); diff --git a/fap/helpers/camera_suite_led.c b/fap/helpers/camera_suite_led.c index 64723b8b4da..c4f1a85d7ac 100644 --- a/fap/helpers/camera_suite_led.c +++ b/fap/helpers/camera_suite_led.c @@ -1,5 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_led.h" +#include "../camera_suite.h" void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_led.h b/fap/helpers/camera_suite_led.h index 9a551db29f1..24c54f272c9 100644 --- a/fap/helpers/camera_suite_led.h +++ b/fap/helpers/camera_suite_led.h @@ -1,5 +1,2 @@ -#pragma once - void camera_suite_led_set_rgb(void* context, int red, int green, int blue); - void camera_suite_led_reset(void* context); diff --git a/fap/helpers/camera_suite_speaker.c b/fap/helpers/camera_suite_speaker.c index 78962dd49c2..84a60cb15b9 100644 --- a/fap/helpers/camera_suite_speaker.c +++ b/fap/helpers/camera_suite_speaker.c @@ -1,7 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_speaker.h" - -#define NOTE_INPUT 587.33f +#include "../camera_suite.h" void camera_suite_play_input_sound(void* context) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_speaker.h b/fap/helpers/camera_suite_speaker.h index 5bc739a3c14..2119bbec597 100644 --- a/fap/helpers/camera_suite_speaker.h +++ b/fap/helpers/camera_suite_speaker.h @@ -1,5 +1,3 @@ -#pragma once - #define NOTE_INPUT 587.33f void camera_suite_play_input_sound(void* context); diff --git a/fap/helpers/camera_suite_storage.h b/fap/helpers/camera_suite_storage.h index e925c34eebd..df035385ee3 100644 --- a/fap/helpers/camera_suite_storage.h +++ b/fap/helpers/camera_suite_storage.h @@ -1,5 +1,3 @@ -#pragma once - #include #include #include @@ -7,6 +5,9 @@ #include "../camera_suite.h" +#ifndef CAMERA_SUITE_STORAGE_H +#define CAMERA_SUITE_STORAGE_H + #define BOILERPLATE_SETTINGS_FILE_VERSION 1 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/camera_suite") #define BOILERPLATE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/camera_suite.conf" @@ -24,3 +25,5 @@ void camera_suite_save_settings(void* context); void camera_suite_read_settings(void* context); + +#endif diff --git a/fap/helpers/camera_suite_uart.c b/fap/helpers/camera_suite_uart.c new file mode 100644 index 00000000000..6a0acb2142e --- /dev/null +++ b/fap/helpers/camera_suite_uart.c @@ -0,0 +1,36 @@ +#include "camera_suite_uart.h" +#include "../camera_suite.h" + +// void camera_suite_uart_alloc(CameraSuite* instance, FuriThreadCallback* callback) { +// // Allocate a stream buffer +// instance->rx_stream = furi_stream_buffer_alloc(2048, 1); + +// // Allocate a thread for this camera to run on. +// FuriThread* thread = furi_thread_alloc_ex("UsbUartWorker", 2048, callback, instance); +// instance->worker_thread = thread; +// furi_thread_start(instance->worker_thread); + +// // Set up UART thread. +// instance->serial_handle = furi_hal_serial_control_acquire(UART_CH); +// furi_check(instance->serial_handle); +// furi_hal_serial_init(instance->serial_handle, 230400); + +// // Enable UART1 and set the IRQ callback. +// furi_hal_serial_async_rx_start(instance->serial_handle, callback, instance, false); +// } + +// void camera_suite_uart_free(CameraSuite* app_instance) { +// furi_assert(app_instance); + +// // Free the worker thread. +// furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventStop); +// furi_thread_join(app_instance->worker_thread); +// furi_thread_free(app_instance->worker_thread); + +// // Free the stream buffer. +// furi_stream_buffer_free(app_instance->rx_stream); + +// // Free the serial handle. +// furi_hal_serial_deinit(app_instance->serial_handle); +// furi_hal_serial_control_release(app_instance->serial_handle); +// } diff --git a/fap/helpers/camera_suite_uart.h b/fap/helpers/camera_suite_uart.h new file mode 100644 index 00000000000..342adc86c23 --- /dev/null +++ b/fap/helpers/camera_suite_uart.h @@ -0,0 +1,4 @@ +#pragma once + +// void camera_suite_uart_alloc(CameraSuite* instance, FuriThreadCallback* callback); +// void camera_suite_uart_free(CameraSuite* app_instance); diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index ad4bdcdec6a..ba854c88ab1 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -1,6 +1,24 @@ #include "../camera_suite.h" #include "camera_suite_view_camera.h" +#include "../helpers/camera_suite_haptic.h" +#include "../helpers/camera_suite_led.h" +#include "../helpers/camera_suite_speaker.h" +#include "../helpers/camera_suite_custom_event.h" +// #include "../helpers/camera_suite_uart.h" + +#define BITMAP_HEADER_LENGTH 62 +#define FRAME_BIT_DEPTH 1 +#define HEADER_LENGTH 3 // 'Y', ':', and row identifier +#define LAST_ROW_INDEX 1008 +#define ROW_BUFFER_LENGTH 16 + +static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { + 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; + static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint8_t orientation) { furi_assert(canvas); furi_assert(x); @@ -15,15 +33,15 @@ static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint } case 1: { // Camera rotated 90 degrees - canvas_draw_dot(canvas, y, FRAME_WIDTH - 1 - x); + canvas_draw_dot(canvas, y, FLIPPER_SCREEN_WIDTH - 1 - x); break; } case 2: { // Camera rotated 180 degrees (upside down) - canvas_draw_dot(canvas, FRAME_WIDTH - 1 - x, FRAME_HEIGHT - 1 - y); + canvas_draw_dot(canvas, FLIPPER_SCREEN_WIDTH - 1 - x, FLIPPER_SCREEN_HEIGHT - 1 - y); break; } case 3: { // Camera rotated 270 degrees - canvas_draw_dot(canvas, FRAME_HEIGHT - 1 - y, x); + canvas_draw_dot(canvas, FLIPPER_SCREEN_HEIGHT - 1 - y, x); break; } } @@ -39,7 +57,7 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) canvas_set_color(canvas, ColorBlack); // Draw the frame. - canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT); + canvas_draw_frame(canvas, 0, 0, FLIPPER_SCREEN_WIDTH, FLIPPER_SCREEN_HEIGHT); for(size_t p = 0; p < FRAME_BUFFER_LENGTH; ++p) { uint8_t x = p % ROW_BUFFER_LENGTH; // 0 .. 15 @@ -246,6 +264,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera furi_assert(input_event); CameraSuiteViewCamera* instance = camera_view_instance; + CameraSuite* app_instance = instance->context; uint8_t data[1]; if(input_event->type == InputTypeRelease) { @@ -273,7 +292,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera // Stop camera stream. data[0] = 's'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); // Go back to the main menu. @@ -295,14 +314,14 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera if(model->is_inverted) { // Camera: Set invert to false on the ESP32-CAM. data[0] = 'i'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); model->is_inverted = false; } else { // Camera: Set invert to true on the ESP32-CAM. data[0] = 'I'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); model->is_inverted = true; @@ -326,14 +345,14 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera if(model->is_dithering_enabled) { // Camera: Disable dithering. data[0] = 'd'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); model->is_dithering_enabled = false; } else { // Camera: Enable dithering. data[0] = 'D'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); model->is_dithering_enabled = true; @@ -358,7 +377,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera // Camera: Increase contrast. data[0] = 'C'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraUp, instance->context); @@ -380,7 +399,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera // Camera: Reduce contrast. data[0] = 'c'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraDown, instance->context); @@ -428,41 +447,39 @@ static void camera_suite_view_camera_enter(void* camera_view_instance) { furi_assert(camera_view_instance); CameraSuiteViewCamera* instance = camera_view_instance; + CameraSuite* app_instance = instance->context; uint8_t data[1]; - // Get the camera suite instance context. - CameraSuite* app_context = instance->context; - // Start serial stream to Flipper Zero. data[0] = 'S'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); // Get/set dither type. - uint8_t dither_type = app_context->dither; - furi_hal_serial_tx(instance->camera_serial_handle, &dither_type, 1); + uint8_t dither_type = app_instance->dither; + furi_hal_serial_tx(app_instance->serial_handle, &dither_type, 1); furi_delay_ms(50); // Make sure the camera is not inverted. data[0] = 'i'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); // Toggle flash on or off based on the current state. If the user has this // on the flash will stay on the entire time the user is in the camera view. - data[0] = app_context->flash ? 'F' : 'f'; - furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + data[0] = app_instance->flash ? 'F' : 'f'; + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); with_view_model( instance->view, UartDumpModel * model, - { camera_suite_view_camera_model_init(model, app_context); }, + { camera_suite_view_camera_model_init(model, app_instance); }, true); } -static void camera_on_irq_cb( +static void camera_callback( FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* camera_view_instance) { @@ -470,11 +487,12 @@ static void camera_on_irq_cb( furi_assert(camera_view_instance); CameraSuiteViewCamera* instance = camera_view_instance; + CameraSuite* app_instance = instance->context; if(event == FuriHalSerialRxEventData) { uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(instance->camera_rx_stream, &data, 1, 0); - furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventRx); + furi_stream_buffer_send(app_instance->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventRx); } } @@ -530,6 +548,7 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { furi_assert(camera_view_instance); CameraSuiteViewCamera* instance = camera_view_instance; + CameraSuite* app_instance = instance->context; while(1) { // Wait for any event on the worker thread. @@ -551,8 +570,7 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { // Allocate a buffer for the data. uint8_t data[buffer_size]; // Read the data from the stream buffer. - length = - furi_stream_buffer_receive(instance->camera_rx_stream, data, buffer_size, 0); + length = furi_stream_buffer_receive(app_instance->rx_stream, data, buffer_size, 0); if(length > 0) { with_view_model( instance->view, @@ -576,15 +594,11 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { } CameraSuiteViewCamera* camera_suite_view_camera_alloc() { - // Allocate memory for the instance CameraSuiteViewCamera* instance = malloc(sizeof(CameraSuiteViewCamera)); // Allocate the view object instance->view = view_alloc(); - // Allocate a stream buffer - instance->camera_rx_stream = furi_stream_buffer_alloc(2048, 1); - // Allocate model view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel)); @@ -603,20 +617,15 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // Set exit callback view_set_exit_callback(instance->view, camera_suite_view_camera_exit); - // Allocate a thread for this camera to run on. - FuriThread* thread = furi_thread_alloc_ex( - "Camera_Suite_Camera_Rx_Thread", 2048, camera_suite_camera_worker, instance); - instance->camera_worker_thread = thread; - furi_thread_start(instance->camera_worker_thread); + // Allocate the UART worker thread for the camera. + // CameraSuite* app_instance = instance->context; + // camera_suite_uart_alloc(app_instance, camera_callback); - // Set up UART thread. - instance->camera_serial_handle = furi_hal_serial_control_acquire(UART_CH); - furi_check(instance->camera_serial_handle); - furi_hal_serial_init(instance->camera_serial_handle, 230400); - - // Enable UART1 and set the IRQ callback. - furi_hal_serial_async_rx_start( - instance->camera_serial_handle, camera_on_irq_cb, instance, false); + with_view_model( + instance->view, + UartDumpModel * model, + { camera_suite_view_camera_model_init(model, instance); }, + true); return instance; } @@ -624,17 +633,10 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { furi_assert(instance); - // Free the worker thread. - furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventStop); - furi_thread_join(instance->camera_worker_thread); - furi_thread_free(instance->camera_worker_thread); + CameraSuite* app_instance = instance->context; - // Free the allocated stream buffer. - furi_stream_buffer_free(instance->camera_rx_stream); - - // Deinitialize the UART. - furi_hal_serial_deinit(instance->camera_serial_handle); - furi_hal_serial_control_release(instance->camera_serial_handle); + // Free the UART worker thread. + // camera_suite_uart_free(app_instance); with_view_model( instance->view, UartDumpModel * model, { UNUSED(model); }, true); @@ -655,4 +657,4 @@ void camera_suite_view_camera_set_callback( furi_assert(callback); camera_view_instance->callback = callback; camera_view_instance->context = context; -} \ No newline at end of file +} diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index b790dae658e..e33c5755d2f 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -1,51 +1,12 @@ #pragma once -#include -#include -#include - -#include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_led.h" -#include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_custom_event.h" - -#define BITMAP_HEADER_LENGTH 62 -#define FRAME_BIT_DEPTH 1 -#define FRAME_BUFFER_LENGTH 1024 -#define FRAME_HEIGHT 64 -#define FRAME_WIDTH 128 -#define HEADER_LENGTH 3 // 'Y', ':', and row identifier -#define LAST_ROW_INDEX 1008 #define RING_BUFFER_LENGTH 19 -#define ROW_BUFFER_LENGTH 16 - -#ifdef xtreme_settings -/** - * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). - * - * @see https://github.com/Flipper-XFW/Xtreme-Firmware - * @see https://github.com/Flipper-XFW/Xtreme-Apps -*/ -#define UART_CH (xtreme_settings.uart_esp_channel) -#else -#define UART_CH (FuriHalSerialIdUsart) -#endif - -static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { - 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, - 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; +#define FRAME_BUFFER_LENGTH 1024 -// Forward declaration typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context); typedef struct CameraSuiteViewCamera { CameraSuiteViewCameraCallback callback; - FuriStreamBuffer* camera_rx_stream; - FuriHalSerialHandle* camera_serial_handle; - FuriThread* camera_worker_thread; - NotificationApp* notification; View* view; void* context; } CameraSuiteViewCamera; @@ -60,13 +21,14 @@ typedef struct UartDumpModel { uint8_t ringbuffer_index; uint8_t row_identifier; uint8_t row_ringbuffer[RING_BUFFER_LENGTH]; + FuriString* log; + size_t log_strlen; } UartDumpModel; -// Function Prototypes CameraSuiteViewCamera* camera_suite_view_camera_alloc(); -View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_view_instance); -void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_view_instance); +View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_suite_static); +void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static); void camera_suite_view_camera_set_callback( - CameraSuiteViewCamera* camera_view_instance, + CameraSuiteViewCamera* camera_suite_view_camera, CameraSuiteViewCameraCallback callback, void* context); diff --git a/fap/views/camera_suite_view_guide.c b/fap/views/camera_suite_view_guide.c index 2ff6a069aa7..e697a9c61e2 100644 --- a/fap/views/camera_suite_view_guide.c +++ b/fap/views/camera_suite_view_guide.c @@ -1,9 +1,5 @@ #include "../camera_suite.h" -#include -#include -#include -#include -#include +#include "camera_suite_view_guide.h" struct CameraSuiteViewGuide { View* view; @@ -80,7 +76,7 @@ void camera_suite_view_guide_exit(void* context) { void camera_suite_view_guide_enter(void* context) { furi_assert(context); - CameraSuiteViewGuide* instance = context; + CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)context; with_view_model( instance->view, CameraSuiteViewGuideModel * model, diff --git a/fap/views/camera_suite_view_guide.h b/fap/views/camera_suite_view_guide.h index cd78d4b0179..5afed50fa1a 100644 --- a/fap/views/camera_suite_view_guide.h +++ b/fap/views/camera_suite_view_guide.h @@ -1,6 +1,5 @@ #pragma once -#include #include "../helpers/camera_suite_custom_event.h" typedef struct CameraSuiteViewGuide CameraSuiteViewGuide; diff --git a/fap/views/camera_suite_view_start.c b/fap/views/camera_suite_view_start.c index 7376b0e1674..44b00e1f88d 100644 --- a/fap/views/camera_suite_view_start.c +++ b/fap/views/camera_suite_view_start.c @@ -1,8 +1,5 @@ #include "../camera_suite.h" -#include -#include -#include -#include +#include "camera_suite_view_start.h" void camera_suite_view_start_set_callback( CameraSuiteViewStart* instance, diff --git a/fap/views/camera_suite_view_start.h b/fap/views/camera_suite_view_start.h index f7116bb5b39..23c799fcaf2 100644 --- a/fap/views/camera_suite_view_start.h +++ b/fap/views/camera_suite_view_start.h @@ -1,6 +1,5 @@ #pragma once -#include #include "../helpers/camera_suite_custom_event.h" typedef void (*CameraSuiteViewStartCallback)(CameraSuiteCustomEvent event, void* context); diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.c index 6ee6ab6dd99..5dcedea5733 100644 --- a/fap/views/camera_suite_view_wifi_camera.c +++ b/fap/views/camera_suite_view_wifi_camera.c @@ -1,16 +1,22 @@ #include "../camera_suite.h" #include "camera_suite_view_wifi_camera.h" +#include "../helpers/camera_suite_haptic.h" +#include "../helpers/camera_suite_led.h" +#include "../helpers/camera_suite_speaker.h" +#include "../helpers/camera_suite_custom_event.h" +// #include "../helpers/camera_suite_uart.h" + static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* wifi_model) { furi_assert(canvas); furi_assert(wifi_model); - CameraSuiteViewWiFiCameraModel* model = wifi_model; + UartDumpModel* model = wifi_model; canvas_clear(canvas); canvas_set_color(canvas, ColorBlack); canvas_set_font(canvas, FontSecondary); - canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT); + canvas_draw_frame(canvas, 0, 0, FLIPPER_SCREEN_HEIGHT, FLIPPER_SCREEN_HEIGHT); canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, "Starting WiFi Stream at:"); @@ -22,6 +28,7 @@ static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { furi_assert(wifi_view_instance); CameraSuiteViewWiFiCamera* instance = wifi_view_instance; + CameraSuite* app_instance = instance->context; while(1) { uint32_t events = @@ -38,14 +45,13 @@ static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { do { size_t buffer_size = 320; uint8_t data[buffer_size]; - length = - furi_stream_buffer_receive(instance->wifi_rx_stream, data, buffer_size, 0); + length = furi_stream_buffer_receive(app_instance->rx_stream, data, buffer_size, 0); if(length > 0) { data[length] = '\0'; with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel * model, + UartDumpModel * model, { furi_string_cat_printf(model->log, "%s", data); @@ -61,7 +67,7 @@ static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { } while(length > 0); with_view_model( - instance->view, CameraSuiteViewWiFiCameraModel * model, { UNUSED(model); }, true); + instance->view, UartDumpModel * model, { UNUSED(model); }, true); } } @@ -74,6 +80,7 @@ static bool furi_assert(input_event); CameraSuiteViewWiFiCamera* instance = wifi_view_instance; + CameraSuite* app_instance = instance->context; uint8_t data[1]; if(input_event->type == InputTypeRelease) { @@ -81,7 +88,7 @@ static bool default: with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel * model, + UartDumpModel * model, { UNUSED(model); // Stop all sounds, reset the LED. @@ -97,17 +104,18 @@ static bool case InputKeyBack: { with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel * model, + UartDumpModel * model, { UNUSED(model); // Stop camera WiFi stream. data[0] = 'w'; - furi_hal_serial_tx(instance->wifi_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); // Go back to the main menu. - instance->callback(CameraSuiteCustomEventSceneCameraBack, instance->context); + instance->callback( + CameraSuiteCustomEventSceneWiFiCameraBack, instance->context); }, true); break; @@ -131,7 +139,7 @@ static void camera_suite_view_wifi_camera_exit(void* wifi_view_instance) { UNUSED(wifi_view_instance); } -static void camera_suite_view_wifi_camera_model_init(CameraSuiteViewWiFiCameraModel* const model) { +static void camera_suite_view_wifi_camera_model_init(UartDumpModel* const model) { model->log = furi_string_alloc(); furi_string_reserve(model->log, 4096); } @@ -140,20 +148,21 @@ static void camera_suite_view_wifi_camera_enter(void* wifi_view_instance) { furi_assert(wifi_view_instance); CameraSuiteViewWiFiCamera* instance = wifi_view_instance; + CameraSuite* app_instance = instance->context; // Start wifi camera stream. uint8_t data[1] = {'W'}; - furi_hal_serial_tx(instance->wifi_serial_handle, data, 1); + furi_hal_serial_tx(app_instance->serial_handle, data, 1); furi_delay_ms(50); with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel * model, + UartDumpModel * model, { camera_suite_view_wifi_camera_model_init(model); }, true); } -static void wifi_camera_on_irq_cb( +static void wifi_camera_callback( FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* wifi_view_instance) { @@ -161,47 +170,27 @@ static void wifi_camera_on_irq_cb( furi_assert(wifi_view_instance); CameraSuiteViewWiFiCamera* instance = wifi_view_instance; + CameraSuite* app_instance = instance->context; if(event == FuriHalSerialRxEventData) { uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(instance->wifi_rx_stream, &data, 1, 0); - furi_thread_flags_set(furi_thread_get_id(instance->wifi_worker_thread), WorkerEventRx); + furi_stream_buffer_send(app_instance->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventRx); } } CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { - // Allocate memory for the instance CameraSuiteViewWiFiCamera* instance = malloc(sizeof(CameraSuiteViewWiFiCamera)); // Allocate the view object instance->view = view_alloc(); // Allocate model - view_allocate_model( - instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewWiFiCameraModel)); + view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel)); - // Set context for the view (furi_assert crashes in events without this) + // Set context for the view view_set_context(instance->view, instance); - // Allocate a stream buffer - instance->wifi_rx_stream = furi_stream_buffer_alloc(1024, 1); - - // Allocate a thread for this camera to run on. - // @NOTICE: THIS SEEMINGLY BREAKS THE CAMERA VIEW THREAD... - FuriThread* thread = furi_thread_alloc_ex( - "Camera_Suite_WiFi_Rx_Thread", 1024, camera_suite_wifi_camera_worker, instance); - instance->wifi_worker_thread = thread; - furi_thread_start(instance->wifi_worker_thread); - - // Set up UART thread. - instance->wifi_serial_handle = furi_hal_serial_control_acquire(UART_CH); - furi_check(instance->wifi_serial_handle); - furi_hal_serial_init(instance->wifi_serial_handle, 230400); - - // Enable UART1 and set the IRQ callback. - furi_hal_serial_async_rx_start( - instance->wifi_serial_handle, wifi_camera_on_irq_cb, instance, false); - // Set draw callback view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_wifi_camera_draw); @@ -214,9 +203,13 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { // Set exit callback view_set_exit_callback(instance->view, camera_suite_view_wifi_camera_exit); + // Allocate the UART worker thread for the camera. + // CameraSuite* app_instance = instance->context; + // camera_suite_uart_alloc(app_instance, wifi_camera_callback); + with_view_model( instance->view, - CameraSuiteViewWiFiCameraModel * model, + UartDumpModel * model, { camera_suite_view_wifi_camera_model_init(model); }, true); @@ -226,23 +219,20 @@ CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* instance) { furi_assert(instance); + CameraSuite* app_instance = instance->context; + // Free the worker thread. - furi_thread_flags_set(furi_thread_get_id(instance->wifi_worker_thread), WorkerEventStop); - furi_thread_join(instance->wifi_worker_thread); - furi_thread_free(instance->wifi_worker_thread); + furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventStop); + furi_thread_join(app_instance->worker_thread); + furi_thread_free(app_instance->worker_thread); // Free the allocated stream buffer. - furi_stream_buffer_free(instance->wifi_rx_stream); + furi_stream_buffer_free(app_instance->rx_stream); - // Deinitialize the UART. - furi_hal_serial_deinit(instance->wifi_serial_handle); - furi_hal_serial_control_release(instance->wifi_serial_handle); + // camera_suite_uart_free(app_instance); with_view_model( - instance->view, - CameraSuiteViewWiFiCameraModel * model, - { furi_string_free(model->log); }, - true); + instance->view, UartDumpModel * model, { furi_string_free(model->log); }, true); view_free(instance->view); free(instance); } diff --git a/fap/views/camera_suite_view_wifi_camera.h b/fap/views/camera_suite_view_wifi_camera.h index f8eba95b44f..a37811c705d 100644 --- a/fap/views/camera_suite_view_wifi_camera.h +++ b/fap/views/camera_suite_view_wifi_camera.h @@ -1,35 +1,25 @@ #pragma once -#include -#include -#include - -#include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_led.h" -#include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_custom_event.h" - typedef void (*CameraSuiteViewWiFiCameraCallback)(CameraSuiteCustomEvent event, void* context); typedef struct CameraSuiteViewWiFiCamera { + CameraSuiteViewWiFiCameraCallback callback; View* view; - CameraSuiteViewCameraCallback callback; void* context; - FuriHalSerialHandle* wifi_serial_handle; - FuriStreamBuffer* wifi_rx_stream; - FuriThread* wifi_worker_thread; } CameraSuiteViewWiFiCamera; -typedef struct { +typedef struct UartWiFiModel { FuriString* log; size_t log_strlen; -} CameraSuiteViewWiFiCameraModel; +} UartWiFiModel; -// Function Prototypes -CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc(); -void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* wifi_view_instance); -View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* wifi_view_instance); void camera_suite_view_wifi_camera_set_callback( CameraSuiteViewWiFiCamera* wifi_view_instance, CameraSuiteViewWiFiCameraCallback callback, void* context); + +CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc(); + +void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* wifi_view_instance); + +View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* wifi_view_instance); From b5f342b4d391ff68ce002f8611ad245695af80aa Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sat, 17 Feb 2024 23:24:38 -0600 Subject: [PATCH 22/43] Cleanup --- fap/helpers/camera_suite_haptic.c | 2 +- fap/helpers/camera_suite_haptic.h | 2 ++ fap/helpers/camera_suite_led.c | 2 +- fap/helpers/camera_suite_led.h | 1 + fap/helpers/camera_suite_speaker.c | 4 +++- fap/helpers/camera_suite_speaker.h | 2 +- fap/helpers/camera_suite_storage.h | 7 ++----- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fap/helpers/camera_suite_haptic.c b/fap/helpers/camera_suite_haptic.c index 237a9600430..5b08f563d2b 100644 --- a/fap/helpers/camera_suite_haptic.c +++ b/fap/helpers/camera_suite_haptic.c @@ -1,5 +1,5 @@ -#include "camera_suite_haptic.h" #include "../camera_suite.h" +#include "camera_suite_haptic.h" void camera_suite_play_happy_bump(void* context) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_haptic.h b/fap/helpers/camera_suite_haptic.h index f25b0f3a4b7..f2f70af9e1f 100644 --- a/fap/helpers/camera_suite_haptic.h +++ b/fap/helpers/camera_suite_haptic.h @@ -1,3 +1,5 @@ +#pragma once + #include void camera_suite_play_happy_bump(void* context); diff --git a/fap/helpers/camera_suite_led.c b/fap/helpers/camera_suite_led.c index c4f1a85d7ac..64723b8b4da 100644 --- a/fap/helpers/camera_suite_led.c +++ b/fap/helpers/camera_suite_led.c @@ -1,5 +1,5 @@ -#include "camera_suite_led.h" #include "../camera_suite.h" +#include "camera_suite_led.h" void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_led.h b/fap/helpers/camera_suite_led.h index 24c54f272c9..074947da150 100644 --- a/fap/helpers/camera_suite_led.h +++ b/fap/helpers/camera_suite_led.h @@ -1,2 +1,3 @@ void camera_suite_led_set_rgb(void* context, int red, int green, int blue); + void camera_suite_led_reset(void* context); diff --git a/fap/helpers/camera_suite_speaker.c b/fap/helpers/camera_suite_speaker.c index 84a60cb15b9..78962dd49c2 100644 --- a/fap/helpers/camera_suite_speaker.c +++ b/fap/helpers/camera_suite_speaker.c @@ -1,5 +1,7 @@ -#include "camera_suite_speaker.h" #include "../camera_suite.h" +#include "camera_suite_speaker.h" + +#define NOTE_INPUT 587.33f void camera_suite_play_input_sound(void* context) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_speaker.h b/fap/helpers/camera_suite_speaker.h index 2119bbec597..359663cc950 100644 --- a/fap/helpers/camera_suite_speaker.h +++ b/fap/helpers/camera_suite_speaker.h @@ -1,4 +1,4 @@ -#define NOTE_INPUT 587.33f +#pragma once void camera_suite_play_input_sound(void* context); diff --git a/fap/helpers/camera_suite_storage.h b/fap/helpers/camera_suite_storage.h index df035385ee3..e925c34eebd 100644 --- a/fap/helpers/camera_suite_storage.h +++ b/fap/helpers/camera_suite_storage.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -5,9 +7,6 @@ #include "../camera_suite.h" -#ifndef CAMERA_SUITE_STORAGE_H -#define CAMERA_SUITE_STORAGE_H - #define BOILERPLATE_SETTINGS_FILE_VERSION 1 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/camera_suite") #define BOILERPLATE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/camera_suite.conf" @@ -25,5 +24,3 @@ void camera_suite_save_settings(void* context); void camera_suite_read_settings(void* context); - -#endif From c1b8f098eb44b61fd77ae74fadf42dcadd251a4d Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 3 Mar 2024 01:05:55 -0600 Subject: [PATCH 23/43] Add more people to the special thanks section. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1ba74d641ec..e2abe7a1c8b 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ A huge thanks to the following people and projects for making this possible: - This project is based on & forked from the [Flipper Zero Camera Application][flipperzero-camera] by [Z4urce][github-profile-z4urce]. Thanks Z4urce for the inspiration and the great work! - I based this projects application structure on the [Flipper Zero Boilerplate Application][flipper-zero-fap-boilerplate] by [leedave][github-profile-leedave]. Thanks leedave for the great boilerplate application that helped me learn how to structure a Flipper Zero application properly! +- [WillyJL](github-profile-willyjl) for your guidance and wisdom. +- [TalkingSasquach](github-profile-talkingsasquach) for all of your helpful [YouTube videos](youtube-talkingsasquach) and [Discord](discord-squachtopia). - The project images were drawn using the a application called "[lopaka][lopaka]" by [sbrin][github-profile-sbrin]. Thanks sbrin for your help in creating the images for this project! - The Flipper Zero community for all your support and feedback! @@ -283,13 +285,17 @@ Cody Tolene [arduino-ide]: https://www.arduino.cc/en/software [btc-address-link]: https://explorer.btc.com/btc/address/bc1qfx3lvspkj0q077u3gnrnxqkqwyvcku2nml86wmudy7yf2u8edmqq0a5vnt +[discord-squachtopia]: https://discord.gg/squachtopia [flipper-zero-apps]: https://docs.flipper.net/apps [flipper-zero-fap-boilerplate]: https://github.com/leedave/flipper-zero-fap-boilerplate [flipperzero-camera]: https://github.com/Z4urce/flipperzero-camera [github-actions-link]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/actions?query=workflow%3A%22Build+%2B+upload.%22 [github-profile-leedave]: https://github.com/leedave [github-profile-sbrin]: https://github.com/sbrin +[github-profile-talkingsasquach]: https://github.com/skizzophrenic +[github-profile-willyjl]: https://github.com/Willy-JL [github-profile-z4urce]: https://github.com/Z4urce [issues-link]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues [lopaka]: https://github.com/sbrin/lopaka [pull-request-link]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/pulls +[youtube-talkingsasquach]: https://www.youtube.com/@TalkingSasquach From af22af6e8d893b13b662fa2df383d7f0d56ca0d4 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 3 Mar 2024 01:25:24 -0600 Subject: [PATCH 24/43] Update README links. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2abe7a1c8b..bc76baadbf5 100644 --- a/README.md +++ b/README.md @@ -228,8 +228,8 @@ A huge thanks to the following people and projects for making this possible: - This project is based on & forked from the [Flipper Zero Camera Application][flipperzero-camera] by [Z4urce][github-profile-z4urce]. Thanks Z4urce for the inspiration and the great work! - I based this projects application structure on the [Flipper Zero Boilerplate Application][flipper-zero-fap-boilerplate] by [leedave][github-profile-leedave]. Thanks leedave for the great boilerplate application that helped me learn how to structure a Flipper Zero application properly! -- [WillyJL](github-profile-willyjl) for your guidance and wisdom. -- [TalkingSasquach](github-profile-talkingsasquach) for all of your helpful [YouTube videos](youtube-talkingsasquach) and [Discord](discord-squachtopia). +- [WillyJL][github-profile-willyjl] for your guidance and wisdom. +- [TalkingSasquach][github-profile-talkingsasquach] for all of your helpful [YouTube videos][youtube-talkingsasquach] and [Discord community][discord-squachtopia]. - The project images were drawn using the a application called "[lopaka][lopaka]" by [sbrin][github-profile-sbrin]. Thanks sbrin for your help in creating the images for this project! - The Flipper Zero community for all your support and feedback! From ef89c293ba7e7cae83fd765a6d60a4d3927a238f Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 00:15:39 -0600 Subject: [PATCH 25/43] Add fan video. Thank you! --- .../images/video_rechtsanwalt_okan_dogan.png | Bin 0 -> 208043 bytes README.md | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .github/images/video_rechtsanwalt_okan_dogan.png diff --git a/.github/images/video_rechtsanwalt_okan_dogan.png b/.github/images/video_rechtsanwalt_okan_dogan.png new file mode 100644 index 0000000000000000000000000000000000000000..ed449d5e4f42e45f558358fdb87b46a3c7ebd4ee GIT binary patch literal 208043 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

[ Back to top ]

-## YouTube +## Videos -This section is dedicated to all the fans that have made videos of this project! Thank you! If you have a video you'd like to share, please let me know by opening an issue [here][issues-link]. +This section is dedicated to all the fans that have made videos of this project! Thank you! + +If you have a video you'd like to share, please let me know by opening an issue [here][issues-link].
@@ -58,8 +60,16 @@ This section is dedicated to all the fans that have made videos of this project! **Flipper Zero ESP32 CAM Camera Module** - by TAKEAPART +[https://www.youtube.com/watch?v=cEl5UnWH_Ok](https://www.youtube.com/watch?v=cEl5UnWH_Ok) + [![Flipper Zero ESP32 CAM Camera Module - TAKEAPART](https://img.youtube.com/vi/cEl5UnWH_Ok/0.jpg)](https://www.youtube.com/watch?v=cEl5UnWH_Ok) +**Tech With Kids - Flipper Zero ESP32 Kamera** - by @rechtsanwalt.okan.dogan + +[https://www.instagram.com/reel/C4DrufKoKrb/](https://www.instagram.com/reel/C4DrufKoKrb/) + +[![Flipper Zero ESP32 CAM Camera Module - @rechtsanwalt.okan.dogan](.github/images/video_rechtsanwalt_okan_dogan.png)](https://www.instagram.com/reel/C4DrufKoKrb/) +

[ Back to top ]

From 94351c2e9b83448850cae78a75e7fc3b75b19840 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 01:13:08 -0600 Subject: [PATCH 26/43] Remove wifi features in fap for upgrade. --- fap/camera_suite.c | 9 +- fap/camera_suite.h | 32 --- fap/docs/CHANGELOG.md | 5 + fap/helpers/camera_suite_custom_event.h | 7 - fap/scenes/camera_suite_scene_config.h | 1 - fap/scenes/camera_suite_scene_menu.c | 14 -- fap/scenes/camera_suite_scene_wifi_camera.c | 51 ---- fap/views/camera_suite_view_camera.c | 2 + fap/views/camera_suite_view_camera.h | 20 +- fap/views/camera_suite_view_wifi_camera.c | 253 -------------------- fap/views/camera_suite_view_wifi_camera.h | 25 -- 11 files changed, 26 insertions(+), 393 deletions(-) delete mode 100644 fap/scenes/camera_suite_scene_wifi_camera.c delete mode 100644 fap/views/camera_suite_view_wifi_camera.c delete mode 100644 fap/views/camera_suite_view_wifi_camera.h diff --git a/fap/camera_suite.c b/fap/camera_suite.c index 107bc64fb83..8c160ff33e0 100644 --- a/fap/camera_suite.c +++ b/fap/camera_suite.c @@ -1,4 +1,5 @@ #include "camera_suite.h" +#include bool camera_suite_custom_event_callback(void* context, uint32_t event) { furi_assert(context); @@ -70,12 +71,6 @@ CameraSuite* camera_suite_app_alloc() { CameraSuiteViewIdCamera, camera_suite_view_camera_get_view(app->camera_suite_view_camera)); - app->camera_suite_view_wifi_camera = camera_suite_view_wifi_camera_alloc(); - view_dispatcher_add_view( - app->view_dispatcher, - CameraSuiteViewIdWiFiCamera, - camera_suite_view_wifi_camera_get_view(app->camera_suite_view_wifi_camera)); - app->camera_suite_view_guide = camera_suite_view_guide_alloc(); view_dispatcher_add_view( app->view_dispatcher, @@ -109,7 +104,6 @@ void camera_suite_app_free(CameraSuite* app) { view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdStartscreen); view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdMenu); view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdCamera); - view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdWiFiCamera); view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdGuide); view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdAppSettings); view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdCamSettings); @@ -121,7 +115,6 @@ void camera_suite_app_free(CameraSuite* app) { // Free remaining resources camera_suite_view_start_free(app->camera_suite_view_start); camera_suite_view_camera_free(app->camera_suite_view_camera); - camera_suite_view_wifi_camera_free(app->camera_suite_view_wifi_camera); camera_suite_view_guide_free(app->camera_suite_view_guide); button_menu_free(app->button_menu); diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 0919f694025..cd3a3ba6f07 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -2,8 +2,6 @@ #include #include -#include -#include #include #include #include @@ -19,26 +17,8 @@ #include "views/camera_suite_view_camera.h" #include "views/camera_suite_view_guide.h" #include "views/camera_suite_view_start.h" -#include "views/camera_suite_view_wifi_camera.h" #define TAG "Camera Suite" -#include -#define FLIPPER_SCREEN_HEIGHT 64 -#define FLIPPER_SCREEN_WIDTH 128 - -#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) - -#ifdef xtreme_settings -/** - * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). - * - * @see https://github.com/Flipper-XFW/Xtreme-Firmware - * @see https://github.com/Flipper-XFW/Xtreme-Apps -*/ -#define UART_CH (xtreme_settings.uart_esp_channel) -#else -#define UART_CH (FuriHalSerialIdUsart) -#endif typedef struct { Gui* gui; @@ -49,7 +29,6 @@ typedef struct { VariableItemList* variable_item_list; CameraSuiteViewStart* camera_suite_view_start; CameraSuiteViewCamera* camera_suite_view_camera; - CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera; CameraSuiteViewGuide* camera_suite_view_guide; uint32_t orientation; uint32_t dither; @@ -59,16 +38,12 @@ typedef struct { uint32_t speaker; uint32_t led; ButtonMenu* button_menu; - FuriHalSerialHandle* serial_handle; - FuriStreamBuffer* rx_stream; - FuriThread* worker_thread; } CameraSuite; typedef enum { CameraSuiteViewIdStartscreen, CameraSuiteViewIdMenu, CameraSuiteViewIdCamera, - CameraSuiteViewIdWiFiCamera, CameraSuiteViewIdGuide, CameraSuiteViewIdAppSettings, CameraSuiteViewIdCamSettings, @@ -111,10 +86,3 @@ typedef enum { CameraSuiteLedOff, CameraSuiteLedOn, } CameraSuiteLedState; - -typedef enum { - // Reserved for StreamBuffer internal event - WorkerEventReserved = (1 << 0), - WorkerEventStop = (1 << 1), - WorkerEventRx = (1 << 2), -} WorkerEventFlags; diff --git a/fap/docs/CHANGELOG.md b/fap/docs/CHANGELOG.md index 4a772567e9c..2638599e02b 100644 --- a/fap/docs/CHANGELOG.md +++ b/fap/docs/CHANGELOG.md @@ -5,6 +5,11 @@ - Full screen 90 degree and 270 degree fill (#6). - WiFi streaming/connection support (#35). +## v1.7 + +- Add support for new Flipper Zero Firmware UART updates. +- Remove staged WiFi streaming/connection support for now. Until I can fully test. + ## v1.6 - Add new splash/start screen. diff --git a/fap/helpers/camera_suite_custom_event.h b/fap/helpers/camera_suite_custom_event.h index eb545b7dbd2..4d472d577a9 100644 --- a/fap/helpers/camera_suite_custom_event.h +++ b/fap/helpers/camera_suite_custom_event.h @@ -15,13 +15,6 @@ typedef enum { CameraSuiteCustomEventSceneCameraRight, CameraSuiteCustomEventSceneCameraOk, CameraSuiteCustomEventSceneCameraBack, - // Scene events: WiFi Camera - CameraSuiteCustomEventSceneWiFiCameraUp, - CameraSuiteCustomEventSceneWiFiCameraDown, - CameraSuiteCustomEventSceneWiFiCameraLeft, - CameraSuiteCustomEventSceneWiFiCameraRight, - CameraSuiteCustomEventSceneWiFiCameraOk, - CameraSuiteCustomEventSceneWiFiCameraBack, // Scene events: Guide CameraSuiteCustomEventSceneGuideUp, CameraSuiteCustomEventSceneGuideDown, diff --git a/fap/scenes/camera_suite_scene_config.h b/fap/scenes/camera_suite_scene_config.h index fce4c9e8cfa..a9f0e057aeb 100644 --- a/fap/scenes/camera_suite_scene_config.h +++ b/fap/scenes/camera_suite_scene_config.h @@ -1,7 +1,6 @@ ADD_SCENE(camera_suite, start, Start) ADD_SCENE(camera_suite, menu, Menu) ADD_SCENE(camera_suite, camera, Camera) -ADD_SCENE(camera_suite, wifi_camera, WiFiCamera) ADD_SCENE(camera_suite, guide, Guide) ADD_SCENE(camera_suite, app_settings, AppSettings) ADD_SCENE(camera_suite, cam_settings, CamSettings) diff --git a/fap/scenes/camera_suite_scene_menu.c b/fap/scenes/camera_suite_scene_menu.c index a1ca022922a..c6c88038bb4 100644 --- a/fap/scenes/camera_suite_scene_menu.c +++ b/fap/scenes/camera_suite_scene_menu.c @@ -3,8 +3,6 @@ enum SubmenuIndex { /** Camera. */ SubmenuIndexSceneCamera = 10, - /** WiFi Camera */ - SubmenuIndexSceneWiFiCamera, /** Cam settings menu. */ SubmenuIndexCamSettings, /** App settings menu. */ @@ -28,13 +26,6 @@ void camera_suite_scene_menu_on_enter(void* context) { camera_suite_scene_menu_submenu_callback, app); - submenu_add_item( - app->submenu, - "Stream Camera to WiFi", - SubmenuIndexSceneWiFiCamera, - camera_suite_scene_menu_submenu_callback, - app); - submenu_add_item( app->submenu, "Camera Settings", @@ -76,11 +67,6 @@ bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) { app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSceneCamera); scene_manager_next_scene(app->scene_manager, CameraSuiteSceneCamera); return true; - } else if(event.event == SubmenuIndexSceneWiFiCamera) { - scene_manager_set_scene_state( - app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSceneWiFiCamera); - scene_manager_next_scene(app->scene_manager, CameraSuiteSceneWiFiCamera); - return true; } else if(event.event == SubmenuIndexAppSettings) { scene_manager_set_scene_state( app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexAppSettings); diff --git a/fap/scenes/camera_suite_scene_wifi_camera.c b/fap/scenes/camera_suite_scene_wifi_camera.c deleted file mode 100644 index 2df3db81bf1..00000000000 --- a/fap/scenes/camera_suite_scene_wifi_camera.c +++ /dev/null @@ -1,51 +0,0 @@ -#include "../camera_suite.h" -#include "../helpers/camera_suite_custom_event.h" -#include "../views/camera_suite_view_wifi_camera.h" - -void camera_suite_view_wifi_camera_callback(CameraSuiteCustomEvent event, void* context) { - furi_assert(context); - CameraSuite* app = context; - view_dispatcher_send_custom_event(app->view_dispatcher, event); -} - -void camera_suite_scene_wifi_camera_on_enter(void* context) { - furi_assert(context); - CameraSuite* app = context; - camera_suite_view_wifi_camera_set_callback( - app->camera_suite_view_wifi_camera, camera_suite_view_wifi_camera_callback, app); - view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdWiFiCamera); -} - -bool camera_suite_scene_wifi_camera_on_event(void* context, SceneManagerEvent event) { - CameraSuite* app = context; - bool consumed = false; - - if(event.type == SceneManagerEventTypeCustom) { - switch(event.event) { - case CameraSuiteCustomEventSceneWiFiCameraLeft: - case CameraSuiteCustomEventSceneWiFiCameraRight: - case CameraSuiteCustomEventSceneWiFiCameraUp: - case CameraSuiteCustomEventSceneWiFiCameraDown: - // Do nothing. - break; - case CameraSuiteCustomEventSceneWiFiCameraBack: - notification_message(app->notification, &sequence_reset_red); - notification_message(app->notification, &sequence_reset_green); - notification_message(app->notification, &sequence_reset_blue); - if(!scene_manager_search_and_switch_to_previous_scene( - app->scene_manager, CameraSuiteSceneMenu)) { - scene_manager_stop(app->scene_manager); - view_dispatcher_stop(app->view_dispatcher); - } - consumed = true; - break; - } - } - - return consumed; -} - -void camera_suite_scene_wifi_camera_on_exit(void* context) { - CameraSuite* app = context; - UNUSED(app); -} \ No newline at end of file diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index ba854c88ab1..c33f5b20dc5 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -12,6 +12,8 @@ #define HEADER_LENGTH 3 // 'Y', ':', and row identifier #define LAST_ROW_INDEX 1008 #define ROW_BUFFER_LENGTH 16 +#define RING_BUFFER_LENGTH 19 +#define FRAME_BUFFER_LENGTH 1024 static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index e33c5755d2f..052b823670f 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -1,12 +1,28 @@ #pragma once -#define RING_BUFFER_LENGTH 19 -#define FRAME_BUFFER_LENGTH 1024 +#include +#include + +#define FLIPPER_SCREEN_HEIGHT 64 +#define FLIPPER_SCREEN_WIDTH 128 + +#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) +#define UART_CH (FuriHalSerialIdUsart) typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context); +typedef enum { + // Reserved for StreamBuffer internal event + WorkerEventReserved = (1 << 0), + WorkerEventStop = (1 << 1), + WorkerEventRx = (1 << 2), +} WorkerEventFlags; + typedef struct CameraSuiteViewCamera { CameraSuiteViewCameraCallback callback; + FuriHalSerialHandle* serial_handle; + FuriStreamBuffer* rx_stream; + FuriThread* worker_thread; View* view; void* context; } CameraSuiteViewCamera; diff --git a/fap/views/camera_suite_view_wifi_camera.c b/fap/views/camera_suite_view_wifi_camera.c deleted file mode 100644 index 5dcedea5733..00000000000 --- a/fap/views/camera_suite_view_wifi_camera.c +++ /dev/null @@ -1,253 +0,0 @@ -#include "../camera_suite.h" -#include "camera_suite_view_wifi_camera.h" - -#include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_led.h" -#include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_custom_event.h" -// #include "../helpers/camera_suite_uart.h" - -static void camera_suite_view_wifi_camera_draw(Canvas* canvas, void* wifi_model) { - furi_assert(canvas); - furi_assert(wifi_model); - - UartDumpModel* model = wifi_model; - - canvas_clear(canvas); - canvas_set_color(canvas, ColorBlack); - canvas_set_font(canvas, FontSecondary); - canvas_draw_frame(canvas, 0, 0, FLIPPER_SCREEN_HEIGHT, FLIPPER_SCREEN_HEIGHT); - - canvas_draw_str_aligned(canvas, 3, 3, AlignLeft, AlignTop, "Starting WiFi Stream at:"); - - // Draw log from camera. - canvas_draw_str_aligned(canvas, 3, 13, AlignLeft, AlignTop, furi_string_get_cstr(model->log)); -} - -static int32_t camera_suite_wifi_camera_worker(void* wifi_view_instance) { - furi_assert(wifi_view_instance); - - CameraSuiteViewWiFiCamera* instance = wifi_view_instance; - CameraSuite* app_instance = instance->context; - - while(1) { - uint32_t events = - furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); - - // Check if an error occurred. - furi_check((events & FuriFlagError) == 0); - - // Check if the thread should stop. - if(events & WorkerEventStop) { - break; - } else if(events & WorkerEventRx) { - size_t length = 0; - do { - size_t buffer_size = 320; - uint8_t data[buffer_size]; - length = furi_stream_buffer_receive(app_instance->rx_stream, data, buffer_size, 0); - if(length > 0) { - data[length] = '\0'; - - with_view_model( - instance->view, - UartDumpModel * model, - { - furi_string_cat_printf(model->log, "%s", data); - - // Truncate if too long. - model->log_strlen += length; - if(model->log_strlen >= 4096 - 1) { - furi_string_right(model->log, model->log_strlen / 2); - model->log_strlen = furi_string_size(model->log) + length; - } - }, - true); - } - } while(length > 0); - - with_view_model( - instance->view, UartDumpModel * model, { UNUSED(model); }, true); - } - } - - return 0; -} - -static bool - camera_suite_view_wifi_camera_input(InputEvent* input_event, void* wifi_view_instance) { - furi_assert(wifi_view_instance); - furi_assert(input_event); - - CameraSuiteViewWiFiCamera* instance = wifi_view_instance; - CameraSuite* app_instance = instance->context; - uint8_t data[1]; - - if(input_event->type == InputTypeRelease) { - switch(input_event->key) { - default: - with_view_model( - instance->view, - UartDumpModel * model, - { - UNUSED(model); - // Stop all sounds, reset the LED. - camera_suite_play_bad_bump(instance->context); - camera_suite_stop_all_sound(instance->context); - camera_suite_led_set_rgb(instance->context, 0, 0, 0); - }, - true); - break; - } - } else if(input_event->type == InputTypePress) { - switch(input_event->key) { - case InputKeyBack: { - with_view_model( - instance->view, - UartDumpModel * model, - { - UNUSED(model); - - // Stop camera WiFi stream. - data[0] = 'w'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); - furi_delay_ms(50); - - // Go back to the main menu. - instance->callback( - CameraSuiteCustomEventSceneWiFiCameraBack, instance->context); - }, - true); - break; - } - case InputKeyLeft: - case InputKeyRight: - case InputKeyUp: - case InputKeyDown: - case InputKeyOk: - case InputKeyMAX: - default: { - break; - } - } - } - - return false; -} - -static void camera_suite_view_wifi_camera_exit(void* wifi_view_instance) { - UNUSED(wifi_view_instance); -} - -static void camera_suite_view_wifi_camera_model_init(UartDumpModel* const model) { - model->log = furi_string_alloc(); - furi_string_reserve(model->log, 4096); -} - -static void camera_suite_view_wifi_camera_enter(void* wifi_view_instance) { - furi_assert(wifi_view_instance); - - CameraSuiteViewWiFiCamera* instance = wifi_view_instance; - CameraSuite* app_instance = instance->context; - - // Start wifi camera stream. - uint8_t data[1] = {'W'}; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); - furi_delay_ms(50); - - with_view_model( - instance->view, - UartDumpModel * model, - { camera_suite_view_wifi_camera_model_init(model); }, - true); -} - -static void wifi_camera_callback( - FuriHalSerialHandle* handle, - FuriHalSerialRxEvent event, - void* wifi_view_instance) { - furi_assert(handle); - furi_assert(wifi_view_instance); - - CameraSuiteViewWiFiCamera* instance = wifi_view_instance; - CameraSuite* app_instance = instance->context; - - if(event == FuriHalSerialRxEventData) { - uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(app_instance->rx_stream, &data, 1, 0); - furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventRx); - } -} - -CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc() { - CameraSuiteViewWiFiCamera* instance = malloc(sizeof(CameraSuiteViewWiFiCamera)); - - // Allocate the view object - instance->view = view_alloc(); - - // Allocate model - view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel)); - - // Set context for the view - view_set_context(instance->view, instance); - - // Set draw callback - view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_wifi_camera_draw); - - // Set input callback - view_set_input_callback(instance->view, camera_suite_view_wifi_camera_input); - - // Set enter callback - view_set_enter_callback(instance->view, camera_suite_view_wifi_camera_enter); - - // Set exit callback - view_set_exit_callback(instance->view, camera_suite_view_wifi_camera_exit); - - // Allocate the UART worker thread for the camera. - // CameraSuite* app_instance = instance->context; - // camera_suite_uart_alloc(app_instance, wifi_camera_callback); - - with_view_model( - instance->view, - UartDumpModel * model, - { camera_suite_view_wifi_camera_model_init(model); }, - true); - - return instance; -} - -void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* instance) { - furi_assert(instance); - - CameraSuite* app_instance = instance->context; - - // Free the worker thread. - furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventStop); - furi_thread_join(app_instance->worker_thread); - furi_thread_free(app_instance->worker_thread); - - // Free the allocated stream buffer. - furi_stream_buffer_free(app_instance->rx_stream); - - // camera_suite_uart_free(app_instance); - - with_view_model( - instance->view, UartDumpModel * model, { furi_string_free(model->log); }, true); - view_free(instance->view); - free(instance); -} - -View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* instance) { - furi_assert(instance); - return instance->view; -} - -void camera_suite_view_wifi_camera_set_callback( - CameraSuiteViewWiFiCamera* instance, - CameraSuiteViewWiFiCameraCallback callback, - void* context) { - furi_assert(instance); - furi_assert(callback); - instance->callback = callback; - instance->context = context; -} diff --git a/fap/views/camera_suite_view_wifi_camera.h b/fap/views/camera_suite_view_wifi_camera.h deleted file mode 100644 index a37811c705d..00000000000 --- a/fap/views/camera_suite_view_wifi_camera.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -typedef void (*CameraSuiteViewWiFiCameraCallback)(CameraSuiteCustomEvent event, void* context); - -typedef struct CameraSuiteViewWiFiCamera { - CameraSuiteViewWiFiCameraCallback callback; - View* view; - void* context; -} CameraSuiteViewWiFiCamera; - -typedef struct UartWiFiModel { - FuriString* log; - size_t log_strlen; -} UartWiFiModel; - -void camera_suite_view_wifi_camera_set_callback( - CameraSuiteViewWiFiCamera* wifi_view_instance, - CameraSuiteViewWiFiCameraCallback callback, - void* context); - -CameraSuiteViewWiFiCamera* camera_suite_view_wifi_camera_alloc(); - -void camera_suite_view_wifi_camera_free(CameraSuiteViewWiFiCamera* wifi_view_instance); - -View* camera_suite_view_wifi_camera_get_view(CameraSuiteViewWiFiCamera* wifi_view_instance); From 86218eb92258e0bebb72d4c9d94686abc2b89861 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 01:27:04 -0600 Subject: [PATCH 27/43] Revert some of the previous work from the other PR. --- fap/camera_suite.h | 6 ++++-- fap/helpers/camera_suite_haptic.c | 2 +- fap/helpers/camera_suite_haptic.h | 4 ++-- fap/helpers/camera_suite_led.c | 2 +- fap/helpers/camera_suite_speaker.c | 2 +- fap/helpers/camera_suite_speaker.h | 2 -- fap/helpers/camera_suite_storage.h | 7 +++++-- fap/views/camera_suite_view_guide.c | 13 ++++++++----- fap/views/camera_suite_view_guide.h | 1 + fap/views/camera_suite_view_start.c | 5 ++++- fap/views/camera_suite_view_start.h | 1 + 11 files changed, 28 insertions(+), 17 deletions(-) diff --git a/fap/camera_suite.h b/fap/camera_suite.h index cd3a3ba6f07..71f2769016e 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -12,11 +12,13 @@ #include #include -#include "helpers/camera_suite_storage.h" #include "scenes/camera_suite_scene.h" -#include "views/camera_suite_view_camera.h" #include "views/camera_suite_view_guide.h" #include "views/camera_suite_view_start.h" +#include "views/camera_suite_view_camera.h" +#include "views/camera_suite_view_wifi_camera.h" + +#include "helpers/camera_suite_storage.h" #define TAG "Camera Suite" diff --git a/fap/helpers/camera_suite_haptic.c b/fap/helpers/camera_suite_haptic.c index 5b08f563d2b..237a9600430 100644 --- a/fap/helpers/camera_suite_haptic.c +++ b/fap/helpers/camera_suite_haptic.c @@ -1,5 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_haptic.h" +#include "../camera_suite.h" void camera_suite_play_happy_bump(void* context) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_haptic.h b/fap/helpers/camera_suite_haptic.h index f2f70af9e1f..9b7651f97d5 100644 --- a/fap/helpers/camera_suite_haptic.h +++ b/fap/helpers/camera_suite_haptic.h @@ -1,7 +1,7 @@ -#pragma once - #include void camera_suite_play_happy_bump(void* context); + void camera_suite_play_bad_bump(void* context); + void camera_suite_play_long_bump(void* context); diff --git a/fap/helpers/camera_suite_led.c b/fap/helpers/camera_suite_led.c index 64723b8b4da..c4f1a85d7ac 100644 --- a/fap/helpers/camera_suite_led.c +++ b/fap/helpers/camera_suite_led.c @@ -1,5 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_led.h" +#include "../camera_suite.h" void camera_suite_led_set_rgb(void* context, int red, int green, int blue) { CameraSuite* app = context; diff --git a/fap/helpers/camera_suite_speaker.c b/fap/helpers/camera_suite_speaker.c index 78962dd49c2..c2a5a7dd0e5 100644 --- a/fap/helpers/camera_suite_speaker.c +++ b/fap/helpers/camera_suite_speaker.c @@ -1,5 +1,5 @@ -#include "../camera_suite.h" #include "camera_suite_speaker.h" +#include "../camera_suite.h" #define NOTE_INPUT 587.33f diff --git a/fap/helpers/camera_suite_speaker.h b/fap/helpers/camera_suite_speaker.h index 359663cc950..226102cf21b 100644 --- a/fap/helpers/camera_suite_speaker.h +++ b/fap/helpers/camera_suite_speaker.h @@ -1,5 +1,3 @@ -#pragma once - void camera_suite_play_input_sound(void* context); void camera_suite_stop_all_sound(void* context); diff --git a/fap/helpers/camera_suite_storage.h b/fap/helpers/camera_suite_storage.h index e925c34eebd..8e7ad86b19c 100644 --- a/fap/helpers/camera_suite_storage.h +++ b/fap/helpers/camera_suite_storage.h @@ -1,5 +1,3 @@ -#pragma once - #include #include #include @@ -7,6 +5,9 @@ #include "../camera_suite.h" +#ifndef CAMERA_SUITE_STORAGE_H +#define CAMERA_SUITE_STORAGE_H + #define BOILERPLATE_SETTINGS_FILE_VERSION 1 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/camera_suite") #define BOILERPLATE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/camera_suite.conf" @@ -24,3 +25,5 @@ void camera_suite_save_settings(void* context); void camera_suite_read_settings(void* context); + +#endif \ No newline at end of file diff --git a/fap/views/camera_suite_view_guide.c b/fap/views/camera_suite_view_guide.c index e697a9c61e2..1fd8bc4a822 100644 --- a/fap/views/camera_suite_view_guide.c +++ b/fap/views/camera_suite_view_guide.c @@ -1,5 +1,9 @@ #include "../camera_suite.h" -#include "camera_suite_view_guide.h" +#include +#include +#include +#include +#include struct CameraSuiteViewGuide { View* view; @@ -39,10 +43,9 @@ static void camera_suite_view_guide_model_init(CameraSuiteViewGuideModel* const model->some_value = 1; } -bool camera_suite_view_guide_input(InputEvent* event, void* grid_view_instance) { - furi_assert(grid_view_instance); - - CameraSuiteViewGuide* instance = grid_view_instance; +bool camera_suite_view_guide_input(InputEvent* event, void* context) { + furi_assert(context); + CameraSuiteViewGuide* instance = context; if(event->type == InputTypeRelease) { switch(event->key) { diff --git a/fap/views/camera_suite_view_guide.h b/fap/views/camera_suite_view_guide.h index 5afed50fa1a..cd78d4b0179 100644 --- a/fap/views/camera_suite_view_guide.h +++ b/fap/views/camera_suite_view_guide.h @@ -1,5 +1,6 @@ #pragma once +#include #include "../helpers/camera_suite_custom_event.h" typedef struct CameraSuiteViewGuide CameraSuiteViewGuide; diff --git a/fap/views/camera_suite_view_start.c b/fap/views/camera_suite_view_start.c index 44b00e1f88d..7376b0e1674 100644 --- a/fap/views/camera_suite_view_start.c +++ b/fap/views/camera_suite_view_start.c @@ -1,5 +1,8 @@ #include "../camera_suite.h" -#include "camera_suite_view_start.h" +#include +#include +#include +#include void camera_suite_view_start_set_callback( CameraSuiteViewStart* instance, diff --git a/fap/views/camera_suite_view_start.h b/fap/views/camera_suite_view_start.h index 23c799fcaf2..f7116bb5b39 100644 --- a/fap/views/camera_suite_view_start.h +++ b/fap/views/camera_suite_view_start.h @@ -1,5 +1,6 @@ #pragma once +#include #include "../helpers/camera_suite_custom_event.h" typedef void (*CameraSuiteViewStartCallback)(CameraSuiteCustomEvent event, void* context); From bd60b822eef99470923abbb04db6edda5b395579 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 01:48:04 -0600 Subject: [PATCH 28/43] Set up for UART updates. --- fap/helpers/camera_suite_storage.h | 2 +- fap/views/camera_suite_view_camera.c | 230 +++++++++++++-------------- fap/views/camera_suite_view_camera.h | 46 ++++-- fap/views/camera_suite_view_guide.c | 1 - firmware/arduino-cli.yaml | 34 ++-- firmware/camera.cpp | 8 +- firmware/camera.h | 2 +- firmware/camera_config.cpp | 19 +-- firmware/camera_config.h | 4 +- firmware/camera_model.cpp | 10 +- firmware/camera_model.h | 11 +- firmware/firmware.h | 1 - firmware/firmware.ino | 9 +- firmware/process_serial_input.cpp | 10 -- firmware/process_serial_input.h | 1 - firmware/stream_to_serial.cpp | 7 +- firmware/stream_to_wifi.cpp | 91 ----------- firmware/stream_to_wifi.h | 30 ---- 18 files changed, 187 insertions(+), 329 deletions(-) delete mode 100644 firmware/stream_to_wifi.cpp delete mode 100644 firmware/stream_to_wifi.h diff --git a/fap/helpers/camera_suite_storage.h b/fap/helpers/camera_suite_storage.h index 8e7ad86b19c..df035385ee3 100644 --- a/fap/helpers/camera_suite_storage.h +++ b/fap/helpers/camera_suite_storage.h @@ -26,4 +26,4 @@ void camera_suite_save_settings(void* context); void camera_suite_read_settings(void* context); -#endif \ No newline at end of file +#endif diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index c33f5b20dc5..06297be2ee2 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -1,25 +1,14 @@ #include "../camera_suite.h" -#include "camera_suite_view_camera.h" - +#include +#include +#include +#include +#include +#include +#include #include "../helpers/camera_suite_haptic.h" -#include "../helpers/camera_suite_led.h" #include "../helpers/camera_suite_speaker.h" -#include "../helpers/camera_suite_custom_event.h" -// #include "../helpers/camera_suite_uart.h" - -#define BITMAP_HEADER_LENGTH 62 -#define FRAME_BIT_DEPTH 1 -#define HEADER_LENGTH 3 // 'Y', ':', and row identifier -#define LAST_ROW_INDEX 1008 -#define ROW_BUFFER_LENGTH 16 -#define RING_BUFFER_LENGTH 19 -#define FRAME_BUFFER_LENGTH 1024 - -static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { - 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, - 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; +#include "../helpers/camera_suite_led.h" static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint8_t orientation) { furi_assert(canvas); @@ -35,45 +24,45 @@ static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint } case 1: { // Camera rotated 90 degrees - canvas_draw_dot(canvas, y, FLIPPER_SCREEN_WIDTH - 1 - x); + canvas_draw_dot(canvas, y, FRAME_WIDTH - 1 - x); break; } case 2: { // Camera rotated 180 degrees (upside down) - canvas_draw_dot(canvas, FLIPPER_SCREEN_WIDTH - 1 - x, FLIPPER_SCREEN_HEIGHT - 1 - y); + canvas_draw_dot(canvas, FRAME_WIDTH - 1 - x, FRAME_HEIGHT - 1 - y); break; } case 3: { // Camera rotated 270 degrees - canvas_draw_dot(canvas, FLIPPER_SCREEN_HEIGHT - 1 - y, x); + canvas_draw_dot(canvas, FRAME_HEIGHT - 1 - y, x); break; } } } -static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) { +static void camera_suite_view_camera_draw(Canvas* canvas, void* model) { furi_assert(canvas); - furi_assert(uart_dump_model); + furi_assert(model); - UartDumpModel* model = uart_dump_model; + UartDumpModel* uartDumpModel = model; // Clear the screen. canvas_set_color(canvas, ColorBlack); // Draw the frame. - canvas_draw_frame(canvas, 0, 0, FLIPPER_SCREEN_WIDTH, FLIPPER_SCREEN_HEIGHT); + canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT); for(size_t p = 0; p < FRAME_BUFFER_LENGTH; ++p) { uint8_t x = p % ROW_BUFFER_LENGTH; // 0 .. 15 uint8_t y = p / ROW_BUFFER_LENGTH; // 0 .. 63 for(uint8_t i = 0; i < 8; ++i) { - if((model->pixels[p] & (1 << (7 - i))) != 0) { - draw_pixel_by_orientation(canvas, (x * 8) + i, y, model->orientation); + if((uartDumpModel->pixels[p] & (1 << (7 - i))) != 0) { + draw_pixel_by_orientation(canvas, (x * 8) + i, y, uartDumpModel->orientation); } } } // Draw the pinout guide if the camera is not initialized. - if(!model->is_initialized) { + if(!uartDumpModel->is_initialized) { // Clear the screen. canvas_clear(canvas); @@ -169,10 +158,10 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* uart_dump_model) } } -static void save_image_to_flipper_sd_card(void* uart_dump_model) { - furi_assert(uart_dump_model); +static void save_image_to_flipper_sd_card(void* model) { + furi_assert(model); - UartDumpModel* model = uart_dump_model; + UartDumpModel* uartDumpModel = model; // This pointer is used to access the storage. Storage* storage = furi_record_open(RECORD_STORAGE); @@ -192,7 +181,7 @@ static void save_image_to_flipper_sd_card(void* uart_dump_model) { FuriString* file_name = furi_string_alloc(); // Get the current date and time. - FuriHalRtcDateTime datetime = {}; + FuriHalRtcDateTime datetime = {0}; furi_hal_rtc_get_datetime(&datetime); // Create the file name. @@ -214,9 +203,9 @@ static void save_image_to_flipper_sd_card(void* uart_dump_model) { // Free the file name after use. furi_string_free(file_name); - if(!model->is_inverted) { + if(!uartDumpModel->is_inverted) { for(size_t i = 0; i < FRAME_BUFFER_LENGTH; ++i) { - model->pixels[i] = ~model->pixels[i]; + uartDumpModel->pixels[i] = ~uartDumpModel->pixels[i]; } } @@ -235,7 +224,7 @@ static void save_image_to_flipper_sd_card(void* uart_dump_model) { // @todo - Save image based on orientation. for(size_t i = 64; i > 0; --i) { for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) { - row_buffer[j] = model->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j]; + row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j]; } storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH); } @@ -248,30 +237,30 @@ static void save_image_to_flipper_sd_card(void* uart_dump_model) { storage_file_free(file); } -static void camera_suite_view_camera_model_init(UartDumpModel* model, CameraSuite* app_instance) { +static void + camera_suite_view_camera_model_init(UartDumpModel* const model, CameraSuite* instance_context) { furi_assert(model); - furi_assert(app_instance); + furi_assert(instance_context); model->is_dithering_enabled = true; model->is_inverted = false; - uint32_t orientation = app_instance->orientation; + uint32_t orientation = instance_context->orientation; model->orientation = orientation; + for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) { model->pixels[i] = 0; } } -static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera_view_instance) { - furi_assert(camera_view_instance); - furi_assert(input_event); +static bool camera_suite_view_camera_input(InputEvent* event, void* context) { + furi_assert(context); + furi_assert(event); - CameraSuiteViewCamera* instance = camera_view_instance; - CameraSuite* app_instance = instance->context; - uint8_t data[1]; + CameraSuiteViewCamera* instance = context; - if(input_event->type == InputTypeRelease) { - if(input_event->key) { - // Stop all sounds, reset the LED. + if(event->type == InputTypeRelease) { + switch(event->key) { + default: // Stop all sounds, reset the LED. with_view_model( instance->view, UartDumpModel * model, @@ -282,9 +271,10 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera camera_suite_led_set_rgb(instance->context, 0, 0, 0); }, true); + break; } - } else if(input_event->type == InputTypePress) { - switch(input_event->key) { + } else if(event->type == InputTypePress) { + switch(event->key) { case InputKeyBack: { with_view_model( instance->view, @@ -293,8 +283,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera UNUSED(model); // Stop camera stream. - data[0] = 's'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'s'}, 1); furi_delay_ms(50); // Go back to the main menu. @@ -315,15 +304,13 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera if(model->is_inverted) { // Camera: Set invert to false on the ESP32-CAM. - data[0] = 'i'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); furi_delay_ms(50); model->is_inverted = false; } else { // Camera: Set invert to true on the ESP32-CAM. - data[0] = 'I'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'I'}, 1); furi_delay_ms(50); model->is_inverted = true; @@ -346,15 +333,13 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera if(model->is_dithering_enabled) { // Camera: Disable dithering. - data[0] = 'd'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'d'}, 1); furi_delay_ms(50); model->is_dithering_enabled = false; } else { // Camera: Enable dithering. - data[0] = 'D'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'D'}, 1); furi_delay_ms(50); model->is_dithering_enabled = true; @@ -378,8 +363,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Increase contrast. - data[0] = 'C'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'C'}, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraUp, instance->context); @@ -400,8 +384,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Reduce contrast. - data[0] = 'c'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'c'}, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraDown, instance->context); @@ -420,8 +403,7 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera camera_suite_led_set_rgb(instance->context, 0, 0, 255); // @todo - Save picture directly to ESP32-CAM. - // data[0] = 'P'; - // furi_hal_serial_tx(instance->camera_serial_handle, data, 1); + // furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'P'}, 1); // Save currently displayed image to the Flipper Zero SD card. save_image_to_flipper_sd_card(model); @@ -441,60 +423,58 @@ static bool camera_suite_view_camera_input(InputEvent* input_event, void* camera return false; } -static void camera_suite_view_camera_exit(void* camera_view_instance) { - UNUSED(camera_view_instance); +static void camera_suite_view_camera_exit(void* context) { + furi_assert(context); } -static void camera_suite_view_camera_enter(void* camera_view_instance) { - furi_assert(camera_view_instance); +static void camera_suite_view_camera_enter(void* context) { + furi_assert(context); - CameraSuiteViewCamera* instance = camera_view_instance; - CameraSuite* app_instance = instance->context; + // Get the camera suite instance context. + CameraSuiteViewCamera* instance = (CameraSuiteViewCamera*)context; - uint8_t data[1]; + // Get the camera suite instance context. + CameraSuite* instance_context = instance->context; - // Start serial stream to Flipper Zero. - data[0] = 'S'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + // Start camera stream. + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'S'}, 1); furi_delay_ms(50); // Get/set dither type. - uint8_t dither_type = app_instance->dither; - furi_hal_serial_tx(app_instance->serial_handle, &dither_type, 1); + uint8_t dither_type = instance_context->dither; + furi_hal_uart_tx(FuriHalUartIdUSART1, &dither_type, 1); furi_delay_ms(50); // Make sure the camera is not inverted. - data[0] = 'i'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); furi_delay_ms(50); // Toggle flash on or off based on the current state. If the user has this // on the flash will stay on the entire time the user is in the camera view. - data[0] = app_instance->flash ? 'F' : 'f'; - furi_hal_serial_tx(app_instance->serial_handle, data, 1); + uint8_t flash_state = instance_context->flash ? 'F' : 'f'; + furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_state, 1); furi_delay_ms(50); with_view_model( instance->view, UartDumpModel * model, - { camera_suite_view_camera_model_init(model, app_instance); }, + { camera_suite_view_camera_model_init(model, instance_context); }, true); } -static void camera_callback( - FuriHalSerialHandle* handle, - FuriHalSerialRxEvent event, - void* camera_view_instance) { - furi_assert(handle); - furi_assert(camera_view_instance); +static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) { + furi_assert(uartIrqEvent); + furi_assert(data); + furi_assert(context); - CameraSuiteViewCamera* instance = camera_view_instance; - CameraSuite* app_instance = instance->context; + // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`. + CameraSuiteViewCamera* instance = context; - if(event == FuriHalSerialRxEventData) { - uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(app_instance->rx_stream, &data, 1, 0); - furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventRx); + // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the + // `camera_rx_stream` and set the `WorkerEventRx` flag. + if(uartIrqEvent == UartIrqEventRXNE) { + furi_stream_buffer_send(instance->camera_rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventRx); } } @@ -546,16 +526,15 @@ static void process_ringbuffer(UartDumpModel* model, uint8_t const byte) { } } -static int32_t camera_suite_camera_worker(void* camera_view_instance) { - furi_assert(camera_view_instance); +static int32_t camera_suite_camera_worker(void* context) { + furi_assert(context); - CameraSuiteViewCamera* instance = camera_view_instance; - CameraSuite* app_instance = instance->context; + CameraSuiteViewCamera* instance = context; while(1) { // Wait for any event on the worker thread. uint32_t events = - furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); + furi_thread_flags_wait(CAMERA_WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); // Check if an error occurred. furi_check((events & FuriFlagError) == 0); @@ -572,7 +551,8 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { // Allocate a buffer for the data. uint8_t data[buffer_size]; // Read the data from the stream buffer. - length = furi_stream_buffer_receive(app_instance->rx_stream, data, buffer_size, 0); + length = + furi_stream_buffer_receive(instance->camera_rx_stream, data, buffer_size, 0); if(length > 0) { with_view_model( instance->view, @@ -596,11 +576,15 @@ static int32_t camera_suite_camera_worker(void* camera_view_instance) { } CameraSuiteViewCamera* camera_suite_view_camera_alloc() { + // Allocate memory for the instance CameraSuiteViewCamera* instance = malloc(sizeof(CameraSuiteViewCamera)); // Allocate the view object instance->view = view_alloc(); + // Allocate a stream buffer + instance->camera_rx_stream = furi_stream_buffer_alloc(2048, 1); + // Allocate model view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel)); @@ -619,15 +603,20 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // Set exit callback view_set_exit_callback(instance->view, camera_suite_view_camera_exit); - // Allocate the UART worker thread for the camera. - // CameraSuite* app_instance = instance->context; - // camera_suite_uart_alloc(app_instance, camera_callback); + // Allocate a thread for this camera to run on. + FuriThread* thread = furi_thread_alloc_ex( + "Camera_Suite_Camera_Rx_Thread", 2048, camera_suite_camera_worker, instance); + instance->camera_worker_thread = thread; + furi_thread_start(instance->camera_worker_thread); - with_view_model( - instance->view, - UartDumpModel * model, - { camera_suite_view_camera_model_init(model, instance); }, - true); + // Disable console. + furi_hal_console_disable(); + + // 115200 is the default baud rate for the ESP32-CAM. + furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + + // Enable UART1 and set the IRQ callback. + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance); return instance; } @@ -635,10 +624,17 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { furi_assert(instance); - CameraSuite* app_instance = instance->context; + // Remove the IRQ callback. + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); - // Free the UART worker thread. - // camera_suite_uart_free(app_instance); + // Free the worker thread. + furi_thread_free(instance->camera_worker_thread); + + // Free the allocated stream buffer. + furi_stream_buffer_free(instance->camera_rx_stream); + + // Re-enable the console. + // furi_hal_console_enable(); with_view_model( instance->view, UartDumpModel * model, { UNUSED(model); }, true); @@ -652,11 +648,11 @@ View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* instance) { } void camera_suite_view_camera_set_callback( - CameraSuiteViewCamera* camera_view_instance, + CameraSuiteViewCamera* instance, CameraSuiteViewCameraCallback callback, void* context) { - furi_assert(camera_view_instance); + furi_assert(instance); furi_assert(callback); - camera_view_instance->callback = callback; - camera_view_instance->context = context; -} + instance->callback = callback; + instance->context = context; +} \ No newline at end of file diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index 052b823670f..367fee93eaa 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -1,28 +1,55 @@ #pragma once +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#define FLIPPER_SCREEN_HEIGHT 64 -#define FLIPPER_SCREEN_WIDTH 128 +#include "../helpers/camera_suite_custom_event.h" -#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) -#define UART_CH (FuriHalSerialIdUsart) +#define BITMAP_HEADER_LENGTH 62 +#define FRAME_BIT_DEPTH 1 +#define FRAME_BUFFER_LENGTH 1024 +#define FRAME_HEIGHT 64 +#define FRAME_WIDTH 128 +#define HEADER_LENGTH 3 // 'Y', ':', and row identifier +#define LAST_ROW_INDEX 1008 +#define RING_BUFFER_LENGTH 19 +#define ROW_BUFFER_LENGTH 16 -typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context); +static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { + 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; typedef enum { - // Reserved for StreamBuffer internal event - WorkerEventReserved = (1 << 0), + WorkerEventReserved = (1 << 0), // Reserved for StreamBuffer internal event WorkerEventStop = (1 << 1), WorkerEventRx = (1 << 2), } WorkerEventFlags; +#define CAMERA_WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) + +// Forward declaration +typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context); + typedef struct CameraSuiteViewCamera { CameraSuiteViewCameraCallback callback; FuriHalSerialHandle* serial_handle; FuriStreamBuffer* rx_stream; FuriThread* worker_thread; + NotificationApp* notification; View* view; void* context; } CameraSuiteViewCamera; @@ -37,14 +64,13 @@ typedef struct UartDumpModel { uint8_t ringbuffer_index; uint8_t row_identifier; uint8_t row_ringbuffer[RING_BUFFER_LENGTH]; - FuriString* log; - size_t log_strlen; } UartDumpModel; +// Function Prototypes CameraSuiteViewCamera* camera_suite_view_camera_alloc(); View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_suite_static); void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static); void camera_suite_view_camera_set_callback( CameraSuiteViewCamera* camera_suite_view_camera, CameraSuiteViewCameraCallback callback, - void* context); + void* context); \ No newline at end of file diff --git a/fap/views/camera_suite_view_guide.c b/fap/views/camera_suite_view_guide.c index 1fd8bc4a822..4b848846f0e 100644 --- a/fap/views/camera_suite_view_guide.c +++ b/fap/views/camera_suite_view_guide.c @@ -46,7 +46,6 @@ static void camera_suite_view_guide_model_init(CameraSuiteViewGuideModel* const bool camera_suite_view_guide_input(InputEvent* event, void* context) { furi_assert(context); CameraSuiteViewGuide* instance = context; - if(event->type == InputTypeRelease) { switch(event->key) { case InputKeyBack: diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index b32490fd0cc..ac4efa08145 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -1,27 +1,27 @@ board_manager: - additional_urls: - - https://dl.espressif.com/dl/package_esp32_index.json + additional_urls: + - https://espressif.github.io/arduino-esp32/package_esp32_index.json build_cache: - compilations_before_purge: 10 - ttl: 720h0m0s + compilations_before_purge: 10 + ttl: 720h0m0s daemon: - port: "50051" + port: "50051" directories: - data: C:\temp\arduino-cli\data - downloads: C:\temp\arduino-cli\staging - user: C:\temp\arduino-cli\user + data: C:\temp\arduino-cli\data + downloads: C:\temp\arduino-cli\staging + user: C:\temp\arduino-cli\user library: - enable_unsafe_install: false + enable_unsafe_install: false logging: - file: "" - format: text - level: info + file: "" + format: text + level: info metrics: - addr: :9090 - enabled: true + addr: :9090 + enabled: true output: - no_color: false + no_color: false sketch: - always_export_binaries: false + always_export_binaries: false updater: - enable_notification: true + enable_notification: true diff --git a/firmware/camera.cpp b/firmware/camera.cpp index 32b1695403f..cd2861b689f 100644 --- a/firmware/camera.cpp +++ b/firmware/camera.cpp @@ -9,7 +9,7 @@ void initialize_camera() { } } -void set_camera_defaults(CameraFunction camera_function) { +void set_camera_defaults() { // Get the camera sensor reference. sensor_t *cam = esp_camera_sensor_get(); @@ -19,12 +19,6 @@ void set_camera_defaults(CameraFunction camera_function) { cam->set_sharpness(cam, 0); // Set initial sharpness. cam->set_vflip(cam, true); // Set initial vertical flip. cam->set_hmirror(cam, false); // Set initial horizontal mirror. - - if (camera_function == CAMERA_FUNCTION_SERIAL) { - // TODO - } else if (camera_function == CAMERA_FUNCTION_WIFI) { - // TODO - } } void turn_flash_off() { diff --git a/firmware/camera.h b/firmware/camera.h index f5362b6e597..fa2f727762b 100644 --- a/firmware/camera.h +++ b/firmware/camera.h @@ -11,7 +11,7 @@ void initialize_camera(); /** Reset the camera to the default settings. */ -void set_camera_defaults(CameraFunction camera_function); +void set_camera_defaults(); /** Turn the flash off. */ void turn_flash_off(); diff --git a/firmware/camera_config.cpp b/firmware/camera_config.cpp index 3c8f38afda8..492be4e32b2 100644 --- a/firmware/camera_config.cpp +++ b/firmware/camera_config.cpp @@ -3,7 +3,7 @@ /** The camera configuration model. */ camera_config_t camera_config; -void set_camera_config_defaults(CameraFunction camera_function) { +void set_camera_config_defaults() { camera_config.ledc_channel = LEDC_CHANNEL_0; camera_config.ledc_timer = LEDC_TIMER_0; camera_config.pin_d0 = Y2_GPIO_NUM; @@ -24,16 +24,9 @@ void set_camera_config_defaults(CameraFunction camera_function) { camera_config.pin_reset = RESET_GPIO_NUM; camera_config.xclk_freq_hz = 20000000; - if (camera_function == CAMERA_FUNCTION_SERIAL) { - camera_config.pixel_format = PIXFORMAT_GRAYSCALE; - camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; - camera_config.fb_count = 1; - camera_config.frame_size = FRAMESIZE_QQVGA; - } else if (camera_function == CAMERA_FUNCTION_WIFI) { - camera_config.pixel_format = PIXFORMAT_JPEG; - camera_config.jpeg_quality = 8; - camera_config.fb_count = 1; - camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; - camera_config.frame_size = FRAMESIZE_SVGA; - } + camera_config.pixel_format = PIXFORMAT_JPEG; + camera_config.jpeg_quality = 8; + camera_config.fb_count = 1; + camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; + camera_config.frame_size = FRAMESIZE_SVGA; } diff --git a/firmware/camera_config.h b/firmware/camera_config.h index 76a89c56de6..22fef227d87 100644 --- a/firmware/camera_config.h +++ b/firmware/camera_config.h @@ -9,7 +9,7 @@ /** The camera configuration model. */ extern camera_config_t camera_config; -/** Set the camera configuration defaults based on camera function. */ -void set_camera_config_defaults(CameraFunction camera_function); +/** Set the camera configuration defaults. */ +void set_camera_config_defaults(); #endif diff --git a/firmware/camera_model.cpp b/firmware/camera_model.cpp index 88d8718ba50..0ef9b143a12 100644 --- a/firmware/camera_model.cpp +++ b/firmware/camera_model.cpp @@ -3,16 +3,10 @@ /** The camera model. */ CameraModel camera_model; -void set_camera_model_defaults(CameraFunction camera_function) { - if (camera_function == CAMERA_FUNCTION_SERIAL) { - camera_model.isDitheringEnabled = true; - } else if (camera_function == CAMERA_FUNCTION_WIFI) { - camera_model.isDitheringEnabled = false; - } - +void set_camera_model_defaults() { + camera_model.isDitheringEnabled = true; camera_model.isFlashEnabled = false; camera_model.isInvertEnabled = false; camera_model.isStreamToSerialEnabled = false; - camera_model.isStreamToWiFiEnabled = false; camera_model.ditherAlgorithm = FLOYD_STEINBERG; } diff --git a/firmware/camera_model.h b/firmware/camera_model.h index 8f85f2e60a1..dbf741e7198 100644 --- a/firmware/camera_model.h +++ b/firmware/camera_model.h @@ -10,11 +10,6 @@ typedef enum { STUCKI, } DitheringAlgorithm; -typedef enum { - CAMERA_FUNCTION_SERIAL, - CAMERA_FUNCTION_WIFI, -} CameraFunction; - typedef struct { /** Flag to enable or disable dithering. */ bool isDitheringEnabled; @@ -24,8 +19,6 @@ typedef struct { bool isInvertEnabled; /** Flag to stop or start the stream to the Flipper Zero. */ bool isStreamToSerialEnabled; - /** Flag to stop or start the stream to WiFi. */ - bool isStreamToWiFiEnabled; /** Holds the currently selected dithering algorithm. */ DitheringAlgorithm ditherAlgorithm; } CameraModel; @@ -33,7 +26,7 @@ typedef struct { /** The camera model. */ extern CameraModel camera_model; -/** Set the camera model to the default values depending on the camera use. */ -void set_camera_model_defaults(CameraFunction camera_function); +/** Set the camera model to the default values. */ +void set_camera_model_defaults(); #endif diff --git a/firmware/firmware.h b/firmware/firmware.h index 376ffc146f1..ed9c1b63e01 100644 --- a/firmware/firmware.h +++ b/firmware/firmware.h @@ -7,7 +7,6 @@ #include "camera_config.h" #include "camera_model.h" #include "stream_to_serial.h" -#include "stream_to_wifi.h" #include "process_serial_input.h" void setup(); diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 3f9f139a22a..060ad68a5a2 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -5,16 +5,16 @@ void setup() { Serial.begin(230400); // Set initial camera configs for serial streaming. - set_camera_config_defaults(CAMERA_FUNCTION_SERIAL); + set_camera_config_defaults(); // Set initial camera model for serial streaming. - set_camera_model_defaults(CAMERA_FUNCTION_SERIAL); + set_camera_model_defaults(); // Initialize the camera. initialize_camera(); // Set initial camera settings for serial streaming. - set_camera_defaults(CAMERA_FUNCTION_SERIAL); + set_camera_defaults(); } // Main loop of the program. @@ -24,9 +24,6 @@ void loop() { if (camera_model.isStreamToSerialEnabled) { // Process the camera image and output to serial. stream_to_serial(); - } else if (camera_model.isStreamToWiFiEnabled) { - // Stream the camera output to WiFi. - stream_to_wifi(); } else if (camera_model.isFlashEnabled) { // Not currently streaming, turn the flash off if it's enabled. turn_flash_off(); diff --git a/firmware/process_serial_input.cpp b/firmware/process_serial_input.cpp index 057d58e7a88..22e3c69a1b6 100644 --- a/firmware/process_serial_input.cpp +++ b/firmware/process_serial_input.cpp @@ -36,22 +36,12 @@ void process_serial_input() { case 'I': set_inverted(true); break; - case 'P': - // @todo - // save_picture_to_sd_card(); - break; case 's': stop_serial_stream(); break; case 'S': start_serial_stream(); break; - case 'w': - stop_wifi_stream(); - break; - case 'W': - start_wifi_stream(); - break; case '0': set_dithering_algorithm(FLOYD_STEINBERG); break; diff --git a/firmware/process_serial_input.h b/firmware/process_serial_input.h index 0da95eb57c4..6316bc21704 100644 --- a/firmware/process_serial_input.h +++ b/firmware/process_serial_input.h @@ -7,7 +7,6 @@ #include "camera_model.h" #include "pins.h" #include "stream_to_serial.h" -#include "stream_to_wifi.h" /** Handle the serial input commands coming from the Flipper Zero. */ void process_serial_input(); diff --git a/firmware/stream_to_serial.cpp b/firmware/stream_to_serial.cpp index 04889c2b406..92e23ffe752 100644 --- a/firmware/stream_to_serial.cpp +++ b/firmware/stream_to_serial.cpp @@ -59,10 +59,9 @@ void stream_to_serial() { } void start_serial_stream() { - camera_model.isStreamToWiFiEnabled = false; - set_camera_config_defaults(CAMERA_FUNCTION_SERIAL); - set_camera_model_defaults(CAMERA_FUNCTION_SERIAL); - set_camera_defaults(CAMERA_FUNCTION_SERIAL); + set_camera_config_defaults(); + set_camera_model_defaults(); + set_camera_defaults(); camera_model.isStreamToSerialEnabled = true; } diff --git a/firmware/stream_to_wifi.cpp b/firmware/stream_to_wifi.cpp deleted file mode 100644 index 29c6cae1965..00000000000 --- a/firmware/stream_to_wifi.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "stream_to_wifi.h" - -// char password[30] = "test123"; -char ssid[30] = "ESP"; - -bool is_wifi_streaming = false; -char index_html[MAX_HTML_SIZE] = "TEST"; - -DNSServer dnsServer; -AsyncWebServer server(80); - -class RequestHandler : public AsyncWebHandler { -public: - RequestHandler() {} - virtual ~RequestHandler() {} - - bool canHandle(AsyncWebServerRequest *request) { return true; } - - void handleRequest(AsyncWebServerRequest *request) { - request->send_P(200, "text/html", index_html); - } -}; - -void stream_to_wifi() { - if (!is_wifi_streaming) { - Serial.println("Starting WiFi stream..."); - - // Connect to WiFi AP - WiFi.mode(WIFI_AP); - WiFi.softAP(ssid); - WiFi.setSleep(false); - - // Start the web server - start_server(); - - Serial.print("Camera Ready! Use 'http://"); - Serial.print(WiFi.softAPIP()); - Serial.println("' to connect"); - - Serial.flush(); - - is_wifi_streaming = true; - } else { - dnsServer.processNextRequest(); - } -} - -void start_server() { - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - request->send_P(200, "text/html", index_html); - Serial.println("Client connected."); - }); - - server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send( - 200, "text/html", - ""); - }); - - dnsServer.start(53, "*", WiFi.softAPIP()); - server.addHandler(new RequestHandler()).setFilter(ON_AP_FILTER); - server.begin(); -} - -void start_wifi_stream() { - // Physical test indicator that we're streaming. - turn_flash_on(); - - camera_model.isStreamToSerialEnabled = false; - - set_camera_config_defaults(CAMERA_FUNCTION_WIFI); - set_camera_model_defaults(CAMERA_FUNCTION_WIFI); - set_camera_defaults(CAMERA_FUNCTION_WIFI); - - // @todo - Dynamically set ssid and password via prompts. - - camera_model.isStreamToWiFiEnabled = true; - - turn_flash_off(); -} - -void stop_wifi_stream() { - if (is_wifi_streaming) { - WiFi.setSleep(true); - server.end(); - WiFi.softAPdisconnect(true); - WiFi.mode(WIFI_OFF); - is_wifi_streaming = false; - camera_model.isStreamToWiFiEnabled = false; - } -} diff --git a/firmware/stream_to_wifi.h b/firmware/stream_to_wifi.h deleted file mode 100644 index 24ed3dcd725..00000000000 --- a/firmware/stream_to_wifi.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef STREAM_TO_WIFI_H -#define STREAM_TO_WIFI_H - -// #include -// #include - -#include -#include -#include -#include -#include - -#include "camera.h" -#include "camera_model.h" - -#define MAX_HTML_SIZE 20000 - -/** Start the WiFi camera stream. */ -void stream_to_wifi(); - -/** Start the WiFi server. */ -void start_server(); - -/** Start the WiFi camera stream. */ -void start_wifi_stream(); - -/** Stop the WiFi camera stream. */ -void stop_wifi_stream(); - -#endif From 2d9c5a1cc04312810eb515c93b34486fe903c7a9 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 02:03:40 -0600 Subject: [PATCH 29/43] Remove wifi assets from install. --- firmware-assets.bat | 7 ------- firmware-flash.bat | 7 ------- 2 files changed, 14 deletions(-) diff --git a/firmware-assets.bat b/firmware-assets.bat index 33b684ffa2b..0a52a4be331 100644 --- a/firmware-assets.bat +++ b/firmware-assets.bat @@ -59,9 +59,6 @@ echo Checking and setting arduino-cli config... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* -rem Enable for Git installations (ie `arduino-cli lib install --git-url`). -rem @See "https://arduino.github.io/arduino-cli/0.35/configuration/#configuration-keys" -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install true echo Fetching assets... @@ -79,9 +76,6 @@ if %DATA_FLAG% gtr 0 ( :installAssets arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git goto :wrapUp ) else ( set /p SHOULD_REINSTALL="Assets already installed. Reinstall? (Y/N): " @@ -103,7 +97,6 @@ echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false echo. echo The ESP32-CAM development dependencies were installed successfully. diff --git a/firmware-flash.bat b/firmware-flash.bat index 195ebd9b063..5f956985982 100644 --- a/firmware-flash.bat +++ b/firmware-flash.bat @@ -62,9 +62,6 @@ echo Checking and setting arduino-cli configs... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data %CLI_TEMP%\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user %CLI_TEMP%\user %* -rem Enable for Git installations (ie `arduino-cli lib install --git-url`). -rem @See "https://arduino.github.io/arduino-cli/0.35/configuration/#configuration-keys" -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install true echo Fetching assets... @@ -81,9 +78,6 @@ if not exist "%CLI_TEMP%\user" ( if %DATA_FLAG% gtr 0 ( arduino-cli %ARDUINO_CLI_CONFIG_FILE% core update-index arduino-cli %ARDUINO_CLI_CONFIG_FILE% core install esp32:esp32 - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncWebServer.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/AsyncTCP.git - arduino-cli %ARDUINO_CLI_CONFIG_FILE% lib install --git-url https://github.com/me-no-dev/ESPAsyncTCP.git ) else ( echo Assets already installed. Skipping... ) @@ -164,7 +158,6 @@ echo Resetting arduino-cli config back to defaults... arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user -arduino-cli %ARDUINO_CLI_CONFIG_FILE% config set library.enable_unsafe_install false set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): " if /i "!DELETE_TEMP!"=="Y" ( rmdir /s /q %CLI_TEMP% From 30d46b1ce5fa5b35a5e718d772b22534b92c5e4e Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Fri, 8 Mar 2024 02:07:13 -0600 Subject: [PATCH 30/43] Remove unused include. --- fap/camera_suite.h | 1 - 1 file changed, 1 deletion(-) diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 71f2769016e..26531cb5bdd 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -16,7 +16,6 @@ #include "views/camera_suite_view_guide.h" #include "views/camera_suite_view_start.h" #include "views/camera_suite_view_camera.h" -#include "views/camera_suite_view_wifi_camera.h" #include "helpers/camera_suite_storage.h" From 6745e4f89c561f1178a6a1bb753c3722c97d1c6d Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 21:25:52 -0500 Subject: [PATCH 31/43] Update camera view. --- fap/application.fam | 1 + fap/assets/.gitkeep | 0 fap/camera_suite.h | 23 ++++++++++++- fap/views/camera_suite_view_camera.c | 51 ++++++++++++++-------------- fap/views/camera_suite_view_camera.h | 4 +-- firmware/arduino-cli.yaml | 34 +++++++++---------- 6 files changed, 67 insertions(+), 46 deletions(-) create mode 100644 fap/assets/.gitkeep diff --git a/fap/application.fam b/fap/application.fam index 7a15ca5b584..c469f4b5d55 100644 --- a/fap/application.fam +++ b/fap/application.fam @@ -7,6 +7,7 @@ App( fap_category="GPIO", fap_description="A camera suite application for the Flipper Zero ESP32-CAM module.", fap_icon="icons/camera_suite.png", + fap_icon_assets="assets", fap_libs=["assets"], fap_version="1.6", fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam", diff --git a/fap/assets/.gitkeep b/fap/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 26531cb5bdd..80ac33442be 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -16,11 +16,32 @@ #include "views/camera_suite_view_guide.h" #include "views/camera_suite_view_start.h" #include "views/camera_suite_view_camera.h" - #include "helpers/camera_suite_storage.h" +#include + #define TAG "Camera Suite" +#ifdef xtreme_settings +/** + * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). + * + * @see https://github.com/Flipper-XFW/Xtreme-Firmware + * @see https://github.com/Flipper-XFW/Xtreme-Apps +*/ +#define UART_CH (xtreme_settings.uart_esp_channel) +#elif momentum_settings +/** + * Enable the following line for "Momentum Firmware" & "Momentum Apps". + * + * @see https://github.com/Next-Flip/Momentum-Firmware + * @see https://github.com/Next-Flip/Momentum-Apps +*/ +#define UART_CH (momentum_settings.uart_esp_channel) +#else +#define UART_CH (FuriHalSerialIdUsart) +#endif + typedef struct { Gui* gui; NotificationApp* notification; diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index 06297be2ee2..fa8cd0d745d 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -1,8 +1,6 @@ #include "../camera_suite.h" #include #include -#include -#include #include #include #include @@ -181,7 +179,7 @@ static void save_image_to_flipper_sd_card(void* model) { FuriString* file_name = furi_string_alloc(); // Get the current date and time. - FuriHalRtcDateTime datetime = {0}; + DateTime datetime = {0}; furi_hal_rtc_get_datetime(&datetime); // Create the file name. @@ -283,7 +281,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { UNUSED(model); // Stop camera stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'s'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'s'}, 1); furi_delay_ms(50); // Go back to the main menu. @@ -304,13 +302,13 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { if(model->is_inverted) { // Camera: Set invert to false on the ESP32-CAM. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'i'}, 1); furi_delay_ms(50); model->is_inverted = false; } else { // Camera: Set invert to true on the ESP32-CAM. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'I'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'I'}, 1); furi_delay_ms(50); model->is_inverted = true; @@ -333,13 +331,13 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { if(model->is_dithering_enabled) { // Camera: Disable dithering. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'d'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'d'}, 1); furi_delay_ms(50); model->is_dithering_enabled = false; } else { // Camera: Enable dithering. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'D'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'D'}, 1); furi_delay_ms(50); model->is_dithering_enabled = true; @@ -363,7 +361,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Increase contrast. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'C'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'C'}, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraUp, instance->context); @@ -384,7 +382,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // Camera: Reduce contrast. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'c'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'c'}, 1); furi_delay_ms(50); instance->callback(CameraSuiteCustomEventSceneCameraDown, instance->context); @@ -403,7 +401,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { camera_suite_led_set_rgb(instance->context, 0, 0, 255); // @todo - Save picture directly to ESP32-CAM. - // furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'P'}, 1); + // furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'P'}, 1); // Save currently displayed image to the Flipper Zero SD card. save_image_to_flipper_sd_card(model); @@ -437,22 +435,22 @@ static void camera_suite_view_camera_enter(void* context) { CameraSuite* instance_context = instance->context; // Start camera stream. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'S'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'S'}, 1); furi_delay_ms(50); // Get/set dither type. uint8_t dither_type = instance_context->dither; - furi_hal_uart_tx(FuriHalUartIdUSART1, &dither_type, 1); + furi_hal_serial_tx(instance->serial_handle, &dither_type, 1); furi_delay_ms(50); // Make sure the camera is not inverted. - furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[]){'i'}, 1); + furi_hal_serial_tx(instance->serial_handle, (uint8_t[]){'i'}, 1); furi_delay_ms(50); // Toggle flash on or off based on the current state. If the user has this // on the flash will stay on the entire time the user is in the camera view. uint8_t flash_state = instance_context->flash ? 'F' : 'f'; - furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_state, 1); + furi_hal_serial_tx(instance->serial_handle, &flash_state, 1); furi_delay_ms(50); with_view_model( @@ -462,17 +460,16 @@ static void camera_suite_view_camera_enter(void* context) { true); } -static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) { - furi_assert(uartIrqEvent); - furi_assert(data); +static void + camera_on_irq_cb(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) { + furi_assert(handle); furi_assert(context); // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`. CameraSuiteViewCamera* instance = context; - // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the - // `camera_rx_stream` and set the `WorkerEventRx` flag. - if(uartIrqEvent == UartIrqEventRXNE) { + if(event == FuriHalSerialRxEventData) { + uint8_t data = furi_hal_serial_async_rx(handle); furi_stream_buffer_send(instance->camera_rx_stream, &data, 1, 0); furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventRx); } @@ -609,11 +606,12 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { instance->camera_worker_thread = thread; furi_thread_start(instance->camera_worker_thread); - // Disable console. - furi_hal_console_disable(); + // Allocate the serial handle for the camera. + instance->serial_handle = furi_hal_serial_control_acquire(UART_CH); + furi_check(instance->serial_handle); // 115200 is the default baud rate for the ESP32-CAM. - furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + furi_hal_serial_init(instance->serial_handle, 230400); // Enable UART1 and set the IRQ callback. furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance); @@ -633,8 +631,9 @@ void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { // Free the allocated stream buffer. furi_stream_buffer_free(instance->camera_rx_stream); - // Re-enable the console. - // furi_hal_console_enable(); + // Deinitialize the serial handle and release the control. + furi_hal_serial_deinit(instance->serial_handle); + furi_hal_serial_control_release(instance->serial_handle); with_view_model( instance->view, UartDumpModel * model, { UNUSED(model); }, true); diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index 367fee93eaa..c79442743aa 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -46,9 +46,9 @@ typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void typedef struct CameraSuiteViewCamera { CameraSuiteViewCameraCallback callback; + FuriStreamBuffer* camera_rx_stream; FuriHalSerialHandle* serial_handle; - FuriStreamBuffer* rx_stream; - FuriThread* worker_thread; + FuriThread* camera_worker_thread; NotificationApp* notification; View* view; void* context; diff --git a/firmware/arduino-cli.yaml b/firmware/arduino-cli.yaml index ac4efa08145..15f76f7331d 100644 --- a/firmware/arduino-cli.yaml +++ b/firmware/arduino-cli.yaml @@ -1,27 +1,27 @@ board_manager: - additional_urls: - - https://espressif.github.io/arduino-esp32/package_esp32_index.json + additional_urls: + - https://espressif.github.io/arduino-esp32/package_esp32_index.json build_cache: - compilations_before_purge: 10 - ttl: 720h0m0s + compilations_before_purge: 10 + ttl: 720h0m0s daemon: - port: "50051" + port: "50051" directories: - data: C:\temp\arduino-cli\data - downloads: C:\temp\arduino-cli\staging - user: C:\temp\arduino-cli\user + data: C:\temp\arduino-cli\data + downloads: C:\temp\arduino-cli\staging + user: C:\temp\arduino-cli\user library: - enable_unsafe_install: false + enable_unsafe_install: false logging: - file: "" - format: text - level: info + file: "" + format: text + level: info metrics: - addr: :9090 - enabled: true + addr: :9090 + enabled: true output: - no_color: false + no_color: false sketch: - always_export_binaries: false + always_export_binaries: false updater: - enable_notification: true + enable_notification: true From cbe996bfd283d087c2c84ed39ad4d29ba354e651 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 21:27:31 -0500 Subject: [PATCH 32/43] Remove asset icons. --- fap/camera_suite.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 80ac33442be..73091263cc1 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -18,8 +18,6 @@ #include "views/camera_suite_view_camera.h" #include "helpers/camera_suite_storage.h" -#include - #define TAG "Camera Suite" #ifdef xtreme_settings From c57bffde1185d07f31a00144e49c039508974ed0 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 21:30:51 -0500 Subject: [PATCH 33/43] Update furi thread use. --- fap/views/camera_suite_view_camera.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index fa8cd0d745d..3ba6d6c35ad 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -614,7 +614,7 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { furi_hal_serial_init(instance->serial_handle, 230400); // Enable UART1 and set the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance); + furi_hal_serial_async_rx_start(instance->serial_handle, camera_on_irq_cb, instance, false); return instance; } @@ -622,10 +622,9 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) { furi_assert(instance); - // Remove the IRQ callback. - furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL); - // Free the worker thread. + furi_thread_flags_set(furi_thread_get_id(instance->camera_worker_thread), WorkerEventStop); + furi_thread_join(instance->camera_worker_thread); furi_thread_free(instance->camera_worker_thread); // Free the allocated stream buffer. From 370fd5e2cdcb72cb3ca08a6dda4342ef71a49051 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 21:38:37 -0500 Subject: [PATCH 34/43] Update to use expansion --- fap/camera_suite.c | 11 +++++++++++ fap/views/camera_suite_view_camera.c | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/fap/camera_suite.c b/fap/camera_suite.c index 8c160ff33e0..c8289d72eaf 100644 --- a/fap/camera_suite.c +++ b/fap/camera_suite.c @@ -1,5 +1,6 @@ #include "camera_suite.h" #include +#include bool camera_suite_custom_event_callback(void* context, uint32_t event) { furi_assert(context); @@ -130,6 +131,11 @@ void camera_suite_app_free(CameraSuite* app) { /** Main entry point for initialization. */ int32_t camera_suite_app(void* p) { UNUSED(p); + + // Disable expansion protocol to avoid interference with UART Handle + Expansion* expansion = furi_record_open(RECORD_EXPANSION); + expansion_disable(expansion); + CameraSuite* app = camera_suite_app_alloc(); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); // Init with start scene. @@ -139,5 +145,10 @@ int32_t camera_suite_app(void* p) { camera_suite_save_settings(app); furi_hal_power_suppress_charge_exit(); camera_suite_app_free(app); + + // Return previous state of expansion + expansion_enable(expansion); + furi_record_close(RECORD_EXPANSION); + return 0; } diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index 3ba6d6c35ad..61a33507cac 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -613,7 +613,7 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // 115200 is the default baud rate for the ESP32-CAM. furi_hal_serial_init(instance->serial_handle, 230400); - // Enable UART1 and set the IRQ callback. + // Start the asynchronous receive. furi_hal_serial_async_rx_start(instance->serial_handle, camera_on_irq_cb, instance, false); return instance; From b1526a4a10228fece0c3096a2fd0584ff7514d65 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 21:51:50 -0500 Subject: [PATCH 35/43] Misc updates. --- fap/application.fam | 2 -- fap/helpers/camera_suite_speaker.h | 2 ++ fap/helpers/camera_suite_storage.c | 2 +- fap/views/camera_suite_view_camera.c | 2 -- fap/views/camera_suite_view_camera.h | 4 ++-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fap/application.fam b/fap/application.fam index c469f4b5d55..527148ae178 100644 --- a/fap/application.fam +++ b/fap/application.fam @@ -7,8 +7,6 @@ App( fap_category="GPIO", fap_description="A camera suite application for the Flipper Zero ESP32-CAM module.", fap_icon="icons/camera_suite.png", - fap_icon_assets="assets", - fap_libs=["assets"], fap_version="1.6", fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam", name="[ESP32] Camera Suite", diff --git a/fap/helpers/camera_suite_speaker.h b/fap/helpers/camera_suite_speaker.h index 226102cf21b..2119bbec597 100644 --- a/fap/helpers/camera_suite_speaker.h +++ b/fap/helpers/camera_suite_speaker.h @@ -1,3 +1,5 @@ +#define NOTE_INPUT 587.33f + void camera_suite_play_input_sound(void* context); void camera_suite_stop_all_sound(void* context); diff --git a/fap/helpers/camera_suite_storage.c b/fap/helpers/camera_suite_storage.c index cbc4cf3c608..712346d43b1 100644 --- a/fap/helpers/camera_suite_storage.c +++ b/fap/helpers/camera_suite_storage.c @@ -93,9 +93,9 @@ void camera_suite_read_settings(void* context) { FURI_LOG_E(TAG, "Missing Header Data"); camera_suite_close_config_file(fff_file); camera_suite_close_storage(); + furi_string_free(temp_str); return; } - furi_string_free(temp_str); if(file_version < BOILERPLATE_SETTINGS_FILE_VERSION) { diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index 61a33507cac..93d027778b5 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -609,8 +609,6 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() { // Allocate the serial handle for the camera. instance->serial_handle = furi_hal_serial_control_acquire(UART_CH); furi_check(instance->serial_handle); - - // 115200 is the default baud rate for the ESP32-CAM. furi_hal_serial_init(instance->serial_handle, 230400); // Start the asynchronous receive. diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index c79442743aa..815930978b9 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include #include #include @@ -73,4 +73,4 @@ void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static); void camera_suite_view_camera_set_callback( CameraSuiteViewCamera* camera_suite_view_camera, CameraSuiteViewCameraCallback callback, - void* context); \ No newline at end of file + void* context); From 5fdd7807b7a226367629aaf0a2f43098db75d530 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 22:03:43 -0500 Subject: [PATCH 36/43] Remove unused files. --- fap/helpers/camera_suite_uart.c | 36 --------------------------------- fap/helpers/camera_suite_uart.h | 4 ---- 2 files changed, 40 deletions(-) delete mode 100644 fap/helpers/camera_suite_uart.c delete mode 100644 fap/helpers/camera_suite_uart.h diff --git a/fap/helpers/camera_suite_uart.c b/fap/helpers/camera_suite_uart.c deleted file mode 100644 index 6a0acb2142e..00000000000 --- a/fap/helpers/camera_suite_uart.c +++ /dev/null @@ -1,36 +0,0 @@ -#include "camera_suite_uart.h" -#include "../camera_suite.h" - -// void camera_suite_uart_alloc(CameraSuite* instance, FuriThreadCallback* callback) { -// // Allocate a stream buffer -// instance->rx_stream = furi_stream_buffer_alloc(2048, 1); - -// // Allocate a thread for this camera to run on. -// FuriThread* thread = furi_thread_alloc_ex("UsbUartWorker", 2048, callback, instance); -// instance->worker_thread = thread; -// furi_thread_start(instance->worker_thread); - -// // Set up UART thread. -// instance->serial_handle = furi_hal_serial_control_acquire(UART_CH); -// furi_check(instance->serial_handle); -// furi_hal_serial_init(instance->serial_handle, 230400); - -// // Enable UART1 and set the IRQ callback. -// furi_hal_serial_async_rx_start(instance->serial_handle, callback, instance, false); -// } - -// void camera_suite_uart_free(CameraSuite* app_instance) { -// furi_assert(app_instance); - -// // Free the worker thread. -// furi_thread_flags_set(furi_thread_get_id(app_instance->worker_thread), WorkerEventStop); -// furi_thread_join(app_instance->worker_thread); -// furi_thread_free(app_instance->worker_thread); - -// // Free the stream buffer. -// furi_stream_buffer_free(app_instance->rx_stream); - -// // Free the serial handle. -// furi_hal_serial_deinit(app_instance->serial_handle); -// furi_hal_serial_control_release(app_instance->serial_handle); -// } diff --git a/fap/helpers/camera_suite_uart.h b/fap/helpers/camera_suite_uart.h deleted file mode 100644 index 342adc86c23..00000000000 --- a/fap/helpers/camera_suite_uart.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// void camera_suite_uart_alloc(CameraSuite* instance, FuriThreadCallback* callback); -// void camera_suite_uart_free(CameraSuite* app_instance); From 467755f86abc9d7eec1558a03844197718e1c5f2 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 22:05:16 -0500 Subject: [PATCH 37/43] Move uart settings to camera suite view cam .h --- fap/camera_suite.h | 20 -------------------- fap/views/camera_suite_view_camera.h | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/fap/camera_suite.h b/fap/camera_suite.h index 73091263cc1..47cccf963dc 100644 --- a/fap/camera_suite.h +++ b/fap/camera_suite.h @@ -20,26 +20,6 @@ #define TAG "Camera Suite" -#ifdef xtreme_settings -/** - * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). - * - * @see https://github.com/Flipper-XFW/Xtreme-Firmware - * @see https://github.com/Flipper-XFW/Xtreme-Apps -*/ -#define UART_CH (xtreme_settings.uart_esp_channel) -#elif momentum_settings -/** - * Enable the following line for "Momentum Firmware" & "Momentum Apps". - * - * @see https://github.com/Next-Flip/Momentum-Firmware - * @see https://github.com/Next-Flip/Momentum-Apps -*/ -#define UART_CH (momentum_settings.uart_esp_channel) -#else -#define UART_CH (FuriHalSerialIdUsart) -#endif - typedef struct { Gui* gui; NotificationApp* notification; diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index 815930978b9..eb7f4c263d9 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -17,6 +17,26 @@ #include "../helpers/camera_suite_custom_event.h" +#ifdef xtreme_settings +/** + * Enable the following line for "Xtreme Firmware" & "Xtreme Apps" (Flipper-XFW). + * + * @see https://github.com/Flipper-XFW/Xtreme-Firmware + * @see https://github.com/Flipper-XFW/Xtreme-Apps +*/ +#define UART_CH (xtreme_settings.uart_esp_channel) +#elif momentum_settings +/** + * Enable the following line for "Momentum Firmware" & "Momentum Apps". + * + * @see https://github.com/Next-Flip/Momentum-Firmware + * @see https://github.com/Next-Flip/Momentum-Apps +*/ +#define UART_CH (momentum_settings.uart_esp_channel) +#else +#define UART_CH (FuriHalSerialIdUsart) +#endif + #define BITMAP_HEADER_LENGTH 62 #define FRAME_BIT_DEPTH 1 #define FRAME_BUFFER_LENGTH 1024 From 532f04ebcd064e6b4dcc666e6c67cf6515b46ee2 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 22:53:35 -0500 Subject: [PATCH 38/43] Fix up firmware config mishap. --- firmware/camera_config.cpp | 7 +++---- firmware/camera_model.cpp | 1 - firmware/firmware.ino | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firmware/camera_config.cpp b/firmware/camera_config.cpp index 492be4e32b2..614c8e744e1 100644 --- a/firmware/camera_config.cpp +++ b/firmware/camera_config.cpp @@ -24,9 +24,8 @@ void set_camera_config_defaults() { camera_config.pin_reset = RESET_GPIO_NUM; camera_config.xclk_freq_hz = 20000000; - camera_config.pixel_format = PIXFORMAT_JPEG; - camera_config.jpeg_quality = 8; - camera_config.fb_count = 1; + camera_config.pixel_format = PIXFORMAT_GRAYSCALE; camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; - camera_config.frame_size = FRAMESIZE_SVGA; + camera_config.fb_count = 1; + camera_config.frame_size = FRAMESIZE_QQVGA; } diff --git a/firmware/camera_model.cpp b/firmware/camera_model.cpp index 0ef9b143a12..164ba07d666 100644 --- a/firmware/camera_model.cpp +++ b/firmware/camera_model.cpp @@ -7,6 +7,5 @@ void set_camera_model_defaults() { camera_model.isDitheringEnabled = true; camera_model.isFlashEnabled = false; camera_model.isInvertEnabled = false; - camera_model.isStreamToSerialEnabled = false; camera_model.ditherAlgorithm = FLOYD_STEINBERG; } diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 060ad68a5a2..9c73b7776b7 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -1,6 +1,8 @@ #include "firmware.h" void setup() { + camera_model.isStreamToSerialEnabled = false; + // Begin serial communication. Serial.begin(230400); From 46cc5fc6ec5a768a6957906c201ceb9ec133bb08 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 23:13:49 -0500 Subject: [PATCH 39/43] Update imports. --- fap/application.fam | 1 + fap/views/camera_suite_view_camera.h | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fap/application.fam b/fap/application.fam index 527148ae178..7a15ca5b584 100644 --- a/fap/application.fam +++ b/fap/application.fam @@ -7,6 +7,7 @@ App( fap_category="GPIO", fap_description="A camera suite application for the Flipper Zero ESP32-CAM module.", fap_icon="icons/camera_suite.png", + fap_libs=["assets"], fap_version="1.6", fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam", name="[ESP32] Camera Suite", diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h index eb7f4c263d9..40cc3c1cd60 100644 --- a/fap/views/camera_suite_view_camera.h +++ b/fap/views/camera_suite_view_camera.h @@ -25,13 +25,14 @@ * @see https://github.com/Flipper-XFW/Xtreme-Apps */ #define UART_CH (xtreme_settings.uart_esp_channel) -#elif momentum_settings +#elif defined momentum_settings /** * Enable the following line for "Momentum Firmware" & "Momentum Apps". * * @see https://github.com/Next-Flip/Momentum-Firmware * @see https://github.com/Next-Flip/Momentum-Apps */ +#include #define UART_CH (momentum_settings.uart_esp_channel) #else #define UART_CH (FuriHalSerialIdUsart) From f4c7388db07c7cca138da17de15e7a191d0b71e4 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 23:15:28 -0500 Subject: [PATCH 40/43] Skip release build of F0 until stable. --- .github/workflows/pull-request-release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-release.yml b/.github/workflows/pull-request-release.yml index 0ba84d3aefe..77ce57eddb0 100644 --- a/.github/workflows/pull-request-release.yml +++ b/.github/workflows/pull-request-release.yml @@ -16,8 +16,9 @@ jobs: - name: "rc" sdk-channel: rc - - name: "release" - sdk-channel: release + # Skip release build until stable. + # - name: "release" + # sdk-channel: release name: "PR Build: ${{ matrix.name }}" steps: From c7b5912020bcac3a24c6de6f8ec03d6a0ea63418 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 23:21:51 -0500 Subject: [PATCH 41/43] Add pre-release artifacts to test in PR. --- .github/workflows/pull-request-release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pull-request-release.yml b/.github/workflows/pull-request-release.yml index 77ce57eddb0..3c2ca590e48 100644 --- a/.github/workflows/pull-request-release.yml +++ b/.github/workflows/pull-request-release.yml @@ -42,3 +42,9 @@ jobs: app-dir: ./fap skip-setup: true task: lint + + - name: "Create FAP Download Pre Release (${{ matrix.name }})" + uses: actions/upload-artifact@v3 + with: + name: ${{ github.event.repository.name }}-${{ matrix.name }}-${{ steps.build-app.outputs.suffix }}.zip + path: ${{ steps.build-app.outputs.fap-artifacts }} From a3593182de5bc4278b272e5cb95b1241986dfbcb Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Mon, 11 Mar 2024 23:31:30 -0500 Subject: [PATCH 42/43] Re-add release build. Look into datetime typing issue. --- .github/workflows/pull-request-release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request-release.yml b/.github/workflows/pull-request-release.yml index 3c2ca590e48..1e466d02543 100644 --- a/.github/workflows/pull-request-release.yml +++ b/.github/workflows/pull-request-release.yml @@ -16,9 +16,8 @@ jobs: - name: "rc" sdk-channel: rc - # Skip release build until stable. - # - name: "release" - # sdk-channel: release + - name: "release" + sdk-channel: release name: "PR Build: ${{ matrix.name }}" steps: From f1c0261a4ef050a969f1b4008c26792a30995125 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Tue, 12 Mar 2024 00:00:34 -0500 Subject: [PATCH 43/43] Save image names with random numbers intead of DateTime for now (not yet supported in release F0 branch). --- fap/views/camera_suite_view_camera.c | 40 +++++++++++++++++++--------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c index 93d027778b5..e01f6cd4729 100644 --- a/fap/views/camera_suite_view_camera.c +++ b/fap/views/camera_suite_view_camera.c @@ -179,19 +179,33 @@ static void save_image_to_flipper_sd_card(void* model) { FuriString* file_name = furi_string_alloc(); // Get the current date and time. - DateTime datetime = {0}; - furi_hal_rtc_get_datetime(&datetime); - - // Create the file name. - furi_string_printf( - file_name, - EXT_PATH("DCIM/%.4d%.2d%.2d-%.2d%.2d%.2d.bmp"), - datetime.year, - datetime.month, - datetime.day, - datetime.hour, - datetime.minute, - datetime.second); + + // Not supported in "Release" F0 build. + // TODO: Remove when DateTime is supported in "Release" F0 build. + // FuriHalRtcDateTime datetime = {0}; + + // Only supported in "RC" & "Dev" builds. + // TODO: Uncomment when DateTime is supported in "Release" F0 build. + // DateTime datetime = {0}; + + // TODO: Uncomment when DateTime is supported in "Release" F0 build. + // furi_hal_rtc_get_datetime(&datetime); + + // Create the file name using DateTime. + // TODO: Uncomment when DateTime is supported in "Release" F0 build. + // furi_string_printf( + // file_name, + // EXT_PATH("DCIM/%.4d%.2d%.2d-%.2d%.2d%.2d.bmp"), + // datetime.year, + // datetime.month, + // datetime.day, + // datetime.hour, + // datetime.minute, + // datetime.second); + + // Just use a random number for now instead of DateTime. + int random_number = rand(); + furi_string_printf(file_name, EXT_PATH("DCIM/%d.bmp"), random_number); // Open the file for writing. If the file does not exist (it shouldn't), // create it.

_`9sj^s#o(&a}HECj;PMPd^0 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 XqNciB9OWy600000NkvXXu0mjfuX7JM literal 0 HcmV?d00001 diff --git a/README.md b/README.md index bc76baadbf5..850da3a64b3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Table of Contents - [Introduction](#intro) -- [YouTube](#youtube) +- [Videos](#videos) - [Hardware Requirements](#hardware-requirements) - [Hardware Installation](#hardware-installation) - [Firmware Installation](#firmware-installation) @@ -46,9 +46,11 @@ There will be many more features added in the future! If you have any ideas or s